mirror of
https://github.com/Koenkk/zigbee2mqtt.git
synced 2026-07-01 17:41:37 +00:00
Device Availability: Allow battery devices to report offline/online based on device.lastSeen (#2464)
* Remove execution bits for deviceReceive.js * deviceAvailability.isPingable should return true if 'availability_lastseen_timeout' is set for device * deviceAvailability.handleInterval should know how to handle lastseen_timeout setting * Allow availability_lastseen_timeout for all devices * Use a default lastseen timeout of 24h * Updates * Updates * Updates * 25 hours instead of 24.
This commit is contained in:
committed by
Koen Kanters
parent
6bd9e74e0c
commit
bcf79a7e34
@@ -12,6 +12,8 @@ const forcedPingable = [
|
||||
|
||||
const toZigbeeCandidates = ['state', 'brightness', 'color', 'color_temp'];
|
||||
|
||||
const Hours25 = 1000 * 60 * 60 * 25;
|
||||
|
||||
/**
|
||||
* This extensions pings devices to check if they are online.
|
||||
*/
|
||||
@@ -34,7 +36,7 @@ class DeviceAvailability extends BaseExtension {
|
||||
});
|
||||
}
|
||||
|
||||
isPingable(device) {
|
||||
isAllowed(device) {
|
||||
// Whitelist is not empty and device is in it, enable availability
|
||||
if (this.whitelist.length > 0) {
|
||||
return this.whitelist.includes(device.ieeeAddr);
|
||||
@@ -45,29 +47,39 @@ class DeviceAvailability extends BaseExtension {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
isPingable(device) {
|
||||
// Device is on forcedPingable-list, enable availability
|
||||
if (forcedPingable.find((d) => d.zigbeeModel.includes(device.modelID))) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Device is a mains powered router
|
||||
const result = utils.isRouter(device) && !utils.isBatteryPowered(device);
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
getAllPingableDevices() {
|
||||
return this.zigbee.getClients().filter((d) => this.isPingable(d));
|
||||
}
|
||||
|
||||
onMQTTConnected() {
|
||||
// As some devices are not checked for availability (e.g. battery powered devices)
|
||||
// we mark all device as online by default.
|
||||
this.zigbee.getClients().forEach((device) => this.publishAvailability(device, true));
|
||||
for (const device of this.zigbee.getClients()) {
|
||||
// Mark all devices as online on start
|
||||
this.publishAvailability(device, true);
|
||||
|
||||
// Start timers for all devices
|
||||
this.getAllPingableDevices().forEach((device) => this.setTimer(device));
|
||||
if (this.isAllowed(device)) {
|
||||
if (this.isPingable(device)) {
|
||||
this.setTimerPingable(device);
|
||||
} else {
|
||||
this.timers[device.ieeeAddr] = setInterval(() => {
|
||||
this.handleIntervalNotPingable(device);
|
||||
}, utils.secondsToMilliseconds(300));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async handleInterval(device) {
|
||||
async handleIntervalPingable(device) {
|
||||
// When a device is already unavailable, log the ping failed on 'debug' instead of 'error'.
|
||||
const entity = this.zigbee.resolveEntity(device.ieeeAddr);
|
||||
if (!entity) {
|
||||
@@ -85,17 +97,31 @@ class DeviceAvailability extends BaseExtension {
|
||||
this.publishAvailability(device, false);
|
||||
logger[level](`Failed to ping '${entity.name}'`);
|
||||
} finally {
|
||||
this.setTimer(device);
|
||||
this.setTimerPingable(device);
|
||||
}
|
||||
}
|
||||
|
||||
setTimer(device) {
|
||||
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) {
|
||||
if (this.timers[device.ieeeAddr]) {
|
||||
clearTimeout(this.timers[device.ieeeAddr]);
|
||||
}
|
||||
|
||||
this.timers[device.ieeeAddr] = setTimeout(async () => {
|
||||
await this.handleInterval(device);
|
||||
await this.handleIntervalPingable(device);
|
||||
}, utils.secondsToMilliseconds(this.availability_timeout));
|
||||
}
|
||||
|
||||
@@ -151,37 +177,28 @@ class DeviceAvailability extends BaseExtension {
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.isPingable(device)) {
|
||||
// When a zigbee message from a device is received we know the device is still alive.
|
||||
// => reset the timer.
|
||||
this.setTimer(device);
|
||||
if (this.isAllowed(device)) {
|
||||
this.publishAvailability(data.device, true);
|
||||
|
||||
const online = this.state.hasOwnProperty(device.ieeeAddr) && this.state[device.ieeeAddr];
|
||||
const offline = this.state.hasOwnProperty(device.ieeeAddr) && !this.state[device.ieeeAddr];
|
||||
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);
|
||||
|
||||
if (!online && !offline) {
|
||||
// A new device has been connected
|
||||
this.publishAvailability(device, true);
|
||||
} else if (offline) {
|
||||
// When a message is received and the device is marked as offline, mark it online.
|
||||
this.publishAvailability(device, true);
|
||||
} else {
|
||||
/* istanbul ignore else */
|
||||
const online = this.state.hasOwnProperty(device.ieeeAddr) && this.state[device.ieeeAddr];
|
||||
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
|
||||
* endDeviceAnnce is typically send when a device comes online.
|
||||
* deviceAnnounce is typically send when a device comes online.
|
||||
*
|
||||
* This isn't needed for TRADFRI devices as they already send the state themself.
|
||||
*/
|
||||
this.onReconnect(device);
|
||||
}
|
||||
}
|
||||
} else if (type === 'deviceJoined') {
|
||||
this.publishAvailability(data.device, true);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,6 +4,8 @@ const zigbeeHerdsman = require('./stub/zigbeeHerdsman');
|
||||
zigbeeHerdsman.returnDevices.push('0x000b57fffec6a5b3');
|
||||
zigbeeHerdsman.returnDevices.push('0x00124b00120144ae');
|
||||
zigbeeHerdsman.returnDevices.push('0x0017880104e45553');
|
||||
zigbeeHerdsman.returnDevices.push('0x0017880104e45517');
|
||||
|
||||
const MQTT = require('./stub/mqtt');
|
||||
const settings = require('../lib/util/settings');
|
||||
const Controller = require('../lib/controller');
|
||||
@@ -186,15 +188,6 @@ describe('Device availability', () => {
|
||||
expect(endpoint.read).toHaveBeenCalledTimes(0);
|
||||
});
|
||||
|
||||
it('Should do nothing when receiving message from non-pingable device', async () => {
|
||||
MQTT.publish.mockClear();
|
||||
const device = zigbeeHerdsman.devices.WXKG11LM;
|
||||
const payload = {device, endpoint: device.getEndpoint(1), type: 'attributeReport', linkquality: 10, cluster: 'genBasic', data: {modelId: device.modelID}};
|
||||
await zigbeeHerdsman.events.message(payload);
|
||||
await flushPromises();
|
||||
expect(MQTT.publish).toHaveBeenCalledTimes(0);
|
||||
});
|
||||
|
||||
it('Should not do anything when message has no device', async () => {
|
||||
MQTT.publish.mockClear();
|
||||
const device = zigbeeHerdsman.devices.bulb_color;
|
||||
@@ -248,16 +241,18 @@ describe('Device availability', () => {
|
||||
it('Should not ping non-whitelisted devices if availability_whitelist is set', async () => {
|
||||
const device = zigbeeHerdsman.devices.bulb;
|
||||
getExtension().state[device.ieeeAddr] = false;
|
||||
settings.set(['advanced', 'availability_whitelist'], [device.ieeeAddr])
|
||||
settings.set(['advanced', 'availability_whitelist'], ['0x000b57fffec6a5b3'])
|
||||
await controller.stop();
|
||||
await flushPromises();
|
||||
controller = new Controller();
|
||||
await controller.start();
|
||||
await flushPromises();
|
||||
device.ping.mockClear();
|
||||
jest.advanceTimersByTime(11 * 1000);
|
||||
await flushPromises();
|
||||
expect(device.ping).toHaveBeenCalledTimes(0);
|
||||
MQTT.publish.mockClear();
|
||||
await zigbeeHerdsman.events.deviceAnnounce({device});
|
||||
expect(MQTT.publish).toHaveBeenCalledTimes(0);
|
||||
});
|
||||
|
||||
it('Should not read when device has no modelID and reconnects', async () => {
|
||||
@@ -305,4 +300,29 @@ describe('Device availability', () => {
|
||||
expect.any(Function)
|
||||
);
|
||||
});
|
||||
|
||||
it('Should mark non-pingable device as non-available when offline for longer than 24 hours', async () => {
|
||||
const device = zigbeeHerdsman.devices.remote;
|
||||
const defaultLastSeen = device.lastSeen;
|
||||
device.lastSeen = Date.now();
|
||||
MQTT.publish.mockClear();
|
||||
jest.advanceTimersByTime(1000 * 60 * 60 * 1); // 1 hours
|
||||
expect(MQTT.publish).toHaveBeenCalledTimes(0);
|
||||
device.lastSeen = device.lastSeen - (1000 * 60 * 60 * 25);
|
||||
jest.advanceTimersByTime(1000 * 60 * 60 * 1); // 1 hours
|
||||
expect(MQTT.publish).toHaveBeenCalledTimes(1);
|
||||
expect(MQTT.publish).toHaveBeenCalledWith(
|
||||
'zigbee2mqtt/remote/availability',
|
||||
'offline',
|
||||
{ retain: true, qos: 0 },
|
||||
expect.any(Function)
|
||||
);
|
||||
|
||||
// Shouldn't do anything more when device is removed
|
||||
settings.removeDevice(device.ieeeAddr);
|
||||
jest.advanceTimersByTime(1000 * 60 * 60 * 1); // 1 hours
|
||||
expect(MQTT.publish).toHaveBeenCalledTimes(1);
|
||||
|
||||
device.lastSeen = defaultLastSeen;
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user