Merge pull request #23 from icanos/dev

merge to 0.2.0
This commit is contained in:
Marcus Westin 2019-12-31 13:52:07 +01:00 committed by GitHub
commit 3fa3c52913
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
5 changed files with 202 additions and 86 deletions

View file

@ -8,7 +8,7 @@ API_LOGIN_URL = 'login';
API_SITES_URL = 'functions/getSites'; API_SITES_URL = 'functions/getSites';
// #region logging // #region logging
const debug = ''; let debug = '';
const getLogger = () => { const getLogger = () => {
const consoleLogger = msg => console.log('plejd-api', msg); const consoleLogger = msg => console.log('plejd-api', msg);
@ -34,6 +34,15 @@ class PlejdApi extends EventEmitter {
this.site = null; this.site = null;
} }
updateSettings(settings) {
if (settings.debug) {
debug = 'console';
}
else {
debug = '';
}
}
login() { login() {
logger('login()'); logger('login()');
const self = this; const self = this;
@ -55,7 +64,7 @@ class PlejdApi extends EventEmitter {
'password': this.password 'password': this.password
}) })
.then((response) => { .then((response) => {
logger('got session token response'); console.log('plejd-api: got session token response');
self.sessionToken = response.data.sessionToken; self.sessionToken = response.data.sessionToken;
self.emit('loggedIn'); self.emit('loggedIn');
}) })
@ -86,14 +95,14 @@ class PlejdApi extends EventEmitter {
instance.post(API_SITES_URL) instance.post(API_SITES_URL)
.then((response) => { .then((response) => {
logger('got sites response'); console.log('plejd-api: got sites response');
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); callback(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 + ' (code: ' + error.response.status + ')');
return Promise.reject('unable to retrieve the crypto key. error: ' + error); return Promise.reject('unable to retrieve the crypto key. error: ' + error);
}); });
} }

View file

@ -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,53 @@ class PlejdService extends EventEmitter {
this.wireEvents(); this.wireEvents();
} }
turnOn(id, brightness) { updateSettings(settings) {
logger('turning on ' + id + ' at brightness ' + brightness); if (settings.debug) {
debug = 'console';
}
else {
debug = '';
}
}
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(() => {
let currentBrightness = parseInt((brightnessStep * i) + 1);
if (currentBrightness > 254) {
currentBrightness = 254;
}
this._turnOn(id, currentBrightness);
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 +125,39 @@ 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] ? this.plejdDevices[id].dim : 250;
const steps = command.transition * 2;
const brightnessStep = initialBrightness / steps;
let currentBrightness = initialBrightness;
let i = 0;
const transitionRef = setInterval(() => {
currentBrightness = parseInt(initialBrightness - (brightnessStep * i));
if (currentBrightness <= 0 || i >= steps) {
clearInterval(transitionRef);
// finally, we turn it off
this._turnOff(id);
return;
}
this._turnOn(id, currentBrightness);
i++;
}, 500);
}
else {
this._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.write(payload);
} }
@ -143,11 +220,15 @@ 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) {
logger('connection timed out after 10 s. trying next.'); logger('connection timed out after 10 s. cleaning up and trying next.');
self.device.removeAllListeners('servicesDiscover');
self.device.removeAllListeners('connect');
self.device.removeAllListeners('disconnect');
self.deviceIdx++; self.deviceIdx++;
self.connect(); self.connect();
@ -168,12 +249,16 @@ class PlejdService extends EventEmitter {
disconnect() { disconnect() {
logger('disconnect()'); logger('disconnect()');
if (this.state !== STATE_CONNECTED) { if (this.state !== STATE_CONNECTED && this.state !== STATE_AUTHENTICATED) {
return; return;
} }
clearInterval(this.pingRef); clearInterval(this.pingRef);
this.device.removeAllListeners('servicesDiscover');
this.device.removeAllListeners('connect');
this.device.removeAllListeners('disconnect');
this.unsubscribeCharacteristics(); this.unsubscribeCharacteristics();
this.device.disconnect(); this.device.disconnect();
@ -435,19 +520,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,6 +1,6 @@
{ {
"name": "Plejd", "name": "Plejd",
"version": "0.1.1", "version": "0.2.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

@ -3,7 +3,11 @@ const mqtt = require('./mqtt');
const fs = require('fs'); const fs = require('fs');
const PlejdService = require('./ble'); const PlejdService = require('./ble');
const version = "0.2.0";
async function main() { async function main() {
console.log('starting Plejd add-on v. ' + version);
const rawData = fs.readFileSync('/data/plejd.json'); const rawData = fs.readFileSync('/data/plejd.json');
const config = JSON.parse(rawData); const config = JSON.parse(rawData);
@ -28,31 +32,35 @@ 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); plejd.on('sceneTriggered', (scene) => {
client.updateBrightness(deviceId, dim); client.sceneTriggered(scene);
}); });
// 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) { client.on('settingsChanged', (settings) => {
plejd.turnOn(deviceId, brightness); if (settings.module === 'mqtt') {
client.updateSettings(settings);
} }
else { else if (settings.module === 'ble') {
plejd.turnOff(deviceId); plejd.updateSettings(settings);
} }
}); else if (settings.module === 'api') {
plejdApi.updateSettings(settings);
}
});
}); });
}); });

View file

@ -5,7 +5,7 @@ const _ = require('lodash');
const startTopic = 'hass/status'; const startTopic = 'hass/status';
// #region logging // #region logging
const debug = ''; let debug = '';
const getLogger = () => { const getLogger = () => {
const consoleLogger = msg => console.log('plejd-mqtt', msg); const consoleLogger = msg => console.log('plejd-mqtt', msg);
@ -27,32 +27,19 @@ 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 getSceneEventTopic = () => `plejd/event/scene`;
const getDiscoveryDimmablePayload = device => ({ const getSettingsTopic = () => `plejd/settings`;
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
@ -92,6 +79,12 @@ class MqttClient extends EventEmitter {
logger('error: unable to subscribe to control topics'); logger('error: unable to subscribe to control topics');
} }
}); });
this.client.subscribe(getSettingsTopic(), (err) => {
if (err) {
console.log('error: could not subscribe to settings topic');
}
});
}); });
this.client.on('close', () => { this.client.on('close', () => {
@ -99,28 +92,33 @@ 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');
} }
else if (topic === getSettingsTopic()) {
if (_.includes(topic, 'setBrightness')) { self.emit('settingsChanged', command);
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);
logger('got state update for ' + device.name + ' with state: ' + command);
self.emit('stateChanged', device.id, parseInt(command)); if (_.includes(topic, 'set')) {
const device = self.devices.find(x => getCommandTopic(x) === topic);
self.emit('stateChanged', device.id, command);
} }
}); });
} }
updateSettings(settings) {
if (settings.debug) {
debug = 'console';
}
else {
debug = '';
}
}
reconnect() { reconnect() {
this.client.reconnect(); this.client.reconnect();
} }
@ -134,16 +132,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 +144,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 +152,31 @@ 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'
}
}
this.client.publish( this.client.publish(
getStateTopic(device), getStateTopic(device),
state.toString() JSON.stringify(payload)
); );
} }
updateBrightness(deviceId, brightness) { sceneTriggered(scene) {
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( this.client.publish(
getBrightnessTopic(device), getSceneEventTopic(),
brightness.toString() JSON.stringify({ scene: scene })
); );
} }
} }