From 41ece365aac0f6d411ae3fc09eb6ac6ddb1b7759 Mon Sep 17 00:00:00 2001 From: Koen Kanters Date: Wed, 2 Feb 2022 19:28:44 +0100 Subject: [PATCH] Allow to configure availablility payloads to be a JSON object. #10802 --- lib/controller.ts | 1 + lib/eventBus.ts | 7 +++++++ lib/extension/availability.ts | 6 +++++- lib/extension/homeassistant.ts | 8 +++++++- lib/mqtt.ts | 13 +++++++++--- lib/types/types.d.ts | 1 + lib/util/settings.schema.json | 7 +++++++ lib/util/settings.ts | 1 + lib/util/utils.ts | 6 +++++- test/availability.test.js | 10 ++++++++++ test/bridge.test.js | 2 +- test/homeassistant.test.js | 36 ++++++++++++++++++++++++++++++++++ 12 files changed, 91 insertions(+), 7 deletions(-) diff --git a/lib/controller.ts b/lib/controller.ts index ac2500363..24e9050da 100644 --- a/lib/controller.ts +++ b/lib/controller.ts @@ -111,6 +111,7 @@ class Controller { if (startResult === 'reset') { settings.set(['advanced', 'homeassistant_legacy_entity_attributes'], false); settings.set(['advanced', 'legacy_api'], false); + settings.set(['advanced', 'legacy_availability_payload'], false); settings.set(['device_options', 'legacy'], false); this.enableDisableExtension(false, 'BridgeLegacy'); } diff --git a/lib/eventBus.ts b/lib/eventBus.ts index a390d2be2..3ff02d948 100644 --- a/lib/eventBus.ts +++ b/lib/eventBus.ts @@ -27,6 +27,13 @@ export default class EventBus { this.on('permitJoinChanged', callback, key); } + public emitPublishAvailability(): void { + this.emitter.emit('publishAvailability'); + } + public onPublishAvailability(key: ListenerKey, callback: () => void): void { + this.on('publishAvailability', callback, key); + } + public emitEntityRenamed(data: eventdata.EntityRenamed): void { this.emitter.emit('deviceRenamed', data); } diff --git a/lib/extension/availability.ts b/lib/extension/availability.ts index e8e5a4c47..67efb30bf 100644 --- a/lib/extension/availability.ts +++ b/lib/extension/availability.ts @@ -103,7 +103,11 @@ export default class Availability extends Extension { this.eventBus.onDeviceLeave(this, (data) => clearTimeout(this.timers[data.ieeeAddr])); this.eventBus.onDeviceAnnounce(this, (data) => this.retrieveState(data.device)); this.eventBus.onLastSeenChanged(this, this.onLastSeenChanged); + this.eventBus.onPublishAvailability(this, this.publishAvailabilityForAllDevices); + this.publishAvailabilityForAllDevices(); + } + @bind private publishAvailabilityForAllDevices(): void { for (const device of this.zigbee.devices(false)) { if (utils.isAvailabilityEnabledForDevice(device, settings.get())) { // Publish initial availablility @@ -143,7 +147,7 @@ export default class Availability extends Extension { } const topic = `${device.name}/availability`; - const payload = available ? 'online' : 'offline'; + const payload = utils.availabilityPayload(available ? 'online' : 'offline', settings.get()); this.availabilityCache[device.ieeeAddr] = available; this.mqtt.publish(topic, payload, {retain: true, qos: 0}); } diff --git a/lib/extension/homeassistant.ts b/lib/extension/homeassistant.ts index 945b6fa7a..c9a66b77a 100644 --- a/lib/extension/homeassistant.ts +++ b/lib/extension/homeassistant.ts @@ -104,6 +104,9 @@ export default class HomeAssistant extends Extension { for (const entity of [...this.zigbee.devices(false), ...this.zigbee.groups()]) { this.discover(entity, true); } + + // Send availability messages, this is required if the legacy_availability_payload option has been changed. + this.eventBus.emitPublishAvailability(); } private exposeToConfig(exposes: zhc.DefinitionExpose[], entityType: 'device' | 'group', @@ -1084,6 +1087,10 @@ export default class HomeAssistant extends Extension { payload.availability.push({topic: `${baseTopic}/availability`}); } + if (!settings.get().advanced.legacy_availability_payload) { + payload.availability.forEach((a: KeyValue) => a.value_template = '{{ value_json.state }}'); + } + const commandTopicPrefix = payload.command_topic_prefix ? `${payload.command_topic_prefix}/` : ''; delete payload.command_topic_prefix; const commandTopicPostfix = payload.command_topic_postfix ? `/${payload.command_topic_postfix}` : ''; @@ -1280,7 +1287,6 @@ export default class HomeAssistant extends Extension { data.message.toLowerCase() === 'online') { const timer = setTimeout(async () => { // Publish all device states. - this.mqtt.publish('bridge/state', 'online', {retain: true, qos: 0}); for (const device of this.zigbee.devices(false)) { if (this.state.exists(device)) { this.publishEntityState(device, this.state.get(device)); diff --git a/lib/mqtt.ts b/lib/mqtt.ts index e1cc2ac9c..e9182043e 100644 --- a/lib/mqtt.ts +++ b/lib/mqtt.ts @@ -22,7 +22,7 @@ export default class MQTT { const options: mqtt.IClientOptions = { will: { topic: `${settings.get().mqtt.base_topic}/bridge/state`, - payload: 'offline', + payload: utils.availabilityPayload('offline', settings.get()), retain: settings.get().mqtt.force_disable_retain ? false : true, qos: 1, }, @@ -71,6 +71,7 @@ export default class MQTT { this.client = mqtt.connect(mqttSettings.server, options); // @ts-ignore https://github.com/Koenkk/zigbee2mqtt/issues/9822 this.client.stream.setMaxListeners(0); + this.eventBus.onPublishAvailability(this, () => this.publishStateOnline()); const onConnect = this.onConnect; this.client.on('connect', async () => { @@ -94,12 +95,18 @@ export default class MQTT { logger.info('Connected to MQTT server'); this.subscribe(`${settings.get().mqtt.base_topic}/#`); - await this.publish('bridge/state', 'online', {retain: true, qos: 0}); + await this.publishStateOnline(); + } + + async publishStateOnline(): Promise { + await this.publish('bridge/state', utils.availabilityPayload('online', settings.get()), {retain: true, qos: 0}); } async disconnect(): Promise { clearTimeout(this.connectionTimer); - await this.publish('bridge/state', 'offline', {retain: true, qos: 0}); + await this.publish('bridge/state', utils.availabilityPayload('offline', settings.get()), + {retain: true, qos: 0}); + this.eventBus.removeListeners(this); logger.info('Disconnecting from MQTT server'); this.client.end(); } diff --git a/lib/types/types.d.ts b/lib/types/types.d.ts index 7b7b87463..96db79325 100644 --- a/lib/types/types.d.ts +++ b/lib/types/types.d.ts @@ -238,6 +238,7 @@ declare global { device_options: KeyValue, advanced: { legacy_api: boolean, + legacy_availability_payload: boolean, log_rotation: boolean, log_symlink_current: boolean, log_output: ('console' | 'file' | 'syslog')[], diff --git a/lib/util/settings.schema.json b/lib/util/settings.schema.json index e44118462..ef9c5db63 100644 --- a/lib/util/settings.schema.json +++ b/lib/util/settings.schema.json @@ -432,6 +432,13 @@ "description": "Disables the legacy api (false = disable)", "default": true }, + "legacy_availability_payload": { + "type": "boolean", + "title": "Legacy availability payload", + "requiresRestart": true, + "description": "Payload to be used for device availabilty and bridge/state topics. true = text, false = JSON", + "default": true + }, "log_rotation": { "type": "boolean", "title": "Log rotation", diff --git a/lib/util/settings.ts b/lib/util/settings.ts index b1fb10ba2..1d69bb819 100644 --- a/lib/util/settings.ts +++ b/lib/util/settings.ts @@ -72,6 +72,7 @@ const defaults: RecursivePartial = { device_options: {}, advanced: { legacy_api: true, + legacy_availability_payload: true, log_rotation: true, log_symlink_current: false, log_output: ['console', 'file'], diff --git a/lib/util/utils.ts b/lib/util/utils.ts index 0286b5c0c..af02edc84 100644 --- a/lib/util/utils.ts +++ b/lib/util/utils.ts @@ -277,6 +277,10 @@ function isZHGroup(obj: unknown): obj is zh.Group { return obj.constructor.name.toLowerCase() === 'group'; } +function availabilityPayload(state: 'online' | 'offline', settings: Settings): string { + return settings.advanced.legacy_availability_payload ? state : JSON.stringify({state}); +} + const hours = (hours: number): number => 1000 * 60 * 60 * hours; const minutes = (minutes: number): number => 1000 * 60 * minutes; const seconds = (seconds: number): number => 1000 * seconds; @@ -302,5 +306,5 @@ export default { equalsPartial, getObjectProperty, getResponse, parseJSON, loadModuleFromText, loadModuleFromFile, getExternalConvertersDefinitions, removeNullPropertiesFromObject, toNetworkAddressHex, toSnakeCase, parseEntityID, isEndpoint, isZHGroup, hours, minutes, seconds, validateFriendlyName, sleep, - sanitizeImageParameter, isAvailabilityEnabledForDevice, publishLastSeen, + sanitizeImageParameter, isAvailabilityEnabledForDevice, publishLastSeen, availabilityPayload, }; diff --git a/test/availability.test.js b/test/availability.test.js index 766515561..409b8714a 100644 --- a/test/availability.test.js +++ b/test/availability.test.js @@ -282,6 +282,16 @@ describe('Availability', () => { 'offline', {retain: true, qos: 0}, expect.any(Function)); }); + it('Should publish availabiltiy payload in JSON format', async () => { + settings.set(['advanced', 'legacy_availability_payload'], false); + await resetExtension(); + MQTT.publish.mockClear(); + await advancedTime(utils.hours(26)); + expect(devices.remote.ping).toHaveBeenCalledTimes(0); + expect(MQTT.publish).toHaveBeenCalledWith('zigbee2mqtt/remote/availability', + stringify({state: 'offline'}), {retain: true, qos: 0}, expect.any(Function)); + }); + it('Deprecated - should allow to block via advanced.availability_blocklist', async () => { settings.set(['advanced', 'availability_blocklist'], [devices.bulb_color.ieeeAddr]); await resetExtension(); diff --git a/test/bridge.test.js b/test/bridge.test.js index 934d8ab11..2bd33ebdf 100644 --- a/test/bridge.test.js +++ b/test/bridge.test.js @@ -66,7 +66,7 @@ describe('Bridge', () => { const directory = settings.get().advanced.log_directory; expect(MQTT.publish).toHaveBeenCalledWith( 'zigbee2mqtt/bridge/info', - stringify({ "restart_required": false, "commit": version.commitHash, "config": { "advanced": { "output":"json","adapter_concurrent": null, "adapter_delay": null, "availability_blacklist": [], "availability_blocklist": [], "availability_passlist": [], "availability_whitelist": [], "cache_state": true, "cache_state_persistent": true, "cache_state_send_on_startup": true, "channel": 11, "elapsed": false, "ext_pan_id": [221, 221, 221, 221, 221, 221, 221, 221], "last_seen": "disable", "legacy_api": false, "log_directory": directory, "log_file": "log.txt", "log_level": "info", "log_output": ["console", "file"], "log_rotation": true, "log_symlink_current": false, "log_syslog": {}, "pan_id": 6754, "report": false, "soft_reset_timeout": 0, "timestamp_format": "YYYY-MM-DD HH:mm:ss" }, "blocklist": [], "device_options": {}, "devices": { "0x000b57fffec6a5b2": { "friendly_name": "bulb", "retain": true }, "0x000b57fffec6a5b3": { "friendly_name": "bulb_color", "retain": false }, "0x000b57fffec6a5b4": { "friendly_name": "bulb_color_2", "retain": false }, "0x000b57fffec6a5b7": { "friendly_name": "bulb_2", "retain": false }, "0x0017880104a44559": { "friendly_name": "J1_cover" }, "0x0017880104e43559": { "friendly_name": "U202DST600ZB" }, "0x0017880104e44559": { "friendly_name": "3157100_thermostat" }, "0x0017880104e45517": { "friendly_name": "remote", "retain": true }, "0x0017880104e45520": { "friendly_name": "button", "retain": false }, "0x0017880104e45521": { "friendly_name": "button_double_key", "retain": false }, "0x0017880104e45522": { "friendly_name": "weather_sensor", "qos": 1, "retain": false }, "0x0017880104e45523": { "friendly_name": "occupancy_sensor", "retain": false }, "0x0017880104e45524": { "friendly_name": "power_plug", "retain": false }, "0x0017880104e45526": { "friendly_name": "GL-S-007ZS" }, "0x0017880104e45529": { "friendly_name": "unsupported2", "retain": false }, "0x0017880104e45530": { "friendly_name": "button_double_key_interviewing", "retain": false }, "0x0017880104e45540": { "friendly_name": "ikea_onoff" }, "0x0017880104e45541": { "friendly_name": "wall_switch", "retain": false }, "0x0017880104e45542": { "friendly_name": "wall_switch_double", "retain": false }, "0x0017880104e45543": { "friendly_name": "led_controller_1", "retain": false }, "0x0017880104e45544": { "friendly_name": "led_controller_2", "retain": false }, "0x0017880104e45545": { "friendly_name": "dimmer_wall_switch", "retain": false }, "0x0017880104e45547": { "friendly_name": "curtain", "retain": false }, "0x0017880104e45548": { "friendly_name": "fan", "retain": false }, "0x0017880104e45549": { "friendly_name": "siren", "retain": false }, "0x0017880104e45550": { "friendly_name": "thermostat", "retain": false }, "0x0017880104e45551": { "friendly_name": "smart vent", "retain": false }, "0x0017880104e45552": { "friendly_name": "j1", "retain": false }, "0x0017880104e45553": { "friendly_name": "bulb_enddevice", "retain": false }, "0x0017880104e45559": { "friendly_name": "cc2530_router", "retain": false }, "0x0017880104e45560": { "friendly_name": "livolo", "retain": false }, "0x0017880104e45724":{"friendly_name":"GLEDOPTO_2ID"}, "0x0017882104a44559": { "friendly_name": "TS0601_thermostat" }, "0x0017882194e45543": { "friendly_name": "QS-Zigbee-D02-TRIAC-2C-LN" }, "0x90fd9ffffe4b64aa": { "friendly_name": "SP600_OLD" }, "0x90fd9ffffe4b64ab": { "friendly_name": "SP600_NEW" }, "0x90fd9ffffe4b64ac": { "friendly_name": "MKS-CM-W5" }, "0x90fd9ffffe4b64ae": { "friendly_name": "tradfri_remote", "retain": false }, "0x90fd9ffffe4b64af": { "friendly_name": "roller_shutter" }, "0x90fd9ffffe4b64ax": { "friendly_name": "ZNLDP12LM" }, "0x0017880104e45561": { "friendly_name": "temperature_sensor" }, "0x0017880104e45562": { "friendly_name": "heating_actuator" } }, "external_converters": [], "groups": { "1": { "friendly_name": "group_1", "retain": false }, "9": { "devices": ['bulb_color_2', 'bulb_2', 'wall_switch_double/right'], "friendly_name": "ha_discovery_group"}, "11": { "devices": ["bulb_2"], "friendly_name": "group_with_tradfri", "retain": false }, "14": { "devices": ["power_plug", "bulb_2"], "friendly_name": "switch_group", "retain": false }, "12": { "devices": ["TS0601_thermostat"], "friendly_name": "thermostat_group", "retain": false }, "15071": { "devices": ["bulb_color_2", "bulb_2"], "friendly_name": "group_tradfri_remote", "retain": false }, "2": { "friendly_name": "group_2", "retain": false }, "21":{"devices":["GLEDOPTO_2ID/cct"],"friendly_name":"gledopto_group"} }, "homeassistant": false, "map_options": { "graphviz": { "colors": { "fill": { "coordinator": "#e04e5d", "enddevice": "#fff8ce", "router": "#4ea3e0" }, "font": { "coordinator": "#ffffff", "enddevice": "#000000", "router": "#ffffff" }, "line": { "active": "#009900", "inactive": "#994444" } } } }, "mqtt": { "base_topic": "zigbee2mqtt", "include_device_information": false, "server": "mqtt://localhost", "force_disable_retain": false, }, "ota":{"disable_automatic_update_check":false,"update_check_interval":1440}, "passlist": [], "permit_join": true, "serial": { "disable_led": false, "port": "/dev/dummy" } }, "coordinator": { "ieee_address":"0x00124b00120144ae","meta": { "revision": 20190425, "version": 1 }, "type": "z-Stack" }, "log_level": "info", "network": { "channel": 15, "extended_pan_id": [0, 11, 22], "pan_id": 5674 }, "permit_join": false, "version": version.version, config_schema: settings.schema }), + stringify({ "restart_required": false, "commit": version.commitHash, "config": { "advanced": { "legacy_availability_payload": true,"output":"json","adapter_concurrent": null, "adapter_delay": null, "availability_blacklist": [], "availability_blocklist": [], "availability_passlist": [], "availability_whitelist": [], "cache_state": true, "cache_state_persistent": true, "cache_state_send_on_startup": true, "channel": 11, "elapsed": false, "ext_pan_id": [221, 221, 221, 221, 221, 221, 221, 221], "last_seen": "disable", "legacy_api": false, "log_directory": directory, "log_file": "log.txt", "log_level": "info", "log_output": ["console", "file"], "log_rotation": true, "log_symlink_current": false, "log_syslog": {}, "pan_id": 6754, "report": false, "soft_reset_timeout": 0, "timestamp_format": "YYYY-MM-DD HH:mm:ss" }, "blocklist": [], "device_options": {}, "devices": { "0x000b57fffec6a5b2": { "friendly_name": "bulb", "retain": true }, "0x000b57fffec6a5b3": { "friendly_name": "bulb_color", "retain": false }, "0x000b57fffec6a5b4": { "friendly_name": "bulb_color_2", "retain": false }, "0x000b57fffec6a5b7": { "friendly_name": "bulb_2", "retain": false }, "0x0017880104a44559": { "friendly_name": "J1_cover" }, "0x0017880104e43559": { "friendly_name": "U202DST600ZB" }, "0x0017880104e44559": { "friendly_name": "3157100_thermostat" }, "0x0017880104e45517": { "friendly_name": "remote", "retain": true }, "0x0017880104e45520": { "friendly_name": "button", "retain": false }, "0x0017880104e45521": { "friendly_name": "button_double_key", "retain": false }, "0x0017880104e45522": { "friendly_name": "weather_sensor", "qos": 1, "retain": false }, "0x0017880104e45523": { "friendly_name": "occupancy_sensor", "retain": false }, "0x0017880104e45524": { "friendly_name": "power_plug", "retain": false }, "0x0017880104e45526": { "friendly_name": "GL-S-007ZS" }, "0x0017880104e45529": { "friendly_name": "unsupported2", "retain": false }, "0x0017880104e45530": { "friendly_name": "button_double_key_interviewing", "retain": false }, "0x0017880104e45540": { "friendly_name": "ikea_onoff" }, "0x0017880104e45541": { "friendly_name": "wall_switch", "retain": false }, "0x0017880104e45542": { "friendly_name": "wall_switch_double", "retain": false }, "0x0017880104e45543": { "friendly_name": "led_controller_1", "retain": false }, "0x0017880104e45544": { "friendly_name": "led_controller_2", "retain": false }, "0x0017880104e45545": { "friendly_name": "dimmer_wall_switch", "retain": false }, "0x0017880104e45547": { "friendly_name": "curtain", "retain": false }, "0x0017880104e45548": { "friendly_name": "fan", "retain": false }, "0x0017880104e45549": { "friendly_name": "siren", "retain": false }, "0x0017880104e45550": { "friendly_name": "thermostat", "retain": false }, "0x0017880104e45551": { "friendly_name": "smart vent", "retain": false }, "0x0017880104e45552": { "friendly_name": "j1", "retain": false }, "0x0017880104e45553": { "friendly_name": "bulb_enddevice", "retain": false }, "0x0017880104e45559": { "friendly_name": "cc2530_router", "retain": false }, "0x0017880104e45560": { "friendly_name": "livolo", "retain": false }, "0x0017880104e45724":{"friendly_name":"GLEDOPTO_2ID"}, "0x0017882104a44559": { "friendly_name": "TS0601_thermostat" }, "0x0017882194e45543": { "friendly_name": "QS-Zigbee-D02-TRIAC-2C-LN" }, "0x90fd9ffffe4b64aa": { "friendly_name": "SP600_OLD" }, "0x90fd9ffffe4b64ab": { "friendly_name": "SP600_NEW" }, "0x90fd9ffffe4b64ac": { "friendly_name": "MKS-CM-W5" }, "0x90fd9ffffe4b64ae": { "friendly_name": "tradfri_remote", "retain": false }, "0x90fd9ffffe4b64af": { "friendly_name": "roller_shutter" }, "0x90fd9ffffe4b64ax": { "friendly_name": "ZNLDP12LM" }, "0x0017880104e45561": { "friendly_name": "temperature_sensor" }, "0x0017880104e45562": { "friendly_name": "heating_actuator" } }, "external_converters": [], "groups": { "1": { "friendly_name": "group_1", "retain": false }, "9": { "devices": ['bulb_color_2', 'bulb_2', 'wall_switch_double/right'], "friendly_name": "ha_discovery_group"}, "11": { "devices": ["bulb_2"], "friendly_name": "group_with_tradfri", "retain": false }, "14": { "devices": ["power_plug", "bulb_2"], "friendly_name": "switch_group", "retain": false }, "12": { "devices": ["TS0601_thermostat"], "friendly_name": "thermostat_group", "retain": false }, "15071": { "devices": ["bulb_color_2", "bulb_2"], "friendly_name": "group_tradfri_remote", "retain": false }, "2": { "friendly_name": "group_2", "retain": false }, "21":{"devices":["GLEDOPTO_2ID/cct"],"friendly_name":"gledopto_group"} }, "homeassistant": false, "map_options": { "graphviz": { "colors": { "fill": { "coordinator": "#e04e5d", "enddevice": "#fff8ce", "router": "#4ea3e0" }, "font": { "coordinator": "#ffffff", "enddevice": "#000000", "router": "#ffffff" }, "line": { "active": "#009900", "inactive": "#994444" } } } }, "mqtt": { "base_topic": "zigbee2mqtt", "include_device_information": false, "server": "mqtt://localhost", "force_disable_retain": false, }, "ota":{"disable_automatic_update_check":false,"update_check_interval":1440}, "passlist": [], "permit_join": true, "serial": { "disable_led": false, "port": "/dev/dummy" } }, "coordinator": { "ieee_address":"0x00124b00120144ae","meta": { "revision": 20190425, "version": 1 }, "type": "z-Stack" }, "log_level": "info", "network": { "channel": 15, "extended_pan_id": [0, 11, 22], "pan_id": 5674 }, "permit_join": false, "version": version.version, config_schema: settings.schema }), { retain: true, qos: 0 }, expect.any(Function) ); diff --git a/test/homeassistant.test.js b/test/homeassistant.test.js index b8beded35..d3181d51c 100644 --- a/test/homeassistant.test.js +++ b/test/homeassistant.test.js @@ -1763,6 +1763,42 @@ describe('HomeAssistant extension', () => { ); }); + it('Should discover with json availability payload value_template', async () => { + settings.set(['advanced', 'legacy_availability_payload'], false); + await resetExtension(); + + const payload = { + "availability":[{"topic":"zigbee2mqtt/bridge/state","value_template":'{{ value_json.state }}'}], + "brightness":true, + "brightness_scale":254, + "color_mode":true, + "command_topic":"zigbee2mqtt/ha_discovery_group/set", + "device":{ + "identifiers":["zigbee2mqtt_1221051039810110150109113116116_9"], + "name":"ha_discovery_group", + "sw_version": version, + }, + "max_mireds": 454, + "min_mireds": 250, + "json_attributes_topic":"zigbee2mqtt/ha_discovery_group", + "name":"ha_discovery_group", + "schema":"json", + "state_topic":"zigbee2mqtt/ha_discovery_group", + "supported_color_modes":[ + "xy", + "color_temp" + ], + "unique_id":"9_light_zigbee2mqtt" + }; + + expect(MQTT.publish).toHaveBeenCalledWith( + 'homeassistant/light/1221051039810110150109113116116_9/light/config', + stringify(payload), + { retain: true, qos: 0 }, + expect.any(Function), + ); + }); + it('Should discover last_seen when enabled', async () => { settings.set(['advanced', 'last_seen'], 'ISO_8601'); await resetExtension();