From 6524b7bd9b842e401dbecd03a45df77e28cc042b Mon Sep 17 00:00:00 2001 From: Gergely Markics <5822419+ugrug@users.noreply.github.com> Date: Sat, 29 Dec 2018 19:55:59 +0100 Subject: [PATCH] Add device availability functionality for HASS based on router devices ping and attribute reporting also available on battery-powered devices (#761) * Discovery on HASS restart and last_message attribute added - On restarting Home Assistant, resending device discovery information - Add timestamp on receiving message from Zigbee * Add option: add_timestamp in settings * typo * Update homeassistant.js * Update homeassistant.js * Update homeassistant.js * Update controller.js * Update zigbee.js * Add files via upload * Update zigbee.js * Update deviceAvailabilityHandler.js * Update deviceAvailabilityHandler.js * Update deviceAvailabilityHandler.js * Update deviceAvailabilityHandler.js * Update deviceAvailabilityHandler.js * Update deviceAvailabilityHandler.js * Update deviceAvailabilityHandler.js * Update homeassistant.js * Update deviceAvailabilityHandler.js * Update deviceAvailabilityHandler.js * Update homeassistant.js * Fix checkonline callback. * Refactor. * Refactor. --- lib/controller.js | 7 ++ lib/extension/deviceAvailability.js | 111 ++++++++++++++++++++++++++++ lib/extension/homeassistant.js | 10 ++- lib/util/settings.js | 4 + lib/zigbee.js | 6 +- npm-shrinkwrap.json | 6 +- package.json | 2 +- 7 files changed, 138 insertions(+), 8 deletions(-) create mode 100644 lib/extension/deviceAvailability.js diff --git a/lib/controller.js b/lib/controller.js index be9d6acef3..a3fe97ae23 100644 --- a/lib/controller.js +++ b/lib/controller.js @@ -17,6 +17,7 @@ const ExtensionDeviceReceive = require('./extension/deviceReceive'); const ExtensionMarkOnlineXiaomi = require('./extension/markOnlineXiaomi'); const ExtensionBridgeConfig = require('./extension/bridgeConfig'); const ExtensionGroups = require('./extension/groups'); +const DeviceAvailability = require('./extension/deviceAvailability'); class Controller { constructor() { @@ -53,6 +54,12 @@ class Controller { this.zigbee, this.mqtt, this.state, this.publishDeviceState )); } + + if (settings.get().experimental.availablility_timeout) { + this.extensions.push(new DeviceAvailability( + this.zigbee, this.mqtt, this.state, this.publishDeviceState + )); + } } onMQTTConnected() { diff --git a/lib/extension/deviceAvailability.js b/lib/extension/deviceAvailability.js new file mode 100644 index 0000000000..8d99cdce93 --- /dev/null +++ b/lib/extension/deviceAvailability.js @@ -0,0 +1,111 @@ +const logger = require('../util/logger'); +const settings = require('../util/settings'); +const utils = require('../util/utils'); +const Queue = require('queue'); + +/** + * This extensions set availablity based on optionally polling router devices + * and optionally check device publish with attribute reporting + */ +class DeviceAvailabilityHandler { + constructor(zigbee, mqtt, state, publishDeviceState) { + this.zigbee = zigbee; + this.mqtt = mqtt; + this.availablility_timeout = settings.get().experimental.availablility_timeout; + this.timers = {}; + this.pending = []; + + /** + * Setup command queue. + * The command queue ensures that only 1 command is executed at a time. + * This is to avoid DDoSiNg of the coordinator. + */ + this.queue = new Queue(); + this.queue.concurrency = 1; + this.queue.autostart = true; + } + + getAllPingableDevices() { + return this.zigbee.getAllClients() + .filter((d) => d.type === 'Router' && (d.powerSource && d.powerSource !== 'Battery')); + } + + onMQTTConnected() { + // As some devices are not checked for availability (e.g. battery powered devices) + // we mark all device as online by default. + this.zigbee.getDevices() + .filter((d) => d.type !== 'Coordinator') + .forEach((device) => this.publishAvailability(device.ieeeAddr, true)); + + // Start timers for all devices + this.getAllPingableDevices().forEach((device) => this.setTimer(device.ieeeAddr)); + } + + handleInterval(ieeeAddr) { + // Check if a job is already pending. + // This avoids overflowing of the queue in case the queue is not able to catch-up with the jobs being added. + if (this.pending.includes(ieeeAddr)) { + logger.debug(`Skipping ping for ${ieeeAddr} becuase job is already in queue`); + return; + } + + this.pending.push(ieeeAddr); + + this.queue.push((queueCallback) => { + this.zigbee.ping(ieeeAddr, (error) => { + if (error) { + logger.debug(`Failed to ping ${ieeeAddr}`); + } else { + logger.debug(`Sucesfully pinged ${ieeeAddr}`); + } + + this.publishAvailability(ieeeAddr, !error); + + // Remove from pending jobs. + const index = this.pending.indexOf(ieeeAddr); + if (index !== -1) { + this.pending.splice(index, 1); + } + + this.setTimer(ieeeAddr); + queueCallback(); + }); + }); + } + + setTimer(ieeeAddr) { + if (this.timers[ieeeAddr]) { + clearTimeout(this.timers[ieeeAddr]); + } + + this.timers[ieeeAddr] = setTimeout(() => { + this.handleInterval(ieeeAddr); + }, utils.secondsToMilliseconds(this.availablility_timeout)); + } + + stop() { + this.queue.stop(); + + this.zigbee.getDevices() + .filter((d) => d.type !== 'Coordinator') + .forEach((device) => this.publishAvailability(device.ieeeAddr, false)); + } + + publishAvailability(ieeeAddr, available) { + const deviceSettings = settings.getDevice(ieeeAddr); + const name = deviceSettings ? deviceSettings.friendly_name : ieeeAddr; + const topic = `${name}/availablility`; + const payload = available ? 'online' : 'offline'; + this.mqtt.publish(topic, payload, {retain: true, qos: 0}); + } + + onZigbeeMessage(message, device, mappedDevice) { + // When a zigbee message from a device is received we know the device is still alive. + // => reset the timer. + if (device) { + this.setTimer(device.ieeeAddr); + } + } +} + +module.exports = DeviceAvailabilityHandler; diff --git a/lib/extension/homeassistant.js b/lib/extension/homeassistant.js index 4e837640fe..be8fd70f83 100644 --- a/lib/extension/homeassistant.js +++ b/lib/extension/homeassistant.js @@ -500,7 +500,6 @@ class HomeAssistant { const topic = `${config.type}/${ieeeAddr}/${config.object_id}/config`; const payload = {...config.discovery_payload}; payload.state_topic = `${settings.get().mqtt.base_topic}/${friendlyName}`; - payload.availability_topic = `${settings.get().mqtt.base_topic}/bridge/state`; // Set (unique) name payload.name = `${friendlyName}_${config.object_id}`; @@ -517,6 +516,15 @@ class HomeAssistant { manufacturer: mappedModel.vendor, }; + // Set availablility payload + // When using experimental availablility_timeout each device has it's own availablility topic. + // If not, use the availablility topic of zigbee2mqtt. + if (settings.get().experimental.availablility_timeout) { + payload.availability_topic = `${settings.get().mqtt.base_topic}/${friendlyName}/availablility`; + } else { + payload.availability_topic = `${settings.get().mqtt.base_topic}/bridge/state`; + } + if (payload.command_topic) { payload.command_topic = `${settings.get().mqtt.base_topic}/${friendlyName}/`; diff --git a/lib/util/settings.js b/lib/util/settings.js index 83679af969..fe646350a5 100644 --- a/lib/util/settings.js +++ b/lib/util/settings.js @@ -39,6 +39,10 @@ const defaults = { */ network_key: [1, 3, 5, 7, 9, 11, 13, 15, 0, 2, 4, 6, 8, 10, 12, 13], }, + experimental: { + // Availability timeout in seconds, disabled by default. + availablility_timeout: 0, + }, }; let settings = read(); diff --git a/lib/zigbee.js b/lib/zigbee.js index 150c3a5749..deada02af0 100644 --- a/lib/zigbee.js +++ b/lib/zigbee.js @@ -159,18 +159,18 @@ class Zigbee { } } - ping(deviceID) { + ping(deviceID, callback) { let friendlyName = 'unknown'; const device = this.shepherd._findDevByAddr(deviceID); const ieeeAddr = device.ieeeAddr; + if (settings.getDevice(ieeeAddr)) { friendlyName = settings.getDevice(ieeeAddr).friendly_name; } if (device) { - // Note: checkOnline has the callback argument but does not call callback logger.debug(`Check online ${friendlyName} ${deviceID}`); - this.shepherd.controller.checkOnline(device); + this.shepherd.controller.checkOnline(device, callback); } } diff --git a/npm-shrinkwrap.json b/npm-shrinkwrap.json index c4320ccfba..cd7724ebff 100644 --- a/npm-shrinkwrap.json +++ b/npm-shrinkwrap.json @@ -2180,7 +2180,7 @@ }, "mute-stream": { "version": "0.0.7", - "resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-0.0.7.tgz", + "resolved": "http://registry.npmjs.org/mute-stream/-/mute-stream-0.0.7.tgz", "integrity": "sha1-MHXOk7whuPq0PhvE2n6BFe0ee6s=" }, "nan": { @@ -4795,8 +4795,8 @@ } }, "zigbee-shepherd": { - "version": "git+https://github.com/Koenkk/zigbee-shepherd.git#8d22cb5e0167a9f2d754efc8b228007fcd403441", - "from": "git+https://github.com/Koenkk/zigbee-shepherd.git#8d22cb5e0167a9f2d754efc8b228007fcd403441", + "version": "git+https://github.com/Koenkk/zigbee-shepherd.git#bc2445dc0bb7a2a1d5b4a461c231e28d07f517e7", + "from": "git+https://github.com/Koenkk/zigbee-shepherd.git#bc2445dc0bb7a2a1d5b4a461c231e28d07f517e7", "requires": { "areq": "^0.2.0", "busyman": "^0.3.0", diff --git a/package.json b/package.json index 9c3ca9c81f..3d5fff07f2 100644 --- a/package.json +++ b/package.json @@ -45,7 +45,7 @@ "semver": "*", "winston": "2.4.2", "ziee": "*", - "zigbee-shepherd": "git+https://github.com/Koenkk/zigbee-shepherd.git#8d22cb5e0167a9f2d754efc8b228007fcd403441", + "zigbee-shepherd": "git+https://github.com/Koenkk/zigbee-shepherd.git#bc2445dc0bb7a2a1d5b4a461c231e28d07f517e7", "zigbee-shepherd-converters": "7.0.7", "zive": "*" },