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.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;
}

View file

@ -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;
module.exports = PlejdService;

View file

@ -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/",

View file

@ -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);
}
});

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
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
);
}

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;