implemented support for Plejd scenes, wph-01 and write queues
This commit is contained in:
parent
cafea72d63
commit
d2beca6fb2
6 changed files with 183 additions and 27 deletions
23
plejd/api.js
23
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);
|
||||
|
|
@ -219,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;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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/",
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -114,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.');
|
||||
|
|
@ -126,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);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
|
@ -154,7 +156,7 @@ class MqttClient extends EventEmitter {
|
|||
logger(`sending discovery for ${device.name}`);
|
||||
|
||||
let payload = device.type === 'switch' ? getSwitchPayload(device) : getDiscoveryPayload(device);
|
||||
console.log(`plejd-mqtt: discovered ${device.type} named ${device.name} with PID ${device.id}.`);
|
||||
console.log(`plejd-mqtt: discovered ${device.type} (${device.typeName}) named ${device.name} with PID ${device.id}.`);
|
||||
|
||||
self.deviceMap[device.id] = payload.unique_id;
|
||||
|
||||
|
|
@ -176,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
72
plejd/scene.manager.js
Normal 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;
|
||||
Loading…
Add table
Add a link
Reference in a new issue