2018-12-29 11:55:59 -07:00
|
|
|
const logger = require('../util/logger');
|
|
|
|
const settings = require('../util/settings');
|
|
|
|
const utils = require('../util/utils');
|
2019-09-09 10:48:09 -07:00
|
|
|
const zigbeeHerdsmanConverters = require('zigbee-herdsman-converters');
|
2019-09-17 09:32:16 -07:00
|
|
|
const BaseExtension = require('./baseExtension');
|
2019-01-16 12:41:41 -07:00
|
|
|
|
|
|
|
// Some EndDevices should be pinged
|
|
|
|
// e.g. E11-G13 https://github.com/Koenkk/zigbee2mqtt/issues/775#issuecomment-453683846
|
2020-02-08 15:23:44 -07:00
|
|
|
const pingableModels = [
|
|
|
|
'E11-G13',
|
|
|
|
'53170161',
|
2019-01-16 12:41:41 -07:00
|
|
|
];
|
2018-12-29 11:55:59 -07:00
|
|
|
|
2020-02-08 15:23:44 -07:00
|
|
|
const forcedPingable =
|
|
|
|
zigbeeHerdsmanConverters.devices.filter((d) => pingableModels.includes(d.model));
|
|
|
|
|
2019-09-09 10:48:09 -07:00
|
|
|
const toZigbeeCandidates = ['state', 'brightness', 'color', 'color_temp'];
|
2019-02-01 17:47:53 -07:00
|
|
|
|
2019-12-09 10:12:13 -07:00
|
|
|
const Hours25 = 1000 * 60 * 60 * 25;
|
|
|
|
|
2018-12-29 11:55:59 -07:00
|
|
|
/**
|
2019-02-02 09:58:38 -07:00
|
|
|
* This extensions pings devices to check if they are online.
|
2018-12-29 11:55:59 -07:00
|
|
|
*/
|
2019-09-17 09:32:16 -07:00
|
|
|
class DeviceAvailability extends BaseExtension {
|
2020-01-09 13:47:19 -07:00
|
|
|
constructor(zigbee, mqtt, state, publishEntityState, eventBus) {
|
|
|
|
super(zigbee, mqtt, state, publishEntityState, eventBus);
|
2019-09-17 09:32:16 -07:00
|
|
|
|
2019-01-29 12:17:56 -07:00
|
|
|
this.availability_timeout = settings.get().advanced.availability_timeout;
|
2018-12-29 11:55:59 -07:00
|
|
|
this.timers = {};
|
2019-01-31 13:32:18 -07:00
|
|
|
this.state = {};
|
2019-02-02 10:10:25 -07:00
|
|
|
|
|
|
|
// Initialize blacklist
|
|
|
|
this.blacklist = settings.get().advanced.availability_blacklist.map((e) => {
|
2019-09-09 10:48:09 -07:00
|
|
|
return settings.getEntity(e).ID;
|
2019-02-02 10:10:25 -07:00
|
|
|
});
|
2019-11-23 03:47:37 -07:00
|
|
|
|
|
|
|
// Initialize whitelist
|
|
|
|
this.whitelist = settings.get().advanced.availability_whitelist.map((e) => {
|
|
|
|
return settings.getEntity(e).ID;
|
|
|
|
});
|
2018-12-29 11:55:59 -07:00
|
|
|
}
|
|
|
|
|
2019-12-09 10:12:13 -07:00
|
|
|
isAllowed(device) {
|
2020-01-08 10:02:49 -07:00
|
|
|
const ieeeAddr = device.ieeeAddr;
|
|
|
|
|
|
|
|
const deviceSettings = settings.getDevice(ieeeAddr);
|
2020-04-04 10:15:24 -07:00
|
|
|
const name = deviceSettings.friendly_name;
|
2020-01-08 10:02:49 -07:00
|
|
|
|
2019-11-23 03:47:37 -07:00
|
|
|
// Whitelist is not empty and device is in it, enable availability
|
|
|
|
if (this.whitelist.length > 0) {
|
2020-01-08 10:02:49 -07:00
|
|
|
return this.whitelist.includes(ieeeAddr) || (name && this.whitelist.includes(name));
|
2019-11-23 03:47:37 -07:00
|
|
|
}
|
|
|
|
|
|
|
|
// Device is on blacklist, disable availability
|
2020-01-08 10:02:49 -07:00
|
|
|
if (this.blacklist.includes(ieeeAddr) || (name && this.blacklist.includes(name))) {
|
2019-02-02 10:10:25 -07:00
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
2019-12-09 10:12:13 -07:00
|
|
|
return true;
|
|
|
|
}
|
|
|
|
|
|
|
|
isPingable(device) {
|
2019-11-23 03:47:37 -07:00
|
|
|
// Device is on forcedPingable-list, enable availability
|
2019-09-09 10:48:09 -07:00
|
|
|
if (forcedPingable.find((d) => d.zigbeeModel.includes(device.modelID))) {
|
2019-01-16 12:41:41 -07:00
|
|
|
return true;
|
|
|
|
}
|
|
|
|
|
2019-12-09 10:12:13 -07:00
|
|
|
// Device is a mains powered router
|
2019-04-07 08:47:06 -07:00
|
|
|
const result = utils.isRouter(device) && !utils.isBatteryPowered(device);
|
2019-01-01 14:15:05 -07:00
|
|
|
|
2019-12-09 10:12:13 -07:00
|
|
|
return result;
|
2018-12-29 11:55:59 -07:00
|
|
|
}
|
|
|
|
|
2019-09-23 13:21:27 -07:00
|
|
|
onMQTTConnected() {
|
2019-12-09 10:12:13 -07:00
|
|
|
for (const device of this.zigbee.getClients()) {
|
|
|
|
// Mark all devices as online on start
|
|
|
|
this.publishAvailability(device, true);
|
2018-12-29 11:55:59 -07:00
|
|
|
|
2019-12-09 10:12:13 -07:00
|
|
|
if (this.isAllowed(device)) {
|
|
|
|
if (this.isPingable(device)) {
|
|
|
|
this.setTimerPingable(device);
|
|
|
|
} else {
|
|
|
|
this.timers[device.ieeeAddr] = setInterval(() => {
|
|
|
|
this.handleIntervalNotPingable(device);
|
|
|
|
}, utils.secondsToMilliseconds(300));
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
2018-12-29 11:55:59 -07:00
|
|
|
}
|
|
|
|
|
2019-12-09 10:12:13 -07:00
|
|
|
async handleIntervalPingable(device) {
|
2019-02-09 11:50:32 -07:00
|
|
|
// When a device is already unavailable, log the ping failed on 'debug' instead of 'error'.
|
2019-11-27 10:14:39 -07:00
|
|
|
const entity = this.zigbee.resolveEntity(device.ieeeAddr);
|
|
|
|
if (!entity) {
|
|
|
|
logger.debug(`Stop pinging '${device.ieeeAddr}', device is not known anymore`);
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
2019-09-09 10:48:09 -07:00
|
|
|
const ieeeAddr = device.ieeeAddr;
|
|
|
|
const level = this.state.hasOwnProperty(ieeeAddr) && !this.state[ieeeAddr] ? 'debug' : 'error';
|
|
|
|
try {
|
|
|
|
await device.ping();
|
|
|
|
this.publishAvailability(device, true);
|
2019-11-04 23:39:51 -07:00
|
|
|
logger.debug(`Successfully pinged '${entity.name}'`);
|
2019-09-09 10:48:09 -07:00
|
|
|
} catch (error) {
|
|
|
|
this.publishAvailability(device, false);
|
2019-11-03 06:17:57 -07:00
|
|
|
logger[level](`Failed to ping '${entity.name}'`);
|
2019-09-09 10:48:09 -07:00
|
|
|
} finally {
|
2019-12-09 10:12:13 -07:00
|
|
|
this.setTimerPingable(device);
|
2019-09-09 10:48:09 -07:00
|
|
|
}
|
2018-12-29 11:55:59 -07:00
|
|
|
}
|
|
|
|
|
2019-12-09 10:12:13 -07:00
|
|
|
async handleIntervalNotPingable(device) {
|
|
|
|
const ago = Date.now() - device.lastSeen;
|
|
|
|
const entity = this.zigbee.resolveEntity(device.ieeeAddr);
|
|
|
|
if (!entity || !device.lastSeen) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
logger.debug(`Non-pingable device '${entity.name}' was last seen '${ago / 1000}' seconds ago.`);
|
|
|
|
|
|
|
|
if (ago > Hours25) {
|
|
|
|
this.publishAvailability(device, false);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
setTimerPingable(device) {
|
2019-04-09 10:17:16 -07:00
|
|
|
if (this.timers[device.ieeeAddr]) {
|
|
|
|
clearTimeout(this.timers[device.ieeeAddr]);
|
2018-12-29 11:55:59 -07:00
|
|
|
}
|
|
|
|
|
2019-09-09 10:48:09 -07:00
|
|
|
this.timers[device.ieeeAddr] = setTimeout(async () => {
|
2019-12-09 10:12:13 -07:00
|
|
|
await this.handleIntervalPingable(device);
|
2018-12-30 12:08:31 -07:00
|
|
|
}, utils.secondsToMilliseconds(this.availability_timeout));
|
2018-12-29 11:55:59 -07:00
|
|
|
}
|
|
|
|
|
2019-09-09 10:48:09 -07:00
|
|
|
async stop() {
|
|
|
|
for (const timer of Object.values(this.timers)) {
|
|
|
|
clearTimeout(timer);
|
2019-02-01 17:47:53 -07:00
|
|
|
}
|
|
|
|
|
2019-09-23 13:21:27 -07:00
|
|
|
this.zigbee.getClients().forEach((device) => this.publishAvailability(device, false));
|
2019-09-09 10:48:09 -07:00
|
|
|
}
|
|
|
|
|
|
|
|
async onReconnect(device) {
|
|
|
|
if (device && device.modelID) {
|
2020-04-05 06:41:24 -07:00
|
|
|
const definition = zigbeeHerdsmanConverters.findByZigbeeModel(device.modelID);
|
2019-09-09 10:48:09 -07:00
|
|
|
|
2020-04-05 06:41:24 -07:00
|
|
|
if (definition) {
|
2019-09-09 10:48:09 -07:00
|
|
|
const used = [];
|
2019-11-11 11:25:11 -07:00
|
|
|
try {
|
|
|
|
for (const key of toZigbeeCandidates) {
|
2020-04-05 06:41:24 -07:00
|
|
|
const converter = definition.toZigbee.find((tz) => tz.key.includes(key));
|
2019-11-11 11:25:11 -07:00
|
|
|
if (converter && !used.includes(converter)) {
|
|
|
|
await converter.convertGet(device.endpoints[0], key, {});
|
|
|
|
used.push(converter);
|
|
|
|
}
|
2019-09-09 10:48:09 -07:00
|
|
|
}
|
2019-11-11 11:25:11 -07:00
|
|
|
} catch (error) {
|
|
|
|
const entity = this.zigbee.resolveEntity(device.ieeeAddr);
|
|
|
|
logger.error(`Failed to read state of '${entity.name}' after reconnect`);
|
2019-02-01 17:47:53 -07:00
|
|
|
}
|
2019-09-09 10:48:09 -07:00
|
|
|
}
|
2019-02-01 17:47:53 -07:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2019-09-09 10:48:09 -07:00
|
|
|
publishAvailability(device, available) {
|
|
|
|
const ieeeAddr = device.ieeeAddr;
|
2019-02-01 17:47:53 -07:00
|
|
|
if (this.state.hasOwnProperty(ieeeAddr) && !this.state[ieeeAddr] && available) {
|
2019-09-09 10:48:09 -07:00
|
|
|
this.onReconnect(device);
|
2019-02-01 17:47:53 -07:00
|
|
|
}
|
|
|
|
|
2018-12-29 11:55:59 -07:00
|
|
|
const deviceSettings = settings.getDevice(ieeeAddr);
|
|
|
|
const name = deviceSettings ? deviceSettings.friendly_name : ieeeAddr;
|
2018-12-30 12:08:31 -07:00
|
|
|
const topic = `${name}/availability`;
|
2018-12-29 11:55:59 -07:00
|
|
|
const payload = available ? 'online' : 'offline';
|
2019-11-10 09:56:01 -07:00
|
|
|
if (this.state[ieeeAddr] !== available) {
|
|
|
|
this.state[ieeeAddr] = available;
|
|
|
|
this.mqtt.publish(topic, payload, {retain: true, qos: 0});
|
|
|
|
}
|
2018-12-29 11:55:59 -07:00
|
|
|
}
|
|
|
|
|
2020-04-05 06:41:24 -07:00
|
|
|
onZigbeeEvent(type, data, resolvedEntity) {
|
2019-09-09 10:48:09 -07:00
|
|
|
const device = data.device;
|
|
|
|
if (!device) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
2019-12-09 10:12:13 -07:00
|
|
|
if (this.isAllowed(device)) {
|
|
|
|
this.publishAvailability(data.device, true);
|
|
|
|
|
|
|
|
if (this.isPingable(device)) {
|
|
|
|
// When a zigbee message from a device is received we know the device is still alive.
|
|
|
|
// => reset the timer.
|
|
|
|
this.setTimerPingable(device);
|
|
|
|
|
|
|
|
const online = this.state.hasOwnProperty(device.ieeeAddr) && this.state[device.ieeeAddr];
|
2019-09-09 10:48:09 -07:00
|
|
|
if (online && type === 'deviceAnnounce' && !utils.isIkeaTradfriDevice(device)) {
|
|
|
|
/**
|
|
|
|
* In case the device is powered off AND on within the availability timeout,
|
|
|
|
* zigbee2qmtt does not detect the device as offline (device is still marked online).
|
|
|
|
* When a device is turned on again the state could be out of sync.
|
|
|
|
* https://github.com/Koenkk/zigbee2mqtt/issues/1383#issuecomment-489412168
|
2019-12-09 10:12:13 -07:00
|
|
|
* deviceAnnounce is typically send when a device comes online.
|
2019-09-09 10:48:09 -07:00
|
|
|
*
|
|
|
|
* This isn't needed for TRADFRI devices as they already send the state themself.
|
|
|
|
*/
|
|
|
|
this.onReconnect(device);
|
2019-05-06 11:29:17 -07:00
|
|
|
}
|
2019-01-31 13:32:18 -07:00
|
|
|
}
|
2018-12-29 11:55:59 -07:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2019-02-13 12:55:14 -07:00
|
|
|
module.exports = DeviceAvailability;
|