From 5b3efb4b79126d1a2fa71c0f040bc5dcea4c3475 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Victor=20Hagelb=C3=A4ck?= Date: Tue, 2 Feb 2021 19:23:19 +0100 Subject: [PATCH 01/33] Fix BLE data parsing and move from toString-based Buffer parsing to int-based - parsing only 1 byte for command (hex "98" vs earlier "0098") --- plejd/Logger.js | 23 ++++++++++++++++---- plejd/PlejdService.js | 49 ++++++++++++++++++++++++------------------- 2 files changed, 47 insertions(+), 25 deletions(-) diff --git a/plejd/Logger.js b/plejd/Logger.js index bf908b5..430d27b 100644 --- a/plejd/Logger.js +++ b/plejd/Logger.js @@ -25,20 +25,35 @@ const logFormat = printf((info) => { /** Winston-based logger */ class Logger { + static shouldLogLookup = {}; + constructor() { throw new Error('Please call createLogger instead'); } + static getLogLevel() { + const config = Configuration.getConfiguration(); + // eslint-disable-next-line max-len + const level = (config.logLevel && LEVELS.find((l) => l.startsWith(config.logLevel[0].toLowerCase()))) + || 'info'; + return level; + } + + static shouldLog(logLevel) { + if (!Logger.shouldLogLookup[logLevel]) { + // eslint-disable-next-line max-len + Logger.shouldLogLookup[logLevel] = Logger.logLevels().levels[logLevel] <= Logger.logLevels().levels[Logger.getLogLevel()]; + } + return Logger.shouldLogLookup[logLevel]; + } + /** Created logger will follow Winston createLogger, but * - add module name to logger * - swap debug/verbose levels and omit http to mimic HA standard * Levels (in order): error, warn, info, debug, verbose, silly * */ static getLogger(moduleName) { - const config = Configuration.getConfiguration(); - // eslint-disable-next-line max-len - const level = (config.logLevel && LEVELS.find((l) => l.startsWith(config.logLevel[0].toLowerCase()))) - || 'info'; + const level = Logger.getLogLevel(); const logger = winston.createLogger({ format: combine( diff --git a/plejd/PlejdService.js b/plejd/PlejdService.js index c941a1b..b6f9c8c 100644 --- a/plejd/PlejdService.js +++ b/plejd/PlejdService.js @@ -13,10 +13,10 @@ const LAST_DATA_UUID = '31ba0005-6085-4726-be45-040c957391b5'; const AUTH_UUID = '31ba0009-6085-4726-be45-040c957391b5'; const PING_UUID = '31ba000a-6085-4726-be45-040c957391b5'; -const BLE_CMD_DIM_CHANGE = '00c8'; -const BLE_CMD_DIM2_CHANGE = '0098'; -const BLE_CMD_STATE_CHANGE = '0097'; -const BLE_CMD_SCENE_TRIG = '0021'; +const BLE_CMD_DIM_CHANGE = 0xc8; +const BLE_CMD_DIM2_CHANGE = 0x98; +const BLE_CMD_STATE_CHANGE = 0x97; +const BLE_CMD_SCENE_TRIG = 0x21; const BLUEZ_SERVICE_NAME = 'org.bluez'; const DBUS_OM_INTERFACE = 'org.freedesktop.DBus.ObjectManager'; @@ -688,28 +688,31 @@ class PlejdService extends EventEmitter { const data = value.value; const decoded = this._encryptDecrypt(this.cryptoKey, this.plejdService.addr, data); - const deviceId = parseInt(decoded[0], 10); - // What is bytes 2-3? - const cmd = decoded.toString('hex', 3, 5); - const state = parseInt(decoded.toString('hex', 5, 6), 10); // Overflows for command 0x001b, scene command - // eslint-disable-next-line no-bitwise - const data2 = parseInt(decoded.toString('hex', 6, 8), 16) >> 8; - if (decoded.length < 5) { - logger.debug(`Too short raw event ignored: ${decoded.toString('hex')}`); + if (Logger.shouldLog('debug')) { + // decoded.toString() could potentially be expensive + logger.debug(`Too short raw event ignored: ${decoded.toString('hex')}`); + } // ignore the notification since too small return; } + const deviceId = decoded.readUInt8(0); + // What is bytes 2-3? + const cmd = decoded.readUInt8(4); + const state = decoded.length > 5 ? decoded.readUInt8(5) : 0; + // What is byte 6? + const dim = decoded.length > 7 ? decoded.readUInt8(7) : 0; + // Bytes 8-9 are sometimes present, what are they? + const deviceName = this._getDeviceName(deviceId); - logger.verbose(`Raw event received: ${decoded.toString('hex')}`); - logger.verbose( - `Device ${deviceId}, cmd ${cmd.toString('hex')}, state ${state}, dim/data2 ${data2}`, - ); + if (Logger.shouldLog('debug')) { + // decoded.toString() could potentially be expensive + logger.debug(`Raw event received: ${decoded.toString('hex')}`); + logger.verbose(`Device ${deviceId}, cmd ${cmd.toString(16)}, state ${state}, dim ${dim}`); + } if (cmd === BLE_CMD_DIM_CHANGE || cmd === BLE_CMD_DIM2_CHANGE) { - const dim = data2; - logger.debug(`${deviceName} (${deviceId}) got state+dim update. S: ${state}, D: ${dim}`); this.emit('stateChanged', deviceId, { @@ -733,7 +736,7 @@ class PlejdService extends EventEmitter { }; logger.verbose(`All states: ${this.plejdDevices}`); } else if (cmd === BLE_CMD_SCENE_TRIG) { - const sceneId = parseInt(decoded.toString('hex', 5, 6), 16); + const sceneId = state; const sceneName = this._getDeviceName(sceneId); logger.debug( @@ -741,10 +744,14 @@ class PlejdService extends EventEmitter { ); this.emit('sceneTriggered', deviceId, sceneId); - } else if (cmd === '001b') { + } else if (cmd === 0x1b) { logger.silly('Command 001b seems to be some kind of often repeating ping/mesh data'); } else { - logger.verbose(`Command ${cmd.toString('hex')} unknown. Device ${deviceName} (${deviceId})`); + logger.verbose( + `Command ${cmd.toString(16)} unknown. ${decoded.toString( + 'hex', + )}. Device ${deviceName} (${deviceId})`, + ); } } From bf93ec9545d6fa04cc4b5d506a1c8ffb9ca35b4b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Victor=20Hagelb=C3=A4ck?= Date: Sun, 31 Jan 2021 09:40:35 +0100 Subject: [PATCH 02/33] Fix typo in readme --- plejd/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plejd/README.md b/plejd/README.md index ef101f5..3791b1f 100644 --- a/plejd/README.md +++ b/plejd/README.md @@ -67,7 +67,7 @@ Browse your Hass.io installation using a tool that allows you to manage files, f ### Install older versions or developemnt version -To install older versions, follow the "Manual Installation" instructions above, but copy the code from [one of the releases](https://github.com/icanos/hassio-plejd/releases). To test new functionality you can download the development version, available in the [develop branch](https://github.com/icanos/hassio-plejd/tree/feature/develop). +To install older versions, follow the "Manual Installation" instructions above, but copy the code from [one of the releases](https://github.com/icanos/hassio-plejd/releases). To test new functionality you can download the development version, available in the [develop branch](https://github.com/icanos/hassio-plejd/tree/develop). ### IMPORTANT INFORMATION From 73e04b71dd10b3907147fc09b54a8a71cac911eb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Victor=20Hagelb=C3=A4ck?= Date: Sun, 31 Jan 2021 10:13:33 +0100 Subject: [PATCH 03/33] Rename PlejdService to PlejdBLE --- plejd/{PlejdService.js => PlejdBLE.js} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename plejd/{PlejdService.js => PlejdBLE.js} (100%) diff --git a/plejd/PlejdService.js b/plejd/PlejdBLE.js similarity index 100% rename from plejd/PlejdService.js rename to plejd/PlejdBLE.js From a406a31da46cc1ba311ed4abd971ee94f0f538dd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Victor=20Hagelb=C3=A4ck?= Date: Mon, 1 Feb 2021 21:12:05 +0100 Subject: [PATCH 04/33] Make config extend default config and log merged params verbose at startup --- plejd/Configuration.js | 22 +++++++++++++++++----- 1 file changed, 17 insertions(+), 5 deletions(-) diff --git a/plejd/Configuration.js b/plejd/Configuration.js index 6376bb0..7b3a1d2 100644 --- a/plejd/Configuration.js +++ b/plejd/Configuration.js @@ -1,14 +1,26 @@ const fs = require('fs'); class Configuration { - static _config = null; + static _options = null; - static getConfiguration() { - if (!Configuration._config) { + static getOptions() { + if (!Configuration._options) { const rawData = fs.readFileSync('/data/options.json'); - Configuration._config = JSON.parse(rawData); + const config = JSON.parse(rawData); + + const defaultRawData = fs.readFileSync('/plejd/config.json'); + const defaultConfig = JSON.parse(defaultRawData).options; + + Configuration._options = { ...defaultConfig, ...config }; + + console.log('Config:', { + ...Configuration._options, + username: '---scrubbed---', + password: '---scrubbed---', + mqttPassword: '---scrubbed---', + }); } - return Configuration._config; + return Configuration._options; } } From f2aa7b84c085d46c3d0ebd13c29caa2f0aba2c12 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Victor=20Hagelb=C3=A4ck?= Date: Mon, 1 Feb 2021 21:13:25 +0100 Subject: [PATCH 05/33] Minor refactors --- plejd/test/test.ble.bluez.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/plejd/test/test.ble.bluez.js b/plejd/test/test.ble.bluez.js index d6fa398..6efed02 100644 --- a/plejd/test/test.ble.bluez.js +++ b/plejd/test/test.ble.bluez.js @@ -1,6 +1,6 @@ -const PlejdService = require('../PlejdService'); +const PlejdBLE = require('../PlejdBLE'); const cryptoKey = ''; -const plejd = new PlejdService(cryptoKey, true); +const plejd = new PlejdBLE(cryptoKey, true); plejd.init(); From 4f51063c418920a4563b98331994d6a87d3743da Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Victor=20Hagelb=C3=A4ck?= Date: Mon, 1 Feb 2021 21:18:42 +0100 Subject: [PATCH 06/33] Break out central DeviceRegistry for all Plejd data --- plejd/DeviceRegistry.js | 73 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 73 insertions(+) create mode 100644 plejd/DeviceRegistry.js diff --git a/plejd/DeviceRegistry.js b/plejd/DeviceRegistry.js new file mode 100644 index 0000000..1a26df4 --- /dev/null +++ b/plejd/DeviceRegistry.js @@ -0,0 +1,73 @@ +class DeviceRegistry { + apiSite; + cryptoKey = null; + + deviceIdsByRoom = {}; + deviceIdsBySerial = {}; + + // Dictionaries of [id]: device per type + plejdDevices = {}; + roomDevices = {}; + sceneDevices = {}; + + get allDevices() { + return [ + ...Object.values(this.plejdDevices), + ...Object.values(this.roomDevices), + ...Object.values(this.sceneDevices), + ]; + } + + addPlejdDevice(device) { + this.plejdDevices[device.id] = device; + this.deviceIdsBySerial[device.serialNumber] = device.id; + if (!this.deviceIdsByRoom[device.roomId]) { + this.deviceIdsByRoom[device.roomId] = []; + } + this.deviceIdsByRoom[device.roomId].push(device.id); + } + + addScene(scene) { + this.plejdDevices[scene.id] = scene; + } + + setApiSite(siteDetails) { + this.apiSite = siteDetails; + } + + clearPlejdDevices() { + this.plejdDevices = {}; + this.deviceIdsByRoom = {}; + this.deviceIdsBySerial = {}; + } + + addRoomDevice(device) { + this.roomDevices[device.id] = device; + } + + clearRoomDevices() { + this.roomDevices = {}; + } + + clearSceneDevices() { + this.sceneDevices = {}; + } + + getDevice(deviceId) { + return this.plejdDevices[deviceId]; + } + + getDeviceBySerialNumber(serialNumber) { + return this.plejdDevices[this.deviceIdsBySerial[serialNumber]]; + } + + getDeviceName(deviceId) { + return (this.plejdDevices[deviceId] || {}).name; + } + + getSceneName(sceneId) { + return (this.sceneDevices[sceneId] || {}).name; + } +} + +module.exports = DeviceRegistry; From 75b9a1a8d72d3778d6fa026e9be003e0b786bead Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Victor=20Hagelb=C3=A4ck?= Date: Mon, 1 Feb 2021 21:19:22 +0100 Subject: [PATCH 07/33] Clarify MqttClient structure, logging, and start using DeviceRegistry --- plejd/MqttClient.js | 118 +++++++++++++++++++++++++++----------------- 1 file changed, 73 insertions(+), 45 deletions(-) diff --git a/plejd/MqttClient.js b/plejd/MqttClient.js index f0ceca5..88b877d 100644 --- a/plejd/MqttClient.js +++ b/plejd/MqttClient.js @@ -1,5 +1,7 @@ const EventEmitter = require('events'); const mqtt = require('mqtt'); + +const Configuration = require('./Configuration'); const Logger = require('./Logger'); const startTopics = ['hass/status', 'homeassistant/status']; @@ -19,6 +21,18 @@ const getAvailabilityTopic = (plug) => `${getPath(plug)}/availability`; const getCommandTopic = (plug) => `${getPath(plug)}/set`; const getSceneEventTopic = () => 'plejd/event/scene'; +const decodeTopicRegexp = new RegExp( + /(?[^[]+)\/(?.+)\/plejd\/(?.+)\/(?config|state|availability|set|scene)/, +); + +const decodeTopic = (topic) => { + const matches = decodeTopicRegexp.exec(topic); + if (!matches) { + return null; + } + return matches.groups; +}; + const getDiscoveryPayload = (device) => ({ schema: 'json', name: device.name, @@ -54,23 +68,21 @@ const getSwitchPayload = (device) => ({ // #endregion class MqttClient extends EventEmitter { - constructor(mqttBroker, username, password) { + deviceRegistry; + + constructor(deviceRegistry) { super(); - this.mqttBroker = mqttBroker; - this.username = username; - this.password = password; - this.deviceMap = {}; - this.devices = []; + this.config = Configuration.getOptions(); + this.deviceRegistry = deviceRegistry; } init() { logger.info('Initializing MQTT connection for Plejd addon'); - const self = this; - this.client = mqtt.connect(this.mqttBroker, { - username: this.username, - password: this.password, + this.client = mqtt.connect(this.config.mqttBroker, { + username: this.config.mqttUsername, + password: this.config.mqttPassword, }); this.client.on('connect', () => { @@ -81,7 +93,7 @@ class MqttClient extends EventEmitter { logger.error('Unable to subscribe to status topics'); } - self.emit('connected'); + this.emit('connected'); }); this.client.subscribe(getSubscribePath(), (err) => { @@ -93,32 +105,52 @@ class MqttClient extends EventEmitter { this.client.on('close', () => { logger.verbose('Warning: mqtt channel closed event, reconnecting...'); - self.reconnect(); + this.reconnect(); }); this.client.on('message', (topic, message) => { // const command = message.toString(); - const command = message.toString().substring(0, 1) === '{' - ? JSON.parse(message.toString()) - : message.toString(); + const command = + message.toString().substring(0, 1) === '{' + ? JSON.parse(message.toString()) + : message.toString(); if (startTopics.includes(topic)) { logger.info('Home Assistant has started. lets do discovery.'); - self.emit('connected'); - } else if (topic.includes('set')) { - logger.verbose(`Got mqtt command on ${topic} - ${message}`); - const device = self.devices.find((x) => getCommandTopic(x) === topic); - if (device) { - self.emit('stateChanged', device, command); - } else { - logger.warn( - `Device for topic ${topic} not found! Can happen if HA calls previously existing devices.`, - ); - } - } else if (topic.includes('state')) { - logger.verbose(`State update sent over mqtt to HA ${topic} - ${message}`); + this.emit('connected'); } else { - logger.verbose(`Warning: Got unrecognized mqtt command on ${topic} - ${message}`); + const decodedTopic = decodeTopic(topic); + if (decodedTopic) { + const device = this.deviceRegistry.getDevice(decodedTopic.id); + const deviceName = device ? device.name : ''; + + switch (decodedTopic.command) { + case 'set': + logger.verbose( + `Got mqtt SET command for ${decodedTopic.type}, ${deviceName} (${decodedTopic.id}): ${message}`, + ); + + if (device) { + this.emit('stateChanged', device, command); + } else { + logger.warn( + `Device for topic ${topic} not found! Can happen if HA calls previously existing devices.`, + ); + } + break; + case 'state': + case 'config': + case 'availability': + logger.verbose( + `Sent mqtt ${decodedTopic.command} command for ${decodedTopic.type}, ${deviceName} (${decodedTopic.id}). ${decodedTopic.command === 'availability' ? message : ''}`, + ); + break; + default: + logger.verbose(`Warning: Unknown command ${decodedTopic.command} in decoded topic`); + } + } else { + logger.verbose(`Warning: Got unrecognized mqtt command on '${topic}': ${message}`); + } } }); } @@ -128,37 +160,33 @@ class MqttClient extends EventEmitter { } disconnect(callback) { - this.devices.forEach((device) => { + this.deviceRegistry.allDevices.forEach((device) => { this.client.publish(getAvailabilityTopic(device), 'offline'); }); this.client.end(callback); } - discover(devices) { - this.devices = devices; + sendDiscoveryToHomeAssistant() { + logger.debug(`Sending discovery of ${this.deviceRegistry.allDevices.length} device(s).`); - const self = this; - logger.debug(`Sending discovery of ${devices.length} device(s).`); - - devices.forEach((device) => { + this.deviceRegistry.allDevices.forEach((device) => { logger.debug(`Sending discovery for ${device.name}`); - const payload = device.type === 'switch' ? getSwitchPayload(device) : getDiscoveryPayload(device); + const payload = + device.type === 'switch' ? getSwitchPayload(device) : getDiscoveryPayload(device); logger.info( `Discovered ${device.type} (${device.typeName}) named ${device.name} with PID ${device.id}.`, ); - self.deviceMap[device.id] = payload.unique_id; - - self.client.publish(getConfigPath(device), JSON.stringify(payload)); + this.client.publish(getConfigPath(device), JSON.stringify(payload)); setTimeout(() => { - self.client.publish(getAvailabilityTopic(device), 'online'); + this.client.publish(getAvailabilityTopic(device), 'online'); }, 2000); }); } updateState(deviceId, data) { - const device = this.devices.find((x) => x.id === deviceId); + const device = this.deviceRegistry.getDevice(deviceId); if (!device) { logger.warn(`Unknown device id ${deviceId} - not handled by us.`); @@ -193,9 +221,9 @@ class MqttClient extends EventEmitter { this.client.publish(getAvailabilityTopic(device), 'online'); } - sceneTriggered(scene) { - logger.verbose(`Scene triggered: ${scene}`); - this.client.publish(getSceneEventTopic(), JSON.stringify({ scene })); + sceneTriggered(sceneId) { + logger.verbose(`Scene triggered: ${sceneId}`); + this.client.publish(getSceneEventTopic(), JSON.stringify({ scene: sceneId })); } } From 377c6a75db6914df73ce0ef2bddc256d53458f22 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Victor=20Hagelb=C3=A4ck?= Date: Mon, 1 Feb 2021 21:22:53 +0100 Subject: [PATCH 08/33] Rewrite PlejdAPI to async/await, set data in central DeviceRegistry --- plejd/PlejdApi.js | 452 ++++++++++++++++++++++------------------------ 1 file changed, 217 insertions(+), 235 deletions(-) diff --git a/plejd/PlejdApi.js b/plejd/PlejdApi.js index 39bc44d..5e2fb6a 100644 --- a/plejd/PlejdApi.js +++ b/plejd/PlejdApi.js @@ -1,5 +1,6 @@ -const axios = require('axios'); -const EventEmitter = require('events'); +const axios = require('axios').default; + +const Configuration = require('./Configuration'); const Logger = require('./Logger'); const API_APP_ID = 'zHtVqXt8k4yFyk2QGmgp48D9xZr2G94xWYnF4dak'; @@ -10,271 +11,147 @@ const API_SITE_DETAILS_URL = 'functions/getSiteById'; const logger = Logger.getLogger('plejd-api'); -class PlejdApi extends EventEmitter { - constructor(siteName, username, password, includeRoomsAsLights) { - super(); +class PlejdApi { + config; + deviceRegistry; + sessionToken; + siteId; + siteDetails; - this.includeRoomsAsLights = includeRoomsAsLights; - this.siteName = siteName; - this.username = username; - this.password = password; - - this.sessionToken = ''; - this.site = null; + constructor(deviceRegistry) { + this.config = Configuration.getOptions(); + this.deviceRegistry = deviceRegistry; } - login() { + async init() { + logger.info('init()'); + await this.login(); + await this.getSites(); + await this.getSiteDetails(); + this.getDevices(); + } + + async login() { logger.info('login()'); - logger.info(`logging into ${this.siteName}`); - const self = this; + logger.info(`logging into ${this.config.site}`); - const instance = axios.create({ - baseURL: API_BASE_URL, - headers: { - 'X-Parse-Application-Id': API_APP_ID, - 'Content-Type': 'application/json', - }, - }); + logger.debug(`sending POST to ${API_BASE_URL}${API_LOGIN_URL}`); - return new Promise((resolve, reject) => { - logger.debug(`sending POST to ${API_BASE_URL}${API_LOGIN_URL}`); + try { + const response = await this._getAxiosInstance().post(API_LOGIN_URL, { + username: this.config.username, + password: this.config.password, + }); - instance - .post(API_LOGIN_URL, { - username: this.username, - password: this.password, - }) - .then((response) => { - logger.info('got session token response'); - self.sessionToken = response.data.sessionToken; + logger.info('got session token response'); + this.sessionToken = response.data.sessionToken; - if (!self.sessionToken) { - logger.error('No session token received'); - reject(new Error('no session token received.')); - } + if (!this.sessionToken) { + logger.error('No session token received'); + throw new Error('API: No session token received.'); + } + } catch (error) { + if (error.response.status === 400) { + logger.error('Server returned status 400. probably invalid credentials, please verify.'); + } else if (error.response.status === 403) { + logger.error( + 'Server returned status 403, forbidden. Plejd seems to do this sometimes, despite correct credentials. Possibly waiting a long time will fix this.', + ); + } else { + logger.error('Unable to retrieve session token response: ', error); + } + logger.verbose(`Error details: ${JSON.stringify(error.response, null, 2)}`); - resolve(); - }) - .catch((error) => { - if (error.response.status === 400) { - logger.error( - 'Server returned status 400. probably invalid credentials, please verify.', - ); - } else { - logger.error('Unable to retrieve session token response: ', error); - } - - reject(new Error(`unable to retrieve session token response: ${error}`)); - }); - }); + throw new Error(`API: Unable to retrieve session token response: ${error}`); + } } - getSites() { + async getSites() { logger.info('Get all Plejd sites for account...'); - const self = this; - const instance = axios.create({ - baseURL: API_BASE_URL, - headers: { - 'X-Parse-Application-Id': API_APP_ID, - 'X-Parse-Session-Token': this.sessionToken, - 'Content-Type': 'application/json', - }, - }); + logger.debug(`sending POST to ${API_BASE_URL}${API_SITE_LIST_URL}`); - return new Promise((resolve, reject) => { - logger.debug(`sending POST to ${API_BASE_URL}${API_SITE_LIST_URL}`); + try { + const response = await this._getAxiosInstance().post(API_SITE_LIST_URL); - instance - .post(API_SITE_LIST_URL) - .then((response) => { - logger.info('got site list response'); - const site = response.data.result.find((x) => x.site.title === self.siteName); + const sites = response.data.result; + logger.info( + `Got site list response with ${sites.length}: ${sites.map((s) => s.site.title).join(', ')}`, + ); + logger.silly('All sites found:'); + logger.silly(JSON.stringify(sites, null, 2)); - if (!site) { - logger.error(`error: failed to find a site named ${self.siteName}`); - reject(new Error(`failed to find a site named ${self.siteName}`)); - return; - } + const site = sites.find((x) => x.site.title === this.config.site); - resolve(site); - }) - .catch((error) => { - logger.error('error: unable to retrieve list of sites. error: ', error); - return reject(new Error(`plejd-api: unable to retrieve list of sites. error: ${error}`)); - }); - }); + if (!site) { + logger.error(`Failed to find a site named ${this.config.site}`); + throw new Error(`API: Failed to find a site named ${this.config.site}`); + } + + logger.info(`Site found matching configuration name ${this.config.site}`); + logger.silly(JSON.stringify(site, null, 2)); + this.siteId = site.site.siteId; + } catch (error) { + logger.error('error: unable to retrieve list of sites. error: ', error); + throw new Error(`API: unable to retrieve list of sites. error: ${error}`); + } } - getSite(siteId) { - logger.info('Get site details...'); - const self = this; + async getSiteDetails() { + logger.info(`Get site details for ${this.siteId}...`); - const instance = axios.create({ - baseURL: API_BASE_URL, - headers: { - 'X-Parse-Application-Id': API_APP_ID, - 'X-Parse-Session-Token': this.sessionToken, - 'Content-Type': 'application/json', - }, - }); + logger.debug(`sending POST to ${API_BASE_URL}${API_SITE_DETAILS_URL}`); - return new Promise((resolve, reject) => { - logger.debug(`sending POST to ${API_BASE_URL}${API_SITE_DETAILS_URL}`); + try { + const response = await this._getAxiosInstance().post(API_SITE_DETAILS_URL, { + siteId: this.siteId, + }); - instance - .post(API_SITE_DETAILS_URL, { siteId }) - .then((response) => { - logger.info('got site details response'); - if (response.data.result.length === 0) { - const msg = `no site with ID ${siteId} was found.`; - logger.error(`error: ${msg}`); - reject(msg); - return; - } + logger.info('got site details response'); - self.site = response.data.result[0]; - self.cryptoKey = self.site.plejdMesh.cryptoKey; + if (response.data.result.length === 0) { + logger.error(`No site with ID ${this.siteId} was found.`); + throw new Error(`API: No site with ID ${this.siteId} was found.`); + } - resolve(self.cryptoKey); - }) - .catch((error) => { - logger.error('error: unable to retrieve the crypto key. error: ', error); - return reject(new Error(`plejd-api: unable to retrieve the crypto key. error: ${error}`)); - }); - }); + this.siteDetails = response.data.result[0]; + this.deviceRegistry.setApiSite(this.siteDetails); + + logger.info(`Site details for site id ${this.siteId} found`); + logger.silly(JSON.stringify(this.siteDetails, null, 2)); + + this.deviceRegistry.cryptoKey = this.siteDetails.plejdMesh.cryptoKey; + if (!this.deviceRegistry.cryptoKey) { + throw new Error('API: No crypto key set for site'); + } + } catch (error) { + logger.error(`Unable to retrieve site details for ${this.siteId}. error: `, error); + throw new Error(`API: Unable to retrieve site details. error: ${error}`); + } } getDevices() { - const devices = []; + logger.info('Getting devices from site details response...'); - logger.verbose(JSON.stringify(this.site)); + this._getPlejdDevices(); + this._getRoomDevices(); + this._getSceneDevices(); + } - const roomDevices = {}; + _getAxiosInstance() { + const headers = { + 'X-Parse-Application-Id': API_APP_ID, + 'Content-Type': 'application/json', + }; - for (let i = 0; i < this.site.devices.length; i++) { - const device = this.site.devices[i]; - const { deviceId } = device; - - const settings = this.site.outputSettings.find((x) => x.deviceParseId === device.objectId); - let deviceNum = this.site.deviceAddress[deviceId]; - - if (settings) { - const outputs = this.site.outputAddress[deviceId]; - deviceNum = outputs[settings.output]; - } - - // check if device is dimmable - const plejdDevice = this.site.plejdDevices.find((x) => x.deviceId === deviceId); - const deviceType = this._getDeviceType(plejdDevice.hardwareId); - const { name, type } = deviceType; - let { dimmable } = deviceType; - - if (settings) { - dimmable = settings.dimCurve !== 'NonDimmable'; - } - - const newDevice = { - id: deviceNum, - name: device.title, - type, - typeName: name, - dimmable, - version: plejdDevice.firmware.version, - serialNumber: plejdDevice.deviceId, - }; - - 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]; - - let switchDevice = { - id: first, - name: `${device.title} knapp vä`, - type, - typeName: name, - 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, - typeName: name, - 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 { - if (roomDevices[device.roomId]) { - roomDevices[device.roomId].push(newDevice); - } else { - roomDevices[device.roomId] = [newDevice]; - } - - devices.push(newDevice); - } + if (this.sessionToken) { + headers['X-Parse-Session-Token'] = this.sessionToken; } - if (this.includeRoomsAsLights) { - logger.debug('includeRoomsAsLights is set to true, adding rooms too.'); - for (let i = 0; i < this.site.rooms.length; i++) { - const room = this.site.rooms[i]; - const { roomId } = room; - const roomAddress = this.site.roomAddress[roomId]; - - const newDevice = { - id: roomAddress, - name: room.title, - type: 'light', - typeName: 'Room', - dimmable: roomDevices[roomId].filter((x) => x.dimmable).length > 0, - }; - - devices.push(newDevice); - } - logger.debug('includeRoomsAsLights done.'); - } - - // add scenes as switches - const scenes = this.site.scenes.filter((x) => x.hiddenFromSceneList === false); - - // eslint-disable-next-line no-restricted-syntax - 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; + return axios.create({ + baseURL: API_BASE_URL, + headers, + }); } // eslint-disable-next-line class-methods-use-this @@ -324,6 +201,111 @@ class PlejdApi extends EventEmitter { throw new Error(`Unknown device type with id ${hardwareId}`); } } + + _getPlejdDevices() { + this.deviceRegistry.clearPlejdDevices(); + + this.siteDetails.devices.forEach((device) => { + const { deviceId } = device; + + const settings = this.siteDetails.outputSettings.find( + (x) => x.deviceParseId === device.objectId, + ); + + let deviceNum = this.siteDetails.deviceAddress[deviceId]; + + if (settings) { + const outputs = this.siteDetails.outputAddress[deviceId]; + deviceNum = outputs[settings.output]; + } + + // check if device is dimmable + const plejdDevice = this.siteDetails.plejdDevices.find((x) => x.deviceId === deviceId); + const deviceType = this._getDeviceType(plejdDevice.hardwareId); + const { name, type } = deviceType; + let { dimmable } = deviceType; + + if (settings) { + dimmable = settings.dimCurve !== 'NonDimmable'; + } + + const newDevice = { + id: deviceNum, + name: device.title, + type, + typeName: name, + dimmable, + roomId: device.roomId, + version: plejdDevice.firmware.version, + serialNumber: plejdDevice.deviceId, + }; + + if (newDevice.typeName === 'WPH-01') { + // WPH-01 is special, it has two buttons which needs to be + // registered separately. + const inputs = this.siteDetails.inputAddress[deviceId]; + const first = inputs[0]; + const second = inputs[1]; + + this.deviceRegistry.addPlejdDevice({ + ...newDevice, + id: first, + name: `${device.title} left`, + }); + + this.deviceRegistry.addPlejdDevice({ + ...newDevice, + id: second, + name: `${device.title} right`, + }); + } else { + this.deviceRegistry.addPlejdDevice(newDevice); + } + }); + } + + _getRoomDevices() { + if (this.config.includeRoomsAsLights) { + logger.debug('includeRoomsAsLights is set to true, adding rooms too.'); + this.siteDetails.rooms.forEach((room) => { + const { roomId } = room; + const roomAddress = this.siteDetails.roomAddress[roomId]; + + const newDevice = { + id: roomAddress, + name: room.title, + type: 'light', + typeName: 'Room', + dimmable: this.deviceIdsByRoom[roomId].some( + (deviceId) => this.plejdDevices[deviceId].dimmable, + ), + }; + + this.deviceRegistry.addRoomDevice(newDevice); + }); + logger.debug('includeRoomsAsLights done.'); + } + } + + _getSceneDevices() { + // add scenes as switches + const scenes = this.siteDetails.scenes.filter((x) => x.hiddenFromSceneList === false); + + scenes.forEach((scene) => { + const sceneNum = this.siteDetails.sceneIndex[scene.sceneId]; + const newScene = { + id: sceneNum, + name: scene.title, + type: 'switch', + typeName: 'Scene', + dimmable: false, + version: '1.0', + serialNumber: scene.objectId, + }; + + this.deviceRegistry.addScene(newScene); + }); + } } module.exports = PlejdApi; From c5ee71d503913958819280eaa53398cf0ee70c38 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Victor=20Hagelb=C3=A4ck?= Date: Mon, 1 Feb 2021 21:24:27 +0100 Subject: [PATCH 09/33] Improve SceneManager structure and use central DeviceRegistry --- plejd/SceneManager.js | 52 ++++++++++++++++++++++++------------------- 1 file changed, 29 insertions(+), 23 deletions(-) diff --git a/plejd/SceneManager.js b/plejd/SceneManager.js index ee3bc18..b1a02d6 100644 --- a/plejd/SceneManager.js +++ b/plejd/SceneManager.js @@ -1,48 +1,54 @@ -/* eslint-disable max-classes-per-file */ const EventEmitter = require('events'); +const Logger = require('./Logger'); const Scene = require('./Scene'); +const logger = Logger.getLogger('scene-manager'); class SceneManager extends EventEmitter { - constructor(site, devices) { + deviceRegistry; + plejdBle; + scenes; + + constructor(deviceRegistry, plejdBle) { super(); - this.site = site; - this.scenes = []; - this.devices = devices; - - this.init(); + this.deviceRegistry = deviceRegistry; + this.plejdBle = plejdBle; + this.scenes = {}; } init() { - const scenes = this.site.scenes.filter((x) => x.hiddenFromSceneList === false); - // eslint-disable-next-line no-restricted-syntax - for (const scene of scenes) { - const idx = this.site.sceneIndex[scene.sceneId]; - this.scenes.push(new Scene(idx, scene, this.site.sceneSteps)); - } + const scenes = this.deviceRegistry.apiSite.scenes.filter( + (x) => x.hiddenFromSceneList === false, + ); + + this.scenes = {}; + scenes.forEach((scene) => { + const idx = this.deviceRegistry.apiSite.sceneIndex[scene.sceneId]; + this.scenes[scene.sceneId] = new Scene(idx, scene, this.deviceRegistry.apiSite.sceneSteps); + }); } - executeScene(sceneIndex, ble) { - const scene = this.scenes.find((x) => x.id === sceneIndex); + executeScene(sceneIndex) { + const scene = this.scenes[sceneIndex]; if (!scene) { + logger.info(`Scene with id ${sceneIndex} not found`); + logger.verbose(`Scenes: ${JSON.stringify(this.scenes, null, 2)}`); return; } - // eslint-disable-next-line no-restricted-syntax - for (const step of scene.steps) { - const device = this.devices.find((x) => x.serialNumber === step.deviceId); + scene.steps.forEach((step) => { + const device = this.deviceRegistry.getDeviceBySerialNumber(step.deviceId); if (device) { if (device.dimmable && step.state) { - ble.turnOn(device.id, { brightness: step.brightness }); + this.plejdBle.turnOn(device.id, { brightness: step.brightness }); } else if (!device.dimmable && step.state) { - ble.turnOn(device.id, {}); + this.plejdBle.turnOn(device.id, {}); } else if (!step.state) { - ble.turnOff(device.id, {}); + this.plejdBle.turnOff(device.id, {}); } } - } + }); } } module.exports = SceneManager; -/* eslint-disable */ From 62a6359544904a00852f410c4a5c953e01b41754 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Victor=20Hagelb=C3=A4ck?= Date: Mon, 1 Feb 2021 21:29:44 +0100 Subject: [PATCH 10/33] Restructure PlejdBLE slightly to async/await and clarify flow by logging --- plejd/PlejdBLE.js | 189 ++++++++++++++++++++++++++------------------- plejd/package.json | 3 +- 2 files changed, 110 insertions(+), 82 deletions(-) diff --git a/plejd/PlejdBLE.js b/plejd/PlejdBLE.js index b6f9c8c..353c6a2 100644 --- a/plejd/PlejdBLE.js +++ b/plejd/PlejdBLE.js @@ -4,6 +4,8 @@ const xor = require('buffer-xor'); const EventEmitter = require('events'); const Logger = require('./Logger'); +const Configuration = require('./Configuration'); + const logger = Logger.getLogger('plejd-ble'); // UUIDs @@ -30,24 +32,26 @@ const GATT_CHRC_ID = 'org.bluez.GattCharacteristic1'; const MAX_TRANSITION_STEPS_PER_SECOND = 5; // Could be made a setting const MAX_RETRY_COUNT = 5; // Could be made a setting -class PlejdService extends EventEmitter { - constructor(cryptoKey, devices, sceneManager, connectionTimeout, writeQueueWaitTime) { +const delay = (timeout) => new Promise((resolve) => setTimeout(resolve, timeout)); + +class PlejBLE extends EventEmitter { + config; + deviceRegistry; + + constructor(deviceRegistry) { super(); logger.info('Starting Plejd BLE, resetting all device states.'); - this.cryptoKey = Buffer.from(cryptoKey.replace(/-/g, ''), 'hex'); + this.config = Configuration.getOptions(); + this.deviceRegistry = deviceRegistry; - this.sceneManager = sceneManager; this.connectedDevice = null; this.plejdService = null; this.bleDevices = []; this.bleDeviceTransitionTimers = {}; this.plejdDevices = {}; - this.devices = devices; this.connectEventHooked = false; - this.connectionTimeout = connectionTimeout; - this.writeQueueWaitTime = writeQueueWaitTime; this.writeQueue = []; this.writeQueueRef = null; this.initInProgress = null; @@ -61,14 +65,19 @@ class PlejdService extends EventEmitter { ping: null, }; - this.bus = dbus.systemBus(); + this.bus = null; this.adapter = null; - - logger.debug('wiring events and waiting for BLE interface to power up.'); - this.wireEvents(); } async init() { + logger.info('init()'); + this.bus = dbus.systemBus(); + + logger.debug('wiring events and waiting for BLE interface to power up.'); + this.wireEvents(); + + this.cryptoKey = Buffer.from(this.deviceRegistry.cryptoKey.replace(/-/g, ''), 'hex'); + if (this.objectManager) { this.objectManager.removeAllListeners(); } @@ -86,7 +95,6 @@ class PlejdService extends EventEmitter { clearInterval(this.pingRef); clearTimeout(this.writeQueueRef); - logger.info('init()'); const bluez = await this.bus.getProxyObject(BLUEZ_SERVICE_NAME, '/'); this.objectManager = await bluez.getInterface(DBUS_OM_INTERFACE); @@ -101,31 +109,49 @@ class PlejdService extends EventEmitter { if (!this.adapter) { logger.error('Unable to find a bluetooth adapter that is compatible.'); - return Promise.reject(new Error('Unable to find a bluetooth adapter that is compatible.')); + throw new Error('Unable to find a bluetooth adapter that is compatible.'); } + logger.verbose( + `Iterating ${ + Object.keys(managedObjects).length + } BLE managedObjects looking for ${BLUEZ_DEVICE_ID}`, + ); + // eslint-disable-next-line no-restricted-syntax for (const path of Object.keys(managedObjects)) { /* eslint-disable no-await-in-loop */ - const interfaces = Object.keys(managedObjects[path]); + try { + const interfaces = Object.keys(managedObjects[path]); - if (interfaces.indexOf(BLUEZ_DEVICE_ID) > -1) { - const proxyObject = await this.bus.getProxyObject(BLUEZ_SERVICE_NAME, path); - const device = await proxyObject.getInterface(BLUEZ_DEVICE_ID); + if (interfaces.indexOf(BLUEZ_DEVICE_ID) > -1) { + const proxyObject = await this.bus.getProxyObject(BLUEZ_SERVICE_NAME, path); + const device = await proxyObject.getInterface(BLUEZ_DEVICE_ID); - const connected = managedObjects[path][BLUEZ_DEVICE_ID].Connected.value; + logger.verbose(`Found ${path} - ${JSON.stringify(device.device)}`); - if (connected) { - logger.info(`disconnecting ${path}`); - await device.Disconnect(); + const connected = managedObjects[path][BLUEZ_DEVICE_ID].Connected.value; + + if (connected) { + logger.info(`disconnecting ${path}. This can take up to 180 seconds`); + await device.Disconnect(); + } + + logger.verbose(`Removing ${path} from adapter.`); + await this.adapter.RemoveDevice(path); } - - await this.adapter.RemoveDevice(path); + } catch (err) { + logger.error(`Error handling ${path}`, err); } /* eslint-enable no-await-in-loop */ } - this.objectManager.on('InterfacesAdded', this.onInterfacesAdded.bind(this)); + logger.verbose('All active BLE device connections cleaned up.'); + + logger.verbose('Setting up interfacesAdded subscription and discovery filter'); + this.objectManager.on('InterfacesAdded', (path, interfaces) => + this.onInterfacesAdded(path, interfaces), + ); this.adapter.SetDiscoveryFilter({ UUIDs: new dbus.Variant('as', [PLEJD_SERVICE]), @@ -133,26 +159,33 @@ class PlejdService extends EventEmitter { }); try { + logger.verbose('Starting BLE discovery... This can take up to 180 seconds.'); await this.adapter.StartDiscovery(); + logger.verbose('Started BLE discovery'); } catch (err) { - logger.error('Failed to start discovery. Make sure no other add-on is currently scanning.'); - return Promise.reject( - new Error('Failed to start discovery. Make sure no other add-on is currently scanning.'), + logger.error('Failed to start discovery.', err); + if (err.message.includes('Operation already in progress')) { + logger.info( + 'If you continue to get "operation already in progress" error, you can try power cycling the bluetooth adapter. Get root console access, run "bluetoothctl" => "power off" => "power on" => "exit" => restart addon.', + ); + } + throw new Error( + 'Failed to start discovery. Make sure no other add-on is currently scanning.', ); } - return new Promise((resolve) => setTimeout( - () => resolve( - this._internalInit().catch((err) => { - logger.error('InternalInit exception! Will rethrow.', err); - throw err; - }), - ), - this.connectionTimeout * 1000, - )); + + await delay(this.config.connectionTimeout * 1000); + + await this._internalInit(); + + logger.info('Init done'); } async _internalInit() { - logger.debug(`Got ${this.bleDevices.length} device(s).`); + logger.debug(`InternalInit(). Got ${this.bleDevices.length} device(s).`); + if (this.bleDevices.length === 0) { + logger.warn('No devices, init will presumably not work'); + } // eslint-disable-next-line no-restricted-syntax for (const plejd of this.bleDevices) { @@ -170,7 +203,7 @@ class PlejdService extends EventEmitter { 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); + plejd.device = this.deviceRegistry.getDeviceBySerialNumber(fixedPlejdPath); logger.debug(`Discovered ${plejd.path} with rssi ${plejd.rssi}`); } catch (err) { @@ -197,10 +230,12 @@ class PlejdService extends EventEmitter { } } - setTimeout(async () => { - await this.onDeviceConnected(connectedDevice); - await this.adapter.StopDiscovery(); - }, this.connectionTimeout * 1000); + await delay(this.config.connectionTimeout * 1000); + + await this.onDeviceConnected(connectedDevice); + + logger.verbose('Stopping discovery...'); + await this.adapter.StopDiscovery(); } async _getInterface(managedObjects, iface) { @@ -225,6 +260,7 @@ class PlejdService extends EventEmitter { } async onInterfacesAdded(path, interfaces) { + logger.silly(`Interface added ${path}, inspecting...`); // const [adapter, dev, service, characteristic] = path.split('/').slice(3); const interfaceKeys = Object.keys(interfaces); @@ -237,11 +273,13 @@ class PlejdService extends EventEmitter { } else { logger.error('Uh oh, no Plejd device!'); } + } else { + logger.silly('Not the right device id'); } } turnOn(deviceId, command) { - const deviceName = this._getDeviceName(deviceId); + const deviceName = this.deviceRegistry.getDeviceName(deviceId); logger.info( `Plejd got turn on command for ${deviceName} (${deviceId}), brightness ${command.brightness}${ command.transition ? `, transition: ${command.transition}` : '' @@ -251,7 +289,7 @@ class PlejdService extends EventEmitter { } turnOff(deviceId, command) { - const deviceName = this._getDeviceName(deviceId); + const deviceName = this.deviceRegistry.getDeviceName(deviceId); logger.info( `Plejd got turn off command for ${deviceName} (${deviceId})${ command.transition ? `, transition: ${command.transition}` : '' @@ -272,14 +310,14 @@ class PlejdService extends EventEmitter { : null; this._clearDeviceTransitionTimer(deviceId); - const isDimmable = this.devices.find((d) => d.id === deviceId).dimmable; + const isDimmable = this.deviceRegistry.getDevice(deviceId).dimmable; if ( - transition > 1 - && isDimmable - && (initialBrightness || initialBrightness === 0) - && (targetBrightness || targetBrightness === 0) - && targetBrightness !== initialBrightness + transition > 1 && + isDimmable && + (initialBrightness || initialBrightness === 0) && + (targetBrightness || targetBrightness === 0) && + targetBrightness !== initialBrightness ) { // Transition time set, known initial and target brightness // Calculate transition interval time based on delta brightness and max steps per second @@ -386,14 +424,6 @@ class PlejdService extends EventEmitter { }); } - triggerScene(sceneIndex) { - const sceneName = this._getDeviceName(sceneIndex); - logger.info( - `Triggering scene ${sceneName} (${sceneIndex}). Scene name might be misleading if there is a device with the same numeric id.`, - ); - this.sceneManager.executeScene(sceneIndex, this); - } - async authenticate() { logger.info('authenticate()'); @@ -429,14 +459,16 @@ class PlejdService extends EventEmitter { ); return this.initInProgress; } - this.initInProgress = new Promise((resolve) => setTimeout(async () => { - const result = await this.init().catch((err) => { - logger.error('TrottledInit exception calling init(). Will re-throw.', err); - throw err; - }); - this.initInProgress = null; - resolve(result); - }, delay)); + this.initInProgress = new Promise((resolve) => + setTimeout(async () => { + const result = await this.init().catch((err) => { + logger.error('TrottledInit exception calling init(). Will re-throw.', err); + throw err; + }); + this.initInProgress = null; + resolve(result); + }, delay), + ); return this.initInProgress; } @@ -457,7 +489,7 @@ class PlejdService extends EventEmitter { } else { logger.debug('Write failed ', err); } - await this.throttledInit(this.connectionTimeout * 1000); + await this.throttledInit(this.config.connectionTimeout * 1000); return false; } } @@ -516,22 +548,22 @@ class PlejdService extends EventEmitter { logger.info('startWriteQueue()'); clearTimeout(this.writeQueueRef); - this.writeQueueRef = setTimeout(() => this.runWriteQueue(), this.writeQueueWaitTime); + this.writeQueueRef = setTimeout(() => this.runWriteQueue(), this.config.writeQueueWaitTime); } async runWriteQueue() { try { while (this.writeQueue.length > 0) { const queueItem = this.writeQueue.pop(); - const deviceName = this._getDeviceName(queueItem.deviceId); + const deviceName = this.deviceRegistry.getDeviceName(queueItem.deviceId); logger.debug( `Write queue: Processing ${deviceName} (${queueItem.deviceId}). Command ${queueItem.log}. Total queue length: ${this.writeQueue.length}`, ); if (this.writeQueue.some((item) => item.deviceId === queueItem.deviceId)) { logger.verbose( - `Skipping ${deviceName} (${queueItem.deviceId}) ` - + `${queueItem.log} due to more recent command in queue.`, + `Skipping ${deviceName} (${queueItem.deviceId}) ` + + `${queueItem.log} due to more recent command in queue.`, ); // Skip commands if new ones exist for the same deviceId // still process all messages in order @@ -559,7 +591,7 @@ class PlejdService extends EventEmitter { logger.error('Error in writeQueue loop, values probably not written to Plejd', e); } - this.writeQueueRef = setTimeout(() => this.runWriteQueue(), this.writeQueueWaitTime); + this.writeQueueRef = setTimeout(() => this.runWriteQueue(), this.config.writeQueueWaitTime); } async _processPlejdService(path, characteristics) { @@ -615,9 +647,10 @@ class PlejdService extends EventEmitter { async onDeviceConnected(device) { logger.info('onDeviceConnected()'); - logger.debug(`Device: ${device}`); + logger.debug(`Device: ${JSON.stringify(device.device)}`); if (!device) { - logger.error('Device is null. Should we break/return when this happens?'); + logger.error('onDeviceConnected device is null. Returning.'); + return; } const objects = await this.objectManager.GetManagedObjects(); @@ -724,7 +757,7 @@ class PlejdService extends EventEmitter { state, dim, }; - logger.verbose(`All states: ${JSON.stringify(this.plejdDevices)}`); + logger.verbose(`All states: ${JSON.stringify(this.plejdDevices, null, 2)}`); } else if (cmd === BLE_CMD_STATE_CHANGE) { logger.debug(`${deviceName} (${deviceId}) got state update. S: ${state}`); this.emit('stateChanged', deviceId, { @@ -794,10 +827,6 @@ class PlejdService extends EventEmitter { return Buffer.from(output, 'ascii'); } - _getDeviceName(deviceId) { - return (this.devices.find((d) => d.id === deviceId) || {}).name; - } - // eslint-disable-next-line class-methods-use-this _reverseBuffer(src) { const buffer = Buffer.allocUnsafe(src.length); @@ -811,4 +840,4 @@ class PlejdService extends EventEmitter { } } -module.exports = PlejdService; +module.exports = PlejBLE; diff --git a/plejd/package.json b/plejd/package.json index 71665b9..6ba45e5 100644 --- a/plejd/package.json +++ b/plejd/package.json @@ -1,13 +1,12 @@ { "dependencies": { - "@abandonware/bluetooth-hci-socket": "0.5.3-3", + "@abandonware/bluetooth-hci-socket": "~0.5.3-7", "axios": "~0.21.1", "buffer-xor": "~2.0.2", "dbus-next": "~0.9.1", "fs": "0.0.1-security", "jspack": "~0.0.4", "mqtt": "~3.0.0", - "sleep": "~6.1.0", "winston": "~3.3.3" }, "devDependencies": { From 5ac7d9893de817d39de3f689716ec3297df5aba3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Victor=20Hagelb=C3=A4ck?= Date: Mon, 1 Feb 2021 21:30:44 +0100 Subject: [PATCH 11/33] Break out main program flow to PlejdAddon class and add catching of init errors with slow retry --- plejd/PlejdAddon.js | 152 ++++++++++++++++++++++++++++++++++++++++++++ plejd/main.js | 141 +++------------------------------------- 2 files changed, 162 insertions(+), 131 deletions(-) create mode 100644 plejd/PlejdAddon.js diff --git a/plejd/PlejdAddon.js b/plejd/PlejdAddon.js new file mode 100644 index 0000000..761cb95 --- /dev/null +++ b/plejd/PlejdAddon.js @@ -0,0 +1,152 @@ +const EventEmitter = require('events'); + +const Configuration = require('./Configuration'); +const Logger = require('./Logger'); +const PlejdApi = require('./PlejdApi'); +const PlejdBLE = require('./PlejdBLE'); +const MqttClient = require('./MqttClient'); +const SceneManager = require('./SceneManager'); +const DeviceRegistry = require('./DeviceRegistry'); + +const logger = Logger.getLogger('plejd-main'); + +class PlejdAddon extends EventEmitter { + bleInitTimeout; + config; + deviceRegistry; + plejdApi; + plejdBLE; + mqttClient; + sceneManager; + + constructor() { + super(); + + this.config = Configuration.getOptions(); + this.deviceRegistry = new DeviceRegistry(); + + this.plejdApi = new PlejdApi(this.deviceRegistry); + this.plejdBLE = new PlejdBLE(this.deviceRegistry); + this.sceneManager = new SceneManager(this.deviceRegistry, this.plejdBle); + this.mqttClient = new MqttClient(this.deviceRegistry); + } + + async init() { + logger.info('Main Plejd addon init()...'); + + await this.plejdApi.init(); + this.sceneManager.init(); + + ['SIGINT', 'SIGHUP', 'SIGTERM'].forEach((signal) => { + process.on(signal, () => { + this.mqttClient.disconnect(() => process.exit(0)); + }); + }); + + this.mqttClient.on('connected', () => { + try { + logger.verbose('connected to mqtt.'); + this.mqttClient.sendDiscoveryToHomeAssistant(); + } catch (err) { + logger.error('Error in MqttClient.connected callback in main.js', err); + } + }); + + // subscribe to changes from HA + this.mqttClient.on('stateChanged', (device, command) => { + try { + 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. + this.sceneManager.executeScene(device.id); + return; + } + + let state = 'OFF'; + let commandObj = {}; + + if (typeof command === 'string') { + // switch command + state = command; + commandObj = { + 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. + this.mqttClient.updateState(deviceId, { + state: state === 'ON' ? 1 : 0, + }); + } else { + // eslint-disable-next-line prefer-destructuring + state = command.state; + commandObj = command; + } + + if (state === 'ON') { + this.plejdBLE.turnOn(deviceId, commandObj); + } else { + this.plejdBLE.turnOff(deviceId, commandObj); + } + } catch (err) { + logger.error('Error in MqttClient.stateChanged callback in main.js', err); + } + }); + + this.mqttClient.init(); + + // init the BLE interface + this.plejdBLE.on('connectFailed', () => { + logger.verbose('Were unable to connect, will retry.'); + this._bleInitLoop(); + }); + + // this.plejdBLE.init(); + + this.plejdBLE.on('authenticated', () => { + logger.verbose('plejd: connected via bluetooth.'); + }); + + // subscribe to changes from Plejd + this.plejdBLE.on('stateChanged', (deviceId, command) => { + try { + this.mqttClient.updateState(deviceId, command); + } catch (err) { + logger.error('Error in PlejdService.stateChanged callback in main.js', err); + } + }); + + this.plejdBLE.on('sceneTriggered', (deviceId, sceneId) => { + try { + this.mqttClient.sceneTriggered(sceneId); + } catch (err) { + logger.error('Error in PlejdService.sceneTriggered callback in main.js', err); + } + }); + + await this._bleInitLoop(); + } + + async _bleInitLoop() { + try { + if (this.bleInitTimeout) { + clearTimeout(this.bleInitTimeout); + } + await this.plejdBLE.init(); + } catch (err) { + logger.warn('Failed BLE init, trying again in 35s', err); + this.bleInitTimer = setTimeout(() => { + try { + this._bleInitLoop(); + } catch (err2) { + logger.warn('Why do we need to catch error here?', err2); + } + }, 35000); + } + } +} + +module.exports = PlejdAddon; diff --git a/plejd/main.js b/plejd/main.js index 1136140..fa54005 100644 --- a/plejd/main.js +++ b/plejd/main.js @@ -1,144 +1,23 @@ -const PlejdApi = require('./PlejdApi'); -const MqttClient = require('./MqttClient'); - const Logger = require('./Logger'); -const PlejdService = require('./PlejdService'); -const SceneManager = require('./SceneManager'); -const Configuration = require('./Configuration'); +const PlejdAddon = require('./PlejdAddon'); const logger = Logger.getLogger('plejd-main'); const version = '0.5.1'; async function main() { - logger.info(`Starting Plejd add-on v. ${version}`); + try { + logger.info(`Starting Plejd add-on v. ${version}`); - const config = Configuration.getConfiguration(); + const addon = new PlejdAddon(); - if (!config.connectionTimeout) { - config.connectionTimeout = 2; + await addon.init(); + + logger.info('main() finished'); + } catch (err) { + logger.error('Catastrophic error. Resetting entire addon in 1 minute', err); + setTimeout(() => main(), 60000); } - - const plejdApi = new PlejdApi( - config.site, - config.username, - config.password, - config.includeRoomsAsLights, - ); - const client = new MqttClient(config.mqttBroker, config.mqttUsername, config.mqttPassword); - - ['SIGINT', 'SIGHUP', 'SIGTERM'].forEach((signal) => { - process.on(signal, () => { - client.disconnect(() => process.exit(0)); - }); - }); - - plejdApi.login().then(() => { - // load all sites and find the one that we want (from config) - plejdApi.getSites().then((site) => { - // load the site and retrieve the crypto key - plejdApi.getSite(site.site.siteId).then((cryptoKey) => { - // parse all devices from the API - const devices = plejdApi.getDevices(); - - client.on('connected', () => { - try { - logger.verbose('connected to mqtt.'); - client.discover(devices); - } catch (err) { - logger.error('Error in MqttClient.connected callback in main.js', err); - } - }); - - client.init(); - - // init the BLE interface - const sceneManager = new SceneManager(plejdApi.site, devices); - const plejd = new PlejdService( - cryptoKey, - devices, - sceneManager, - config.connectionTimeout, - config.writeQueueWaitTime, - ); - plejd.on('connectFailed', () => { - logger.verbose('Were unable to connect, will retry connection in 10 seconds.'); - setTimeout(() => { - plejd - .init() - .catch((e) => logger.error('Error in init() from connectFailed in main.js', e)); - }, 10000); - }); - - plejd.init(); - - plejd.on('authenticated', () => { - logger.verbose('plejd: connected via bluetooth.'); - }); - - // subscribe to changes from Plejd - plejd.on('stateChanged', (deviceId, command) => { - try { - client.updateState(deviceId, command); - } catch (err) { - logger.error('Error in PlejdService.stateChanged callback in main.js', err); - } - }); - - plejd.on('sceneTriggered', (deviceId, scene) => { - try { - client.sceneTriggered(scene); - } catch (err) { - logger.error('Error in PlejdService.sceneTriggered callback in main.js', err); - } - }); - - // subscribe to changes from HA - client.on('stateChanged', (device, command) => { - try { - 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, - }; - - // 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 { - // eslint-disable-next-line prefer-destructuring - state = command.state; - commandObj = command; - } - - if (state === 'ON') { - plejd.turnOn(deviceId, commandObj); - } else { - plejd.turnOff(deviceId, commandObj); - } - } catch (err) { - logger.error('Error in MqttClient.stateChanged callback in main.js', err); - } - }); - }); - }); - }); } main(); From 4e7ec6a1da75aa3dd43de5a4f4a11df702c844b7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Victor=20Hagelb=C3=A4ck?= Date: Mon, 1 Feb 2021 21:31:02 +0100 Subject: [PATCH 12/33] Update DOCKERFILE with new file names --- plejd/Dockerfile | 152 ++++++++++++++++++++++++----------------------- 1 file changed, 77 insertions(+), 75 deletions(-) diff --git a/plejd/Dockerfile b/plejd/Dockerfile index 8c84189..b769332 100644 --- a/plejd/Dockerfile +++ b/plejd/Dockerfile @@ -1,75 +1,77 @@ -ARG BUILD_FROM=hassioaddons/base:8.0.6 -FROM $BUILD_FROM - -ENV LANG C.UTF-8 - -# Set shell -SHELL ["/bin/bash", "-o", "pipefail", "-c"] - -# Copy data for add-on -COPY ./config.json /plejd/ -COPY ./Configuration.js /plejd/ -COPY ./Logger.js /plejd/ -COPY ./main.js /plejd/ -COPY ./MqttClient.js /plejd/ -COPY ./package.json /plejd/ -COPY ./PlejdApi.js /plejd/ -COPY ./PlejdService.js /plejd/ -COPY ./Scene.js /plejd/ -COPY ./SceneManager.js /plejd/ -COPY ./SceneStep.js /plejd/ - -ARG BUILD_ARCH - -# Install Node -RUN apk add --no-cache jq -RUN \ - apk add --no-cache --virtual .build-dependencies \ - g++ \ - gcc \ - libc-dev \ - linux-headers \ - make \ - python3 \ - bluez \ - eudev-dev \ - \ - && apk add --no-cache \ - git \ - nodejs \ - npm \ - dbus-dev \ - glib-dev \ - \ - && npm config set unsafe-perm true - -WORKDIR /plejd -RUN npm install \ - --no-audit \ - --no-update-notifier \ - --unsafe-perm - -# Copy root filesystem -COPY rootfs / - -# Build arguments -ARG BUILD_DATE -ARG BUILD_REF -ARG BUILD_VERSION - -# Labels -LABEL \ - io.hass.name="Plejd" \ - io.hass.description="Adds support for the Swedish home automation devices from Plejd." \ - io.hass.arch="${BUILD_ARCH}" \ - io.hass.type="addon" \ - io.hass.version=${BUILD_VERSION} \ - maintainer="Marcus Westin " \ - org.label-schema.description="Adds support for the Swedish home automation devices from Plejd." \ - org.label-schema.build-date=${BUILD_DATE} \ - org.label-schema.name="Plejd" \ - org.label-schema.schema-version="1.0" \ - org.label-schema.usage="https://github.com/icanos/hassio-plejd/tree/master/README.md" \ - org.label-schema.vcs-ref=${BUILD_REF} \ - org.label-schema.vcs-url="https://github.com/icanos/hassio-plejd" - +ARG BUILD_FROM=hassioaddons/base:8.0.6 +FROM $BUILD_FROM + +ENV LANG C.UTF-8 + +# Set shell +SHELL ["/bin/bash", "-o", "pipefail", "-c"] + +# Copy data for add-on +COPY ./config.json /plejd/ +COPY ./Configuration.js /plejd/ +COPY ./DeviceRegistry.js /plejd/ +COPY ./Logger.js /plejd/ +COPY ./main.js /plejd/ +COPY ./MqttClient.js /plejd/ +COPY ./package.json /plejd/ +COPY ./PlejdAddon.js /plejd/ +COPY ./PlejdApi.js /plejd/ +COPY ./PlejdBLE.js /plejd/ +COPY ./Scene.js /plejd/ +COPY ./SceneManager.js /plejd/ +COPY ./SceneStep.js /plejd/ + +ARG BUILD_ARCH + +# Install Node +RUN apk add --no-cache jq +RUN \ + apk add --no-cache --virtual .build-dependencies \ + g++ \ + gcc \ + libc-dev \ + linux-headers \ + make \ + python3 \ + bluez \ + eudev-dev \ + \ + && apk add --no-cache \ + git \ + nodejs \ + npm \ + dbus-dev \ + glib-dev \ + \ + && npm config set unsafe-perm true + +WORKDIR /plejd +RUN npm install \ + --no-audit \ + --no-update-notifier \ + --unsafe-perm + +# Copy root filesystem +COPY rootfs / + +# Build arguments +ARG BUILD_DATE +ARG BUILD_REF +ARG BUILD_VERSION + +# Labels +LABEL \ + io.hass.name="Plejd" \ + io.hass.description="Adds support for the Swedish home automation devices from Plejd." \ + io.hass.arch="${BUILD_ARCH}" \ + io.hass.type="addon" \ + io.hass.version=${BUILD_VERSION} \ + maintainer="Marcus Westin " \ + org.label-schema.description="Adds support for the Swedish home automation devices from Plejd." \ + org.label-schema.build-date=${BUILD_DATE} \ + org.label-schema.name="Plejd" \ + org.label-schema.schema-version="1.0" \ + org.label-schema.usage="https://github.com/icanos/hassio-plejd/tree/master/README.md" \ + org.label-schema.vcs-ref=${BUILD_REF} \ + org.label-schema.vcs-url="https://github.com/icanos/hassio-plejd" + From 898a9d822f24b1094d730fd0af3e055477cbc48d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Victor=20Hagelb=C3=A4ck?= Date: Mon, 1 Feb 2021 21:36:40 +0100 Subject: [PATCH 13/33] Lint and style fixes --- plejd/.eslintrc.js | 1 + plejd/Configuration.js | 1 + plejd/MqttClient.js | 19 ++++++++++--------- plejd/PlejdBLE.js | 38 +++++++++++++++++--------------------- 4 files changed, 29 insertions(+), 30 deletions(-) diff --git a/plejd/.eslintrc.js b/plejd/.eslintrc.js index 15a3195..ff88ace 100644 --- a/plejd/.eslintrc.js +++ b/plejd/.eslintrc.js @@ -23,6 +23,7 @@ module.exports = { function getRules() { return { + 'lines-between-class-members': ['error', 'always', { exceptAfterSingleLine: true }], // Allows modification of properties passed to functions. // Notably used in array.forEach(e => {e.prop = val;}) 'no-param-reassign': ['error', { props: false }], diff --git a/plejd/Configuration.js b/plejd/Configuration.js index 7b3a1d2..690c472 100644 --- a/plejd/Configuration.js +++ b/plejd/Configuration.js @@ -13,6 +13,7 @@ class Configuration { Configuration._options = { ...defaultConfig, ...config }; + // eslint-disable-next-line no-console console.log('Config:', { ...Configuration._options, username: '---scrubbed---', diff --git a/plejd/MqttClient.js b/plejd/MqttClient.js index 88b877d..282bf65 100644 --- a/plejd/MqttClient.js +++ b/plejd/MqttClient.js @@ -109,12 +109,6 @@ class MqttClient extends EventEmitter { }); this.client.on('message', (topic, message) => { - // const command = message.toString(); - const command = - message.toString().substring(0, 1) === '{' - ? JSON.parse(message.toString()) - : message.toString(); - if (startTopics.includes(topic)) { logger.info('Home Assistant has started. lets do discovery.'); this.emit('connected'); @@ -124,6 +118,10 @@ class MqttClient extends EventEmitter { const device = this.deviceRegistry.getDevice(decodedTopic.id); const deviceName = device ? device.name : ''; + const command = message.toString().substring(0, 1) === '{' + ? JSON.parse(message.toString()) + : message.toString(); + switch (decodedTopic.command) { case 'set': logger.verbose( @@ -142,7 +140,11 @@ class MqttClient extends EventEmitter { case 'config': case 'availability': logger.verbose( - `Sent mqtt ${decodedTopic.command} command for ${decodedTopic.type}, ${deviceName} (${decodedTopic.id}). ${decodedTopic.command === 'availability' ? message : ''}`, + `Sent mqtt ${decodedTopic.command} command for ${ + decodedTopic.type + }, ${deviceName} (${decodedTopic.id}). ${ + decodedTopic.command === 'availability' ? message : '' + }`, ); break; default: @@ -172,8 +174,7 @@ class MqttClient extends EventEmitter { this.deviceRegistry.allDevices.forEach((device) => { logger.debug(`Sending discovery for ${device.name}`); - const payload = - device.type === 'switch' ? getSwitchPayload(device) : getDiscoveryPayload(device); + const payload = device.type === 'switch' ? getSwitchPayload(device) : getDiscoveryPayload(device); logger.info( `Discovered ${device.type} (${device.typeName}) named ${device.name} with PID ${device.id}.`, ); diff --git a/plejd/PlejdBLE.js b/plejd/PlejdBLE.js index 353c6a2..ec77f35 100644 --- a/plejd/PlejdBLE.js +++ b/plejd/PlejdBLE.js @@ -149,9 +149,7 @@ class PlejBLE extends EventEmitter { logger.verbose('All active BLE device connections cleaned up.'); logger.verbose('Setting up interfacesAdded subscription and discovery filter'); - this.objectManager.on('InterfacesAdded', (path, interfaces) => - this.onInterfacesAdded(path, interfaces), - ); + this.objectManager.on('InterfacesAdded', (path, interfaces) => this.onInterfacesAdded(path, interfaces)); this.adapter.SetDiscoveryFilter({ UUIDs: new dbus.Variant('as', [PLEJD_SERVICE]), @@ -313,11 +311,11 @@ class PlejBLE extends EventEmitter { const isDimmable = this.deviceRegistry.getDevice(deviceId).dimmable; if ( - transition > 1 && - isDimmable && - (initialBrightness || initialBrightness === 0) && - (targetBrightness || targetBrightness === 0) && - targetBrightness !== initialBrightness + transition > 1 + && isDimmable + && (initialBrightness || initialBrightness === 0) + && (targetBrightness || targetBrightness === 0) + && targetBrightness !== initialBrightness ) { // Transition time set, known initial and target brightness // Calculate transition interval time based on delta brightness and max steps per second @@ -452,23 +450,21 @@ class PlejBLE extends EventEmitter { this.characteristics.lastData.StartNotify(); } - async throttledInit(delay) { + async throttledInit(delayMs) { if (this.initInProgress) { logger.debug( 'ThrottledInit already in progress. Skipping this call and returning existing promise.', ); return this.initInProgress; } - this.initInProgress = new Promise((resolve) => - setTimeout(async () => { - const result = await this.init().catch((err) => { - logger.error('TrottledInit exception calling init(). Will re-throw.', err); - throw err; - }); - this.initInProgress = null; - resolve(result); - }, delay), - ); + this.initInProgress = new Promise((resolve) => setTimeout(async () => { + const result = await this.init().catch((err) => { + logger.error('TrottledInit exception calling init(). Will re-throw.', err); + throw err; + }); + this.initInProgress = null; + resolve(result); + }, delayMs)); return this.initInProgress; } @@ -562,8 +558,8 @@ class PlejBLE extends EventEmitter { if (this.writeQueue.some((item) => item.deviceId === queueItem.deviceId)) { logger.verbose( - `Skipping ${deviceName} (${queueItem.deviceId}) ` + - `${queueItem.log} due to more recent command in queue.`, + `Skipping ${deviceName} (${queueItem.deviceId}) ` + + `${queueItem.log} due to more recent command in queue.`, ); // Skip commands if new ones exist for the same deviceId // still process all messages in order From 7436c56c259262b8e41d94932476525bb7d0e6dd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Victor=20Hagelb=C3=A4ck?= Date: Mon, 8 Feb 2021 07:38:31 +0100 Subject: [PATCH 14/33] First async/await rewrite of PlejdBLE --- plejd/PlejdBLE.js | 375 +++++++++++++++++++++++++++------------------- 1 file changed, 219 insertions(+), 156 deletions(-) diff --git a/plejd/PlejdBLE.js b/plejd/PlejdBLE.js index ec77f35..52ff3c2 100644 --- a/plejd/PlejdBLE.js +++ b/plejd/PlejdBLE.js @@ -9,11 +9,12 @@ const Configuration = require('./Configuration'); const logger = Logger.getLogger('plejd-ble'); // UUIDs -const PLEJD_SERVICE = '31ba0001-6085-4726-be45-040c957391b5'; -const DATA_UUID = '31ba0004-6085-4726-be45-040c957391b5'; -const LAST_DATA_UUID = '31ba0005-6085-4726-be45-040c957391b5'; -const AUTH_UUID = '31ba0009-6085-4726-be45-040c957391b5'; -const PING_UUID = '31ba000a-6085-4726-be45-040c957391b5'; +const BLE_UUID_SUFFIX = '6085-4726-be45-040c957391b5'; +const PLEJD_SERVICE = `31ba0001-${BLE_UUID_SUFFIX}`; +const DATA_UUID = `31ba0004-${BLE_UUID_SUFFIX}`; +const LAST_DATA_UUID = `31ba0005-${BLE_UUID_SUFFIX}`; +const AUTH_UUID = `31ba0009-${BLE_UUID_SUFFIX}`; +const PING_UUID = `31ba000a-${BLE_UUID_SUFFIX}`; const BLE_CMD_DIM_CHANGE = 0xc8; const BLE_CMD_DIM2_CHANGE = 0x98; @@ -34,22 +35,29 @@ const MAX_RETRY_COUNT = 5; // Could be made a setting const delay = (timeout) => new Promise((resolve) => setTimeout(resolve, timeout)); -class PlejBLE extends EventEmitter { +class PlejBLEHandler extends EventEmitter { + adapter; + adapterProperties; config; deviceRegistry; + // Refer to BLE-states.md regarding the internal BLE/bluez state machine of Bluetooth states + // These states refer to the state machine of this file + static STATES = ['MAIN_INIT', 'GET_ADAPTER_PROXY']; + constructor(deviceRegistry) { super(); - logger.info('Starting Plejd BLE, resetting all device states.'); + logger.info('Starting Plejd BLE Handler, resetting all device states.'); this.config = Configuration.getOptions(); - this.deviceRegistry = deviceRegistry; this.connectedDevice = null; + this.deviceRegistry = deviceRegistry; this.plejdService = null; this.bleDevices = []; this.bleDeviceTransitionTimers = {}; + this.discoveryTimeout = null; this.plejdDevices = {}; this.connectEventHooked = false; this.writeQueue = []; @@ -66,15 +74,13 @@ class PlejBLE extends EventEmitter { }; this.bus = null; - this.adapter = null; } async init() { logger.info('init()'); this.bus = dbus.systemBus(); - - logger.debug('wiring events and waiting for BLE interface to power up.'); - this.wireEvents(); + this.adapter = null; + this.adapterProperties = null; this.cryptoKey = Buffer.from(this.deviceRegistry.cryptoKey.replace(/-/g, ''), 'hex'); @@ -93,25 +99,174 @@ class PlejBLE extends EventEmitter { ping: null, }; - clearInterval(this.pingRef); - clearTimeout(this.writeQueueRef); + await this._getInterface(); + await this._startGetPlejdDevice(); + logger.info('Init done'); + } + + async _initDiscoveredPlejdDevice(path) { + logger.debug(`initDiscoveredPlejdDevice(). Got ${path} device`); + + logger.debug(`Inspecting ${path}`); + + try { + const proxyObject = await this.bus.getProxyObject(BLUEZ_SERVICE_NAME, path); + const device = await proxyObject.getInterface(BLUEZ_DEVICE_ID); + const properties = await proxyObject.getInterface(DBUS_PROP_INTERFACE); + + const plejd = { path }; + + 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.deviceRegistry.getDeviceBySerialNumber(fixedPlejdPath); + + logger.debug(`Discovered ${plejd.path} with rssi ${plejd.rssi}, name ${plejd.device.name}`); + // Todo: Connect should probably be done here + this.bleDevices.push(plejd); + } catch (err) { + logger.error(`Failed inspecting ${path}. `, err); + } + } + + async _inspectDevicesDiscovered() { + if (this.bleDevices.length === 0) { + logger.error('Discovery timeout elapsed, no devices found. Exiting...'); + throw new Error('Discovery timeout elapsed, no devices found'); + } + + logger.info(`Device discovery done, found ${this.bleDevices.length} Plejd devices`); + + const sortedDevices = this.bleDevices.sort((a, b) => b.rssi - a.rssi); + + // eslint-disable-next-line no-restricted-syntax + for (const plejd of sortedDevices) { + try { + console.log('Inspecting', plejd); + if (plejd.instance) { + logger.info(`Connecting to ${plejd.path}`); + // eslint-disable-next-line no-await-in-loop + await plejd.instance.Connect(); + + logger.verbose('Connected. Waiting for timeout before reading characteristics...'); + // eslint-disable-next-line no-await-in-loop + await delay(this.config.connectionTimeout * 1000); + + // eslint-disable-next-line no-await-in-loop + const connectedPlejdDevice = await this._onDeviceConnected(plejd); + if (connectedPlejdDevice) { + break; + } + } + } catch (err) { + logger.warn('Unable to connect.', err); + } + } + + try { + logger.verbose('Stopping discovery...'); + await this.adapter.StopDiscovery(); + logger.verbose('Stopped BLE discovery'); + } catch (err) { + logger.error('Failed to stop discovery.', err); + if (err.message.includes('Operation already in progress')) { + logger.info( + 'If you continue to get "operation already in progress" error, you can try power cycling the bluetooth adapter. Get root console access, run "bluetoothctl" => "power off" => "power on" => "exit" => restart addon.', + ); + try { + await delay(250); + logger.verbose('Power cycling...'); + await this._powerCycleAdapter(); + logger.verbose('Trying again...'); + await this._startGetPlejdDevice(); + } catch (errInner) { + throw new Error('Failed to retry internalInit.'); + } + } + throw new Error( + 'Failed to start discovery. Make sure no other add-on is currently scanning.', + ); + } + + if (!this.connectedDevice) { + logger.error('Could not connect to any Plejd device. Exiting...'); + this.emit('connectFailed'); + throw new Error('Could not connect to any Plejd device'); + } + + // Connected and authenticated, start ping + this.startPing(); + this.startWriteQueue(); + + // After we've authenticated, we need to hook up the event listener + // for changes to lastData. + this.characteristics.lastDataProperties.on('PropertiesChanged', ( + iface, + properties, + // invalidated (third param), + ) => this.onLastDataUpdated(iface, properties)); + this.characteristics.lastData.StartNotify(); + } + + async _getInterface() { const bluez = await this.bus.getProxyObject(BLUEZ_SERVICE_NAME, '/'); this.objectManager = await bluez.getInterface(DBUS_OM_INTERFACE); // We need to find the ble interface which implements the Adapter1 interface const managedObjects = await this.objectManager.GetManagedObjects(); - const result = await this._getInterface(managedObjects, BLUEZ_ADAPTER_ID); + const managedPaths = Object.keys(managedObjects); - if (result) { - this.adapter = result[1]; + console.log('Managed objects', managedObjects); + console.log('Managed paths', managedPaths); + + // eslint-disable-next-line no-restricted-syntax + for (const path of managedPaths) { + const pathInterfaces = Object.keys(managedObjects[path]); + if (pathInterfaces.indexOf(BLUEZ_ADAPTER_ID) > -1) { + logger.debug(`Found BLE interface '${BLUEZ_ADAPTER_ID}' at ${path}`); + try { + // eslint-disable-next-line no-await-in-loop + const adapterObject = await this.bus.getProxyObject(BLUEZ_SERVICE_NAME, path); + console.log('Got adapter results', adapterObject); + // eslint-disable-next-line no-await-in-loop + this.adapterProperties = await adapterObject.getInterface(DBUS_PROP_INTERFACE); + // eslint-disable-next-line no-await-in-loop + await this._powerOnAdapter(); + this.adapter = adapterObject.getInterface(BLUEZ_ADAPTER_ID); + // eslint-disable-next-line no-await-in-loop + await this._cleanExistingConnections(managedObjects); + return this.adapter; + } catch (err) { + logger.error(`Failed to get interface '${BLUEZ_ADAPTER_ID}'. `, err); + } + } } - if (!this.adapter) { - logger.error('Unable to find a bluetooth adapter that is compatible.'); - throw new Error('Unable to find a bluetooth adapter that is compatible.'); - } + this.adapter = null; + logger.error('Unable to find a bluetooth adapter that is compatible.'); + throw new Error('Unable to find a bluetooth adapter that is compatible.'); + } + async _powerCycleAdapter() { + await this._powerOffAdapter(); + await this._powerOnAdapter(); + } + + async _powerOnAdapter() { + await this.adapterProperties.Set(BLUEZ_ADAPTER_ID, 'Powered', new dbus.Variant('b', 1)); + await delay(1000); + } + + async _powerOffAdapter() { + await this.adapterProperties.Set(BLUEZ_ADAPTER_ID, 'Powered', new dbus.Variant('b', 0)); + await delay(1000); + } + + async _cleanExistingConnections(managedObjects) { logger.verbose( `Iterating ${ Object.keys(managedObjects).length @@ -147,9 +302,13 @@ class PlejBLE extends EventEmitter { } logger.verbose('All active BLE device connections cleaned up.'); + } + async _startGetPlejdDevice() { logger.verbose('Setting up interfacesAdded subscription and discovery filter'); - this.objectManager.on('InterfacesAdded', (path, interfaces) => this.onInterfacesAdded(path, interfaces)); + this.objectManager.on('InterfacesAdded', (path, interfaces) => + this.onInterfacesAdded(path, interfaces), + ); this.adapter.SetDiscoveryFilter({ UUIDs: new dbus.Variant('as', [PLEJD_SERVICE]), @@ -158,6 +317,7 @@ class PlejBLE extends EventEmitter { try { logger.verbose('Starting BLE discovery... This can take up to 180 seconds.'); + this._scheduleInternalInit(); await this.adapter.StartDiscovery(); logger.verbose('Started BLE discovery'); } catch (err) { @@ -171,90 +331,11 @@ class PlejBLE extends EventEmitter { 'Failed to start discovery. Make sure no other add-on is currently scanning.', ); } - - await delay(this.config.connectionTimeout * 1000); - - await this._internalInit(); - - logger.info('Init done'); } - async _internalInit() { - logger.debug(`InternalInit(). Got ${this.bleDevices.length} device(s).`); - if (this.bleDevices.length === 0) { - logger.warn('No devices, init will presumably not work'); - } - - // eslint-disable-next-line no-restricted-syntax - for (const plejd of this.bleDevices) { - /* eslint-disable no-await-in-loop */ - logger.debug(`Inspecting ${plejd.path}`); - - try { - const proxyObject = await this.bus.getProxyObject(BLUEZ_SERVICE_NAME, plejd.path); - const device = await proxyObject.getInterface(BLUEZ_DEVICE_ID); - const properties = await proxyObject.getInterface(DBUS_PROP_INTERFACE); - - 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.deviceRegistry.getDeviceBySerialNumber(fixedPlejdPath); - - logger.debug(`Discovered ${plejd.path} with rssi ${plejd.rssi}`); - } catch (err) { - logger.error(`Failed inspecting ${plejd.path}. `, err); - } - /* eslint-enable no-await-in-loop */ - } - - const sortedDevices = this.bleDevices.sort((a, b) => b.rssi - a.rssi); - let connectedDevice = null; - - // eslint-disable-next-line no-restricted-syntax - for (const plejd of sortedDevices) { - try { - if (plejd.instance) { - logger.info(`Connecting to ${plejd.path}`); - // eslint-disable-next-line no-await-in-loop - await plejd.instance.Connect(); - connectedDevice = plejd; - break; - } - } catch (err) { - logger.error('Warning: unable to connect, will retry. ', err); - } - } - - await delay(this.config.connectionTimeout * 1000); - - await this.onDeviceConnected(connectedDevice); - - logger.verbose('Stopping discovery...'); - await this.adapter.StopDiscovery(); - } - - async _getInterface(managedObjects, iface) { - const managedPaths = Object.keys(managedObjects); - - // eslint-disable-next-line no-restricted-syntax - for (const path of managedPaths) { - const pathInterfaces = Object.keys(managedObjects[path]); - if (pathInterfaces.indexOf(iface) > -1) { - logger.debug(`Found BLE interface '${iface}' at ${path}`); - try { - // eslint-disable-next-line no-await-in-loop - const adapterObject = await this.bus.getProxyObject(BLUEZ_SERVICE_NAME, path); - return [path, adapterObject.getInterface(iface), adapterObject]; - } catch (err) { - logger.error(`Failed to get interface '${iface}'. `, err); - } - } - } - - return null; + _scheduleInternalInit() { + clearTimeout(this.discoveryTimeout); + setTimeout(() => this._inspectDevicesDiscovered(), this.config.connectionTimeout * 1000); } async onInterfacesAdded(path, interfaces) { @@ -265,9 +346,8 @@ class PlejBLE extends EventEmitter { if (interfaceKeys.indexOf(BLUEZ_DEVICE_ID) > -1) { if (interfaces[BLUEZ_DEVICE_ID].UUIDs.value.indexOf(PLEJD_SERVICE) > -1) { logger.debug(`Found Plejd service on ${path}`); - this.bleDevices.push({ - path, - }); + + await this._initDiscoveredPlejdDevice(path); } else { logger.error('Uh oh, no Plejd device!'); } @@ -311,11 +391,11 @@ class PlejBLE extends EventEmitter { const isDimmable = this.deviceRegistry.getDevice(deviceId).dimmable; if ( - transition > 1 - && isDimmable - && (initialBrightness || initialBrightness === 0) - && (targetBrightness || targetBrightness === 0) - && targetBrightness !== initialBrightness + transition > 1 && + isDimmable && + (initialBrightness || initialBrightness === 0) && + (targetBrightness || targetBrightness === 0) && + targetBrightness !== initialBrightness ) { // Transition time set, known initial and target brightness // Calculate transition interval time based on delta brightness and max steps per second @@ -435,19 +515,8 @@ class PlejBLE extends EventEmitter { await this.characteristics.auth.WriteValue([...response], {}); } catch (err) { logger.error('Failed to authenticate: ', err); + throw new Error('Failed to authenticate'); } - - // auth done, start ping - this.startPing(); - this.startWriteQueue(); - - // After we've authenticated, we need to hook up the event listener - // for changes to lastData. - this.characteristics.lastDataProperties.on( - 'PropertiesChanged', - this.onLastDataUpdated.bind(this), - ); - this.characteristics.lastData.StartNotify(); } async throttledInit(delayMs) { @@ -457,14 +526,16 @@ class PlejBLE extends EventEmitter { ); return this.initInProgress; } - this.initInProgress = new Promise((resolve) => setTimeout(async () => { - const result = await this.init().catch((err) => { - logger.error('TrottledInit exception calling init(). Will re-throw.', err); - throw err; - }); - this.initInProgress = null; - resolve(result); - }, delayMs)); + this.initInProgress = new Promise((resolve) => + setTimeout(async () => { + const result = await this.init().catch((err) => { + logger.error('TrottledInit exception calling init(). Will re-throw.', err); + throw err; + }); + this.initInProgress = null; + resolve(result); + }, delayMs), + ); return this.initInProgress; } @@ -558,8 +629,8 @@ class PlejBLE extends EventEmitter { if (this.writeQueue.some((item) => item.deviceId === queueItem.deviceId)) { logger.verbose( - `Skipping ${deviceName} (${queueItem.deviceId}) ` - + `${queueItem.log} due to more recent command in queue.`, + `Skipping ${deviceName} (${queueItem.deviceId}) ` + + `${queueItem.log} due to more recent command in queue.`, ); // Skip commands if new ones exist for the same deviceId // still process all messages in order @@ -641,26 +712,26 @@ class PlejBLE extends EventEmitter { }; } - async onDeviceConnected(device) { + async _onDeviceConnected(device) { + this.connectedDevice = null; logger.info('onDeviceConnected()'); logger.debug(`Device: ${JSON.stringify(device.device)}`); - if (!device) { - logger.error('onDeviceConnected device is null. Returning.'); - return; - } const objects = await this.objectManager.GetManagedObjects(); const paths = Object.keys(objects); const characteristics = []; + console.log(`Iterating looking for ${GATT_CHRC_ID}`, paths); // eslint-disable-next-line no-restricted-syntax for (const path of paths) { const interfaces = Object.keys(objects[path]); + console.log('Interfaces', path, interfaces); if (interfaces.indexOf(GATT_CHRC_ID) > -1) { characteristics.push(path); } } + console.log('Characteristics', characteristics); // eslint-disable-next-line no-restricted-syntax for (const path of paths) { const interfaces = Object.keys(objects[path]); @@ -683,23 +754,23 @@ class PlejBLE extends EventEmitter { } if (!this.plejdService) { - logger.info("warning: wasn't able to connect to Plejd, will retry."); - this.emit('connectFailed'); - return; + logger.warn("Wasn't able to connect to Plejd, will retry."); + return null; } if (!this.characteristics.auth) { logger.error('unable to enumerate characteristics.'); - this.emit('connectFailed'); - return; + return null; } this.connectedDevice = device.device; await this.authenticate(); + + return this.connectedDevice; } // eslint-disable-next-line no-unused-vars - async onLastDataUpdated(iface, properties, invalidated) { + async onLastDataUpdated(iface, properties) { if (iface !== GATT_CHRC_ID) { return; } @@ -734,7 +805,7 @@ class PlejBLE extends EventEmitter { const dim = decoded.length > 7 ? decoded.readUInt8(7) : 0; // Bytes 8-9 are sometimes present, what are they? - const deviceName = this._getDeviceName(deviceId); + const deviceName = this.deviceRegistry.getDeviceName(deviceId); if (Logger.shouldLog('debug')) { // decoded.toString() could potentially be expensive logger.debug(`Raw event received: ${decoded.toString('hex')}`); @@ -766,7 +837,7 @@ class PlejBLE extends EventEmitter { logger.verbose(`All states: ${this.plejdDevices}`); } else if (cmd === BLE_CMD_SCENE_TRIG) { const sceneId = state; - const sceneName = this._getDeviceName(sceneId); + const sceneName = this.deviceRegistry.getSceneName(sceneId); logger.debug( `${sceneName} (${sceneId}) scene triggered (device id ${deviceId}). Name can be misleading if there is a device with the same numeric id.`, @@ -784,14 +855,6 @@ class PlejBLE extends EventEmitter { } } - wireEvents() { - logger.info('wireEvents()'); - const self = this; - - this.on('pingFailed', this.onPingFailed.bind(self)); - this.on('pingSuccess', this.onPingSuccess.bind(self)); - } - // eslint-disable-next-line class-methods-use-this _createChallengeResponse(key, challenge) { const intermediate = crypto.createHash('sha256').update(xor(key, challenge)).digest(); @@ -836,4 +899,4 @@ class PlejBLE extends EventEmitter { } } -module.exports = PlejBLE; +module.exports = PlejBLEHandler; From 40f79df37e0a7f31eb078c31224701675e41ea1c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Victor=20Hagelb=C3=A4ck?= Date: Mon, 8 Feb 2021 07:39:53 +0100 Subject: [PATCH 15/33] Rename PlejdBLE to PlejdBLEHandler --- plejd/PlejdAddon.js | 23 ++++++++++++----------- plejd/{PlejdBLE.js => PlejdBLEHandler.js} | 0 2 files changed, 12 insertions(+), 11 deletions(-) rename plejd/{PlejdBLE.js => PlejdBLEHandler.js} (100%) diff --git a/plejd/PlejdAddon.js b/plejd/PlejdAddon.js index 761cb95..e0ac518 100644 --- a/plejd/PlejdAddon.js +++ b/plejd/PlejdAddon.js @@ -3,7 +3,8 @@ const EventEmitter = require('events'); const Configuration = require('./Configuration'); const Logger = require('./Logger'); const PlejdApi = require('./PlejdApi'); -const PlejdBLE = require('./PlejdBLE'); +// const PlejdBLE = require('./PlejdBLE'); +const PlejdBLEHandler = require('./PlejdBLEHandler'); const MqttClient = require('./MqttClient'); const SceneManager = require('./SceneManager'); const DeviceRegistry = require('./DeviceRegistry'); @@ -15,7 +16,7 @@ class PlejdAddon extends EventEmitter { config; deviceRegistry; plejdApi; - plejdBLE; + plejdBLEHandler; mqttClient; sceneManager; @@ -26,8 +27,8 @@ class PlejdAddon extends EventEmitter { this.deviceRegistry = new DeviceRegistry(); this.plejdApi = new PlejdApi(this.deviceRegistry); - this.plejdBLE = new PlejdBLE(this.deviceRegistry); - this.sceneManager = new SceneManager(this.deviceRegistry, this.plejdBle); + this.plejdBLEHandler = new PlejdBLEHandler(this.deviceRegistry); + this.sceneManager = new SceneManager(this.deviceRegistry, this.plejdBLEHandler); this.mqttClient = new MqttClient(this.deviceRegistry); } @@ -87,9 +88,9 @@ class PlejdAddon extends EventEmitter { } if (state === 'ON') { - this.plejdBLE.turnOn(deviceId, commandObj); + this.plejdBLEHandler.turnOn(deviceId, commandObj); } else { - this.plejdBLE.turnOff(deviceId, commandObj); + this.plejdBLEHandler.turnOff(deviceId, commandObj); } } catch (err) { logger.error('Error in MqttClient.stateChanged callback in main.js', err); @@ -99,19 +100,19 @@ class PlejdAddon extends EventEmitter { this.mqttClient.init(); // init the BLE interface - this.plejdBLE.on('connectFailed', () => { + this.plejdBLEHandler.on('connectFailed', () => { logger.verbose('Were unable to connect, will retry.'); this._bleInitLoop(); }); // this.plejdBLE.init(); - this.plejdBLE.on('authenticated', () => { + this.plejdBLEHandler.on('authenticated', () => { logger.verbose('plejd: connected via bluetooth.'); }); // subscribe to changes from Plejd - this.plejdBLE.on('stateChanged', (deviceId, command) => { + this.plejdBLEHandler.on('stateChanged', (deviceId, command) => { try { this.mqttClient.updateState(deviceId, command); } catch (err) { @@ -119,7 +120,7 @@ class PlejdAddon extends EventEmitter { } }); - this.plejdBLE.on('sceneTriggered', (deviceId, sceneId) => { + this.plejdBLEHandler.on('sceneTriggered', (deviceId, sceneId) => { try { this.mqttClient.sceneTriggered(sceneId); } catch (err) { @@ -135,7 +136,7 @@ class PlejdAddon extends EventEmitter { if (this.bleInitTimeout) { clearTimeout(this.bleInitTimeout); } - await this.plejdBLE.init(); + await this.plejdBLEHandler.init(); } catch (err) { logger.warn('Failed BLE init, trying again in 35s', err); this.bleInitTimer = setTimeout(() => { diff --git a/plejd/PlejdBLE.js b/plejd/PlejdBLEHandler.js similarity index 100% rename from plejd/PlejdBLE.js rename to plejd/PlejdBLEHandler.js From f93d3854d0a35a9299116a86d7def5519fb59735 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Victor=20Hagelb=C3=A4ck?= Date: Mon, 8 Feb 2021 19:54:24 +0100 Subject: [PATCH 16/33] Improve BLE resilience --- plejd/PlejdAddon.js | 31 ++----- plejd/PlejdBLEHandler.js | 180 ++++++++++++++++++++++++--------------- 2 files changed, 119 insertions(+), 92 deletions(-) diff --git a/plejd/PlejdAddon.js b/plejd/PlejdAddon.js index e0ac518..9f2a18a 100644 --- a/plejd/PlejdAddon.js +++ b/plejd/PlejdAddon.js @@ -99,16 +99,11 @@ class PlejdAddon extends EventEmitter { this.mqttClient.init(); - // init the BLE interface - this.plejdBLEHandler.on('connectFailed', () => { - logger.verbose('Were unable to connect, will retry.'); - this._bleInitLoop(); + this.plejdBLEHandler.on('connected', () => { + logger.info('Bluetooth connected. Plejd BLE up and running!'); }); - - // this.plejdBLE.init(); - - this.plejdBLEHandler.on('authenticated', () => { - logger.verbose('plejd: connected via bluetooth.'); + this.plejdBLEHandler.on('reconnecting', () => { + logger.info('Bluetooth reconnecting...'); }); // subscribe to changes from Plejd @@ -128,25 +123,13 @@ class PlejdAddon extends EventEmitter { } }); - await this._bleInitLoop(); - } - - async _bleInitLoop() { try { - if (this.bleInitTimeout) { - clearTimeout(this.bleInitTimeout); - } await this.plejdBLEHandler.init(); } catch (err) { - logger.warn('Failed BLE init, trying again in 35s', err); - this.bleInitTimer = setTimeout(() => { - try { - this._bleInitLoop(); - } catch (err2) { - logger.warn('Why do we need to catch error here?', err2); - } - }, 35000); + logger.error('Failed init() of BLE. Starting reconnect loop.'); + await this.plejdBLEHandler.startReconnectPeriodicallyLoop(); } + logger.info('Main init done'); } } diff --git a/plejd/PlejdBLEHandler.js b/plejd/PlejdBLEHandler.js index 52ff3c2..d167997 100644 --- a/plejd/PlejdBLEHandler.js +++ b/plejd/PlejdBLEHandler.js @@ -40,11 +40,14 @@ class PlejBLEHandler extends EventEmitter { adapterProperties; config; deviceRegistry; + consecutiveWriteFails; // Refer to BLE-states.md regarding the internal BLE/bluez state machine of Bluetooth states // These states refer to the state machine of this file static STATES = ['MAIN_INIT', 'GET_ADAPTER_PROXY']; + static EVENTS = ['connected', 'reconnecting', 'sceneTriggered', 'stateChanged']; + constructor(deviceRegistry) { super(); @@ -62,7 +65,7 @@ class PlejBLEHandler extends EventEmitter { this.connectEventHooked = false; this.writeQueue = []; this.writeQueueRef = null; - this.initInProgress = null; + this.reconnectInProgress = false; // Holds a reference to all characteristics this.characteristics = { @@ -74,6 +77,9 @@ class PlejBLEHandler extends EventEmitter { }; this.bus = null; + + this.on('writeFailed', (error) => this.onWriteFailed(error)); + this.on('writeSuccess', () => this.onWriteSuccess()); } async init() { @@ -81,6 +87,7 @@ class PlejBLEHandler extends EventEmitter { this.bus = dbus.systemBus(); this.adapter = null; this.adapterProperties = null; + this.consecutiveWriteFails = 0; this.cryptoKey = Buffer.from(this.deviceRegistry.cryptoKey.replace(/-/g, ''), 'hex'); @@ -102,7 +109,7 @@ class PlejBLEHandler extends EventEmitter { await this._getInterface(); await this._startGetPlejdDevice(); - logger.info('Init done'); + logger.info('BLE init done, waiting for devices.'); } async _initDiscoveredPlejdDevice(path) { @@ -135,8 +142,9 @@ class PlejBLEHandler extends EventEmitter { async _inspectDevicesDiscovered() { if (this.bleDevices.length === 0) { - logger.error('Discovery timeout elapsed, no devices found. Exiting...'); - throw new Error('Discovery timeout elapsed, no devices found'); + logger.error('Discovery timeout elapsed, no devices found. Starting reconnect loop...'); + this.startReconnectPeriodicallyLoop(); + return; } logger.info(`Device discovery done, found ${this.bleDevices.length} Plejd devices`); @@ -146,7 +154,7 @@ class PlejBLEHandler extends EventEmitter { // eslint-disable-next-line no-restricted-syntax for (const plejd of sortedDevices) { try { - console.log('Inspecting', plejd); + logger.verbose(`Inspecting ${plejd.path}`); if (plejd.instance) { logger.info(`Connecting to ${plejd.path}`); // eslint-disable-next-line no-await-in-loop @@ -184,20 +192,24 @@ class PlejBLEHandler extends EventEmitter { logger.verbose('Trying again...'); await this._startGetPlejdDevice(); } catch (errInner) { - throw new Error('Failed to retry internalInit.'); + logger.error('Failed to retry internalInit. Starting reconnect loop'); + this.startReconnectPeriodicallyLoop(); + return; } } - throw new Error( - 'Failed to start discovery. Make sure no other add-on is currently scanning.', - ); + logger.error('Failed to start discovery. Make sure no other add-on is currently scanning.'); + return; } if (!this.connectedDevice) { - logger.error('Could not connect to any Plejd device. Exiting...'); - this.emit('connectFailed'); - throw new Error('Could not connect to any Plejd device'); + logger.error('Could not connect to any Plejd device. Starting reconnect loop...'); + this.startReconnectPeriodicallyLoop(); + return; } + logger.info(`BLE Connected to ${this.connectedDevice.name}`); + this.emit('connected'); + // Connected and authenticated, start ping this.startPing(); this.startWriteQueue(); @@ -220,8 +232,7 @@ class PlejBLEHandler extends EventEmitter { const managedObjects = await this.objectManager.GetManagedObjects(); const managedPaths = Object.keys(managedObjects); - console.log('Managed objects', managedObjects); - console.log('Managed paths', managedPaths); + logger.verbose(`Managed paths${JSON.stringify(managedPaths, null, 2)}`); // eslint-disable-next-line no-restricted-syntax for (const path of managedPaths) { @@ -231,7 +242,6 @@ class PlejBLEHandler extends EventEmitter { try { // eslint-disable-next-line no-await-in-loop const adapterObject = await this.bus.getProxyObject(BLUEZ_SERVICE_NAME, path); - console.log('Got adapter results', adapterObject); // eslint-disable-next-line no-await-in-loop this.adapterProperties = await adapterObject.getInterface(DBUS_PROP_INTERFACE); // eslint-disable-next-line no-await-in-loop @@ -239,6 +249,9 @@ class PlejBLEHandler extends EventEmitter { this.adapter = adapterObject.getInterface(BLUEZ_ADAPTER_ID); // eslint-disable-next-line no-await-in-loop await this._cleanExistingConnections(managedObjects); + + logger.verbose(`Got adapter ${this.adapter.path}`); + return this.adapter; } catch (err) { logger.error(`Failed to get interface '${BLUEZ_ADAPTER_ID}'. `, err); @@ -283,7 +296,7 @@ class PlejBLEHandler extends EventEmitter { const proxyObject = await this.bus.getProxyObject(BLUEZ_SERVICE_NAME, path); const device = await proxyObject.getInterface(BLUEZ_DEVICE_ID); - logger.verbose(`Found ${path} - ${JSON.stringify(device.device)}`); + logger.verbose(`Found ${path}`); const connected = managedObjects[path][BLUEZ_DEVICE_ID].Connected.value; @@ -306,9 +319,7 @@ class PlejBLEHandler extends EventEmitter { async _startGetPlejdDevice() { logger.verbose('Setting up interfacesAdded subscription and discovery filter'); - this.objectManager.on('InterfacesAdded', (path, interfaces) => - this.onInterfacesAdded(path, interfaces), - ); + this.objectManager.on('InterfacesAdded', (path, interfaces) => this.onInterfacesAdded(path, interfaces)); this.adapter.SetDiscoveryFilter({ UUIDs: new dbus.Variant('as', [PLEJD_SERVICE]), @@ -391,11 +402,11 @@ class PlejBLEHandler extends EventEmitter { const isDimmable = this.deviceRegistry.getDevice(deviceId).dimmable; if ( - transition > 1 && - isDimmable && - (initialBrightness || initialBrightness === 0) && - (targetBrightness || targetBrightness === 0) && - targetBrightness !== initialBrightness + transition > 1 + && isDimmable + && (initialBrightness || initialBrightness === 0) + && (targetBrightness || targetBrightness === 0) + && targetBrightness !== initialBrightness ) { // Transition time set, known initial and target brightness // Calculate transition interval time based on delta brightness and max steps per second @@ -519,24 +530,33 @@ class PlejBLEHandler extends EventEmitter { } } - async throttledInit(delayMs) { - if (this.initInProgress) { + async startReconnectPeriodicallyLoop() { + if (this.reconnectInProgress) { logger.debug( - 'ThrottledInit already in progress. Skipping this call and returning existing promise.', + 'Reconnect already in progress. Skipping this call and returning existing promise.', ); - return this.initInProgress; + return; } - this.initInProgress = new Promise((resolve) => - setTimeout(async () => { - const result = await this.init().catch((err) => { - logger.error('TrottledInit exception calling init(). Will re-throw.', err); - throw err; - }); - this.initInProgress = null; - resolve(result); - }, delayMs), - ); - return this.initInProgress; + clearInterval(this.pingRef); + clearTimeout(this.writeQueueRef); + this.reconnectInProgress = true; + + /* eslint-disable no-await-in-loop */ + // eslint-disable-next-line no-constant-condition + while (true) { + try { + this.emit('reconnecting'); + logger.info('Reconnecting BLE...'); + await this.init(); + break; + } catch (err) { + logger.warn('Failed reconnecting', err); + await delay(1000); + } + } + /* eslint-enable no-await-in-loop */ + + this.reconnectInProgress = false; } async write(data) { @@ -549,6 +569,7 @@ class PlejBLEHandler extends EventEmitter { logger.verbose(`Sending ${data.length} byte(s) of data to Plejd. ${data.toString('hex')}`); const encryptedData = this._encryptDecrypt(this.cryptoKey, this.plejdService.addr, data); await this.characteristics.data.WriteValue([...encryptedData], {}); + await this.onWriteSuccess(); return true; } catch (err) { if (err.message === 'In Progress') { @@ -556,7 +577,7 @@ class PlejBLEHandler extends EventEmitter { } else { logger.debug('Write failed ', err); } - await this.throttledInit(this.config.connectionTimeout * 1000); + await this.onWriteFailed(err); return false; } } @@ -572,18 +593,36 @@ class PlejBLEHandler extends EventEmitter { } // eslint-disable-next-line class-methods-use-this - onPingSuccess(nr) { - logger.silly(`pong: ${nr}`); + onWriteSuccess() { + this.consecutiveWriteFails = 0; } - async onPingFailed(error) { - logger.debug(`onPingFailed(${error})`); - logger.info('ping failed, reconnecting.'); + async onWriteFailed(error) { + this.consecutiveWriteFails++; + logger.debug(`onWriteFailed #${this.consecutiveWriteFails} in a row.`, error); - clearInterval(this.pingRef); - return this.init().catch((err) => { - logger.error('onPingFailed exception calling init(). Will swallow error.', err); - }); + let errorIndicatesDisconnected = false; + if (error.message.contains('error: 0x0e')) { + logger.error("'Unlikely error' (0x0e) pinging Plejd. Will retry.", error); + } else if (error.message.contains('Not connected')) { + logger.error("'Not connected' (0x0e) pinging Plejd. Plejd device is probably disconnected."); + errorIndicatesDisconnected = true; + } else if ( + error.message.contains( + 'Method "WriteValue" with signature "aya{sv}" on interface "org.bluez.GattCharacteristic1" doesn\'t exist', + ) + ) { + logger.error("'Method \"WriteValue\" doesn't exist'. Plejd device is probably disconnected."); + errorIndicatesDisconnected = true; + } + + if (errorIndicatesDisconnected) { + logger.warn('Write error indicates BLE is disconnected. Reconnecting...'); + await this.startReconnectPeriodicallyLoop(); + } else if (this.consecutiveWriteFails >= 5) { + logger.warn('Write failed 5 times in a row, reconnecting....'); + await this.startReconnectPeriodicallyLoop(); + } } async ping() { @@ -596,19 +635,20 @@ class PlejBLEHandler extends EventEmitter { await this.characteristics.ping.WriteValue([...ping], {}); pong = await this.characteristics.ping.ReadValue({}); } catch (err) { - logger.error('Error writing to plejd: ', err); - this.emit('pingFailed', 'write error'); + logger.error(`Error pinging Plejd ${err.message}`); + await this.onWriteFailed(err); return; } // eslint-disable-next-line no-bitwise if (((ping[0] + 1) & 0xff) !== pong[0]) { logger.error('Plejd ping failed'); - this.emit('pingFailed', `plejd ping failed ${ping[0]} - ${pong[0]}`); + await this.onWriteFailed(new Error(`plejd ping failed ${ping[0]} - ${pong[0]}`)); return; } - this.emit('pingSuccess', pong[0]); + logger.silly(`pong: ${pong[0]}`); + await this.onWriteSuccess(); } startWriteQueue() { @@ -629,8 +669,8 @@ class PlejBLEHandler extends EventEmitter { if (this.writeQueue.some((item) => item.deviceId === queueItem.deviceId)) { logger.verbose( - `Skipping ${deviceName} (${queueItem.deviceId}) ` + - `${queueItem.log} due to more recent command in queue.`, + `Skipping ${deviceName} (${queueItem.deviceId}) ` + + `${queueItem.log} due to more recent command in queue.`, ); // Skip commands if new ones exist for the same deviceId // still process all messages in order @@ -691,17 +731,17 @@ class PlejBLEHandler extends EventEmitter { const chUuid = (await prop.Get(GATT_CHRC_ID, 'UUID')).value; if (chUuid === DATA_UUID) { - logger.debug('found DATA characteristic.'); + logger.verbose('found DATA characteristic.'); this.characteristics.data = ch; } else if (chUuid === LAST_DATA_UUID) { - logger.debug('found LAST_DATA characteristic.'); + logger.verbose('found LAST_DATA characteristic.'); this.characteristics.lastData = ch; this.characteristics.lastDataProperties = prop; } else if (chUuid === AUTH_UUID) { - logger.debug('found AUTH characteristic.'); + logger.verbose('found AUTH characteristic.'); this.characteristics.auth = ch; } else if (chUuid === PING_UUID) { - logger.debug('found PING characteristic.'); + logger.verbose('found PING characteristic.'); this.characteristics.ping = ch; } /* eslint-eslint no-await-in-loop */ @@ -715,23 +755,23 @@ class PlejBLEHandler extends EventEmitter { async _onDeviceConnected(device) { this.connectedDevice = null; logger.info('onDeviceConnected()'); - logger.debug(`Device: ${JSON.stringify(device.device)}`); + logger.debug(`Device ${device.path}, ${JSON.stringify(device.device)}`); const objects = await this.objectManager.GetManagedObjects(); const paths = Object.keys(objects); const characteristics = []; - console.log(`Iterating looking for ${GATT_CHRC_ID}`, paths); + logger.verbose(`Iterating connected devices looking for ${GATT_CHRC_ID}`); // eslint-disable-next-line no-restricted-syntax for (const path of paths) { const interfaces = Object.keys(objects[path]); - console.log('Interfaces', path, interfaces); + logger.verbose(`Interfaces ${path}: ${JSON.stringify(interfaces)}`); if (interfaces.indexOf(GATT_CHRC_ID) > -1) { characteristics.push(path); } } - console.log('Characteristics', characteristics); + logger.verbose(`Characteristics found: ${JSON.stringify(characteristics)}`); // eslint-disable-next-line no-restricted-syntax for (const path of paths) { const interfaces = Object.keys(objects[path]); @@ -744,7 +784,7 @@ class PlejBLEHandler extends EventEmitter { } } - logger.info(`trying ${chPaths.length} characteristics`); + logger.verbose(`Trying ${chPaths.length} characteristics on ${path}...`); this.plejdService = await this._processPlejdService(path, chPaths); if (this.plejdService) { @@ -763,6 +803,8 @@ class PlejBLEHandler extends EventEmitter { return null; } + logger.info('Connected device is a Plejd device with the right characteristics.'); + this.connectedDevice = device.device; await this.authenticate(); @@ -791,7 +833,7 @@ class PlejBLEHandler extends EventEmitter { if (decoded.length < 5) { if (Logger.shouldLog('debug')) { // decoded.toString() could potentially be expensive - logger.debug(`Too short raw event ignored: ${decoded.toString('hex')}`); + logger.verbose(`Too short raw event ignored: ${decoded.toString('hex')}`); } // ignore the notification since too small return; @@ -808,8 +850,10 @@ class PlejBLEHandler extends EventEmitter { const deviceName = this.deviceRegistry.getDeviceName(deviceId); if (Logger.shouldLog('debug')) { // decoded.toString() could potentially be expensive - logger.debug(`Raw event received: ${decoded.toString('hex')}`); - logger.verbose(`Device ${deviceId}, cmd ${cmd.toString(16)}, state ${state}, dim ${dim}`); + logger.verbose(`Raw event received: ${decoded.toString('hex')}`); + logger.verbose( + `Decoded: Device ${deviceId}, cmd ${cmd.toString(16)}, state ${state}, dim ${dim}`, + ); } if (cmd === BLE_CMD_DIM_CHANGE || cmd === BLE_CMD_DIM2_CHANGE) { @@ -824,7 +868,7 @@ class PlejBLEHandler extends EventEmitter { state, dim, }; - logger.verbose(`All states: ${JSON.stringify(this.plejdDevices, null, 2)}`); + logger.silly(`All states: ${JSON.stringify(this.plejdDevices, null, 2)}`); } else if (cmd === BLE_CMD_STATE_CHANGE) { logger.debug(`${deviceName} (${deviceId}) got state update. S: ${state}`); this.emit('stateChanged', deviceId, { From c6d7bc2e3ed029a35c330deae46f4f9da98b4bae Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Victor=20Hagelb=C3=A4ck?= Date: Mon, 8 Feb 2021 19:55:18 +0100 Subject: [PATCH 17/33] Minor updates and fixes --- plejd/DeviceRegistry.js | 2 +- plejd/Dockerfile | 2 +- plejd/Logger.js | 2 +- plejd/SceneManager.js | 8 ++++---- plejd/test/test.ble.bluez.js | 2 +- 5 files changed, 8 insertions(+), 8 deletions(-) diff --git a/plejd/DeviceRegistry.js b/plejd/DeviceRegistry.js index 1a26df4..9cf6ae9 100644 --- a/plejd/DeviceRegistry.js +++ b/plejd/DeviceRegistry.js @@ -28,7 +28,7 @@ class DeviceRegistry { } addScene(scene) { - this.plejdDevices[scene.id] = scene; + this.sceneDevices[scene.id] = scene; } setApiSite(siteDetails) { diff --git a/plejd/Dockerfile b/plejd/Dockerfile index b769332..9f558a9 100644 --- a/plejd/Dockerfile +++ b/plejd/Dockerfile @@ -16,7 +16,7 @@ COPY ./MqttClient.js /plejd/ COPY ./package.json /plejd/ COPY ./PlejdAddon.js /plejd/ COPY ./PlejdApi.js /plejd/ -COPY ./PlejdBLE.js /plejd/ +COPY ./PlejdBLEHandler.js /plejd/ COPY ./Scene.js /plejd/ COPY ./SceneManager.js /plejd/ COPY ./SceneStep.js /plejd/ diff --git a/plejd/Logger.js b/plejd/Logger.js index 430d27b..19bf436 100644 --- a/plejd/Logger.js +++ b/plejd/Logger.js @@ -32,7 +32,7 @@ class Logger { } static getLogLevel() { - const config = Configuration.getConfiguration(); + const config = Configuration.getOptions(); // eslint-disable-next-line max-len const level = (config.logLevel && LEVELS.find((l) => l.startsWith(config.logLevel[0].toLowerCase()))) || 'info'; diff --git a/plejd/SceneManager.js b/plejd/SceneManager.js index b1a02d6..42279cb 100644 --- a/plejd/SceneManager.js +++ b/plejd/SceneManager.js @@ -24,14 +24,14 @@ class SceneManager extends EventEmitter { this.scenes = {}; scenes.forEach((scene) => { const idx = this.deviceRegistry.apiSite.sceneIndex[scene.sceneId]; - this.scenes[scene.sceneId] = new Scene(idx, scene, this.deviceRegistry.apiSite.sceneSteps); + this.scenes[scene.id] = new Scene(idx, scene, this.deviceRegistry.apiSite.sceneSteps); }); } - executeScene(sceneIndex) { - const scene = this.scenes[sceneIndex]; + executeScene(sceneId) { + const scene = this.scenes[sceneId]; if (!scene) { - logger.info(`Scene with id ${sceneIndex} not found`); + logger.info(`Scene with id ${sceneId} not found`); logger.verbose(`Scenes: ${JSON.stringify(this.scenes, null, 2)}`); return; } diff --git a/plejd/test/test.ble.bluez.js b/plejd/test/test.ble.bluez.js index 6efed02..acd9022 100644 --- a/plejd/test/test.ble.bluez.js +++ b/plejd/test/test.ble.bluez.js @@ -1,4 +1,4 @@ -const PlejdBLE = require('../PlejdBLE'); +const PlejdBLE = require('../PlejdBLEHandler'); const cryptoKey = ''; From 7c0fc24bc6ce9e21be2167c453d3c142ba20a4b2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Victor=20Hagelb=C3=A4ck?= Date: Mon, 8 Feb 2021 20:08:40 +0100 Subject: [PATCH 18/33] Slight restructure of inspectDevicesDiscovered method to avoid scattered returns --- plejd/PlejdBLEHandler.js | 144 ++++++++++++++++++++------------------- 1 file changed, 74 insertions(+), 70 deletions(-) diff --git a/plejd/PlejdBLEHandler.js b/plejd/PlejdBLEHandler.js index d167997..9f615f9 100644 --- a/plejd/PlejdBLEHandler.js +++ b/plejd/PlejdBLEHandler.js @@ -141,87 +141,91 @@ class PlejBLEHandler extends EventEmitter { } async _inspectDevicesDiscovered() { - if (this.bleDevices.length === 0) { - logger.error('Discovery timeout elapsed, no devices found. Starting reconnect loop...'); - this.startReconnectPeriodicallyLoop(); - return; - } + try { + if (this.bleDevices.length === 0) { + logger.error('Discovery timeout elapsed, no devices found. Starting reconnect loop...'); + throw new Error('Discovery timeout elapsed'); + } - logger.info(`Device discovery done, found ${this.bleDevices.length} Plejd devices`); + logger.info(`Device discovery done, found ${this.bleDevices.length} Plejd devices`); - const sortedDevices = this.bleDevices.sort((a, b) => b.rssi - a.rssi); + const sortedDevices = this.bleDevices.sort((a, b) => b.rssi - a.rssi); + + // eslint-disable-next-line no-restricted-syntax + for (const plejd of sortedDevices) { + try { + logger.verbose(`Inspecting ${plejd.path}`); + if (plejd.instance) { + logger.info(`Connecting to ${plejd.path}`); + // eslint-disable-next-line no-await-in-loop + await plejd.instance.Connect(); + + logger.verbose('Connected. Waiting for timeout before reading characteristics...'); + // eslint-disable-next-line no-await-in-loop + await delay(this.config.connectionTimeout * 1000); + + // eslint-disable-next-line no-await-in-loop + const connectedPlejdDevice = await this._onDeviceConnected(plejd); + if (connectedPlejdDevice) { + break; + } + } + } catch (err) { + logger.warn('Unable to connect.', err); + } + } - // eslint-disable-next-line no-restricted-syntax - for (const plejd of sortedDevices) { try { - logger.verbose(`Inspecting ${plejd.path}`); - if (plejd.instance) { - logger.info(`Connecting to ${plejd.path}`); - // eslint-disable-next-line no-await-in-loop - await plejd.instance.Connect(); - - logger.verbose('Connected. Waiting for timeout before reading characteristics...'); - // eslint-disable-next-line no-await-in-loop - await delay(this.config.connectionTimeout * 1000); - - // eslint-disable-next-line no-await-in-loop - const connectedPlejdDevice = await this._onDeviceConnected(plejd); - if (connectedPlejdDevice) { - break; + logger.verbose('Stopping discovery...'); + await this.adapter.StopDiscovery(); + logger.verbose('Stopped BLE discovery'); + } catch (err) { + logger.error('Failed to stop discovery.', err); + if (err.message.includes('Operation already in progress')) { + logger.info( + 'If you continue to get "operation already in progress" error, you can try power cycling the bluetooth adapter. Get root console access, run "bluetoothctl" => "power off" => "power on" => "exit" => restart addon.', + ); + try { + await delay(250); + logger.verbose('Power cycling...'); + await this._powerCycleAdapter(); + logger.verbose('Trying again...'); + await this._startGetPlejdDevice(); + } catch (errInner) { + logger.error('Failed to retry internalInit. Starting reconnect loop', errInner); + throw new Error('Failed to retry internalInit'); } } - } catch (err) { - logger.warn('Unable to connect.', err); + logger.error('Failed to start discovery. Make sure no other add-on is currently scanning.'); + throw new Error('Failed to start discovery'); } - } - try { - logger.verbose('Stopping discovery...'); - await this.adapter.StopDiscovery(); - logger.verbose('Stopped BLE discovery'); + if (!this.connectedDevice) { + logger.error('Could not connect to any Plejd device. Starting reconnect loop...'); + throw new Error('Could not connect to any Plejd device'); + } + + logger.info(`BLE Connected to ${this.connectedDevice.name}`); + this.emit('connected'); + + // Connected and authenticated, start ping + this.startPing(); + this.startWriteQueue(); + + // After we've authenticated, we need to hook up the event listener + // for changes to lastData. + this.characteristics.lastDataProperties.on('PropertiesChanged', ( + iface, + properties, + // invalidated (third param), + ) => this.onLastDataUpdated(iface, properties)); + this.characteristics.lastData.StartNotify(); } catch (err) { - logger.error('Failed to stop discovery.', err); - if (err.message.includes('Operation already in progress')) { - logger.info( - 'If you continue to get "operation already in progress" error, you can try power cycling the bluetooth adapter. Get root console access, run "bluetoothctl" => "power off" => "power on" => "exit" => restart addon.', - ); - try { - await delay(250); - logger.verbose('Power cycling...'); - await this._powerCycleAdapter(); - logger.verbose('Trying again...'); - await this._startGetPlejdDevice(); - } catch (errInner) { - logger.error('Failed to retry internalInit. Starting reconnect loop'); - this.startReconnectPeriodicallyLoop(); - return; - } - } - logger.error('Failed to start discovery. Make sure no other add-on is currently scanning.'); - return; - } - - if (!this.connectedDevice) { - logger.error('Could not connect to any Plejd device. Starting reconnect loop...'); + // This method is run on a timer, so errors can't e re-thrown. + // Start reconnect loop if errors occur here + logger.debug(`Starting reconnect loop due to ${err.message}`); this.startReconnectPeriodicallyLoop(); - return; } - - logger.info(`BLE Connected to ${this.connectedDevice.name}`); - this.emit('connected'); - - // Connected and authenticated, start ping - this.startPing(); - this.startWriteQueue(); - - // After we've authenticated, we need to hook up the event listener - // for changes to lastData. - this.characteristics.lastDataProperties.on('PropertiesChanged', ( - iface, - properties, - // invalidated (third param), - ) => this.onLastDataUpdated(iface, properties)); - this.characteristics.lastData.StartNotify(); } async _getInterface() { From be86f08bec1c067cdd995e0524369159916b0592 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Victor=20Hagelb=C3=A4ck?= Date: Mon, 8 Feb 2021 22:23:54 +0100 Subject: [PATCH 19/33] Implement caching of API responses including setting to prefer cache to avoid api requests --- plejd/PlejdApi.js | 73 +++++++++++++++++++++++++++++++++++++++++++---- plejd/config.json | 4 ++- 2 files changed, 70 insertions(+), 7 deletions(-) diff --git a/plejd/PlejdApi.js b/plejd/PlejdApi.js index 5e2fb6a..f7f2e64 100644 --- a/plejd/PlejdApi.js +++ b/plejd/PlejdApi.js @@ -1,4 +1,5 @@ const axios = require('axios').default; +const fs = require('fs'); const Configuration = require('./Configuration'); const Logger = require('./Logger'); @@ -25,12 +26,74 @@ class PlejdApi { async init() { logger.info('init()'); - await this.login(); - await this.getSites(); - await this.getSiteDetails(); + const cache = await this.getCachedCopy(); + const cacheExists = cache && cache.siteId && cache.siteDetails && cache.sessionToken; + + logger.debug(`Prefer cache? ${this.config.preferCachedApiResponse}`); + logger.debug(`Cache exists? ${cacheExists ? `Yes, created ${cache.dtCache}` : 'No'}`); + + if (this.config.preferCachedApiResponse && cacheExists) { + logger.info( + `Cache preferred. Skipping api requests and setting api data to response from ${cache.dtCache}`, + ); + logger.silly(`Cached response: ${JSON.stringify(cache, null, 2)}`); + this.siteId = cache.siteId; + this.siteDetails = cache.siteDetails; + this.sessionToken = cache.sessionToken; + } else { + try { + await this.login(); + await this.getSites(); + await this.getSiteDetails(); + this.saveCachedCopy(); + } catch (err) { + if (cacheExists) { + logger.warn('Failed to get api response, using cached copy instead'); + this.siteId = cache.siteId; + this.siteDetails = cache.siteDetails; + this.sessionToken = cache.sessionToken; + } else { + logger.error('Api request failed, no cached fallback available', err); + throw err; + } + } + } + this.deviceRegistry.setApiSite(this.siteDetails); + this.deviceRegistry.cryptoKey = this.siteDetails.plejdMesh.cryptoKey; + this.getDevices(); } + // eslint-disable-next-line class-methods-use-this + async getCachedCopy() { + logger.info('Getting cached api response from disk'); + + try { + const rawData = await fs.promises.readFile('/data/cachedApiResponse.json'); + const cachedCopy = JSON.parse(rawData); + + return cachedCopy; + } catch (err) { + logger.warn('No cached api response could be read. This is normal on the first run', err); + return null; + } + } + + async saveCachedCopy() { + logger.info('Saving cached copy'); + try { + const rawData = JSON.stringify({ + siteId: this.siteId, + siteDetails: this.siteDetails, + sessionToken: this.sessionToken, + dtCache: new Date().toISOString(), + }); + await fs.promises.writeFile('/data/cachedApiResponse.json', rawData); + } catch (err) { + logger.error('Failed to save cache of api response', err); + } + } + async login() { logger.info('login()'); logger.info(`logging into ${this.config.site}`); @@ -115,13 +178,11 @@ class PlejdApi { } this.siteDetails = response.data.result[0]; - this.deviceRegistry.setApiSite(this.siteDetails); logger.info(`Site details for site id ${this.siteId} found`); logger.silly(JSON.stringify(this.siteDetails, null, 2)); - this.deviceRegistry.cryptoKey = this.siteDetails.plejdMesh.cryptoKey; - if (!this.deviceRegistry.cryptoKey) { + if (!this.siteDetails.plejdMesh.cryptoKey) { throw new Error('API: No crypto key set for site'); } } catch (error) { diff --git a/plejd/config.json b/plejd/config.json index aea6676..7cfccd9 100644 --- a/plejd/config.json +++ b/plejd/config.json @@ -1,6 +1,6 @@ { "name": "Plejd", - "version": "0.5.1", + "version": "0.6.0-dev", "slug": "plejd", "description": "Adds support for the Swedish home automation devices from Plejd.", "url": "https://github.com/icanos/hassio-plejd/", @@ -18,6 +18,7 @@ "mqttUsername": "", "mqttPassword": "", "includeRoomsAsLights": false, + "preferCachedApiResponse": false, "logLevel": "info", "connectionTimeout": 2, "writeQueueWaitTime": 400 @@ -30,6 +31,7 @@ "mqttUsername": "str", "mqttPassword": "str", "includeRoomsAsLights": "bool", + "preferCachedApiResponse": "bool", "logLevel": "list(error|warn|info|debug|verbose|silly)", "connectionTimeout": "int", "writeQueueWaitTime": "int" From dca491bf009ecd229288bc98634501bcd374e6bd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Victor=20Hagelb=C3=A4ck?= Date: Tue, 9 Feb 2021 19:20:09 +0100 Subject: [PATCH 20/33] Lifecycle improvement including catching dbus-next error events - Avoid UnhandledPromiseRejectionWarnings from dbus-next - Improve retry logic --- plejd/PlejdBLEHandler.js | 78 ++++++++++++++++++++++------------------ 1 file changed, 43 insertions(+), 35 deletions(-) diff --git a/plejd/PlejdBLEHandler.js b/plejd/PlejdBLEHandler.js index 9f615f9..62bf919 100644 --- a/plejd/PlejdBLEHandler.js +++ b/plejd/PlejdBLEHandler.js @@ -39,8 +39,19 @@ class PlejBLEHandler extends EventEmitter { adapter; adapterProperties; config; - deviceRegistry; + bleDevices = []; + bleDeviceTransitionTimers = {}; + bus = null; + connectedDevice = null; consecutiveWriteFails; + deviceRegistry; + discoveryTimeout = null; + plejdService = null; + plejdDevices = {}; + pingRef = null; + writeQueue = {}; + writeQueueRef = null; + reconnectInProgress = false; // Refer to BLE-states.md regarding the internal BLE/bluez state machine of Bluetooth states // These states refer to the state machine of this file @@ -54,18 +65,7 @@ class PlejBLEHandler extends EventEmitter { logger.info('Starting Plejd BLE Handler, resetting all device states.'); this.config = Configuration.getOptions(); - - this.connectedDevice = null; this.deviceRegistry = deviceRegistry; - this.plejdService = null; - this.bleDevices = []; - this.bleDeviceTransitionTimers = {}; - this.discoveryTimeout = null; - this.plejdDevices = {}; - this.connectEventHooked = false; - this.writeQueue = []; - this.writeQueueRef = null; - this.reconnectInProgress = false; // Holds a reference to all characteristics this.characteristics = { @@ -76,15 +76,23 @@ class PlejBLEHandler extends EventEmitter { ping: null, }; - this.bus = null; - this.on('writeFailed', (error) => this.onWriteFailed(error)); this.on('writeSuccess', () => this.onWriteSuccess()); } async init() { logger.info('init()'); + this.bus = dbus.systemBus(); + this.bus.on('connect', () => { + logger.verbose('dbus-next connected'); + }); + this.bus.on('error', (err) => { + // Uncaught error events will show UnhandledPromiseRejection logs + logger.verbose(`dbus-next error event: ${err.message}`); + }); + // this.bus also has a 'message' event that gets emitted _very_ frequently + this.adapter = null; this.adapterProperties = null; this.consecutiveWriteFails = 0; @@ -171,7 +179,7 @@ class PlejBLEHandler extends EventEmitter { } } } catch (err) { - logger.warn('Unable to connect.', err); + logger.warn('Unable to connect. ', err); } } @@ -350,7 +358,10 @@ class PlejBLEHandler extends EventEmitter { _scheduleInternalInit() { clearTimeout(this.discoveryTimeout); - setTimeout(() => this._inspectDevicesDiscovered(), this.config.connectionTimeout * 1000); + this.discoveryTimeout = setTimeout( + () => this._inspectDevicesDiscovered(), + this.config.connectionTimeout * 1000, + ); } async onInterfacesAdded(path, interfaces) { @@ -535,10 +546,9 @@ class PlejBLEHandler extends EventEmitter { } async startReconnectPeriodicallyLoop() { + logger.verbose('startReconnectPeriodicallyLoop'); if (this.reconnectInProgress) { - logger.debug( - 'Reconnect already in progress. Skipping this call and returning existing promise.', - ); + logger.debug('Reconnect already in progress. Skipping this call.'); return; } clearInterval(this.pingRef); @@ -549,13 +559,13 @@ class PlejBLEHandler extends EventEmitter { // eslint-disable-next-line no-constant-condition while (true) { try { + await delay(5000); this.emit('reconnecting'); logger.info('Reconnecting BLE...'); await this.init(); break; } catch (err) { - logger.warn('Failed reconnecting', err); - await delay(1000); + logger.warn('Failed reconnecting.', err); } } /* eslint-enable no-await-in-loop */ @@ -604,28 +614,26 @@ class PlejBLEHandler extends EventEmitter { async onWriteFailed(error) { this.consecutiveWriteFails++; logger.debug(`onWriteFailed #${this.consecutiveWriteFails} in a row.`, error); + logger.verbose(`Error message: ${error.message}`); let errorIndicatesDisconnected = false; if (error.message.contains('error: 0x0e')) { - logger.error("'Unlikely error' (0x0e) pinging Plejd. Will retry.", error); + logger.error("'Unlikely error' (0x0e) writing to Plejd. Will retry.", error); } else if (error.message.contains('Not connected')) { - logger.error("'Not connected' (0x0e) pinging Plejd. Plejd device is probably disconnected."); + logger.error( + "'Not connected' (0x0e) writing to Plejd. Plejd device is probably disconnected.", + ); errorIndicatesDisconnected = true; - } else if ( - error.message.contains( - 'Method "WriteValue" with signature "aya{sv}" on interface "org.bluez.GattCharacteristic1" doesn\'t exist', - ) - ) { + } else if (error.message.contains('Method "WriteValue" with signature')) { logger.error("'Method \"WriteValue\" doesn't exist'. Plejd device is probably disconnected."); errorIndicatesDisconnected = true; } - if (errorIndicatesDisconnected) { - logger.warn('Write error indicates BLE is disconnected. Reconnecting...'); - await this.startReconnectPeriodicallyLoop(); - } else if (this.consecutiveWriteFails >= 5) { - logger.warn('Write failed 5 times in a row, reconnecting....'); - await this.startReconnectPeriodicallyLoop(); + if (errorIndicatesDisconnected || this.consecutiveWriteFails >= 5) { + logger.warn( + `Write error indicates BLE is disconnected. Retry count ${this.consecutiveWriteFails}. Reconnecting...`, + ); + this.startReconnectPeriodicallyLoop(); } } @@ -882,7 +890,7 @@ class PlejBLEHandler extends EventEmitter { state, dim: 0, }; - logger.verbose(`All states: ${this.plejdDevices}`); + logger.silly(`All states: ${JSON.stringify(this.plejdDevices, null, 2)}`); } else if (cmd === BLE_CMD_SCENE_TRIG) { const sceneId = state; const sceneName = this.deviceRegistry.getSceneName(sceneId); From ef7a5086a1c7dcb3f42d0158ad12b5328010760e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Victor=20Hagelb=C3=A4ck?= Date: Wed, 10 Feb 2021 10:10:28 +0100 Subject: [PATCH 21/33] Handle when scene and device have the same Id - Catch emitted errors in Mqtt --- plejd/DeviceRegistry.js | 4 ++++ plejd/MqttClient.js | 29 +++++++++++++++++++++-------- plejd/PlejdBLEHandler.js | 2 +- plejd/SceneManager.js | 2 +- 4 files changed, 27 insertions(+), 10 deletions(-) diff --git a/plejd/DeviceRegistry.js b/plejd/DeviceRegistry.js index 9cf6ae9..647e5bc 100644 --- a/plejd/DeviceRegistry.js +++ b/plejd/DeviceRegistry.js @@ -65,6 +65,10 @@ class DeviceRegistry { return (this.plejdDevices[deviceId] || {}).name; } + getScene(sceneId) { + return this.sceneDevices[sceneId]; + } + getSceneName(sceneId) { return (this.sceneDevices[sceneId] || {}).name; } diff --git a/plejd/MqttClient.js b/plejd/MqttClient.js index 282bf65..78e3327 100644 --- a/plejd/MqttClient.js +++ b/plejd/MqttClient.js @@ -85,6 +85,10 @@ class MqttClient extends EventEmitter { password: this.config.mqttPassword, }); + this.client.on('error', (err) => { + logger.warn('Error emitted from mqtt client', err); + }); + this.client.on('connect', () => { logger.info('Connected to MQTT.'); @@ -115,17 +119,26 @@ class MqttClient extends EventEmitter { } else { const decodedTopic = decodeTopic(topic); if (decodedTopic) { - const device = this.deviceRegistry.getDevice(decodedTopic.id); - const deviceName = device ? device.name : ''; + let device = this.deviceRegistry.getDevice(decodedTopic.id); - const command = message.toString().substring(0, 1) === '{' - ? JSON.parse(message.toString()) - : message.toString(); + const messageString = message.toString(); + const isJsonMessage = messageString.startsWith('{'); + const command = isJsonMessage + ? JSON.parse(messageString) + : messageString; + + if (!isJsonMessage && messageString === 'ON' && this.deviceRegistry.getScene(decodedTopic.id)) { + // Guess that id that got state command without dim value belongs to Scene, not Device + // This guess could very well be wrong depending on the installation... + logger.warn(`Device id ${decodedTopic.id} belongs to both scene and device, guessing Scene is what should be set to ON. OFF commands still sent to device.`); + device = this.deviceRegistry.getScene(decodedTopic.id); + } + const deviceName = device ? device.name : ''; switch (decodedTopic.command) { case 'set': logger.verbose( - `Got mqtt SET command for ${decodedTopic.type}, ${deviceName} (${decodedTopic.id}): ${message}`, + `Got mqtt SET command for ${decodedTopic.type}, ${deviceName} (${decodedTopic.id}): ${messageString}`, ); if (device) { @@ -143,7 +156,7 @@ class MqttClient extends EventEmitter { `Sent mqtt ${decodedTopic.command} command for ${ decodedTopic.type }, ${deviceName} (${decodedTopic.id}). ${ - decodedTopic.command === 'availability' ? message : '' + decodedTopic.command === 'availability' ? messageString : '' }`, ); break; @@ -151,7 +164,7 @@ class MqttClient extends EventEmitter { logger.verbose(`Warning: Unknown command ${decodedTopic.command} in decoded topic`); } } else { - logger.verbose(`Warning: Got unrecognized mqtt command on '${topic}': ${message}`); + logger.verbose(`Warning: Got unrecognized mqtt command on '${topic}': ${message.toString()}`); } } }); diff --git a/plejd/PlejdBLEHandler.js b/plejd/PlejdBLEHandler.js index 62bf919..54cdd3a 100644 --- a/plejd/PlejdBLEHandler.js +++ b/plejd/PlejdBLEHandler.js @@ -49,7 +49,7 @@ class PlejBLEHandler extends EventEmitter { plejdService = null; plejdDevices = {}; pingRef = null; - writeQueue = {}; + writeQueue = []; writeQueueRef = null; reconnectInProgress = false; diff --git a/plejd/SceneManager.js b/plejd/SceneManager.js index 42279cb..a30e58b 100644 --- a/plejd/SceneManager.js +++ b/plejd/SceneManager.js @@ -24,7 +24,7 @@ class SceneManager extends EventEmitter { this.scenes = {}; scenes.forEach((scene) => { const idx = this.deviceRegistry.apiSite.sceneIndex[scene.sceneId]; - this.scenes[scene.id] = new Scene(idx, scene, this.deviceRegistry.apiSite.sceneSteps); + this.scenes[idx] = new Scene(idx, scene, this.deviceRegistry.apiSite.sceneSteps); }); } From 853511a755a360fb98486575426a051a6f9db2a9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Victor=20Hagelb=C3=A4ck?= Date: Wed, 10 Feb 2021 10:23:27 +0100 Subject: [PATCH 22/33] Clarify login 403 error message --- plejd/PlejdApi.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plejd/PlejdApi.js b/plejd/PlejdApi.js index f7f2e64..3803ea8 100644 --- a/plejd/PlejdApi.js +++ b/plejd/PlejdApi.js @@ -118,7 +118,7 @@ class PlejdApi { logger.error('Server returned status 400. probably invalid credentials, please verify.'); } else if (error.response.status === 403) { logger.error( - 'Server returned status 403, forbidden. Plejd seems to do this sometimes, despite correct credentials. Possibly waiting a long time will fix this.', + 'Server returned status 403, forbidden. Plejd service does this sometimes, despite correct credentials. Possibly throttling logins. Waiting a long time often fixes this.', ); } else { logger.error('Unable to retrieve session token response: ', error); From d133efe228ca6a3d15ac6afce04c0b4500901e50 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Victor=20Hagelb=C3=A4ck?= Date: Thu, 11 Feb 2021 22:45:13 +0100 Subject: [PATCH 23/33] Fix type-o in writeFailed error handling --- plejd/MqttClient.js | 19 +++++++++++++------ plejd/PlejdBLEHandler.js | 20 +++++++++++--------- 2 files changed, 24 insertions(+), 15 deletions(-) diff --git a/plejd/MqttClient.js b/plejd/MqttClient.js index 78e3327..de0b09d 100644 --- a/plejd/MqttClient.js +++ b/plejd/MqttClient.js @@ -123,14 +123,19 @@ class MqttClient extends EventEmitter { const messageString = message.toString(); const isJsonMessage = messageString.startsWith('{'); - const command = isJsonMessage - ? JSON.parse(messageString) - : messageString; + const command = isJsonMessage ? JSON.parse(messageString) : messageString; - if (!isJsonMessage && messageString === 'ON' && this.deviceRegistry.getScene(decodedTopic.id)) { + if ( + !isJsonMessage + && messageString === 'ON' + && this.deviceRegistry.getScene(decodedTopic.id) + ) { // Guess that id that got state command without dim value belongs to Scene, not Device // This guess could very well be wrong depending on the installation... - logger.warn(`Device id ${decodedTopic.id} belongs to both scene and device, guessing Scene is what should be set to ON. OFF commands still sent to device.`); + logger.warn( + `Device id ${decodedTopic.id} belongs to both scene and device, guessing Scene is what should be set to ON.` + + 'OFF commands still sent to device.', + ); device = this.deviceRegistry.getScene(decodedTopic.id); } const deviceName = device ? device.name : ''; @@ -164,7 +169,9 @@ class MqttClient extends EventEmitter { logger.verbose(`Warning: Unknown command ${decodedTopic.command} in decoded topic`); } } else { - logger.verbose(`Warning: Got unrecognized mqtt command on '${topic}': ${message.toString()}`); + logger.verbose( + `Warning: Got unrecognized mqtt command on '${topic}': ${message.toString()}`, + ); } } }); diff --git a/plejd/PlejdBLEHandler.js b/plejd/PlejdBLEHandler.js index 54cdd3a..e48b78d 100644 --- a/plejd/PlejdBLEHandler.js +++ b/plejd/PlejdBLEHandler.js @@ -617,17 +617,17 @@ class PlejBLEHandler extends EventEmitter { logger.verbose(`Error message: ${error.message}`); let errorIndicatesDisconnected = false; - if (error.message.contains('error: 0x0e')) { + + if (error.message.includes('error: 0x0e')) { logger.error("'Unlikely error' (0x0e) writing to Plejd. Will retry.", error); - } else if (error.message.contains('Not connected')) { - logger.error( - "'Not connected' (0x0e) writing to Plejd. Plejd device is probably disconnected.", - ); + } else if (error.message.includes('Not connected')) { + logger.error("'Not connected' writing to Plejd. Plejd device is probably disconnected."); errorIndicatesDisconnected = true; - } else if (error.message.contains('Method "WriteValue" with signature')) { + } else if (error.message.includes('Method "WriteValue" with signature')) { logger.error("'Method \"WriteValue\" doesn't exist'. Plejd device is probably disconnected."); errorIndicatesDisconnected = true; } + logger.verbose(`Made it ${errorIndicatesDisconnected} || ${this.consecutiveWriteFails >= 5}`); if (errorIndicatesDisconnected || this.consecutiveWriteFails >= 5) { logger.warn( @@ -647,14 +647,14 @@ class PlejBLEHandler extends EventEmitter { await this.characteristics.ping.WriteValue([...ping], {}); pong = await this.characteristics.ping.ReadValue({}); } catch (err) { - logger.error(`Error pinging Plejd ${err.message}`); + logger.verbose(`Error pinging Plejd, calling onWriteFailed... ${err.message}`); await this.onWriteFailed(err); return; } // eslint-disable-next-line no-bitwise if (((ping[0] + 1) & 0xff) !== pong[0]) { - logger.error('Plejd ping failed'); + logger.verbose('Plejd ping failed, pong contains wrong data. Calling onWriteFailed...'); await this.onWriteFailed(new Error(`plejd ping failed ${ping[0]} - ${pong[0]}`)); return; } @@ -901,7 +901,9 @@ class PlejBLEHandler extends EventEmitter { this.emit('sceneTriggered', deviceId, sceneId); } else if (cmd === 0x1b) { - logger.silly('Command 001b seems to be some kind of often repeating ping/mesh data'); + logger.silly( + 'Command 001b is the time of the Plejd devices command, not implemented currently', + ); } else { logger.verbose( `Command ${cmd.toString(16)} unknown. ${decoded.toString( From a3244cd6fc9ce6f641d30a6a1f3b066cb5ae6629 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Victor=20Hagelb=C3=A4ck?= Date: Sat, 13 Feb 2021 12:58:18 +0100 Subject: [PATCH 24/33] Read version info from config instead of hard-coded string --- plejd/Configuration.js | 55 ++++++++++++++++++++++++++++++------------ plejd/main.js | 17 ++++++++----- 2 files changed, 51 insertions(+), 21 deletions(-) diff --git a/plejd/Configuration.js b/plejd/Configuration.js index 690c472..88a02b3 100644 --- a/plejd/Configuration.js +++ b/plejd/Configuration.js @@ -2,27 +2,52 @@ const fs = require('fs'); class Configuration { static _options = null; + static _addonInfo = null; static getOptions() { if (!Configuration._options) { - const rawData = fs.readFileSync('/data/options.json'); - const config = JSON.parse(rawData); - - const defaultRawData = fs.readFileSync('/plejd/config.json'); - const defaultConfig = JSON.parse(defaultRawData).options; - - Configuration._options = { ...defaultConfig, ...config }; - - // eslint-disable-next-line no-console - console.log('Config:', { - ...Configuration._options, - username: '---scrubbed---', - password: '---scrubbed---', - mqttPassword: '---scrubbed---', - }); + Configuration._hydrateCache(); } return Configuration._options; } + + static getAddonInfo() { + if (!Configuration._addonInfo) { + Configuration._hydrateCache(); + } + return Configuration._addonInfo; + } + + static _hydrateCache() { + const rawData = fs.readFileSync('/data/options.json'); + const config = JSON.parse(rawData); + + const defaultRawData = fs.readFileSync('/plejd/config.json'); + const defaultConfig = JSON.parse(defaultRawData); + + Configuration._options = { ...defaultConfig.options, ...config }; + Configuration._addonInfo = { + name: defaultConfig.name, + version: defaultConfig.version, + slug: defaultConfig.slug, + description: defaultConfig.description, + url: defaultConfig.url, + arch: defaultConfig.arch, + startup: defaultConfig.startup, + boot: defaultConfig.boot, + host_network: defaultConfig.host_network, + host_dbus: defaultConfig.host_dbus, + apparmor: defaultConfig.apparmor, + }; + + // eslint-disable-next-line no-console + console.log('Config:', { + ...Configuration._options, + username: '---scrubbed---', + password: '---scrubbed---', + mqttPassword: '---scrubbed---', + }); + } } module.exports = Configuration; diff --git a/plejd/main.js b/plejd/main.js index fa54005..ce03914 100644 --- a/plejd/main.js +++ b/plejd/main.js @@ -1,13 +1,17 @@ +const Configuration = require('./Configuration'); const Logger = require('./Logger'); const PlejdAddon = require('./PlejdAddon'); -const logger = Logger.getLogger('plejd-main'); - -const version = '0.5.1'; - async function main() { try { - logger.info(`Starting Plejd add-on v. ${version}`); + // eslint-disable-next-line no-console + console.log('Starting Plejd addon and reading configuration...'); + + const addonInfo = Configuration.getAddonInfo(); + const logger = Logger.getLogger('plejd-main'); + + logger.info(`Plejd add-on, version ${addonInfo.version}`); + logger.verbose(`Addon info: ${JSON.stringify(addonInfo)}`); const addon = new PlejdAddon(); @@ -15,7 +19,8 @@ async function main() { logger.info('main() finished'); } catch (err) { - logger.error('Catastrophic error. Resetting entire addon in 1 minute', err); + // eslint-disable-next-line no-console + console.log('Catastrophic error. Resetting entire addon in 1 minute', err); setTimeout(() => main(), 60000); } } From 017858e3a7b122aa516dccfa73f6f049c7d4f195 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Victor=20Hagelb=C3=A4ck?= Date: Tue, 16 Feb 2021 21:15:04 +0100 Subject: [PATCH 25/33] Update version to 0.6.0 --- plejd/config.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plejd/config.json b/plejd/config.json index 7cfccd9..a160a8f 100644 --- a/plejd/config.json +++ b/plejd/config.json @@ -1,6 +1,6 @@ { "name": "Plejd", - "version": "0.6.0-dev", + "version": "0.6.0", "slug": "plejd", "description": "Adds support for the Swedish home automation devices from Plejd.", "url": "https://github.com/icanos/hassio-plejd/", From 245fe6f487cffc453ff6d3b05f10f0fc3d799bd1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Victor=20Hagelb=C3=A4ck?= Date: Tue, 16 Feb 2021 21:38:15 +0100 Subject: [PATCH 26/33] Update changelog --- plejd/CHANGELOG.md | 24 +++++++++++++++++++++++- 1 file changed, 23 insertions(+), 1 deletion(-) diff --git a/plejd/CHANGELOG.md b/plejd/CHANGELOG.md index bb82c4b..47b2018 100644 --- a/plejd/CHANGELOG.md +++ b/plejd/CHANGELOG.md @@ -1,5 +1,27 @@ # Changelog hassio-plejd Home Assistant Plejd addon +### [0.6.0](https://github.com/icanos/hassio-plejd/tree/0.6.0) (2021-01-30) + +[Full Changelog](https://github.com/icanos/hassio-plejd/compare/0.5.1...0.6.0) + +**Implemented enhancements:** + +- Code restructure testing/input/code review [\#158](https://github.com/icanos/hassio-plejd/issues/158) +- Offline mode [\#148](https://github.com/icanos/hassio-plejd/issues/148) + +**Fixed bugs:** + +- Brightness level incorrect with RTR-01 and WPH-01 [\#159](https://github.com/icanos/hassio-plejd/issues/159) + +**Closed issues:** + +- \[plejd-api\] Unable to retrieve session token response: Request failed with status code 403 Error: Request failed with status code 403 [\#162](https://github.com/icanos/hassio-plejd/issues/162) +- Can't turn on/off lights after last update [\#157](https://github.com/icanos/hassio-plejd/issues/157) +- Brightness level incorrect when changing with RTR-01 or WPH-01 [\#138](https://github.com/icanos/hassio-plejd/issues/138) +- plejd-ble reconnect attempts [\#123](https://github.com/icanos/hassio-plejd/issues/123) +- unable to retrieve session token response: Error: Request failed with status code 404 \(and 403\) [\#99](https://github.com/icanos/hassio-plejd/issues/99) +- Unable to scan BT Plejd [\#97](https://github.com/icanos/hassio-plejd/issues/97) + ### [0.5.1](https://github.com/icanos/hassio-plejd/tree/0.5.1) (2021-01-30) [Full Changelog](https://github.com/icanos/hassio-plejd/compare/0.5.0...0.5.1) @@ -19,7 +41,7 @@ **Implemented enhancements:** -- Adjust code to airbnb style guid, including eslint rules and prettier config +- Adjust code to airbnb style guide, including eslint rules and prettier config - Updated dependencies - Improved readme with info about installation, debugging, and logging From 062bfca11aa8beb81b30600bb30ccdda83603ba8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Victor=20Hagelb=C3=A4ck?= Date: Thu, 18 Feb 2021 10:35:24 +0100 Subject: [PATCH 27/33] Changelog lint fix --- plejd/CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plejd/CHANGELOG.md b/plejd/CHANGELOG.md index 47b2018..bcd950b 100644 --- a/plejd/CHANGELOG.md +++ b/plejd/CHANGELOG.md @@ -15,7 +15,7 @@ **Closed issues:** -- \[plejd-api\] Unable to retrieve session token response: Request failed with status code 403 Error: Request failed with status code 403 [\#162](https://github.com/icanos/hassio-plejd/issues/162) +- \[plejd-api\] Unable to retrieve session token response: Request failed with status code 403 Error: Request failed with status code 403 [\#162](https://github.com/icanos/hassio-plejd/issues/162) - Can't turn on/off lights after last update [\#157](https://github.com/icanos/hassio-plejd/issues/157) - Brightness level incorrect when changing with RTR-01 or WPH-01 [\#138](https://github.com/icanos/hassio-plejd/issues/138) - plejd-ble reconnect attempts [\#123](https://github.com/icanos/hassio-plejd/issues/123) From 12ec9a1b7c6379c79dbc886f4c91ff47b94614ec Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Victor=20Hagelb=C3=A4ck?= Date: Thu, 18 Feb 2021 10:47:33 +0100 Subject: [PATCH 28/33] Implement time parsing and periodic time updating - Relates to #130 --- plejd/PlejdBLEHandler.js | 96 +++++++++++++++++++++++++++++++++------- 1 file changed, 79 insertions(+), 17 deletions(-) diff --git a/plejd/PlejdBLEHandler.js b/plejd/PlejdBLEHandler.js index e48b78d..7e321ca 100644 --- a/plejd/PlejdBLEHandler.js +++ b/plejd/PlejdBLEHandler.js @@ -16,10 +16,13 @@ const LAST_DATA_UUID = `31ba0005-${BLE_UUID_SUFFIX}`; const AUTH_UUID = `31ba0009-${BLE_UUID_SUFFIX}`; const PING_UUID = `31ba000a-${BLE_UUID_SUFFIX}`; -const BLE_CMD_DIM_CHANGE = 0xc8; -const BLE_CMD_DIM2_CHANGE = 0x98; -const BLE_CMD_STATE_CHANGE = 0x97; -const BLE_CMD_SCENE_TRIG = 0x21; +const BLE_CMD_DIM_CHANGE = 0x00c8; +const BLE_CMD_DIM2_CHANGE = 0x0098; +const BLE_CMD_STATE_CHANGE = 0x0097; +const BLE_CMD_SCENE_TRIG = 0x0021; +const BLE_CMD_TIME_UPDATE = 0x001b; + +const BLE_BROADCAST_DEVICE_ID = 0x01; const BLUEZ_SERVICE_NAME = 'org.bluez'; const DBUS_OM_INTERFACE = 'org.freedesktop.DBus.ObjectManager'; @@ -84,13 +87,13 @@ class PlejBLEHandler extends EventEmitter { logger.info('init()'); this.bus = dbus.systemBus(); - this.bus.on('connect', () => { - logger.verbose('dbus-next connected'); - }); this.bus.on('error', (err) => { // Uncaught error events will show UnhandledPromiseRejection logs logger.verbose(`dbus-next error event: ${err.message}`); }); + this.bus.on('connect', () => { + logger.verbose('dbus-next connected'); + }); // this.bus also has a 'message' event that gets emitted _very_ frequently this.adapter = null; @@ -216,7 +219,8 @@ class PlejBLEHandler extends EventEmitter { logger.info(`BLE Connected to ${this.connectedDevice.name}`); this.emit('connected'); - // Connected and authenticated, start ping + // Connected and authenticated, request current time and start ping + this.requestCurrentPlejdTime(); this.startPing(); this.startWriteQueue(); @@ -663,6 +667,25 @@ class PlejBLEHandler extends EventEmitter { await this.onWriteSuccess(); } + async requestCurrentPlejdTime() { + logger.info('Requesting current Plejd time...'); + + // Eg: 0b0102001b: 0b: id, 0102: read, 001b: time + const payload = Buffer.from( + `${this.connectedDevice.id.toString(16).padStart(2, '0')}0102${BLE_CMD_TIME_UPDATE.toString( + 16, + ).padStart(4, '0')}`, + 'hex', + ); + this.writeQueue.unshift({ + deviceId: this.connectedDevice.id, + log: 'RequestTime', + shouldRetry: true, + payload, + }); + setTimeout(() => this.requestCurrentPlejdTime(), 1000 * 3600); // Once per hour + } + startWriteQueue() { logger.info('startWriteQueue()'); clearTimeout(this.writeQueueRef); @@ -852,15 +875,21 @@ class PlejBLEHandler extends EventEmitter { } const deviceId = decoded.readUInt8(0); - // What is bytes 2-3? - const cmd = decoded.readUInt8(4); + // Bytes 2-3 is Command/Request + const cmd = decoded.readUInt16BE(3); + const state = decoded.length > 5 ? decoded.readUInt8(5) : 0; - // What is byte 6? + const dim = decoded.length > 7 ? decoded.readUInt8(7) : 0; - // Bytes 8-9 are sometimes present, what are they? + + if (Logger.shouldLog('silly')) { + // Full dim level is 2 bytes, we could potentially use this + const dimFull = decoded.length > 7 ? decoded.readUInt16LE(6) : 0; + logger.silly(`Dim: ${dim.toString(16)}, full precision: ${dimFull.toString(16)}`); + } const deviceName = this.deviceRegistry.getDeviceName(deviceId); - if (Logger.shouldLog('debug')) { + if (Logger.shouldLog('verbose')) { // decoded.toString() could potentially be expensive logger.verbose(`Raw event received: ${decoded.toString('hex')}`); logger.verbose( @@ -900,10 +929,43 @@ class PlejBLEHandler extends EventEmitter { ); this.emit('sceneTriggered', deviceId, sceneId); - } else if (cmd === 0x1b) { - logger.silly( - 'Command 001b is the time of the Plejd devices command, not implemented currently', - ); + } else if (cmd === BLE_CMD_TIME_UPDATE) { + const now = new Date(); + // Guess Plejd timezone based on HA time zone + const offsetSecondsGuess = now.getTimezoneOffset() * 60; + + // Plejd reports local unix timestamp adjust to local time zone + const plejdTimestampUTC = (decoded.readInt32LE(5) + offsetSecondsGuess) * 1000; + const diffSeconds = Math.round((plejdTimestampUTC - now.getTime()) / 1000); + if ( + deviceId !== BLE_BROADCAST_DEVICE_ID + || Logger.shouldLog('verbose') + || Math.abs(diffSeconds) > 60 + ) { + const plejdTime = new Date(plejdTimestampUTC); + logger.debug(`Plejd time update ${plejdTime.toString()}, diff ${diffSeconds} seconds`); + if (Math.abs(diffSeconds) > 60) { + logger.warn( + `Plejd time off by more than 1 minute. Reported time: ${plejdTime.toString()}, diff ${diffSeconds} seconds`, + ); + const newLocalTimestamp = now.getTime() / 1000 - offsetSecondsGuess; + logger.info(`Setting time to ${now.toString()}`); + const payload = Buffer.alloc(10); + // E.g: 00 0110 001b 38df2360 00 + // 00: set?, 0110: don't respond, 001b: time command, 38df236000: the time + payload.write('000110001b', 0, 'hex'); + payload.writeInt32LE(Math.trunc(newLocalTimestamp), 5); + payload.write('00', 9, 'hex'); + this.writeQueue.unshift({ + deviceId: this.connectedDevice.id, + log: 'SetTime', + shouldRetry: true, + payload, + }); + } else if (deviceId !== BLE_BROADCAST_DEVICE_ID) { + logger.info('Got time response. Plejd time in sync with Home Assistant time'); + } + } } else { logger.verbose( `Command ${cmd.toString(16)} unknown. ${decoded.toString( From 74716557c82c797035ee5cd9c9ba98f888343525 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Victor=20Hagelb=C3=A4ck?= Date: Thu, 18 Feb 2021 20:21:57 +0100 Subject: [PATCH 29/33] Set Plejd time only hourly when explicityly requesting --- plejd/PlejdBLEHandler.js | 38 ++++++++++++++++++++------------------ 1 file changed, 20 insertions(+), 18 deletions(-) diff --git a/plejd/PlejdBLEHandler.js b/plejd/PlejdBLEHandler.js index 7e321ca..a49bbcb 100644 --- a/plejd/PlejdBLEHandler.js +++ b/plejd/PlejdBLEHandler.js @@ -220,7 +220,7 @@ class PlejBLEHandler extends EventEmitter { this.emit('connected'); // Connected and authenticated, request current time and start ping - this.requestCurrentPlejdTime(); + this._requestCurrentPlejdTime(); this.startPing(); this.startWriteQueue(); @@ -667,7 +667,7 @@ class PlejBLEHandler extends EventEmitter { await this.onWriteSuccess(); } - async requestCurrentPlejdTime() { + async _requestCurrentPlejdTime() { logger.info('Requesting current Plejd time...'); // Eg: 0b0102001b: 0b: id, 0102: read, 001b: time @@ -683,7 +683,7 @@ class PlejBLEHandler extends EventEmitter { shouldRetry: true, payload, }); - setTimeout(() => this.requestCurrentPlejdTime(), 1000 * 3600); // Once per hour + setTimeout(() => this._requestCurrentPlejdTime(), 1000 * 3600); // Once per hour } startWriteQueue() { @@ -946,22 +946,24 @@ class PlejBLEHandler extends EventEmitter { logger.debug(`Plejd time update ${plejdTime.toString()}, diff ${diffSeconds} seconds`); if (Math.abs(diffSeconds) > 60) { logger.warn( - `Plejd time off by more than 1 minute. Reported time: ${plejdTime.toString()}, diff ${diffSeconds} seconds`, + `Plejd time off by more than 1 minute. Reported time: ${plejdTime.toString()}, diff ${diffSeconds} seconds. Time will be set hourly.`, ); - const newLocalTimestamp = now.getTime() / 1000 - offsetSecondsGuess; - logger.info(`Setting time to ${now.toString()}`); - const payload = Buffer.alloc(10); - // E.g: 00 0110 001b 38df2360 00 - // 00: set?, 0110: don't respond, 001b: time command, 38df236000: the time - payload.write('000110001b', 0, 'hex'); - payload.writeInt32LE(Math.trunc(newLocalTimestamp), 5); - payload.write('00', 9, 'hex'); - this.writeQueue.unshift({ - deviceId: this.connectedDevice.id, - log: 'SetTime', - shouldRetry: true, - payload, - }); + if (this.connectedDevice && deviceId === this.connectedDevice.id) { + const newLocalTimestamp = (now.getTime() - offsetSecondsGuess) / 1000; + logger.info(`Setting time to ${now.toString()}`); + const payload = Buffer.alloc(10); + // E.g: 00 0110 001b 38df2360 00 + // 00: set?, 0110: don't respond, 001b: time command, 38df236000: the time + payload.write('000110001b', 0, 'hex'); + payload.writeInt32LE(Math.trunc(newLocalTimestamp), 5); + payload.write('00', 9, 'hex'); + this.writeQueue.unshift({ + deviceId: this.connectedDevice.id, + log: 'SetTime', + shouldRetry: true, + payload, + }); + } } else if (deviceId !== BLE_BROADCAST_DEVICE_ID) { logger.info('Got time response. Plejd time in sync with Home Assistant time'); } From fa0ba6be31a56568b46345d0eadb389ef737855d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Victor=20Hagelb=C3=A4ck?= Date: Thu, 18 Feb 2021 21:17:29 +0100 Subject: [PATCH 30/33] Config setting to opt-in to setting Plejd clock time --- plejd/PlejdBLEHandler.js | 18 ++++++++++++------ plejd/README.md | 27 ++++++++++++++------------- plejd/config.json | 2 ++ 3 files changed, 28 insertions(+), 19 deletions(-) diff --git a/plejd/PlejdBLEHandler.js b/plejd/PlejdBLEHandler.js index a49bbcb..418a072 100644 --- a/plejd/PlejdBLEHandler.js +++ b/plejd/PlejdBLEHandler.js @@ -220,7 +220,11 @@ class PlejBLEHandler extends EventEmitter { this.emit('connected'); // Connected and authenticated, request current time and start ping - this._requestCurrentPlejdTime(); + if (this.config.updatePlejdClock) { + this._requestCurrentPlejdTime(); + } else { + logger.info('Plejd clock updates disabled in configuration.'); + } this.startPing(); this.startWriteQueue(); @@ -668,7 +672,7 @@ class PlejBLEHandler extends EventEmitter { } async _requestCurrentPlejdTime() { - logger.info('Requesting current Plejd time...'); + logger.info('Requesting current Plejd clock time...'); // Eg: 0b0102001b: 0b: id, 0102: read, 001b: time const payload = Buffer.from( @@ -943,10 +947,12 @@ class PlejBLEHandler extends EventEmitter { || Math.abs(diffSeconds) > 60 ) { const plejdTime = new Date(plejdTimestampUTC); - logger.debug(`Plejd time update ${plejdTime.toString()}, diff ${diffSeconds} seconds`); - if (Math.abs(diffSeconds) > 60) { + logger.debug( + `Plejd clock time update ${plejdTime.toString()}, diff ${diffSeconds} seconds`, + ); + if (this.config.updatePlejdClock && Math.abs(diffSeconds) > 60) { logger.warn( - `Plejd time off by more than 1 minute. Reported time: ${plejdTime.toString()}, diff ${diffSeconds} seconds. Time will be set hourly.`, + `Plejd clock time off by more than 1 minute. Reported time: ${plejdTime.toString()}, diff ${diffSeconds} seconds. Time will be set hourly.`, ); if (this.connectedDevice && deviceId === this.connectedDevice.id) { const newLocalTimestamp = (now.getTime() - offsetSecondsGuess) / 1000; @@ -965,7 +971,7 @@ class PlejBLEHandler extends EventEmitter { }); } } else if (deviceId !== BLE_BROADCAST_DEVICE_ID) { - logger.info('Got time response. Plejd time in sync with Home Assistant time'); + logger.info('Got time response. Plejd clock time in sync with Home Assistant time'); } } } else { diff --git a/plejd/README.md b/plejd/README.md index 3791b1f..3a16cf0 100644 --- a/plejd/README.md +++ b/plejd/README.md @@ -121,20 +121,21 @@ The above is used to notify the add-on when Home Assistant has started successfu The plugin needs you to configure some settings before working. You find these on the Add-on page after you've installed it. -| Parameter | Value | -| -------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | -| site | Name of your Plejd site, the name is displayed in the Plejd app (top bar). | -| username | Username of your Plejd account, this is used to fetch the crypto key and devices from the Plejd API. | -| password | Password of your Plejd account, this is used to fetch the crypto key and devices from the Plejd API. | -| mqttBroker | URL of the MQTT Broker, eg. mqtt://localhost | -| mqttUsername | Username of the MQTT broker | -| mqttPassword | Password of the MQTT broker | -| includeRoomsAsLights | Adds all rooms as lights, making it possible to turn on/off lights by room instead. Setting this to false will ignore all rooms. | -| logLevel | Minimim log level. Supported values are `error`, `warn`, `info`, `debug`, `verbose`, `silly` with increasing amount of logging. Do not log more than `info` for production purposes. | -| connectionTimeout | Number of seconds to wait when scanning and connecting. Might need to be tweaked on platforms other than RPi 4. Defaults to: 2 seconds. | -| writeQueueWaitTime | Wait time between message sent to Plejd over BLE, defaults to 400. If that doesn't work, try changing the value higher in steps of 50. | +| Parameter | Value | +| -------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| site | Name of your Plejd site, the name is displayed in the Plejd app (top bar). | +| username | Username of your Plejd account, this is used to fetch the crypto key and devices from the Plejd API. | +| password | Password of your Plejd account, this is used to fetch the crypto key and devices from the Plejd API. | +| mqttBroker | URL of the MQTT Broker, eg. mqtt://localhost | +| mqttUsername | Username of the MQTT broker | +| mqttPassword | Password of the MQTT broker | +| includeRoomsAsLights | Adds all rooms as lights, making it possible to turn on/off lights by room instead. Setting this to false will ignore all rooms. | +| updatePlejdClock | Hourly update Plejd devices' clock if out of sync. Clock is used for time-based scenes. Not recommended if you have a Plejd gateway. Clock updates may flicker scene-controlled devices. | +| logLevel | Minimim log level. Supported values are `error`, `warn`, `info`, `debug`, `verbose`, `silly` with increasing amount of logging. Do not log more than `info` for production purposes. | +| connectionTimeout | Number of seconds to wait when scanning and connecting. Might need to be tweaked on platforms other than RPi 4. Defaults to: 2 seconds. | +| writeQueueWaitTime | Wait time between message sent to Plejd over BLE, defaults to 400. If that doesn't work, try changing the value higher in steps of 50. | -## Having issues to get the addon working? +## Troubleshooting If you're having issues to get the addon working, there are a few things you can look into: diff --git a/plejd/config.json b/plejd/config.json index a160a8f..5c545ea 100644 --- a/plejd/config.json +++ b/plejd/config.json @@ -19,6 +19,7 @@ "mqttPassword": "", "includeRoomsAsLights": false, "preferCachedApiResponse": false, + "updatePlejdClock": false, "logLevel": "info", "connectionTimeout": 2, "writeQueueWaitTime": 400 @@ -32,6 +33,7 @@ "mqttPassword": "str", "includeRoomsAsLights": "bool", "preferCachedApiResponse": "bool", + "updatePlejdClock": "bool", "logLevel": "list(error|warn|info|debug|verbose|silly)", "connectionTimeout": "int", "writeQueueWaitTime": "int" From 99f073ba843628da0e891f759ad2384612ea040f Mon Sep 17 00:00:00 2001 From: Victor Date: Thu, 18 Feb 2021 21:38:11 +0100 Subject: [PATCH 31/33] Fix error subtracting time zone diff to time set command --- plejd/PlejdBLEHandler.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plejd/PlejdBLEHandler.js b/plejd/PlejdBLEHandler.js index 418a072..bef0441 100644 --- a/plejd/PlejdBLEHandler.js +++ b/plejd/PlejdBLEHandler.js @@ -955,7 +955,7 @@ class PlejBLEHandler extends EventEmitter { `Plejd clock time off by more than 1 minute. Reported time: ${plejdTime.toString()}, diff ${diffSeconds} seconds. Time will be set hourly.`, ); if (this.connectedDevice && deviceId === this.connectedDevice.id) { - const newLocalTimestamp = (now.getTime() - offsetSecondsGuess) / 1000; + const newLocalTimestamp = now.getTime() / 1000 - offsetSecondsGuess; logger.info(`Setting time to ${now.toString()}`); const payload = Buffer.alloc(10); // E.g: 00 0110 001b 38df2360 00 From 2702365a9576d7f0fa1e7d0e5d772d6e98ea9240 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Victor=20Hagelb=C3=A4ck?= Date: Sat, 20 Feb 2021 08:18:40 +0100 Subject: [PATCH 32/33] Prepeare 0.6.1 release --- plejd/CHANGELOG.md | 12 ++++++++++++ plejd/config.json | 2 +- 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/plejd/CHANGELOG.md b/plejd/CHANGELOG.md index bcd950b..4c70de7 100644 --- a/plejd/CHANGELOG.md +++ b/plejd/CHANGELOG.md @@ -1,5 +1,17 @@ # Changelog hassio-plejd Home Assistant Plejd addon +### [0.6.1](https://github.com/icanos/hassio-plejd/tree/HEAD) (2021-02-20) + +[Full Changelog](https://github.com/icanos/hassio-plejd/compare/0.6.0...0.6.1) + +**Implemented enhancements:** + +- Feature Request: Support setting the Plejd Network System Clock [\#130](https://github.com/icanos/hassio-plejd/issues/130) + +**Closed issues:** + +- Set Plejd devices' clock hourly [\#165](https://github.com/icanos/hassio-plejd/issues/165) + ### [0.6.0](https://github.com/icanos/hassio-plejd/tree/0.6.0) (2021-01-30) [Full Changelog](https://github.com/icanos/hassio-plejd/compare/0.5.1...0.6.0) diff --git a/plejd/config.json b/plejd/config.json index 5c545ea..8deb919 100644 --- a/plejd/config.json +++ b/plejd/config.json @@ -1,6 +1,6 @@ { "name": "Plejd", - "version": "0.6.0", + "version": "0.6.1", "slug": "plejd", "description": "Adds support for the Swedish home automation devices from Plejd.", "url": "https://github.com/icanos/hassio-plejd/", From 99b70a1d6dc4b908ff0bf5f9a34fd9cce23dfed0 Mon Sep 17 00:00:00 2001 From: Victor Date: Sat, 20 Feb 2021 08:23:02 +0100 Subject: [PATCH 33/33] Update CHANGELOG.md --- plejd/CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plejd/CHANGELOG.md b/plejd/CHANGELOG.md index 4c70de7..9200036 100644 --- a/plejd/CHANGELOG.md +++ b/plejd/CHANGELOG.md @@ -1,6 +1,6 @@ # Changelog hassio-plejd Home Assistant Plejd addon -### [0.6.1](https://github.com/icanos/hassio-plejd/tree/HEAD) (2021-02-20) +### [0.6.1](https://github.com/icanos/hassio-plejd/tree/0.6.1) (2021-02-20) [Full Changelog](https://github.com/icanos/hassio-plejd/compare/0.6.0...0.6.1)