2018-12-29 11:55:59 -07:00
|
|
|
const logger = require('../util/logger');
|
|
|
|
const settings = require('../util/settings');
|
|
|
|
const utils = require('../util/utils');
|
2019-01-16 12:41:41 -07:00
|
|
|
const zigbeeShepherdConverters = require('zigbee-shepherd-converters');
|
|
|
|
|
|
|
|
// Some EndDevices should be pinged
|
|
|
|
// e.g. E11-G13 https://github.com/Koenkk/zigbee2mqtt/issues/775#issuecomment-453683846
|
|
|
|
const pingableDevices = [
|
|
|
|
zigbeeShepherdConverters.devices.find((d) => d.model === 'E11-G13'),
|
|
|
|
];
|
2018-12-29 11:55:59 -07:00
|
|
|
|
2019-02-01 17:47:53 -07:00
|
|
|
const toZigbeeCandidates = ['state'];
|
|
|
|
|
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-02-13 12:55:14 -07:00
|
|
|
class DeviceAvailability {
|
2019-02-04 10:36:49 -07:00
|
|
|
constructor(zigbee, mqtt, state, publishEntityState) {
|
2018-12-29 11:55:59 -07:00
|
|
|
this.zigbee = zigbee;
|
|
|
|
this.mqtt = mqtt;
|
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 = {};
|
|
|
|
this.pending = [];
|
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) => {
|
|
|
|
return settings.getIeeeAddrByFriendlyName(e) || e;
|
|
|
|
});
|
2018-12-29 11:55:59 -07:00
|
|
|
}
|
|
|
|
|
2019-01-01 14:15:05 -07:00
|
|
|
isPingable(device) {
|
2019-04-07 08:47:06 -07:00
|
|
|
logger.debug(`Checking if ${device.ieeeAddr} is pingable`);
|
|
|
|
|
2019-02-02 10:10:25 -07:00
|
|
|
if (this.blacklist.includes(device.ieeeAddr)) {
|
2019-04-07 08:47:06 -07:00
|
|
|
logger.debug(`${device.ieeeAddr} is not pingable because of blacklist`);
|
2019-02-02 10:10:25 -07:00
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
2019-01-16 12:41:41 -07:00
|
|
|
if (pingableDevices.find((d) => d.zigbeeModel.includes(device.modelId))) {
|
2019-04-07 08:47:06 -07:00
|
|
|
logger.debug(`${device.ieeeAddr} is pingable because in pingable devices`);
|
2019-01-16 12:41:41 -07:00
|
|
|
return true;
|
|
|
|
}
|
|
|
|
|
2019-04-07 08:47:06 -07:00
|
|
|
const result = utils.isRouter(device) && !utils.isBatteryPowered(device);
|
|
|
|
logger.debug(`${device.ieeeAddr} is pingable (${result}) not router or battery powered`);
|
|
|
|
return result;
|
2019-01-01 14:15:05 -07:00
|
|
|
}
|
|
|
|
|
2018-12-29 11:55:59 -07:00
|
|
|
getAllPingableDevices() {
|
2019-01-01 14:15:05 -07:00
|
|
|
return this.zigbee.getAllClients().filter((d) => this.isPingable(d));
|
2018-12-29 11:55:59 -07:00
|
|
|
}
|
|
|
|
|
|
|
|
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
|
2019-03-24 06:38:08 -07:00
|
|
|
this.getAllPingableDevices().forEach((device) => this.setTimer(device));
|
2018-12-29 11:55:59 -07:00
|
|
|
}
|
|
|
|
|
2019-03-24 06:38:08 -07:00
|
|
|
handleInterval(device) {
|
2018-12-29 11:55:59 -07:00
|
|
|
// 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.
|
2019-03-24 06:38:08 -07:00
|
|
|
const ieeeAddr = device.ieeeAddr;
|
2018-12-29 11:55:59 -07:00
|
|
|
if (this.pending.includes(ieeeAddr)) {
|
|
|
|
logger.debug(`Skipping ping for ${ieeeAddr} becuase job is already in queue`);
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
this.pending.push(ieeeAddr);
|
|
|
|
|
2019-02-09 11:50:32 -07:00
|
|
|
// When a device is already unavailable, log the ping failed on 'debug' instead of 'error'.
|
|
|
|
const errorLogLevel = this.state.hasOwnProperty(ieeeAddr) && !this.state[ieeeAddr] ? 'debug' : 'error';
|
2019-03-24 06:38:08 -07:00
|
|
|
const mechanism = utils.isXiaomiDevice(device) ? 'basic' : 'default';
|
2019-02-09 11:50:32 -07:00
|
|
|
|
|
|
|
this.zigbee.ping(ieeeAddr, errorLogLevel, (error) => {
|
2019-02-01 16:57:51 -07:00
|
|
|
this.publishAvailability(ieeeAddr, !error);
|
|
|
|
|
|
|
|
// Remove from pending jobs.
|
|
|
|
const index = this.pending.indexOf(ieeeAddr);
|
|
|
|
if (index !== -1) {
|
|
|
|
this.pending.splice(index, 1);
|
|
|
|
}
|
|
|
|
|
2019-03-24 06:38:08 -07:00
|
|
|
this.setTimer(device);
|
|
|
|
}, mechanism);
|
2018-12-29 11:55:59 -07:00
|
|
|
}
|
|
|
|
|
2019-04-09 10:17:16 -07:00
|
|
|
setTimer(device) {
|
|
|
|
if (this.timers[device.ieeeAddr]) {
|
|
|
|
clearTimeout(this.timers[device.ieeeAddr]);
|
2018-12-29 11:55:59 -07:00
|
|
|
}
|
|
|
|
|
2019-04-09 10:17:16 -07:00
|
|
|
this.timers[device.ieeeAddr] = setTimeout(() => {
|
|
|
|
this.handleInterval(device);
|
2018-12-30 12:08:31 -07:00
|
|
|
}, utils.secondsToMilliseconds(this.availability_timeout));
|
2018-12-29 11:55:59 -07:00
|
|
|
}
|
|
|
|
|
|
|
|
stop() {
|
|
|
|
this.zigbee.getDevices()
|
|
|
|
.filter((d) => d.type !== 'Coordinator')
|
|
|
|
.forEach((device) => this.publishAvailability(device.ieeeAddr, false));
|
|
|
|
}
|
|
|
|
|
2019-02-01 17:47:53 -07:00
|
|
|
onReconnect(ieeeAddr) {
|
|
|
|
const device = this.zigbee.getDevice(ieeeAddr);
|
|
|
|
let mappedDevice = null;
|
|
|
|
|
|
|
|
if (device && device.modelId) {
|
|
|
|
mappedDevice = zigbeeShepherdConverters.findByZigbeeModel(device.modelId);
|
|
|
|
}
|
|
|
|
|
|
|
|
if (mappedDevice) {
|
|
|
|
const converters = mappedDevice.toZigbee.filter((tz) => {
|
|
|
|
return tz.key.find((k) => toZigbeeCandidates.includes(k));
|
2019-02-01 17:52:21 -07:00
|
|
|
});
|
2019-02-01 17:47:53 -07:00
|
|
|
|
|
|
|
converters.forEach((converter) => {
|
|
|
|
const converted = converter.convert(null, null, null, 'get');
|
|
|
|
if (converted) {
|
|
|
|
this.zigbee.publish(
|
|
|
|
ieeeAddr, 'device', converted.cid, converted.cmd, converted.cmdType,
|
|
|
|
converted.zclData, converted.cfg, null, () => {}
|
|
|
|
);
|
|
|
|
}
|
|
|
|
});
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2018-12-29 11:55:59 -07:00
|
|
|
publishAvailability(ieeeAddr, available) {
|
2019-02-01 17:47:53 -07:00
|
|
|
if (this.state.hasOwnProperty(ieeeAddr) && !this.state[ieeeAddr] && available) {
|
|
|
|
this.onReconnect(ieeeAddr);
|
|
|
|
}
|
|
|
|
|
2019-01-31 13:32:18 -07:00
|
|
|
this.state[ieeeAddr] = available;
|
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';
|
|
|
|
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.
|
2019-01-01 14:15:05 -07:00
|
|
|
if (device && this.isPingable(this.zigbee.getDevice(device.ieeeAddr))) {
|
2019-01-31 13:32:18 -07:00
|
|
|
// When a message is received and the device is marked as offline, mark it online.
|
|
|
|
if (this.state.hasOwnProperty(device.ieeeAddr) && !this.state[device.ieeeAddr]) {
|
|
|
|
this.publishAvailability(device.ieeeAddr, true);
|
2019-02-02 09:58:38 -07:00
|
|
|
} else if (!this.state.hasOwnProperty(device.ieeeAddr)) {
|
|
|
|
// A new device has been connected
|
|
|
|
this.publishAvailability(device.ieeeAddr, true);
|
2019-01-31 13:32:18 -07:00
|
|
|
}
|
|
|
|
|
2019-03-24 06:38:08 -07:00
|
|
|
this.setTimer(device);
|
2018-12-29 11:55:59 -07:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2019-02-13 12:55:14 -07:00
|
|
|
module.exports = DeviceAvailability;
|