From 9129f95c3968614a2ee2e95afc56a3d450f54ace Mon Sep 17 00:00:00 2001 From: Koen Kanters Date: Mon, 23 Aug 2021 19:24:10 +0200 Subject: [PATCH] Add availability_mode for Home Assistant and publish availability only when its enabled for that device. #6281 --- lib/controller.js | 11 ++++---- lib/extension/availabilityNew.ts | 22 ++++++---------- lib/extension/homeassistant.js | 7 +++-- lib/util/utils.js | 5 ++++ test/availabilityNew.test.ts | 2 -- test/homeassistant.test.js | 45 +++++++++++++++++++++----------- 6 files changed, 53 insertions(+), 39 deletions(-) diff --git a/lib/controller.js b/lib/controller.js index c60ed7af..4a69e094 100644 --- a/lib/controller.js +++ b/lib/controller.js @@ -88,13 +88,12 @@ class Controller { this.extensions.push(new ExtensionSoftReset(...args)); } - if (settings.get().advanced.availability_timeout || settings.get().availability) { - if (settings.get().experimental.availability_new) { - this.extensions.push(new ExtensionAvailabilityNew(...args)); - } else { - this.extensions.push(new ExtensionAvailability(...args)); - } + if (settings.get().experimental.availability_new) { + this.extensions.push(new ExtensionAvailabilityNew(...args)); + } else if (settings.get().advanced.availability_timeout) { + this.extensions.push(new ExtensionAvailability(...args)); } + this.extensions.push(new ExtensionExternalExtension(...args)); } diff --git a/lib/extension/availabilityNew.ts b/lib/extension/availabilityNew.ts index a3769adb..e7d560fc 100644 --- a/lib/extension/availabilityNew.ts +++ b/lib/extension/availabilityNew.ts @@ -1,6 +1,6 @@ import ExtensionTS from './extensionts'; import logger from '../util/logger'; -import {sleep} from '../util/utils'; +import {sleep, isAvailabilityNewEnabledForDevice} from '../util/utils'; import * as settings from '../util/settings'; const hours = (hours: number): number => 1000 * 60 * 60 * hours; @@ -9,9 +9,9 @@ const seconds = (seconds: number): number => 1000 * seconds; // TODO // - State retrieval -// - Home Assistant add availability mode // - Honour legacy availability_timeout, availability_blocklist and availability_passlist options. // - Enable for HA addon +// - Add to setting schema class AvailabilityNew extends ExtensionTS { private timers: {[s: string]: NodeJS.Timeout} = {}; private availabilityCache: {[s: string]: boolean} = {}; @@ -25,10 +25,6 @@ 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); @@ -114,11 +110,10 @@ 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)) { + // Publish initial availablility + this.publishAvailability(re, true); - // Publish initial availablility - this.publishAvailability(re, true); - - if (this.isEnabledForDevice(re)) { this.resetTimer(re); // If an active device is initially unavailable, ping it. @@ -134,8 +129,7 @@ class AvailabilityNew extends ExtensionTS { } private publishAvailability(re: ResolvedEntity, logLastSeen: boolean): void { - const enabled = this.isEnabledForDevice(re); - if (enabled && logLastSeen) { + if (logLastSeen) { const ago = Date.now() - re.device.lastSeen; if (this.isActiveDevice(re)) { logger.debug( @@ -145,7 +139,7 @@ class AvailabilityNew extends ExtensionTS { } } - const available = enabled ? this.isAvailable(re) : true; + const available = this.isAvailable(re); if (this.availabilityCache[re.device.ieeeAddr] == available) { return; } @@ -158,7 +152,7 @@ class AvailabilityNew extends ExtensionTS { private lastSeenChanged(data: {device: Device}): void { const re = this.zigbee.resolveEntity(data.device); - if (this.isEnabledForDevice(re)) { + if (isAvailabilityNewEnabledForDevice(re, settings)) { // Remove from ping queue, not necessary anymore since we know the device is online. this.removeFromPingQueue(re); this.resetTimer(re); diff --git a/lib/extension/homeassistant.js b/lib/extension/homeassistant.js index cc684b12..71f5504b 100644 --- a/lib/extension/homeassistant.js +++ b/lib/extension/homeassistant.js @@ -855,8 +855,11 @@ class HomeAssistant extends Extension { // Availability payload payload.availability = [{topic: `${settings.get().mqtt.base_topic}/bridge/state`}]; - const availabilityEnabled = settings.get().availability || settings.get().advanced.availability_timeout || - resolvedEntity.settings.availability; + payload.availability_mode = 'all'; + /* istanbul ignore next */ + const availabilityEnabled = settings.get().experimental.availability_new ? + utils.isAvailabilityNewEnabledForDevice(resolvedEntity, settings) : + settings.get().advanced.availability_timeout; if (resolvedEntity.type === 'device' && availabilityEnabled) { payload.availability.push({topic: `${settings.get().mqtt.base_topic}/${friendlyName}/availability`}); } diff --git a/lib/util/utils.js b/lib/util/utils.js index 50a4710c..33f0f992 100644 --- a/lib/util/utils.js +++ b/lib/util/utils.js @@ -267,6 +267,10 @@ function sanitizeImageParameter(parameter) { return sanitized; } +function isAvailabilityNewEnabledForDevice(re, settings) { + return re.settings.hasOwnProperty('availability') ? !!re.settings.availability : !!settings.get().availability; +} + module.exports = { millisecondsToSeconds: (milliseconds) => milliseconds / 1000, secondsToMilliseconds: (seconds) => seconds * 1000, @@ -292,6 +296,7 @@ module.exports = { validateFriendlyName, loadModuleFromFile, loadModuleFromText, + isAvailabilityNewEnabledForDevice, getKey, sanitizeImageParameter, removeNullPropertiesFromObject, diff --git a/test/availabilityNew.test.ts b/test/availabilityNew.test.ts index 2c693758..d8b0b126 100644 --- a/test/availabilityNew.test.ts +++ b/test/availabilityNew.test.ts @@ -71,8 +71,6 @@ 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 () => { diff --git a/test/homeassistant.test.js b/test/homeassistant.test.js index ae295928..319f2e36 100644 --- a/test/homeassistant.test.js +++ b/test/homeassistant.test.js @@ -62,6 +62,7 @@ describe('HomeAssistant extension', () => { payload = { "availability":[{"topic":"zigbee2mqtt/bridge/state"}], + "availability_mode": "all", "brightness":true, "brightness_scale":254, "color_mode":true, @@ -93,6 +94,7 @@ describe('HomeAssistant extension', () => { payload = { "availability":[{"topic":"zigbee2mqtt/bridge/state"}], + "availability_mode": "all", "command_topic":"zigbee2mqtt/ha_discovery_group/set", "device":{ "identifiers":["zigbee2mqtt_1221051039810110150109113116116_9"], @@ -132,6 +134,7 @@ describe('HomeAssistant extension', () => { 'manufacturer': 'Xiaomi', }, 'availability': [{topic: 'zigbee2mqtt/bridge/state'}], + 'availability_mode': 'all', }; expect(MQTT.publish).toHaveBeenCalledWith( @@ -158,6 +161,7 @@ describe('HomeAssistant extension', () => { 'manufacturer': 'Xiaomi', }, 'availability': [{topic: 'zigbee2mqtt/bridge/state'}], + 'availability_mode': 'all', }; expect(MQTT.publish).toHaveBeenCalledWith( @@ -184,6 +188,7 @@ describe('HomeAssistant extension', () => { 'manufacturer': 'Xiaomi', }, 'availability': [{topic: 'zigbee2mqtt/bridge/state'}], + 'availability_mode': 'all', }; expect(MQTT.publish).toHaveBeenCalledWith( @@ -210,6 +215,7 @@ describe('HomeAssistant extension', () => { 'manufacturer': 'Xiaomi', }, 'availability': [{topic: 'zigbee2mqtt/bridge/state'}], + 'availability_mode': 'all', }; expect(MQTT.publish).toHaveBeenCalledWith( @@ -237,6 +243,7 @@ describe('HomeAssistant extension', () => { 'manufacturer': 'Xiaomi', }, 'availability': [{topic: 'zigbee2mqtt/bridge/state'}], + 'availability_mode': 'all', }; expect(MQTT.publish).toHaveBeenCalledWith( @@ -247,11 +254,8 @@ describe('HomeAssistant extension', () => { ); payload = { - "availability":[ - { - "topic":"zigbee2mqtt/bridge/state" - } - ], + "availability":[{"topic":"zigbee2mqtt/bridge/state"}], + "availability_mode": "all", "command_topic":"zigbee2mqtt/wall_switch_double/left/set", "device":{ "identifiers":[ @@ -279,11 +283,8 @@ describe('HomeAssistant extension', () => { ); payload = { - "availability":[ - { - "topic":"zigbee2mqtt/bridge/state" - } - ], + "availability":[{"topic":"zigbee2mqtt/bridge/state"}], + "availability_mode": "all", "command_topic":"zigbee2mqtt/wall_switch_double/right/set", "device":{ "identifiers":[ @@ -311,11 +312,8 @@ describe('HomeAssistant extension', () => { ); payload = { - "availability":[ - { - "topic":"zigbee2mqtt/bridge/state" - } - ], + "availability":[{"topic":"zigbee2mqtt/bridge/state"}], + "availability_mode": "all", "brightness":true, "brightness_scale":254, "color_mode": true, @@ -388,6 +386,7 @@ describe('HomeAssistant extension', () => { 'manufacturer': 'Xiaomi', }, 'availability': [{topic: 'zigbee2mqtt/bridge/state'}], + 'availability_mode': 'all', }; expect(MQTT.publish).toHaveBeenCalledWith( @@ -414,6 +413,7 @@ describe('HomeAssistant extension', () => { 'manufacturer': 'Xiaomi', }, 'availability': [{topic: 'zigbee2mqtt/bridge/state'}], + 'availability_mode': 'all', }; expect(MQTT.publish).toHaveBeenCalledWith( @@ -440,6 +440,7 @@ describe('HomeAssistant extension', () => { 'manufacturer': 'Xiaomi', }, 'availability': [{topic: 'zigbee2mqtt/bridge/state'}], + 'availability_mode': 'all', }; expect(MQTT.publish).toHaveBeenCalledWith( @@ -497,6 +498,7 @@ describe('HomeAssistant extension', () => { 'manufacturer': 'From Xiaomi', }, 'availability': [{topic: 'zigbee2mqtt/bridge/state'}], + 'availability_mode': 'all', 'expire_after': 90, 'icon': 'mdi:test', }; @@ -524,6 +526,7 @@ describe('HomeAssistant extension', () => { 'manufacturer': 'Not from Xiaomi', }, 'availability': [{topic: 'zigbee2mqtt/bridge/state'}], + 'availability_mode': 'all', 'expire_after': 30, 'icon': 'mdi:test', }; @@ -560,6 +563,7 @@ describe('HomeAssistant extension', () => { payload = { 'availability': [{topic: 'zigbee2mqtt/bridge/state'}], + 'availability_mode': 'all', "command_topic": "zigbee2mqtt/my_switch/set", "device": { "identifiers": [ @@ -659,6 +663,7 @@ describe('HomeAssistant extension', () => { "manufacturer":"Hampton Bay" }, 'availability': [{topic: 'zigbee2mqtt/bridge/state'}], + 'availability_mode': 'all', }; expect(MQTT.publish).toHaveBeenCalledWith( @@ -678,6 +683,7 @@ describe('HomeAssistant extension', () => { payload = { 'availability': [{topic: 'zigbee2mqtt/bridge/state'}], + 'availability_mode': 'all', "away_mode_command_topic":"zigbee2mqtt/TS0601_thermostat/set/away_mode", "away_mode_state_template":"{{ value_json.away_mode }}", "away_mode_state_topic":"zigbee2mqtt/TS0601_thermostat", @@ -754,6 +760,7 @@ describe('HomeAssistant extension', () => { manufacturer: 'Keen Home' }, 'availability': [{topic: 'zigbee2mqtt/bridge/state'}], + 'availability_mode': 'all', }; expect(MQTT.publish).toHaveBeenCalledWith( @@ -789,6 +796,7 @@ describe('HomeAssistant extension', () => { 'manufacturer': 'Xiaomi', }, 'availability': [{topic: 'zigbee2mqtt/bridge/state'}], + 'availability_mode': 'all', }; expect(MQTT.publish).toHaveBeenCalledWith( @@ -932,6 +940,7 @@ describe('HomeAssistant extension', () => { 'manufacturer': 'Xiaomi', }, 'availability': [{topic: 'zigbee2mqtt/bridge/state'}], + 'availability_mode': 'all', }; expect(MQTT.publish).toHaveBeenCalledWith( @@ -1055,6 +1064,7 @@ describe('HomeAssistant extension', () => { 'manufacturer': 'Xiaomi', }, 'availability': [{topic: 'zigbee2mqtt/bridge/state'}, {topic: 'zigbee2mqtt/weather_sensor/availability'}], + 'availability_mode': 'all', }; expect(MQTT.publish).toHaveBeenCalledWith( @@ -1142,6 +1152,7 @@ describe('HomeAssistant extension', () => { 'manufacturer': 'Xiaomi', }, 'availability': [{topic: 'zigbee2mqtt/bridge/state'}], + 'availability_mode': 'all', }; expect(MQTT.publish).toHaveBeenCalledWith( @@ -1213,6 +1224,7 @@ describe('HomeAssistant extension', () => { 'manufacturer': 'Xiaomi', }, 'availability': [{topic: 'zigbee2mqtt/bridge/state'}], + 'availability_mode': 'all', }; expect(MQTT.publish).toHaveBeenCalledWith( @@ -1246,6 +1258,7 @@ describe('HomeAssistant extension', () => { "manufacturer":"IKEA" }, 'availability': [{topic: 'zigbee2mqtt/bridge/state'}], + 'availability_mode': 'all', }; expect(MQTT.publish).toHaveBeenCalledWith( @@ -1694,6 +1707,7 @@ describe('HomeAssistant extension', () => { 'manufacturer': 'Xiaomi', }, 'availability': [{topic: 'zigbee2mqtt/bridge/state'}], + 'availability_mode': 'all', }; expect(MQTT.publish).toHaveBeenCalledWith( @@ -1714,6 +1728,7 @@ describe('HomeAssistant extension', () => { const payload = { "availability":[{"topic":"zigbee2mqtt/bridge/state"}], + "availability_mode": "all", "brightness":true, "brightness_scale":254, "color_mode":true,