Allow to disable/enable availability for device and allow to configure timeout. #6281

This commit is contained in:
Koen Kanters
2021-08-22 22:30:32 +02:00
parent 59f6455f13
commit 9e8dcb2784
3 changed files with 86 additions and 17 deletions
+42 -17
View File
@@ -1,14 +1,16 @@
import ExtensionTS from './extensionts';
import logger from '../util/logger';
import {sleep} from '../util/utils';
import * as settings from '../util/settings';
const hours = (hours: number): number => 1000 * 60 * 60 * hours;
const minutes = (minutes: number): number => 1000 * 60 * minutes;
const seconds = (seconds: number): number => 1000 * seconds;
const ActiveTimeout = minutes(10);
const PassiveTimeout = hours(25);
// TODO
// - State retrieval
// - Home Assistant add availability mode
// - Honour legacy availability_timeout, availability_blocklist and availability_passlist options.
class AvailabilityNew extends ExtensionTS {
private timers: {[s: string]: NodeJS.Timeout} = {};
private availabilityCache: {[s: string]: boolean} = {};
@@ -22,6 +24,24 @@ class AvailabilityNew extends ExtensionTS {
logger.warn('Using experimental new availability feature');
}
private isEnabledForDevice(re: ResolvedEntity): boolean {
return re.settings.hasOwnProperty('availability') ? !!re.settings.availability : !!settings.get().availability;
}
private getTimeout(re: ResolvedEntity): number {
if (typeof re.settings.availability === 'object' && re.settings.availability?.timeout != null) {
return minutes(re.settings.availability.timeout);
}
const key = this.isActiveDevice(re) ? 'active' : 'passive';
const availabilitySettings = settings.get().availability;
if (typeof availabilitySettings === 'object' && availabilitySettings[key]?.timeout != null) {
return minutes(availabilitySettings[key]?.timeout);
}
return key === 'active' ? minutes(10) : hours(25);
}
private isActiveDevice(re: ResolvedEntity): boolean {
return (re.device.type === 'Router' && re.device.powerSource !== 'Battery') ||
re.device.powerSource === 'Mains (single phase)';
@@ -29,7 +49,7 @@ class AvailabilityNew extends ExtensionTS {
private isAvailable(re: ResolvedEntity): boolean {
const ago = Date.now() - re.device.lastSeen;
return this.isActiveDevice(re) ? ago < ActiveTimeout : ago < PassiveTimeout;
return ago < this.getTimeout(re);
}
private resetTimer(re: ResolvedEntity): void {
@@ -39,10 +59,10 @@ class AvailabilityNew extends ExtensionTS {
if (this.isActiveDevice(re)) {
// If device did not check in, ping it, if that fails it will be marked as offline
this.timers[re.device.ieeeAddr] = setTimeout(
() => this.addToPingQueue(re), ActiveTimeout + seconds(1));
() => this.addToPingQueue(re), this.getTimeout(re) + seconds(1));
} else {
this.timers[re.device.ieeeAddr] = setTimeout(
() => this.publishAvailability(re, true), PassiveTimeout + seconds(1));
() => this.publishAvailability(re, true), this.getTimeout(re) + seconds(1));
}
}
@@ -93,14 +113,17 @@ class AvailabilityNew extends ExtensionTS {
override onMQTTConnected(): void {
for (const device of this.zigbee.getClients()) {
const re: ResolvedEntity = this.zigbee.resolveEntity(device);
this.resetTimer(re);
// Publish initial availablility
this.publishAvailability(re, true);
// If an active device is initially unavailable, ping it.
if (this.isActiveDevice(re) && !this.isAvailable(re)) {
this.addToPingQueue(re);
if (this.isEnabledForDevice(re)) {
this.resetTimer(re);
// If an active device is initially unavailable, ping it.
if (this.isActiveDevice(re) && !this.isAvailable(re)) {
this.addToPingQueue(re);
}
}
}
}
@@ -110,7 +133,8 @@ class AvailabilityNew extends ExtensionTS {
}
private publishAvailability(re: ResolvedEntity, logLastSeen: boolean): void {
if (logLastSeen) {
const enabled = this.isEnabledForDevice(re);
if (enabled && logLastSeen) {
const ago = Date.now() - re.device.lastSeen;
if (this.isActiveDevice(re)) {
logger.debug(
@@ -120,7 +144,7 @@ class AvailabilityNew extends ExtensionTS {
}
}
const available = this.isAvailable(re);
const available = enabled ? this.isAvailable(re) : true;
if (this.availabilityCache[re.device.ieeeAddr] == available) {
return;
}
@@ -133,11 +157,12 @@ class AvailabilityNew extends ExtensionTS {
private lastSeenChanged(data: {device: Device}): void {
const re = this.zigbee.resolveEntity(data.device);
// Remove from ping queue, not necessary anymore since we know the device is online.
this.removeFromPingQueue(re);
this.resetTimer(re);
this.publishAvailability(re, false);
if (this.isEnabledForDevice(re)) {
// Remove from ping queue, not necessary anymore since we know the device is online.
this.removeFromPingQueue(re);
this.resetTimer(re);
this.publishAvailability(re, false);
}
}
override stop(): void {
+8
View File
@@ -16,6 +16,10 @@ declare global {
blocklist: string[],
whitelist: string[],
ban: string[],
availability?: boolean | {
active?: {timeout?: number},
passive?: {timeout?: number}
},
permit_join: boolean,
frontend?: {
auth_token?: string,
@@ -117,6 +121,10 @@ declare global {
definition?: {model: string},
name: string,
device?: Device,
settings: {
friendlyName: string,
availability?: {timeout?: number} | boolean,
}
}
type lastSeenChangedHandler = (data: {device: Device}) => void;
+36
View File
@@ -47,6 +47,8 @@ describe('Availability', () => {
beforeEach(async () => {
jest.useFakeTimers('modern').setSystemTime(minutes(1));
data.writeDefaultConfiguration();
settings.set(['devices', '0x000b57fffec6a5b4', 'availability'], false);
settings.set(['devices', '0x000b57fffec6a5b3', 'availability'], true);
// @ts-ignore
Object.values(zigbeeHerdsman.devices).forEach(d => d.lastSeen = minutes(1));
mocks.forEach((m) => m.mockClear());
@@ -69,6 +71,8 @@ describe('Availability', () => {
'online', {retain: true, qos: 0}, expect.any(Function));
expect(MQTT.publish).toHaveBeenCalledWith('zigbee2mqtt/remote/availability',
'online', {retain: true, qos: 0}, expect.any(Function));
expect(MQTT.publish).toHaveBeenCalledWith('zigbee2mqtt/bulb_color_2/availability',
'online', {retain: true, qos: 0}, expect.any(Function));
});
it('Should publish offline for active device when not seen for 10 minutes', async () => {
@@ -84,6 +88,14 @@ describe('Availability', () => {
'offline', {retain: true, qos: 0}, expect.any(Function));
});
it('Shouldnt do anything for a device when availability: false is set', async () => {
MQTT.publish.mockClear();
await advancedTime(minutes(12));
await zigbeeHerdsman.events.lastSeenChanged({device: devices.bulb_color_2});
expect(devices.bulb_color_2.ping).toHaveBeenCalledTimes(0);
});
it('Should publish offline for passive device when not seen for 25 hours', async () => {
MQTT.publish.mockClear();
await advancedTime(hours(26));
@@ -162,4 +174,28 @@ describe('Availability', () => {
expect(MQTT.publish).toHaveBeenCalledWith('zigbee2mqtt/bulb_color/availability',
'online', {retain: true, qos: 0}, expect.any(Function));
});
it('Should allow to change availability timeout via device options', async () => {
settings.set(['devices', '0x000b57fffec6a5b3', 'availability'], {timeout: 40});
await resetExtension();
MQTT.publish.mockClear();
await advancedTime(minutes(25));
expect(devices.bulb_color.ping).toHaveBeenCalledTimes(0);
await advancedTime(minutes(17));
expect(devices.bulb_color.ping).toHaveBeenCalledTimes(1);
});
it('Should allow to change availability timeout via avaiability options', async () => {
settings.set(['availability'], {active: {timeout: 30}});
await resetExtension();
MQTT.publish.mockClear();
await advancedTime(minutes(25));
expect(devices.bulb_color.ping).toHaveBeenCalledTimes(0);
await advancedTime(minutes(7));
expect(devices.bulb_color.ping).toHaveBeenCalledTimes(1);
});
});