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.
This commit is contained in:
Gergely Markics
2018-12-29 19:55:59 +01:00
committed by Koen Kanters
parent 2a197a4ba8
commit afeed4f372
7 changed files with 138 additions and 8 deletions
+7
View File
@@ -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() {
+111
View File
@@ -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;
+9 -1
View File
@@ -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}/`;
+4
View File
@@ -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();
+3 -3
View File
@@ -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);
}
}
+3 -3
View File
@@ -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",
+1 -1
View File
@@ -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": "*"
},