diff --git a/plejd/ble.js b/plejd/ble.js index 1663822..e184046 100644 --- a/plejd/ble.js +++ b/plejd/ble.js @@ -4,7 +4,7 @@ const xor = require('buffer-xor'); const _ = require('lodash'); const EventEmitter = require('events'); -let debug = ''; +let debug = 'console'; const getLogger = () => { const consoleLogger = msg => console.log('plejd', msg); @@ -34,6 +34,11 @@ const STATE_DISCONNECTED = 'disconnected'; const STATE_UNINITIALIZED = 'uninitialized'; const STATE_INITIALIZED = 'initialized'; +const BLE_CMD_DIM_CHANGE = '00c8'; +const BLE_CMD_DIM2_CHANGE = '0098'; +const BLE_CMD_STATE_CHANGE = '0097'; +const BLE_CMD_SCENE_TRIG = '0021'; + class PlejdService extends EventEmitter { constructor(cryptoKey, keepAlive = false) { super(); @@ -51,6 +56,8 @@ class PlejdService extends EventEmitter { this.writeQueue = []; + this.plejdDevices = {}; + // Holds a reference to all characteristics this.characteristicState = STATE_UNINITIALIZED; this.characteristics = { @@ -64,13 +71,39 @@ class PlejdService extends EventEmitter { this.wireEvents(); } - turnOn(id, brightness) { - logger('turning on ' + id + ' at brightness ' + brightness); + turnOn(id, command) { + logger('turning on ' + id + ' at brightness ' + (!command.brightness ? 255 : command.brightness)); + const brightness = command.brightness ? command.brightness : 0; + if (command.transition) { + // we have a transition time, split the target brightness + // into pieces spread of the transition time + const steps = command.transition * 2; + const brightnessStep = brightness / steps; + + let i = 0; + const transitionRef = setInterval(() => { + this._turnOn(id, (brightnessStep * i) + 1); + + if (i >= steps) { + clearInterval(transitionRef); + } + + i++; + }, 500); + } + else { + this._turnOn(id, brightness); + } + } + + _turnOn(id, brightness) { var payload; - if (!brightness) { + if (!brightness || brightness === 0) { + logger('no brightness specified, setting to previous known.'); payload = Buffer.from((id).toString(16).padStart(2, '0') + '0110009701', 'hex'); } else { + logger('brightness is ' + brightness); brightness = brightness << 8 | brightness; payload = Buffer.from((id).toString(16).padStart(2, '0') + '0110009801' + (brightness).toString(16).padStart(4, '0'), 'hex'); } @@ -78,9 +111,35 @@ class PlejdService extends EventEmitter { this.write(payload); } - turnOff(id) { + turnOff(id, command) { logger('turning off ' + id); + if (command.transition) { + // we have a transition time, split the target brightness (which will be 0) + // into pieces spread of the transition time + const initialBrightness = this.plejdDevices[id].dim; + const steps = command.transition * 2; + const brightnessStep = initialBrightness / steps; + let currentBrightness = initialBrightness; + + let i = 0; + const transitionRef = setInterval(() => { + currentBrightness = initialBrightness - (brightnessStep * i); + if (currentBrightness <= 0) { + clearInterval(transitionRef); + } + + this._turnOn(id, currentBrightness); + + if (i >= steps) { + clearInterval(transitionRef); + } + + i++; + }, 500); + } + + // finally, we turn it off var payload = Buffer.from((id).toString(16).padStart(2, '0') + '0110009700', 'hex'); this.write(payload); } @@ -143,7 +202,7 @@ class PlejdService extends EventEmitter { ) ); - logger('connecting to ' + this.device.id + ' with addr ' + this.device.address + ' and rssi ' + this.device.rssi); + console.log('connecting to ' + this.device.id + ' with addr ' + this.device.address + ' and rssi ' + this.device.rssi); setTimeout(() => { if (self.state !== STATE_CONNECTED && self.state !== STATE_AUTHENTICATED) { if (self.deviceIdx < Object.keys(self.devices).length) { @@ -435,19 +494,39 @@ class PlejdService extends EventEmitter { let dim = 0; let device = parseInt(decoded[0], 10); - if (decoded.toString('hex', 3, 5) === '00c8' || decoded.toString('hex', 3, 5) === '0098') { + if (decoded.length < 5) { + // ignore the notification since too small + return; + } + + const cmd = decoded.toString('hex', 3, 5); + + if (debug) { + logger('raw event received: ' + decoded.toString('hex')); + } + + if (cmd === BLE_CMD_DIM_CHANGE || cmd === BLE_CMD_DIM2_CHANGE) { state = parseInt(decoded.toString('hex', 5, 6), 10); dim = parseInt(decoded.toString('hex', 6, 8), 16) >> 8; logger('d: ' + device + ' got state+dim update: ' + state + ' - ' + dim); - this.emit('dimChanged', device, state, dim); + this.emit('stateChanged', device, { state: state, brightness: dim }); } - else if (decoded.toString('hex', 3, 5) === '0097') { + else if (cmd === BLE_CMD_STATE_CHANGE) { state = parseInt(decoded.toString('hex', 5, 6), 10); logger('d: ' + device + ' got state update: ' + state); - this.emit('stateChanged', device, state); + this.emit('stateChanged', device, { state: state }); } + else if (cmd === BLE_CMD_SCENE_TRIG) { + const scene = parseInt(decoded.toString('hex', 5, 6), 10); + this.emit('sceneTriggered', device, scene); + } + + this.plejdDevices[device] = { + state: state, + dim: dim + }; } wireEvents() { diff --git a/plejd/main.js b/plejd/main.js index 78057a5..bcc9b78 100644 --- a/plejd/main.js +++ b/plejd/main.js @@ -1,11 +1,11 @@ -const plejd = require('./plejd'); const api = require('./api'); const mqtt = require('./mqtt'); const fs = require('fs'); const PlejdService = require('./ble'); async function main() { - const rawData = fs.readFileSync('/data/plejd.json'); + //const rawData = fs.readFileSync('/data/plejd.json'); + const rawData = fs.readFileSync('plejd.json'); const config = JSON.parse(rawData); const plejdApi = new api.PlejdApi(config.site, config.username, config.password); @@ -29,29 +29,17 @@ async function main() { }); // subscribe to changes from Plejd - plejd.on('stateChanged', (deviceId, state) => { - client.updateState(deviceId, state); - }); - plejd.on('dimChanged', (deviceId, state, dim) => { - client.updateState(deviceId, state); - client.updateBrightness(deviceId, dim); + plejd.on('stateChanged', (deviceId, command) => { + client.updateState(deviceId, command); }); // subscribe to changes from HA - client.on('stateChanged', (deviceId, state) => { - if (state) { - plejd.turnOn(deviceId); + client.on('stateChanged', (deviceId, command) => { + if (command.state === 'ON') { + plejd.turnOn(deviceId, command); } else { - plejd.turnOff(deviceId); - } - }); - client.on('brightnessChanged', (deviceId, brightness) => { - if (brightness > 0) { - plejd.turnOn(deviceId, brightness); - } - else { - plejd.turnOff(deviceId); + plejd.turnOff(deviceId, command); } }); }); diff --git a/plejd/mqtt.js b/plejd/mqtt.js index 30b2a3d..5898525 100644 --- a/plejd/mqtt.js +++ b/plejd/mqtt.js @@ -27,32 +27,17 @@ const getSubscribePath = () => `${discoveryPrefix}/+/${nodeId}/#`; const getPath = ({ id, type }) => `${discoveryPrefix}/${type}/${nodeId}/${id}`; const getConfigPath = plug => `${getPath(plug)}/config`; -const getAvailabilityTopic = plug => `${getPath(plug)}/availability`; const getStateTopic = plug => `${getPath(plug)}/state`; -const getBrightnessCommandTopic = plug => `${getPath(plug)}/setBrightness`; -const getBrightnessTopic = plug => `${getPath(plug)}/brightness`; const getCommandTopic = plug => `${getPath(plug)}/set`; -const getDiscoveryDimmablePayload = device => ({ - name: device.name, - unique_id: `light.plejd.${device.name.toLowerCase().replace(/ /g, '')}`, - state_topic: getStateTopic(device), - command_topic: getCommandTopic(device), - brightness_command_topic: getBrightnessCommandTopic(device), - brightness_state_topic: getBrightnessTopic(device), - payload_on: 1, - payload_off: 0, - optimistic: false -}); - const getDiscoveryPayload = device => ({ + schema: 'json', name: device.name, unique_id: `light.plejd.${device.name.toLowerCase().replace(/ /g, '')}`, state_topic: getStateTopic(device), command_topic: getCommandTopic(device), - payload_on: 1, - payload_off: 0, - optimistic: false + optimistic: false, + brightness: `${device.dimmable}` }); // #endregion @@ -99,24 +84,17 @@ class MqttClient extends EventEmitter { }); this.client.on('message', (topic, message) => { - const command = message.toString(); + //const command = message.toString(); + const command = JSON.parse(message.toString()); if (topic === startTopic) { logger('home assistant has started. lets do discovery.'); self.emit('connected'); } - if (_.includes(topic, 'setBrightness')) { - const device = self.devices.find(x => getBrightnessCommandTopic(x) === topic); - logger('got brightness update for ' + device.name + ' with brightness: ' + command); - - self.emit('brightnessChanged', device.id, parseInt(command)); - } - else if (_.includes(topic, 'set') && _.includes(['0', '1'], command)) { + if (_.includes(topic, 'set')) { const device = self.devices.find(x => getCommandTopic(x) === topic); - logger('got state update for ' + device.name + ' with state: ' + command); - - self.emit('stateChanged', device.id, parseInt(command)); + self.emit('stateChanged', device.id, command); } }); } @@ -134,16 +112,8 @@ class MqttClient extends EventEmitter { devices.forEach((device) => { logger(`sending discovery for ${device.name}`); - let payload = null; - - if (device.type === 'switch') { - payload = getDiscoveryPayload(device); - } - else { - payload = device.dimmable ? getDiscoveryDimmablePayload(device) : getDiscoveryPayload(device); - } - - console.log(`discovered ${device.name} with Plejd ID ${device.id}.`); + let payload = getDiscoveryPayload(device); + console.log(`discovered ${device.type}: ${device.name} with Plejd ID ${device.id}.`); self.deviceMap[device.id] = payload.unique_id; @@ -154,7 +124,7 @@ class MqttClient extends EventEmitter { }); } - updateState(deviceId, state) { + updateState(deviceId, data) { const device = this.devices.find(x => x.id === deviceId); if (!device) { @@ -162,27 +132,26 @@ class MqttClient extends EventEmitter { return; } - logger('updating state for ' + device.name + ': ' + state); + logger('updating state for ' + device.name + ': ' + data.state); + let payload = null; + + if (device.dimmable) { + payload = { + state: data.state === 1 ? 'ON' : 'OFF', + brightness: data.brightness + } + } + else { + payload = { + state: data.state === 1 ? 'ON' : 'OFF' + } + } + + logger(JSON.stringify(payload)); this.client.publish( getStateTopic(device), - state.toString() - ); - } - - updateBrightness(deviceId, brightness) { - const device = this.devices.find(x => x.id === deviceId); - - if (!device) { - logger('error: ' + deviceId + ' is not handled by us.'); - return; - } - - logger('updating brightness for ' + device.name + ': ' + brightness); - - this.client.publish( - getBrightnessTopic(device), - brightness.toString() + JSON.stringify(payload) ); } }