mirror of
https://github.com/Koenkk/zigbee2mqtt.git
synced 2026-07-02 10:01:37 +00:00
* Availability * Updates
This commit is contained in:
@@ -1,17 +1,17 @@
|
||||
import ExtensionTS from './extensionts';
|
||||
import logger from '../util/logger';
|
||||
import {sleep, isAvailabilityNewEnabledForDevice, hours, minutes, seconds} from '../util/utils';
|
||||
import {sleep, isAvailabilityEnabledForDevice, hours, minutes, seconds} from '../util/utils';
|
||||
import * as settings from '../util/settings';
|
||||
import debounce from 'debounce';
|
||||
|
||||
// TODO
|
||||
// - State retrieval
|
||||
// - Honour legacy availability_timeout, availability_blocklist and availability_passlist options.
|
||||
// - Enable for HA addon
|
||||
// - Add to setting schema
|
||||
// - Add to setting schema (when old availability is removed)
|
||||
class AvailabilityNew extends ExtensionTS {
|
||||
private timers: {[s: string]: NodeJS.Timeout} = {};
|
||||
private availabilityCache: {[s: string]: boolean} = {};
|
||||
private pingQueue: ResolvedEntity[] = [];
|
||||
private retrieveStateDebouncers: {[s: string]: () => void} = {};
|
||||
private pingQueue: ResolvedDevice[] = [];
|
||||
private pingQueueExecuting = false;
|
||||
|
||||
constructor(zigbee: TempZigbee, mqtt: TempMQTT, state: TempState,
|
||||
@@ -21,12 +21,12 @@ class AvailabilityNew extends ExtensionTS {
|
||||
logger.warn('Using experimental new availability feature');
|
||||
}
|
||||
|
||||
private getTimeout(re: ResolvedEntity): number {
|
||||
if (typeof re.settings.availability === 'object' && re.settings.availability?.timeout != null) {
|
||||
return minutes(re.settings.availability.timeout);
|
||||
private getTimeout(rd: ResolvedDevice): number {
|
||||
if (typeof rd.settings.availability === 'object' && rd.settings.availability?.timeout != null) {
|
||||
return minutes(rd.settings.availability.timeout);
|
||||
}
|
||||
|
||||
const key = this.isActiveDevice(re) ? 'active' : 'passive';
|
||||
const key = this.isActiveDevice(rd) ? 'active' : 'passive';
|
||||
const availabilitySettings = settings.get().availability;
|
||||
if (typeof availabilitySettings === 'object' && availabilitySettings[key]?.timeout != null) {
|
||||
return minutes(availabilitySettings[key]?.timeout);
|
||||
@@ -35,37 +35,37 @@ class AvailabilityNew extends ExtensionTS {
|
||||
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)';
|
||||
private isActiveDevice(rd: ResolvedDevice): boolean {
|
||||
return (rd.device.type === 'Router' && rd.device.powerSource !== 'Battery') ||
|
||||
rd.device.powerSource === 'Mains (single phase)';
|
||||
}
|
||||
|
||||
private isAvailable(re: ResolvedEntity): boolean {
|
||||
const ago = Date.now() - re.device.lastSeen;
|
||||
return ago < this.getTimeout(re);
|
||||
private isAvailable(rd: ResolvedDevice): boolean {
|
||||
const ago = Date.now() - rd.device.lastSeen;
|
||||
return ago < this.getTimeout(rd);
|
||||
}
|
||||
|
||||
private resetTimer(re: ResolvedEntity): void {
|
||||
clearTimeout(this.timers[re.device.ieeeAddr]);
|
||||
private resetTimer(rd: ResolvedDevice): void {
|
||||
clearTimeout(this.timers[rd.device.ieeeAddr]);
|
||||
|
||||
// If the timer triggers, the device is not avaiable anymore otherwise resetTimer already have been called
|
||||
if (this.isActiveDevice(re)) {
|
||||
if (this.isActiveDevice(rd)) {
|
||||
// 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), this.getTimeout(re) + seconds(1));
|
||||
this.timers[rd.device.ieeeAddr] = setTimeout(
|
||||
() => this.addToPingQueue(rd), this.getTimeout(rd) + seconds(1));
|
||||
} else {
|
||||
this.timers[re.device.ieeeAddr] = setTimeout(
|
||||
() => this.publishAvailability(re, true), this.getTimeout(re) + seconds(1));
|
||||
this.timers[rd.device.ieeeAddr] = setTimeout(
|
||||
() => this.publishAvailability(rd, true), this.getTimeout(rd) + seconds(1));
|
||||
}
|
||||
}
|
||||
|
||||
private addToPingQueue(re: ResolvedEntity): void {
|
||||
this.pingQueue.push(re);
|
||||
private addToPingQueue(rd: ResolvedDevice): void {
|
||||
this.pingQueue.push(rd);
|
||||
this.pingQueueExecuteNext();
|
||||
}
|
||||
|
||||
private removeFromPingQueue(re: ResolvedEntity): void {
|
||||
const index = this.pingQueue.findIndex((r) => r.device.ieeeAddr === re.device.ieeeAddr);
|
||||
private removeFromPingQueue(rd: ResolvedDevice): void {
|
||||
const index = this.pingQueue.findIndex((r) => r.device.ieeeAddr === rd.device.ieeeAddr);
|
||||
index != -1 && this.pingQueue.splice(index, 1);
|
||||
}
|
||||
|
||||
@@ -73,29 +73,29 @@ class AvailabilityNew extends ExtensionTS {
|
||||
if (this.pingQueue.length === 0 || this.pingQueueExecuting) return;
|
||||
this.pingQueueExecuting = true;
|
||||
|
||||
const re = this.pingQueue[0];
|
||||
const rd = this.pingQueue[0];
|
||||
let pingedSuccessfully = false;
|
||||
const available = this.availabilityCache[re.device.ieeeAddr] || this.isAvailable(re);
|
||||
const available = this.availabilityCache[rd.device.ieeeAddr] || this.isAvailable(rd);
|
||||
const attempts = available ? 2 : 1;
|
||||
for (let i = 0; i < attempts; i++) {
|
||||
try {
|
||||
// Enable recovery if device is marked as available and first ping fails.
|
||||
const disableRecovery = !(i == 1 && available);
|
||||
await re.device.ping(disableRecovery);
|
||||
await rd.device.ping(disableRecovery);
|
||||
pingedSuccessfully = true;
|
||||
logger.debug(`Succesfully pinged '${re.name}' (attempt ${i + 1}/${attempts})`);
|
||||
logger.debug(`Succesfully pinged '${rd.name}' (attempt ${i + 1}/${attempts})`);
|
||||
break;
|
||||
} catch (error) {
|
||||
logger.error(`Failed to ping '${re.name}' (attempt ${i + 1}/${attempts}, ${error.message})`);
|
||||
logger.error(`Failed to ping '${rd.name}' (attempt ${i + 1}/${attempts}, ${error.message})`);
|
||||
// Try again in 3 seconds.
|
||||
const lastAttempt = i - 1 === attempts;
|
||||
!lastAttempt && await sleep(3);
|
||||
}
|
||||
}
|
||||
|
||||
this.publishAvailability(re, !pingedSuccessfully);
|
||||
this.resetTimer(re);
|
||||
this.removeFromPingQueue(re);
|
||||
this.publishAvailability(rd, !pingedSuccessfully);
|
||||
this.resetTimer(rd);
|
||||
this.removeFromPingQueue(rd);
|
||||
|
||||
// Sleep 2 seconds before executing next ping
|
||||
await sleep(2);
|
||||
@@ -105,16 +105,16 @@ class AvailabilityNew extends ExtensionTS {
|
||||
|
||||
override onMQTTConnected(): void {
|
||||
for (const device of this.zigbee.getClients()) {
|
||||
const re: ResolvedEntity = this.zigbee.resolveEntity(device);
|
||||
if (isAvailabilityNewEnabledForDevice(re, settings.get())) {
|
||||
const rd = this.zigbee.resolveEntity(device) as ResolvedDevice;
|
||||
if (isAvailabilityEnabledForDevice(rd, settings.get())) {
|
||||
// Publish initial availablility
|
||||
this.publishAvailability(re, true);
|
||||
this.publishAvailability(rd, true);
|
||||
|
||||
this.resetTimer(re);
|
||||
this.resetTimer(rd);
|
||||
|
||||
// If an active device is initially unavailable, ping it.
|
||||
if (this.isActiveDevice(re) && !this.isAvailable(re)) {
|
||||
this.addToPingQueue(re);
|
||||
if (this.isActiveDevice(rd) && !this.isAvailable(rd)) {
|
||||
this.addToPingQueue(rd);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -124,44 +124,50 @@ class AvailabilityNew extends ExtensionTS {
|
||||
this.zigbee.on('lastSeenChanged', this.lastSeenChanged);
|
||||
}
|
||||
|
||||
override onZigbeeEvent(type: ZigbeeEventType, data: ZigbeeEventData, resolvedEntity: ResolvedEntity): void {
|
||||
resolvedEntity;
|
||||
|
||||
override onZigbeeEvent(type: ZigbeeEventType, data: ZigbeeEventData, re: ResolvedEntity): void {
|
||||
/* istanbul ignore else */
|
||||
if (type === 'deviceLeave') {
|
||||
clearTimeout(this.timers[data.ieeeAddr]);
|
||||
} else if (type === 'deviceAnnounce') {
|
||||
this.retrieveState(re as ResolvedDevice);
|
||||
}
|
||||
}
|
||||
|
||||
private publishAvailability(re: ResolvedEntity, logLastSeen: boolean): void {
|
||||
private publishAvailability(rd: ResolvedDevice, logLastSeen: boolean): void {
|
||||
if (logLastSeen) {
|
||||
const ago = Date.now() - re.device.lastSeen;
|
||||
if (this.isActiveDevice(re)) {
|
||||
const ago = Date.now() - rd.device.lastSeen;
|
||||
if (this.isActiveDevice(rd)) {
|
||||
logger.debug(
|
||||
`Active device '${re.name}' was last seen '${(ago / minutes(1)).toFixed(2)}' minutes ago.`);
|
||||
`Active device '${rd.name}' was last seen '${(ago / minutes(1)).toFixed(2)}' minutes ago.`);
|
||||
} else {
|
||||
logger.debug(`Passive device '${re.name}' was last seen '${(ago / hours(1)).toFixed(2)}' hours ago.`);
|
||||
logger.debug(`Passive device '${rd.name}' was last seen '${(ago / hours(1)).toFixed(2)}' hours ago.`);
|
||||
}
|
||||
}
|
||||
|
||||
const available = this.isAvailable(re);
|
||||
if (this.availabilityCache[re.device.ieeeAddr] == available) {
|
||||
const available = this.isAvailable(rd);
|
||||
if (this.availabilityCache[rd.device.ieeeAddr] == available) {
|
||||
return;
|
||||
}
|
||||
|
||||
const topic = `${re.name}/availability`;
|
||||
if (rd.device.ieeeAddr in this.availabilityCache && available &&
|
||||
this.availabilityCache[rd.device.ieeeAddr] === false) {
|
||||
logger.debug(`Device '${rd.name}' reconnected`);
|
||||
this.retrieveState(rd);
|
||||
}
|
||||
|
||||
const topic = `${rd.name}/availability`;
|
||||
const payload = available ? 'online' : 'offline';
|
||||
this.availabilityCache[re.device.ieeeAddr] = available;
|
||||
this.availabilityCache[rd.device.ieeeAddr] = available;
|
||||
this.mqtt.publish(topic, payload, {retain: true, qos: 0});
|
||||
}
|
||||
|
||||
private lastSeenChanged(data: {device: Device}): void {
|
||||
const re = this.zigbee.resolveEntity(data.device);
|
||||
if (isAvailabilityNewEnabledForDevice(re, settings.get())) {
|
||||
const rd = this.zigbee.resolveEntity(data.device) as ResolvedDevice;
|
||||
if (isAvailabilityEnabledForDevice(rd, settings.get())) {
|
||||
// Remove from ping queue, not necessary anymore since we know the device is online.
|
||||
this.removeFromPingQueue(re);
|
||||
this.resetTimer(re);
|
||||
this.publishAvailability(re, false);
|
||||
this.removeFromPingQueue(rd);
|
||||
this.resetTimer(rd);
|
||||
this.publishAvailability(rd, false);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -170,6 +176,31 @@ class AvailabilityNew extends ExtensionTS {
|
||||
this.zigbee.removeListener('lastSeenChanged', this.lastSeenChanged);
|
||||
super.stop();
|
||||
}
|
||||
|
||||
private retrieveState(rd: ResolvedDevice): void {
|
||||
/**
|
||||
* Retrieve state of a device in a debounced manner, this function is called on a 'deviceAnnounce' which a
|
||||
* device can send multiple times after each other.
|
||||
*/
|
||||
if (rd.definition && !rd.device.interviewing && !this.retrieveStateDebouncers[rd.device.ieeeAddr]) {
|
||||
this.retrieveStateDebouncers[rd.device.ieeeAddr] = debounce(async () => {
|
||||
try {
|
||||
logger.debug(`Retrieving state of '${rd.name}' after reconnect`);
|
||||
// Color and color temperature converters do both, only needs to be called once.
|
||||
const keySet = [['state'], ['brightness'], ['color', 'color_temp']];
|
||||
for (const keys of keySet) {
|
||||
const converter = rd.definition.toZigbee.find((c) => c.key.find((k) => keys.includes(k)));
|
||||
await converter?.convertGet?.(rd.endpoint, keys[0],
|
||||
{message: this.state.get(rd.device.ieeeAddr) || {}});
|
||||
}
|
||||
} catch (error) {
|
||||
logger.error(`Failed to read state of '${rd.name}' after reconnect`);
|
||||
}
|
||||
}, seconds(2));
|
||||
}
|
||||
|
||||
this.retrieveStateDebouncers[rd.device.ieeeAddr]();
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = AvailabilityNew;
|
||||
|
||||
@@ -858,7 +858,7 @@ class HomeAssistant extends Extension {
|
||||
payload.availability_mode = 'all';
|
||||
/* istanbul ignore next */
|
||||
const availabilityEnabled = settings.get().experimental.availability_new ?
|
||||
utils.isAvailabilityNewEnabledForDevice(resolvedEntity, settings) :
|
||||
utils.isAvailabilityEnabledForDevice(resolvedEntity, settings) :
|
||||
settings.get().advanced.availability_timeout;
|
||||
if (resolvedEntity.type === 'device' && availabilityEnabled) {
|
||||
payload.availability.push({topic: `${settings.get().mqtt.base_topic}/${friendlyName}/availability`});
|
||||
|
||||
+15
-5
@@ -1,9 +1,9 @@
|
||||
import type {Device as ZHDevice} from 'zigbee-herdsman/dist/controller/model';
|
||||
import type {Device as ZHDevice, Endpoint} from 'zigbee-herdsman/dist/controller/model';
|
||||
|
||||
declare global {
|
||||
type Device = ZHDevice;
|
||||
|
||||
type ZigbeeEventType = 'deviceLeave';
|
||||
type ZigbeeEventType = 'deviceLeave' | 'deviceAnnounce';
|
||||
|
||||
interface ZigbeeEventData {
|
||||
ieeeAddr: string;
|
||||
@@ -124,9 +124,17 @@ declare global {
|
||||
|
||||
interface ResolvedEntity {
|
||||
type: 'device' | 'group',
|
||||
definition?: {model: string},
|
||||
}
|
||||
|
||||
interface ResolvedDevice {
|
||||
type: 'device',
|
||||
definition?: {
|
||||
model: string
|
||||
toZigbee: {key: string[], convertGet?: (entity: Endpoint, key: string, meta: {}) => Promise<void>}[]
|
||||
},
|
||||
name: string,
|
||||
device?: Device,
|
||||
endpoint: Endpoint,
|
||||
device: Device,
|
||||
settings: {
|
||||
friendlyName: string,
|
||||
availability?: {timeout?: number} | boolean,
|
||||
@@ -146,7 +154,9 @@ declare global {
|
||||
publish: (topic: string, payload: string, options: {}, base?: string, skipLog?: boolean, skipReceive?: boolean) => Promise<void>;
|
||||
}
|
||||
|
||||
interface TempState {}
|
||||
interface TempState {
|
||||
get: (ID: string) => {} | null;
|
||||
}
|
||||
|
||||
interface TempEventBus {
|
||||
removeListenersExtension: (extension: string) => void;
|
||||
|
||||
@@ -406,7 +406,7 @@ export function get(): Settings {
|
||||
return _settingsWithDefaults;
|
||||
}
|
||||
|
||||
export function set(path: string[], value: string | number): void {
|
||||
export function set(path: string[], value: string | number | boolean | KeyValue): void {
|
||||
/* eslint-disable-next-line */
|
||||
let settings: any = getInternalSettings();
|
||||
|
||||
|
||||
+16
-2
@@ -254,8 +254,22 @@ export function sanitizeImageParameter(parameter: string): string {
|
||||
return sanitized;
|
||||
}
|
||||
|
||||
export function isAvailabilityNewEnabledForDevice(re: ResolvedEntity, settings: Settings): boolean {
|
||||
return re.settings.hasOwnProperty('availability') ? !!re.settings.availability : !!settings.availability;
|
||||
export function isAvailabilityEnabledForDevice(rd: ResolvedDevice, settings: Settings): boolean {
|
||||
if (rd.settings.hasOwnProperty('availability')) {
|
||||
return !!rd.settings.availability;
|
||||
}
|
||||
|
||||
// availability_timeout = deprecated
|
||||
const enabledGlobal = settings.advanced.availability_timeout || settings.availability;
|
||||
if (!enabledGlobal) return false;
|
||||
|
||||
const passlist = settings.advanced.availability_passlist.concat(settings.advanced.availability_whitelist);
|
||||
if (passlist.length > 0) {
|
||||
return passlist.includes(rd.name) || passlist.includes(rd.device.ieeeAddr);
|
||||
}
|
||||
|
||||
const blocklist = settings.advanced.availability_blacklist.concat(settings.advanced.availability_blocklist);
|
||||
return !blocklist.includes(rd.name) && !blocklist.includes(rd.device.ieeeAddr);
|
||||
}
|
||||
|
||||
export function isXiaomiDevice(device: Device): boolean {
|
||||
|
||||
Generated
+6
@@ -1916,6 +1916,12 @@
|
||||
"@babel/types": "^7.3.0"
|
||||
}
|
||||
},
|
||||
"@types/debounce": {
|
||||
"version": "1.2.0",
|
||||
"resolved": "https://registry.npmjs.org/@types/debounce/-/debounce-1.2.0.tgz",
|
||||
"integrity": "sha512-bWG5wapaWgbss9E238T0R6bfo5Fh3OkeoSt245CM7JJwVwpw6MEBCbIxLq5z8KzsE3uJhzcIuQkyiZmzV3M/Dw==",
|
||||
"dev": true
|
||||
},
|
||||
"@types/graceful-fs": {
|
||||
"version": "4.1.5",
|
||||
"resolved": "https://registry.npmjs.org/@types/graceful-fs/-/graceful-fs-4.1.5.tgz",
|
||||
|
||||
@@ -63,6 +63,7 @@
|
||||
"@babel/core": "^7.15.0",
|
||||
"@babel/preset-env": "^7.15.0",
|
||||
"@babel/preset-typescript": "^7.15.0",
|
||||
"@types/debounce": "^1.2.0",
|
||||
"@types/humanize-duration": "^3.25.1",
|
||||
"@types/jest": "^27.0.1",
|
||||
"@types/js-yaml": "^4.0.3",
|
||||
|
||||
+112
-48
@@ -1,30 +1,25 @@
|
||||
const data = require('./stub/data');
|
||||
const logger = require('./stub/logger');
|
||||
const stringify = require('json-stable-stringify-without-jsonify');
|
||||
const zigbeeHerdsman = require('./stub/zigbeeHerdsman');
|
||||
zigbeeHerdsman.returnDevices.push('0x000b57fffec6a5b3');
|
||||
zigbeeHerdsman.returnDevices.push('0x000b57fffec6a5b4');
|
||||
zigbeeHerdsman.returnDevices.push('0x00124b00120144ae');
|
||||
zigbeeHerdsman.returnDevices.push('0x0017880104e45517');
|
||||
const MQTT = require('./stub/mqtt');
|
||||
const utils = require('../lib/util/utils');
|
||||
const settings = require('../lib/util/settings');
|
||||
const Controller = require('../lib/controller');
|
||||
const flushPromises = require('./lib/flushPromises');
|
||||
import data from './stub/data';
|
||||
import logger from './stub/logger';
|
||||
import MQTT from './stub/mqtt';
|
||||
import zigbeeHerdsman from './stub/zigbeeHerdsman';
|
||||
|
||||
import * as utils from '../lib/util/utils';
|
||||
import * as settings from '../lib/util/settings';
|
||||
import Controller from '../lib/controller';
|
||||
import flushPromises from './lib/flushPromises';
|
||||
|
||||
const devices = zigbeeHerdsman.devices;
|
||||
const mocks = [MQTT.publish, logger.warn, logger.debug];
|
||||
|
||||
const hours = (hours) => 1000 * 60 * 60 * hours;
|
||||
const minutes = (minutes) => 1000 * 60 * minutes;
|
||||
zigbeeHerdsman.returnDevices.concat(
|
||||
[devices.bulb_color.ieeeAddr, devices.bulb_color_2.ieeeAddr, devices.coordinator.ieeeAddr, devices.remote])
|
||||
|
||||
describe('Availability', () => {
|
||||
let controller;
|
||||
let extension;
|
||||
let devices = zigbeeHerdsman.devices;
|
||||
|
||||
let resetExtension = async () => {
|
||||
await controller.enableDisableExtension(false, 'AvailabilityNew');
|
||||
await controller.enableDisableExtension(true, 'AvailabilityNew');
|
||||
extension = controller.extensions.find((e) => e.constructor.name === 'AvailabilityNew');
|
||||
}
|
||||
|
||||
const advancedTime = async (value) => {
|
||||
@@ -34,7 +29,7 @@ describe('Availability', () => {
|
||||
}
|
||||
|
||||
beforeAll(async () => {
|
||||
jest.spyOn(utils, 'sleep').mockImplementation(() => {});
|
||||
jest.spyOn(utils, 'sleep').mockImplementation(async (seconds: number) => {});
|
||||
jest.useFakeTimers('modern');
|
||||
settings.reRead();
|
||||
settings.set(['availability'], true);
|
||||
@@ -45,12 +40,14 @@ describe('Availability', () => {
|
||||
});
|
||||
|
||||
beforeEach(async () => {
|
||||
jest.useFakeTimers('modern').setSystemTime(minutes(1));
|
||||
jest.useFakeTimers('modern').setSystemTime(utils.minutes(1));
|
||||
data.writeDefaultConfiguration();
|
||||
settings.set(['devices', '0x000b57fffec6a5b4', 'availability'], false);
|
||||
settings.set(['devices', '0x000b57fffec6a5b3', 'availability'], true);
|
||||
settings.reRead();
|
||||
settings.set(['availability'], true);
|
||||
settings.set(['experimental', 'availability_new'], true);
|
||||
settings.set(['devices', devices.bulb_color_2.ieeeAddr, 'availability'], false);
|
||||
// @ts-ignore
|
||||
Object.values(zigbeeHerdsman.devices).forEach(d => d.lastSeen = minutes(1));
|
||||
Object.values(devices).forEach(d => d.lastSeen = utils.minutes(1));
|
||||
mocks.forEach((m) => m.mockClear());
|
||||
await resetExtension();
|
||||
// @ts-ignore
|
||||
@@ -59,44 +56,46 @@ describe('Availability', () => {
|
||||
|
||||
afterEach(async () => {
|
||||
// @ts-ignore
|
||||
Object.values(zigbeeHerdsman.devices).forEach(d => d.lastSeen = minutes(1));
|
||||
Object.values(devices).forEach(d => d.lastSeen = utils.minutes(1));
|
||||
})
|
||||
|
||||
afterAll(async () => {
|
||||
jest.useRealTimers();
|
||||
})
|
||||
|
||||
it('Should publish availabilty on startup', async () => {
|
||||
it('Should publish availabilty on startup for device where it is enabled for', async () => {
|
||||
expect(MQTT.publish).toHaveBeenCalledWith('zigbee2mqtt/bulb_color/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).not.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 () => {
|
||||
MQTT.publish.mockClear();
|
||||
|
||||
await advancedTime(minutes(5));
|
||||
await advancedTime(utils.minutes(5));
|
||||
expect(devices.bulb_color.ping).toHaveBeenCalledTimes(0);
|
||||
|
||||
await advancedTime(minutes(7));
|
||||
await advancedTime(utils.minutes(7));
|
||||
expect(devices.bulb_color.ping).toHaveBeenCalledTimes(1);
|
||||
expect(devices.bulb_color.ping).toHaveBeenNthCalledWith(1, true);
|
||||
expect(MQTT.publish).toHaveBeenCalledWith('zigbee2mqtt/bulb_color/availability',
|
||||
'offline', {retain: true, qos: 0}, expect.any(Function));
|
||||
});
|
||||
|
||||
it('Shouldnt do anything for a device when availability: false is set', async () => {
|
||||
it('Shouldnt do anything for a device when availability: false is set for device', async () => {
|
||||
MQTT.publish.mockClear();
|
||||
await zigbeeHerdsman.events.lastSeenChanged({device: devices.bulb_color_2}); // Coverage satisfaction
|
||||
|
||||
await advancedTime(minutes(12));
|
||||
await zigbeeHerdsman.events.lastSeenChanged({device: devices.bulb_color_2});
|
||||
await advancedTime(utils.minutes(12));
|
||||
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));
|
||||
await advancedTime(utils.hours(26));
|
||||
expect(devices.remote.ping).toHaveBeenCalledTimes(0);
|
||||
expect(MQTT.publish).toHaveBeenCalledWith('zigbee2mqtt/remote/availability',
|
||||
'offline', {retain: true, qos: 0}, expect.any(Function));
|
||||
@@ -105,15 +104,15 @@ describe('Availability', () => {
|
||||
it('Should reset ping timer when device last seen changes for active device', async () => {
|
||||
MQTT.publish.mockClear();
|
||||
|
||||
await advancedTime(minutes(5));
|
||||
await advancedTime(utils.minutes(5));
|
||||
expect(devices.bulb_color.ping).toHaveBeenCalledTimes(0);
|
||||
|
||||
await zigbeeHerdsman.events.lastSeenChanged({device: devices.bulb_color});
|
||||
|
||||
await advancedTime(minutes(7));
|
||||
await advancedTime(utils.minutes(7));
|
||||
expect(devices.bulb_color.ping).toHaveBeenCalledTimes(0);
|
||||
|
||||
await advancedTime(minutes(10));
|
||||
await advancedTime(utils.minutes(10));
|
||||
expect(devices.bulb_color.ping).toHaveBeenCalledTimes(1);
|
||||
expect(devices.bulb_color.ping).toHaveBeenNthCalledWith(1, true);
|
||||
expect(MQTT.publish).toHaveBeenCalledWith('zigbee2mqtt/bulb_color/availability',
|
||||
@@ -123,17 +122,17 @@ describe('Availability', () => {
|
||||
it('Should ping again when first ping fails', async () => {
|
||||
MQTT.publish.mockClear();
|
||||
|
||||
await advancedTime(minutes(5));
|
||||
await advancedTime(utils.minutes(5));
|
||||
expect(devices.bulb_color.ping).toHaveBeenCalledTimes(0);
|
||||
|
||||
await zigbeeHerdsman.events.lastSeenChanged({device: devices.bulb_color});
|
||||
|
||||
await advancedTime(minutes(7));
|
||||
await advancedTime(utils.minutes(7));
|
||||
expect(devices.bulb_color.ping).toHaveBeenCalledTimes(0);
|
||||
|
||||
devices.bulb_color.ping.mockImplementationOnce(() => {throw new Error('failed')});
|
||||
devices.bulb_color.lastSeen = Date.now() + minutes(10);
|
||||
await advancedTime(minutes(10));
|
||||
devices.bulb_color.lastSeen = Date.now() + utils.minutes(10);
|
||||
await advancedTime(utils.minutes(10));
|
||||
expect(devices.bulb_color.ping).toHaveBeenCalledTimes(2);
|
||||
expect(devices.bulb_color.ping).toHaveBeenNthCalledWith(1, true);
|
||||
expect(devices.bulb_color.ping).toHaveBeenNthCalledWith(2, false);
|
||||
@@ -144,16 +143,16 @@ describe('Availability', () => {
|
||||
it('Should reset ping timer when device last seen changes for passive device', async () => {
|
||||
MQTT.publish.mockClear();
|
||||
|
||||
await advancedTime(hours(24));
|
||||
await advancedTime(utils.hours(24));
|
||||
expect(devices.remote.ping).toHaveBeenCalledTimes(0);
|
||||
|
||||
await zigbeeHerdsman.events.lastSeenChanged({device: devices.remote});
|
||||
|
||||
await advancedTime(hours(25));
|
||||
await advancedTime(utils.hours(25));
|
||||
expect(devices.remote.ping).toHaveBeenCalledTimes(0);
|
||||
|
||||
devices.remote.ping.mockImplementationOnce(() => {throw new Error('failed')});
|
||||
await advancedTime(hours(3));
|
||||
await advancedTime(utils.hours(3));
|
||||
expect(devices.remote.ping).toHaveBeenCalledTimes(0);
|
||||
expect(MQTT.publish).toHaveBeenCalledWith('zigbee2mqtt/remote/availability',
|
||||
'offline', {retain: true, qos: 0}, expect.any(Function));
|
||||
@@ -162,7 +161,7 @@ describe('Availability', () => {
|
||||
it('Should immediately mark device as online when it lastSeen changes', async () => {
|
||||
MQTT.publish.mockClear();
|
||||
|
||||
await advancedTime(minutes(15));
|
||||
await advancedTime(utils.minutes(15));
|
||||
expect(MQTT.publish).toHaveBeenCalledWith('zigbee2mqtt/bulb_color/availability',
|
||||
'offline', {retain: true, qos: 0}, expect.any(Function));
|
||||
|
||||
@@ -178,10 +177,10 @@ describe('Availability', () => {
|
||||
await resetExtension();
|
||||
MQTT.publish.mockClear();
|
||||
|
||||
await advancedTime(minutes(25));
|
||||
await advancedTime(utils.minutes(25));
|
||||
expect(devices.bulb_color.ping).toHaveBeenCalledTimes(0);
|
||||
|
||||
await advancedTime(minutes(17));
|
||||
await advancedTime(utils.minutes(17));
|
||||
expect(devices.bulb_color.ping).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
@@ -190,10 +189,10 @@ describe('Availability', () => {
|
||||
await resetExtension();
|
||||
MQTT.publish.mockClear();
|
||||
|
||||
await advancedTime(minutes(25));
|
||||
await advancedTime(utils.minutes(25));
|
||||
expect(devices.bulb_color.ping).toHaveBeenCalledTimes(0);
|
||||
|
||||
await advancedTime(minutes(7));
|
||||
await advancedTime(utils.minutes(7));
|
||||
expect(devices.bulb_color.ping).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
@@ -201,13 +200,78 @@ describe('Availability', () => {
|
||||
await resetExtension();
|
||||
MQTT.publish.mockClear();
|
||||
|
||||
await advancedTime(minutes(9));
|
||||
await advancedTime(utils.minutes(9));
|
||||
expect(devices.bulb_color.ping).toHaveBeenCalledTimes(0);
|
||||
|
||||
await zigbeeHerdsman.events.deviceLeave({ieeeAddr: devices.bulb_color.ieeeAddr});
|
||||
await flushPromises();
|
||||
|
||||
await advancedTime(minutes(3));
|
||||
await advancedTime(utils.minutes(3));
|
||||
expect(devices.bulb_color.ping).toHaveBeenCalledTimes(0);
|
||||
});
|
||||
|
||||
it('Should allow to be disabled', async () => {
|
||||
settings.set(['availability'], false);
|
||||
await resetExtension();
|
||||
|
||||
await advancedTime(utils.minutes(12));
|
||||
expect(devices.bulb_color.ping).toHaveBeenCalledTimes(0);
|
||||
});
|
||||
|
||||
it('Should retrieve device state when it reconnects', async () => {
|
||||
MQTT.publish.mockClear();
|
||||
|
||||
const endpoint = devices.bulb_color.getEndpoint(1);
|
||||
endpoint.read.mockClear();
|
||||
|
||||
await zigbeeHerdsman.events.deviceAnnounce({device: devices.bulb_color});
|
||||
await flushPromises();
|
||||
await advancedTime(utils.seconds(1));
|
||||
await zigbeeHerdsman.events.deviceAnnounce({device: devices.bulb_color});
|
||||
await flushPromises();
|
||||
|
||||
expect(endpoint.read).toHaveBeenCalledTimes(0);
|
||||
await advancedTime(utils.seconds(2));
|
||||
|
||||
expect(endpoint.read).toHaveBeenCalledTimes(3);
|
||||
expect(endpoint.read).toHaveBeenCalledWith('genOnOff', ['onOff']);
|
||||
expect(endpoint.read).toHaveBeenCalledWith('genLevelCtrl', ['currentLevel']);
|
||||
expect(endpoint.read).toHaveBeenCalledWith('lightingColorCtrl',
|
||||
['colorMode', 'currentX', 'currentY', 'enhancedCurrentHue', 'currentSaturation', 'colorTemperature']);
|
||||
|
||||
// Should stop when one request fails
|
||||
endpoint.read.mockClear();
|
||||
await zigbeeHerdsman.events.deviceAnnounce({device: devices.bulb_color});
|
||||
await flushPromises();
|
||||
endpoint.read.mockImplementationOnce(() => {throw new Error('')});
|
||||
await advancedTime(utils.seconds(3));
|
||||
expect(endpoint.read).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('Deprecated - should allow to block via advanced.availability_blocklist', async () => {
|
||||
settings.set(['advanced', 'availability_blocklist'], [devices.bulb_color.ieeeAddr]);
|
||||
await resetExtension();
|
||||
|
||||
await advancedTime(utils.minutes(12));
|
||||
expect(devices.bulb_color.ping).toHaveBeenCalledTimes(0);
|
||||
});
|
||||
|
||||
it('Deprecated - should allow to pass certain devices via availability_passlist', async () => {
|
||||
settings.set(['advanced', 'availability_passlist'], [devices.bulb_color_2.ieeeAddr]);
|
||||
settings.changeEntityOptions(devices.bulb_color_2.ieeeAddr, {availability: null});
|
||||
await resetExtension();
|
||||
|
||||
await advancedTime(utils.minutes(12));
|
||||
expect(devices.bulb_color.ping).toHaveBeenCalledTimes(0);
|
||||
expect(devices.bulb_color_2.ping).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('Deprecated - should allow to enable via availability_timeout', async () => {
|
||||
settings.set(['availability'], false);
|
||||
settings.set(['advanced', 'availability_timeout'], 60);
|
||||
await resetExtension();
|
||||
|
||||
await advancedTime(utils.minutes(12));
|
||||
expect(devices.bulb_color.ping).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user