diff --git a/plejd/api.js b/plejd/api.js index f9f62bc..3caecc8 100644 --- a/plejd/api.js +++ b/plejd/api.js @@ -100,8 +100,7 @@ class PlejdApi extends EventEmitter { self.site = response.data.result.find(x => x.site.title == self.siteName); self.cryptoKey = self.site.plejdMesh.cryptoKey; - //callback(self.cryptoKey); - this.emit('ready', self.cryptoKey); + this.emit('ready', self.cryptoKey, self.site); }) .catch((error) => { console.log('error: unable to retrieve the crypto key. error: ' + error); @@ -149,16 +148,59 @@ class PlejdApi extends EventEmitter { 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]) { - roomDevices[device.roomId].push(newDevice); + let switchDevice = { + 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 { - 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) { @@ -176,12 +218,28 @@ class PlejdApi extends EventEmitter { dimmable: roomDevices[roomId].find(x => x.dimmable).length > 0 }; - logger(JSON.stringify(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; } diff --git a/plejd/ble.bluez.js b/plejd/ble.bluez.js index b7b0641..0bca4fc 100644 --- a/plejd/ble.bluez.js +++ b/plejd/ble.bluez.js @@ -40,16 +40,21 @@ const GATT_SERVICE_ID = 'org.bluez.GattService1'; const GATT_CHRC_ID = 'org.bluez.GattCharacteristic1'; class PlejdService extends EventEmitter { - constructor(cryptoKey, connectionTimeout, keepAlive = false) { + constructor(cryptoKey, devices, sceneManager, connectionTimeout, keepAlive = false) { super(); this.cryptoKey = Buffer.from(cryptoKey.replace(/-/g, ''), 'hex'); + this.sceneManager = sceneManager; + this.connectedDevice = null; this.plejdService = null; this.bleDevices = []; this.plejdDevices = {}; + this.devices = devices; this.connectEventHooked = false; this.connectionTimeout = connectionTimeout; + this.writeQueue = []; + this.writeQueueRef = null; // Holds a reference to all characteristics this.characteristics = { @@ -72,6 +77,7 @@ class PlejdService extends EventEmitter { this.objectManager.removeAllListeners(); } + this.connectedDevice = null; this.bleDevices = []; this.characteristics = { data: null, @@ -82,6 +88,7 @@ class PlejdService extends EventEmitter { }; clearInterval(this.pingRef); + clearInterval(this.writeQueueRef); console.log('init()'); 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['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']); } 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'); } - this.write(payload); + this.writeQueue.unshift(payload); } turnOff(id, command) { @@ -304,7 +316,12 @@ class PlejdService extends EventEmitter { _turnOff(id) { 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() { @@ -326,6 +343,7 @@ class PlejdService extends EventEmitter { // auth done, start ping await this.startPing(); + await this.startWriteQueue(); // After we've authenticated, we need to hook up the event listener // for changes to lastData. @@ -403,6 +421,18 @@ class PlejdService extends EventEmitter { 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) { const proxyObject = await this.bus.getProxyObject(BLUEZ_SERVICE_NAME, path); const service = await proxyObject.getInterface(GATT_SERVICE_ID); @@ -502,6 +532,7 @@ class PlejdService extends EventEmitter { return; } + this.connectedDevice = device['device']; await this.authenticate(); } @@ -610,4 +641,4 @@ class PlejdService extends EventEmitter { } } -module.exports = PlejdService; \ No newline at end of file +module.exports = PlejdService; diff --git a/plejd/config.json b/plejd/config.json index 610dc37..3e59077 100644 --- a/plejd/config.json +++ b/plejd/config.json @@ -1,6 +1,6 @@ { "name": "Plejd", - "version": "0.3.5", + "version": "0.4.0", "slug": "plejd", "description": "Adds support for the Swedish home automation devices from Plejd.", "url": "https://github.com/icanos/hassio-plejd/", diff --git a/plejd/main.js b/plejd/main.js index 68934cf..83e200e 100644 --- a/plejd/main.js +++ b/plejd/main.js @@ -2,8 +2,9 @@ const api = require('./api'); const mqtt = require('./mqtt'); const fs = require('fs'); const PlejdService = require('./ble.bluez'); +const SceneManager = require('./scene.manager'); -const version = "0.3.5"; +const version = "0.4.0"; async function main() { 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); plejdApi.once('loggedIn', () => { - plejdApi.on('ready', (cryptoKey) => { + plejdApi.on('ready', (cryptoKey, site) => { const devices = plejdApi.getDevices(); client.on('connected', () => { @@ -30,7 +31,8 @@ async function main() { client.init(); // 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', () => { console.log('plejd-ble: were unable to connect, will retry connection in 10 seconds.'); setTimeout(() => { @@ -54,12 +56,39 @@ async function main() { }); // subscribe to changes from HA - client.on('stateChanged', (deviceId, command) => { - if (command.state === 'ON') { - plejd.turnOn(deviceId, command); + client.on('stateChanged', (device, command) => { + const deviceId = device.id; + + 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 { - plejd.turnOff(deviceId, command); + state = command.state; + commandObj = command; + } + + if (state === 'ON') { + plejd.turnOn(deviceId, commandObj); + } + else { + plejd.turnOff(deviceId, commandObj); } }); diff --git a/plejd/mqtt.js b/plejd/mqtt.js index adda828..b03e383 100644 --- a/plejd/mqtt.js +++ b/plejd/mqtt.js @@ -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 class MqttClient extends EventEmitter { @@ -100,7 +114,9 @@ class MqttClient extends EventEmitter { this.client.on('message', (topic, message) => { //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) { logger('home assistant has started. lets do discovery.'); @@ -112,7 +128,7 @@ class MqttClient extends EventEmitter { if (_.includes(topic, 'set')) { 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) => { logger(`sending discovery for ${device.name}`); - let payload = getDiscoveryPayload(device); - console.log(`plejd-mqtt: discovered ${device.type} named ${device.name} with PID ${device.id}.`); + let payload = device.type === 'switch' ? getSwitchPayload(device) : getDiscoveryPayload(device); + console.log(`plejd-mqtt: discovered ${device.type} (${device.typeName}) named ${device.name} with PID ${device.id}.`); self.deviceMap[device.id] = payload.unique_id; @@ -162,21 +178,28 @@ class MqttClient extends EventEmitter { logger('updating state for ' + device.name + ': ' + data.state); let payload = null; - if (device.dimmable) { - payload = { - state: data.state === 1 ? 'ON' : 'OFF', - brightness: data.brightness - } + if (device.type === 'switch') { + payload = data.state === 1 ? 'ON' : 'OFF'; } else { - payload = { - state: data.state === 1 ? 'ON' : 'OFF' + if (device.dimmable) { + 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( getStateTopic(device), - JSON.stringify(payload) + payload ); } diff --git a/plejd/scene.manager.js b/plejd/scene.manager.js new file mode 100644 index 0000000..e392b6a --- /dev/null +++ b/plejd/scene.manager.js @@ -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; \ No newline at end of file