initial impl of transitions

This commit is contained in:
icanos 2019-12-21 15:01:15 +00:00
parent 82c4384097
commit 0c8e502b2f
3 changed files with 124 additions and 88 deletions

View file

@ -4,7 +4,7 @@ const xor = require('buffer-xor');
const _ = require('lodash'); const _ = require('lodash');
const EventEmitter = require('events'); const EventEmitter = require('events');
let debug = ''; let debug = 'console';
const getLogger = () => { const getLogger = () => {
const consoleLogger = msg => console.log('plejd', msg); const consoleLogger = msg => console.log('plejd', msg);
@ -34,6 +34,11 @@ const STATE_DISCONNECTED = 'disconnected';
const STATE_UNINITIALIZED = 'uninitialized'; const STATE_UNINITIALIZED = 'uninitialized';
const STATE_INITIALIZED = 'initialized'; 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 { class PlejdService extends EventEmitter {
constructor(cryptoKey, keepAlive = false) { constructor(cryptoKey, keepAlive = false) {
super(); super();
@ -51,6 +56,8 @@ class PlejdService extends EventEmitter {
this.writeQueue = []; this.writeQueue = [];
this.plejdDevices = {};
// Holds a reference to all characteristics // Holds a reference to all characteristics
this.characteristicState = STATE_UNINITIALIZED; this.characteristicState = STATE_UNINITIALIZED;
this.characteristics = { this.characteristics = {
@ -64,13 +71,39 @@ class PlejdService extends EventEmitter {
this.wireEvents(); this.wireEvents();
} }
turnOn(id, brightness) { turnOn(id, command) {
logger('turning on ' + id + ' at brightness ' + brightness); 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; 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'); payload = Buffer.from((id).toString(16).padStart(2, '0') + '0110009701', 'hex');
} else { } else {
logger('brightness is ' + brightness);
brightness = brightness << 8 | brightness; brightness = brightness << 8 | brightness;
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');
} }
@ -78,9 +111,35 @@ class PlejdService extends EventEmitter {
this.write(payload); this.write(payload);
} }
turnOff(id) { turnOff(id, command) {
logger('turning off ' + id); 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'); var payload = Buffer.from((id).toString(16).padStart(2, '0') + '0110009700', 'hex');
this.write(payload); 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(() => { setTimeout(() => {
if (self.state !== STATE_CONNECTED && self.state !== STATE_AUTHENTICATED) { if (self.state !== STATE_CONNECTED && self.state !== STATE_AUTHENTICATED) {
if (self.deviceIdx < Object.keys(self.devices).length) { if (self.deviceIdx < Object.keys(self.devices).length) {
@ -435,19 +494,39 @@ class PlejdService extends EventEmitter {
let dim = 0; let dim = 0;
let device = parseInt(decoded[0], 10); 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); state = parseInt(decoded.toString('hex', 5, 6), 10);
dim = parseInt(decoded.toString('hex', 6, 8), 16) >> 8; dim = parseInt(decoded.toString('hex', 6, 8), 16) >> 8;
logger('d: ' + device + ' got state+dim update: ' + state + ' - ' + dim); 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); state = parseInt(decoded.toString('hex', 5, 6), 10);
logger('d: ' + device + ' got state update: ' + state); 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() { wireEvents() {

View file

@ -1,11 +1,11 @@
const plejd = require('./plejd');
const api = require('./api'); const api = require('./api');
const mqtt = require('./mqtt'); const mqtt = require('./mqtt');
const fs = require('fs'); const fs = require('fs');
const PlejdService = require('./ble'); const PlejdService = require('./ble');
async function main() { 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 config = JSON.parse(rawData);
const plejdApi = new api.PlejdApi(config.site, config.username, config.password); const plejdApi = new api.PlejdApi(config.site, config.username, config.password);
@ -29,29 +29,17 @@ async function main() {
}); });
// subscribe to changes from Plejd // subscribe to changes from Plejd
plejd.on('stateChanged', (deviceId, state) => { plejd.on('stateChanged', (deviceId, command) => {
client.updateState(deviceId, state); client.updateState(deviceId, command);
});
plejd.on('dimChanged', (deviceId, state, dim) => {
client.updateState(deviceId, state);
client.updateBrightness(deviceId, dim);
}); });
// subscribe to changes from HA // subscribe to changes from HA
client.on('stateChanged', (deviceId, state) => { client.on('stateChanged', (deviceId, command) => {
if (state) { if (command.state === 'ON') {
plejd.turnOn(deviceId); plejd.turnOn(deviceId, command);
} }
else { else {
plejd.turnOff(deviceId); plejd.turnOff(deviceId, command);
}
});
client.on('brightnessChanged', (deviceId, brightness) => {
if (brightness > 0) {
plejd.turnOn(deviceId, brightness);
}
else {
plejd.turnOff(deviceId);
} }
}); });
}); });

View file

@ -27,32 +27,17 @@ const getSubscribePath = () => `${discoveryPrefix}/+/${nodeId}/#`;
const getPath = ({ id, type }) => const getPath = ({ id, type }) =>
`${discoveryPrefix}/${type}/${nodeId}/${id}`; `${discoveryPrefix}/${type}/${nodeId}/${id}`;
const getConfigPath = plug => `${getPath(plug)}/config`; const getConfigPath = plug => `${getPath(plug)}/config`;
const getAvailabilityTopic = plug => `${getPath(plug)}/availability`;
const getStateTopic = plug => `${getPath(plug)}/state`; 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 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 => ({ const getDiscoveryPayload = device => ({
schema: 'json',
name: device.name, name: device.name,
unique_id: `light.plejd.${device.name.toLowerCase().replace(/ /g, '')}`, unique_id: `light.plejd.${device.name.toLowerCase().replace(/ /g, '')}`,
state_topic: getStateTopic(device), state_topic: getStateTopic(device),
command_topic: getCommandTopic(device), command_topic: getCommandTopic(device),
payload_on: 1, optimistic: false,
payload_off: 0, brightness: `${device.dimmable}`
optimistic: false
}); });
// #endregion // #endregion
@ -99,24 +84,17 @@ 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());
if (topic === startTopic) { if (topic === startTopic) {
logger('home assistant has started. lets do discovery.'); logger('home assistant has started. lets do discovery.');
self.emit('connected'); self.emit('connected');
} }
if (_.includes(topic, 'setBrightness')) { if (_.includes(topic, 'set')) {
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)) {
const device = self.devices.find(x => getCommandTopic(x) === topic); const device = self.devices.find(x => getCommandTopic(x) === topic);
logger('got state update for ' + device.name + ' with state: ' + command); self.emit('stateChanged', device.id, command);
self.emit('stateChanged', device.id, parseInt(command));
} }
}); });
} }
@ -134,16 +112,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 = null; let payload = getDiscoveryPayload(device);
console.log(`discovered ${device.type}: ${device.name} with Plejd ID ${device.id}.`);
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}.`);
self.deviceMap[device.id] = payload.unique_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); const device = this.devices.find(x => x.id === deviceId);
if (!device) { if (!device) {
@ -162,27 +132,26 @@ class MqttClient extends EventEmitter {
return; 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( this.client.publish(
getStateTopic(device), getStateTopic(device),
state.toString() JSON.stringify(payload)
);
}
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()
); );
} }
} }