mirror of
https://github.com/Koenkk/zigbee2mqtt.git
synced 2026-08-22 10:39:58 +00:00
Allow to disable/enable availability for device and allow to configure timeout. #6281
This commit is contained in:
@@ -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 {
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user