Merge pull request #64 from icanos/feature/scenes

scene support, wph-01 and write queues
This commit is contained in:
Marcus Westin 2020-02-29 16:56:02 +01:00 committed by GitHub
commit 4c9fa6fa26
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
6 changed files with 247 additions and 34 deletions

View file

@ -100,8 +100,7 @@ class PlejdApi extends EventEmitter {
self.site = response.data.result.find(x => x.site.title == self.siteName); self.site = response.data.result.find(x => x.site.title == self.siteName);
self.cryptoKey = self.site.plejdMesh.cryptoKey; self.cryptoKey = self.site.plejdMesh.cryptoKey;
//callback(self.cryptoKey); this.emit('ready', self.cryptoKey, self.site);
this.emit('ready', self.cryptoKey);
}) })
.catch((error) => { .catch((error) => {
console.log('error: unable to retrieve the crypto key. error: ' + error); console.log('error: unable to retrieve the crypto key. error: ' + error);
@ -149,16 +148,59 @@ class PlejdApi extends EventEmitter {
serialNumber: plejdDevice.deviceId serialNumber: plejdDevice.deviceId
}; };
logger(JSON.stringify(newDevice)); if (newDevice.typeName === 'WPH-01') {
// WPH-01 is special, it has two buttons which needs to be
// registered separately.
const inputs = this.site.inputAddress[deviceId];
const first = inputs[0];
const second = inputs[1];
if (roomDevices[device.roomId]) { let switchDevice = {
roomDevices[device.roomId].push(newDevice); id: first,
name: device.title + ' knapp vä',
type: type,
typeName: name,
dimmable: dimmable,
version: plejdDevice.firmware.version,
serialNumber: plejdDevice.deviceId
};
if (roomDevices[device.roomId]) {
roomDevices[device.roomId].push(switchDevice);
}
else {
roomDevices[device.roomId] = [switchDevice];
}
devices.push(switchDevice);
switchDevice = {
id: second,
name: device.title + ' knapp hö',
type: type,
typeName: name,
dimmable: dimmable,
version: plejdDevice.firmware.version,
serialNumber: plejdDevice.deviceId
};
if (roomDevices[device.roomId]) {
roomDevices[device.roomId].push(switchDevice);
}
else {
roomDevices[device.roomId] = [switchDevice];
}
devices.push(switchDevice);
} }
else { else {
roomDevices[device.roomId] = [newDevice]; if (roomDevices[device.roomId]) {
} roomDevices[device.roomId].push(newDevice);
}
else {
roomDevices[device.roomId] = [newDevice];
}
devices.push(newDevice); devices.push(newDevice);
}
} }
if (this.includeRoomsAsLights) { if (this.includeRoomsAsLights) {
@ -176,12 +218,28 @@ class PlejdApi extends EventEmitter {
dimmable: roomDevices[roomId].find(x => x.dimmable).length > 0 dimmable: roomDevices[roomId].find(x => x.dimmable).length > 0
}; };
logger(JSON.stringify(newDevice));
devices.push(newDevice); devices.push(newDevice);
} }
} }
// add scenes as switches
const scenes = this.site.scenes.filter(x => x.hiddenFromSceneList == false);
for (const scene of scenes) {
const sceneNum = this.site.sceneIndex[scene.sceneId];
const newScene = {
id: sceneNum,
name: scene.title,
type: 'switch',
typeName: 'Scene',
dimmable: false,
version: '1.0',
serialNumber: scene.objectId
};
devices.push(newScene);
}
return devices; return devices;
} }

View file

@ -40,16 +40,21 @@ const GATT_SERVICE_ID = 'org.bluez.GattService1';
const GATT_CHRC_ID = 'org.bluez.GattCharacteristic1'; const GATT_CHRC_ID = 'org.bluez.GattCharacteristic1';
class PlejdService extends EventEmitter { class PlejdService extends EventEmitter {
constructor(cryptoKey, connectionTimeout, keepAlive = false) { constructor(cryptoKey, devices, sceneManager, connectionTimeout, keepAlive = false) {
super(); super();
this.cryptoKey = Buffer.from(cryptoKey.replace(/-/g, ''), 'hex'); this.cryptoKey = Buffer.from(cryptoKey.replace(/-/g, ''), 'hex');
this.sceneManager = sceneManager;
this.connectedDevice = null;
this.plejdService = null; this.plejdService = null;
this.bleDevices = []; this.bleDevices = [];
this.plejdDevices = {}; this.plejdDevices = {};
this.devices = devices;
this.connectEventHooked = false; this.connectEventHooked = false;
this.connectionTimeout = connectionTimeout; this.connectionTimeout = connectionTimeout;
this.writeQueue = [];
this.writeQueueRef = null;
// Holds a reference to all characteristics // Holds a reference to all characteristics
this.characteristics = { this.characteristics = {
@ -72,6 +77,7 @@ class PlejdService extends EventEmitter {
this.objectManager.removeAllListeners(); this.objectManager.removeAllListeners();
} }
this.connectedDevice = null;
this.bleDevices = []; this.bleDevices = [];
this.characteristics = { this.characteristics = {
data: null, data: null,
@ -82,6 +88,7 @@ class PlejdService extends EventEmitter {
}; };
clearInterval(this.pingRef); clearInterval(this.pingRef);
clearInterval(this.writeQueueRef);
console.log('init()'); console.log('init()');
const bluez = await this.bus.getProxyObject(BLUEZ_SERVICE_NAME, '/'); const bluez = await this.bus.getProxyObject(BLUEZ_SERVICE_NAME, '/');
@ -152,6 +159,11 @@ class PlejdService extends EventEmitter {
plejd['rssi'] = (await properties.Get(BLUEZ_DEVICE_ID, 'RSSI')).value; plejd['rssi'] = (await properties.Get(BLUEZ_DEVICE_ID, 'RSSI')).value;
plejd['instance'] = device; plejd['instance'] = device;
const segments = plejd['path'].split('/');
let fixedPlejdPath = segments[segments.length - 1].replace('dev_', '');
fixedPlejdPath = fixedPlejdPath.replace(/_/g, '');
plejd['device'] = this.devices.find(x => x.serialNumber === fixedPlejdPath);
logger('discovered ' + plejd['path'] + ' with rssi ' + plejd['rssi']); logger('discovered ' + plejd['path'] + ' with rssi ' + plejd['rssi']);
} }
catch (err) { catch (err) {
@ -267,7 +279,7 @@ class PlejdService extends EventEmitter {
payload = Buffer.from((id).toString(16).padStart(2, '0') + '0110009801' + (brightness).toString(16).padStart(4, '0'), 'hex'); payload = Buffer.from((id).toString(16).padStart(2, '0') + '0110009801' + (brightness).toString(16).padStart(4, '0'), 'hex');
} }
this.write(payload); this.writeQueue.unshift(payload);
} }
turnOff(id, command) { turnOff(id, command) {
@ -304,7 +316,12 @@ class PlejdService extends EventEmitter {
_turnOff(id) { _turnOff(id) {
var payload = Buffer.from((id).toString(16).padStart(2, '0') + '0110009700', 'hex'); var payload = Buffer.from((id).toString(16).padStart(2, '0') + '0110009700', 'hex');
this.write(payload); this.writeQueue.unshift(payload);
}
triggerScene(sceneIndex) {
console.log('triggering scene with ID ' + sceneIndex);
this.sceneManager.executeScene(sceneIndex, this);
} }
async authenticate() { async authenticate() {
@ -326,6 +343,7 @@ class PlejdService extends EventEmitter {
// auth done, start ping // auth done, start ping
await this.startPing(); await this.startPing();
await this.startWriteQueue();
// After we've authenticated, we need to hook up the event listener // After we've authenticated, we need to hook up the event listener
// for changes to lastData. // for changes to lastData.
@ -403,6 +421,18 @@ class PlejdService extends EventEmitter {
this.emit('pingSuccess', pong[0]); this.emit('pingSuccess', pong[0]);
} }
async startWriteQueue() {
console.log('startWriteQueue()');
clearInterval(this.writeQueueRef);
this.writeQueueRef = setInterval(async () => {
while (this.writeQueue.length > 0) {
const data = this.writeQueue.pop();
await this.write(data);
}
}, 400);
}
async _processPlejdService(path, characteristics) { async _processPlejdService(path, characteristics) {
const proxyObject = await this.bus.getProxyObject(BLUEZ_SERVICE_NAME, path); const proxyObject = await this.bus.getProxyObject(BLUEZ_SERVICE_NAME, path);
const service = await proxyObject.getInterface(GATT_SERVICE_ID); const service = await proxyObject.getInterface(GATT_SERVICE_ID);
@ -502,6 +532,7 @@ class PlejdService extends EventEmitter {
return; return;
} }
this.connectedDevice = device['device'];
await this.authenticate(); await this.authenticate();
} }
@ -610,4 +641,4 @@ class PlejdService extends EventEmitter {
} }
} }
module.exports = PlejdService; module.exports = PlejdService;

View file

@ -1,6 +1,6 @@
{ {
"name": "Plejd", "name": "Plejd",
"version": "0.3.5", "version": "0.4.0",
"slug": "plejd", "slug": "plejd",
"description": "Adds support for the Swedish home automation devices from Plejd.", "description": "Adds support for the Swedish home automation devices from Plejd.",
"url": "https://github.com/icanos/hassio-plejd/", "url": "https://github.com/icanos/hassio-plejd/",

View file

@ -2,8 +2,9 @@ const api = require('./api');
const mqtt = require('./mqtt'); const mqtt = require('./mqtt');
const fs = require('fs'); const fs = require('fs');
const PlejdService = require('./ble.bluez'); const PlejdService = require('./ble.bluez');
const SceneManager = require('./scene.manager');
const version = "0.3.5"; const version = "0.4.0";
async function main() { async function main() {
console.log('starting Plejd add-on v. ' + version); console.log('starting Plejd add-on v. ' + version);
@ -19,7 +20,7 @@ async function main() {
const client = new mqtt.MqttClient(config.mqttBroker, config.mqttUsername, config.mqttPassword); const client = new mqtt.MqttClient(config.mqttBroker, config.mqttUsername, config.mqttPassword);
plejdApi.once('loggedIn', () => { plejdApi.once('loggedIn', () => {
plejdApi.on('ready', (cryptoKey) => { plejdApi.on('ready', (cryptoKey, site) => {
const devices = plejdApi.getDevices(); const devices = plejdApi.getDevices();
client.on('connected', () => { client.on('connected', () => {
@ -30,7 +31,8 @@ async function main() {
client.init(); client.init();
// init the BLE interface // init the BLE interface
const plejd = new PlejdService(cryptoKey, config.connectionTimeout, true); const sceneManager = new SceneManager(site, devices);
const plejd = new PlejdService(cryptoKey, devices, sceneManager, config.connectionTimeout, true);
plejd.on('connectFailed', () => { plejd.on('connectFailed', () => {
console.log('plejd-ble: were unable to connect, will retry connection in 10 seconds.'); console.log('plejd-ble: were unable to connect, will retry connection in 10 seconds.');
setTimeout(() => { setTimeout(() => {
@ -54,12 +56,39 @@ async function main() {
}); });
// subscribe to changes from HA // subscribe to changes from HA
client.on('stateChanged', (deviceId, command) => { client.on('stateChanged', (device, command) => {
if (command.state === 'ON') { const deviceId = device.id;
plejd.turnOn(deviceId, command);
if (device.typeName === 'Scene') {
// we're triggering a scene, lets do that and jump out.
// since scenes aren't "real" devices.
plejd.triggerScene(device.id);
return;
}
let state = 'OFF';
let commandObj = {};
if (typeof command === 'string') {
// switch command
state = command;
commandObj = { state: state };
// since the switch doesn't get any updates on whether it's on or not,
// we fake this by directly send the updateState back to HA in order for
// it to change state.
client.updateState(deviceId, { state: state === 'ON' ? 1 : 0 });
} }
else { else {
plejd.turnOff(deviceId, command); state = command.state;
commandObj = command;
}
if (state === 'ON') {
plejd.turnOn(deviceId, commandObj);
}
else {
plejd.turnOff(deviceId, commandObj);
} }
}); });

View file

@ -49,6 +49,20 @@ const getDiscoveryPayload = device => ({
} }
}); });
const getSwitchPayload = device => ({
name: device.name,
state_topic: getStateTopic(device),
command_topic: getCommandTopic(device),
optimistic: false,
device: {
identifiers: device.serialNumber + '_' + device.id,
manufacturer: 'Plejd',
model: device.typeName,
name: device.name,
sw_version: device.version
}
});
// #endregion // #endregion
class MqttClient extends EventEmitter { class MqttClient extends EventEmitter {
@ -100,7 +114,9 @@ class MqttClient extends EventEmitter {
this.client.on('message', (topic, message) => { this.client.on('message', (topic, message) => {
//const command = message.toString(); //const command = message.toString();
const command = JSON.parse(message.toString()); const command = message.toString().substring(0, 1) === '{'
? JSON.parse(message.toString())
: message.toString();
if (topic === startTopic) { if (topic === startTopic) {
logger('home assistant has started. lets do discovery.'); logger('home assistant has started. lets do discovery.');
@ -112,7 +128,7 @@ class MqttClient extends EventEmitter {
if (_.includes(topic, 'set')) { if (_.includes(topic, 'set')) {
const device = self.devices.find(x => getCommandTopic(x) === topic); const device = self.devices.find(x => getCommandTopic(x) === topic);
self.emit('stateChanged', device.id, command); self.emit('stateChanged', device, command);
} }
}); });
} }
@ -139,8 +155,8 @@ class MqttClient extends EventEmitter {
devices.forEach((device) => { devices.forEach((device) => {
logger(`sending discovery for ${device.name}`); logger(`sending discovery for ${device.name}`);
let payload = getDiscoveryPayload(device); let payload = device.type === 'switch' ? getSwitchPayload(device) : getDiscoveryPayload(device);
console.log(`plejd-mqtt: discovered ${device.type} named ${device.name} with PID ${device.id}.`); console.log(`plejd-mqtt: discovered ${device.type} (${device.typeName}) named ${device.name} with PID ${device.id}.`);
self.deviceMap[device.id] = payload.unique_id; self.deviceMap[device.id] = payload.unique_id;
@ -162,21 +178,28 @@ class MqttClient extends EventEmitter {
logger('updating state for ' + device.name + ': ' + data.state); logger('updating state for ' + device.name + ': ' + data.state);
let payload = null; let payload = null;
if (device.dimmable) { if (device.type === 'switch') {
payload = { payload = data.state === 1 ? 'ON' : 'OFF';
state: data.state === 1 ? 'ON' : 'OFF',
brightness: data.brightness
}
} }
else { else {
payload = { if (device.dimmable) {
state: data.state === 1 ? 'ON' : 'OFF' payload = {
state: data.state === 1 ? 'ON' : 'OFF',
brightness: data.brightness
}
} }
else {
payload = {
state: data.state === 1 ? 'ON' : 'OFF'
}
}
payload = JSON.stringify(payload);
} }
this.client.publish( this.client.publish(
getStateTopic(device), getStateTopic(device),
JSON.stringify(payload) payload
); );
} }

72
plejd/scene.manager.js Normal file
View file

@ -0,0 +1,72 @@
const EventEmitter = require('events');
const _ = require('lodash');
class SceneManager extends EventEmitter {
constructor(site, devices) {
super();
this.site = site;
this.scenes = [];
this.devices = devices;
this.init();
}
init() {
const scenes = this.site.scenes.filter(x => x.hiddenFromSceneList == false);
for (const scene of scenes) {
const idx = this.site.sceneIndex[scene.sceneId];
this.scenes.push(new Scene(idx, scene, this.site.sceneSteps));
}
}
executeScene(sceneIndex, ble) {
const scene = this.scenes.find(x => x.id === sceneIndex);
if (!scene) {
return;
}
for (const step of scene.steps) {
const device = this.devices.find(x => x.serialNumber === step.deviceId);
if (!device) {
continue;
}
if (device.dimmable && step.state) {
ble.turnOn(device.id, { brightness: step.brightness });
}
else if (!device.dimmable && step.state) {
ble.turnOn(device.id, {});
}
else if (!step.state) {
ble.turnOff(device.id, {});
}
}
}
}
class Scene {
constructor(idx, scene, steps) {
this.id = idx;
this.title = scene.title;
this.sceneId = scene.sceneId;
const sceneSteps = steps.filter(x => x.sceneId === scene.sceneId);
this.steps = [];
for (const step of sceneSteps) {
this.steps.push(new SceneStep(step));
}
}
}
class SceneStep {
constructor(step) {
this.sceneId = step.sceneId;
this.deviceId = step.deviceId;
this.state = step.state === 'On' ? 1 : 0;
this.brightness = step.value;
}
}
module.exports = SceneManager;