diff --git a/lib/extension/homeassistant.ts b/lib/extension/homeassistant.ts index a1bdf1af2..3354ace62 100644 --- a/lib/extension/homeassistant.ts +++ b/lib/extension/homeassistant.ts @@ -7,13 +7,15 @@ import assert from 'assert'; import Extension from './extension'; import bind from 'bind-decorator'; -// eslint-disable-next-line camelcase -interface DiscoveryEntry {mockProperties: string[], type: string, object_id: string, discovery_payload: KeyValue} +interface MockProperty {property: string, value: KeyValue | string} -const sensorClick = { +// eslint-disable-next-line camelcase +interface DiscoveryEntry {mockProperties: MockProperty[], type: string, object_id: string, discovery_payload: KeyValue} + +const sensorClick: DiscoveryEntry = { type: 'sensor', object_id: 'click', - mockProperties: ['click'], + mockProperties: [{property: 'click', value: null}], discovery_payload: { icon: 'mdi:toggle-switch', value_template: '{{ value_json.click }}', @@ -37,7 +39,7 @@ const featurePropertyWithoutEndpoint = (feature: zhc.DefinitionExposeFeature): s * This extensions handles integration with HomeAssistant */ export default class HomeAssistant extends Extension { - private discovered: {[s: string]: {topics: Set, mockProperties: Set}} = {}; + private discovered: {[s: string]: {topics: Set, mockProperties: Set}} = {}; private mapping: {[s: string]: DiscoveryEntry[]} = {}; private discoveredTriggers : {[s: string]: Set}= {}; private discoveryTopic = settings.get().advanced.homeassistant_discovery_topic; @@ -107,7 +109,7 @@ export default class HomeAssistant extends Extension { const discoveryEntry: DiscoveryEntry = { type: 'light', object_id: endpoint ? `light_${endpoint}` : 'light', - mockProperties: [state.property], + mockProperties: [{property: state.property, value: null}], discovery_payload: { brightness: !!hasBrightness, schema: 'json', @@ -151,7 +153,7 @@ export default class HomeAssistant extends Extension { const discoveryEntry: DiscoveryEntry = { type: 'switch', object_id: endpoint ? `switch_${endpoint}` : 'switch', - mockProperties: [property], + mockProperties: [{property: property, value: null}], discovery_payload: { payload_off: state.value_off, payload_on: state.value_on, @@ -216,7 +218,7 @@ export default class HomeAssistant extends Extension { const state = firstExpose.features.find((f) => f.name === 'running_state'); if (state) { - discoveryEntry.mockProperties.push(state.property); + discoveryEntry.mockProperties.push({property: state.property, value: null}); discoveryEntry.discovery_payload.action_topic = true; discoveryEntry.discovery_payload.action_template = `{% set values = ` + `{None:None,'idle':'off','heat':'heating','cool':'cooling','fan_only':'fan'}` + @@ -271,7 +273,7 @@ export default class HomeAssistant extends Extension { const discoveryEntry: DiscoveryEntry = { type: 'number', object_id: endpoint ? `${tempCalibration.name}_${endpoint}` : `${tempCalibration.name}`, - mockProperties: [tempCalibration.property], + mockProperties: [{property: tempCalibration.property, value: null}], discovery_payload: { value_template: `{{ value_json.${tempCalibration.property} }}`, command_topic: true, @@ -290,10 +292,10 @@ export default class HomeAssistant extends Extension { const piHeatingDemand = firstExpose.features.find((f) => f.name === 'pi_heating_demand'); if (piHeatingDemand) { - const discoveryEntry = { + const discoveryEntry: DiscoveryEntry = { type: 'sensor', object_id: endpoint ? `${piHeatingDemand.name}_${endpoint}` : `${piHeatingDemand.name}`, - mockProperties: [piHeatingDemand.property], + mockProperties: [{property: piHeatingDemand.property, value: null}], discovery_payload: { value_template: `{{ value_json.${piHeatingDemand.property} }}`, ...(piHeatingDemand.unit && {unit_of_measurement: piHeatingDemand.unit}), @@ -313,7 +315,7 @@ export default class HomeAssistant extends Extension { const discoveryEntry: DiscoveryEntry = { type: 'lock', object_id: 'lock', - mockProperties: [state.property], + mockProperties: [{property: state.property, value: null}], discovery_payload: { command_topic: true, value_template: `{{ value_json.${state.property} }}`, @@ -396,7 +398,7 @@ export default class HomeAssistant extends Extension { const discoveryEntry: DiscoveryEntry = { type: 'fan', object_id: 'fan', - mockProperties: ['fan_state'], + mockProperties: [{property: 'fan_state', value: null}], discovery_payload: { state_topic: true, state_value_template: '{{ value_json.fan_state }}', @@ -494,7 +496,7 @@ export default class HomeAssistant extends Extension { if (firstExpose.access & ACCESS_SET) { const discoveryEntry: DiscoveryEntry = { type: 'switch', - mockProperties: [firstExpose.property], + mockProperties: [{property: firstExpose.property, value: null}], object_id: endpoint ? `switch_${firstExpose.name}_${endpoint}` : `switch_${firstExpose.name}`, @@ -512,10 +514,10 @@ export default class HomeAssistant extends Extension { }; discoveryEntries.push(discoveryEntry); } else { - const discoveryEntry = { + const discoveryEntry: DiscoveryEntry = { type: 'binary_sensor', object_id: endpoint ? `${firstExpose.name}_${endpoint}` : `${firstExpose.name}`, - mockProperties: [firstExpose.property], + mockProperties: [{property: firstExpose.property, value: null}], discovery_payload: { value_template: `{{ value_json.${firstExpose.property} }}`, payload_on: firstExpose.value_on, @@ -630,10 +632,10 @@ export default class HomeAssistant extends Extension { const allowsSet = firstExpose.access & ACCESS_SET; - const discoveryEntry = { + const discoveryEntry: DiscoveryEntry = { type: 'sensor', object_id: endpoint ? `${firstExpose.name}_${endpoint}` : `${firstExpose.name}`, - mockProperties: [firstExpose.property], + mockProperties: [{property: firstExpose.property, value: null}], discovery_payload: { value_template: `{{ value_json.${firstExpose.property} }}`, enabled_by_default: !allowsSet, @@ -652,7 +654,7 @@ export default class HomeAssistant extends Extension { const discoveryEntry: DiscoveryEntry = { type: 'number', object_id: endpoint ? `${firstExpose.name}_${endpoint}` : `${firstExpose.name}`, - mockProperties: [firstExpose.property], + mockProperties: [{property: firstExpose.property, value: null}], discovery_payload: { value_template: `{{ value_json.${firstExpose.property} }}`, command_topic: true, @@ -699,7 +701,7 @@ export default class HomeAssistant extends Extension { discoveryEntries.push({ type: 'sensor', object_id: firstExpose.property, - mockProperties: [firstExpose.property], + mockProperties: [{property: firstExpose.property, value: null}], discovery_payload: { value_template: `{{ value_json.${firstExpose.property} }}`, enabled_by_default: !(firstExpose.access & ACCESS_SET), @@ -716,7 +718,7 @@ export default class HomeAssistant extends Extension { discoveryEntries.push({ type: 'select', object_id: firstExpose.property, - mockProperties: [firstExpose.property], + mockProperties: [{property: firstExpose.property, value: null}], discovery_payload: { value_template: `{{ value_json.${firstExpose.property} }}`, state_topic: true, @@ -735,10 +737,10 @@ export default class HomeAssistant extends Extension { action: {icon: 'mdi:gesture-double-tap'}, }; - const discoveryEntry = { + const discoveryEntry: DiscoveryEntry = { type: 'sensor', object_id: firstExpose.property, - mockProperties: [firstExpose.property], + mockProperties: [{property: firstExpose.property, value: null}], discovery_payload: { value_template: `{{ value_json.${firstExpose.property} }}`, ...lookup[firstExpose.name], @@ -770,7 +772,7 @@ export default class HomeAssistant extends Extension { // deprecated this.mapping[def.model].push({ type: 'sensor', - mockProperties: ['brightness'], + mockProperties: [{property: 'brightness', value: null}], object_id: 'brightness', discovery_payload: { unit_of_measurement: 'brightness', @@ -923,7 +925,7 @@ export default class HomeAssistant extends Extension { configs.push({ type: 'sensor', object_id: 'last_seen', - mockProperties: ['last_seen'], + mockProperties: [{property: 'last_seen', value: null}], discovery_payload: { value_template: '{{ value_json.last_seen }}', icon: 'mdi:clock', @@ -938,7 +940,7 @@ export default class HomeAssistant extends Extension { const updateStateSensor: DiscoveryEntry = { type: 'sensor', object_id: 'update_state', - mockProperties: [], + mockProperties: [{property: 'update', value: {state: null}}], discovery_payload: { icon: 'mdi:update', value_template: `{{ value_json['update']['state'] }}`, @@ -948,10 +950,10 @@ export default class HomeAssistant extends Extension { }; configs.push(updateStateSensor); - const updateAvailableSensor = { + const updateAvailableSensor: DiscoveryEntry = { type: 'binary_sensor', object_id: 'update_available', - mockProperties: ['update_available'], + mockProperties: [{property: 'update_available', value: null}], discovery_payload: { payload_on: true, payload_off: false, @@ -1193,7 +1195,8 @@ export default class HomeAssistant extends Extension { const topic = this.getDiscoveryTopic(config, entity); this.mqtt.publish(topic, stringify(payload), {retain: true, qos: 0}, this.discoveryTopic, false, false); this.discovered[discoverKey].topics.add(topic); - config.mockProperties?.forEach((property) => this.discovered[discoverKey].mockProperties.add(property)); + config.mockProperties?.forEach((mockProperty) => + this.discovered[discoverKey].mockProperties.add(mockProperty)); }); } @@ -1296,9 +1299,9 @@ export default class HomeAssistant extends Extension { override adjustMessageBeforePublish(entity: Device | Group, message: KeyValue): void { const discoverKey = this.getDiscoverKey(entity); - this.discovered[discoverKey]?.mockProperties?.forEach((property) => { - if (!message.hasOwnProperty(property)) { - message[property] = null; + this.discovered[discoverKey]?.mockProperties?.forEach((mockProperty) => { + if (!message.hasOwnProperty(mockProperty.property)) { + message[mockProperty.property] = mockProperty.value; } }); diff --git a/test/frontend.test.js b/test/frontend.test.js index 2368dbe81..167ff0363 100644 --- a/test/frontend.test.js +++ b/test/frontend.test.js @@ -140,7 +140,7 @@ describe('Frontend', () => { expect(MQTT.publish).toHaveBeenCalledTimes(1); expect(MQTT.publish).toHaveBeenCalledWith( 'zigbee2mqtt/bulb_color', - stringify({state: 'ON', linkquality: null, update_available: null}), + stringify({state: 'ON', linkquality: null, update_available: null, update: {state: null}}), { retain: false, qos: 0 }, expect.any(Function) ); @@ -151,7 +151,7 @@ describe('Frontend', () => { // Received message on socket expect(mockWSClient.implementation.send).toHaveBeenCalledTimes(1); - expect(mockWSClient.implementation.send).toHaveBeenCalledWith(stringify({topic: 'bulb_color', payload: {state: 'ON', linkquality: null, update_available: null}})); + expect(mockWSClient.implementation.send).toHaveBeenCalledWith(stringify({topic: 'bulb_color', payload: {state: 'ON', linkquality: null, update_available: null, update: {state: null}}})); // Shouldnt set when not ready mockWSClient.implementation.send.mockClear(); diff --git a/test/homeassistant.test.js b/test/homeassistant.test.js index e50300ddb..ec195abf3 100644 --- a/test/homeassistant.test.js +++ b/test/homeassistant.test.js @@ -827,7 +827,7 @@ describe('HomeAssistant extension', () => { expect(MQTT.publish).toHaveBeenCalledTimes(1); expect(MQTT.publish).toHaveBeenCalledWith( 'zigbee2mqtt/bulb_color', - stringify({"color":{"hue": 0, "saturation": 100, "h": 0, "s": 100}, "color_mode": "hs", "linkquality": null, "state": null, "update_available": null}), + stringify({"color":{"hue": 0, "saturation": 100, "h": 0, "s": 100}, "color_mode": "hs", "linkquality": null, "state": null, "update_available": null, "update": {"state": null}}), { retain: false, qos: 0 }, expect.any(Function), ); @@ -843,7 +843,7 @@ describe('HomeAssistant extension', () => { expect(MQTT.publish).toHaveBeenCalledTimes(1); expect(MQTT.publish).toHaveBeenCalledWith( 'zigbee2mqtt/bulb_color', - stringify({"color": {"x": 0.4576,"y": 0.41}, "color_mode": "xy", "linkquality": null,"state": null, "update_available": null}), + stringify({"color": {"x": 0.4576,"y": 0.41}, "color_mode": "xy", "linkquality": null,"state": null, "update_available": null, "update": {"state": null}}), { retain: false, qos: 0 }, expect.any(Function), ); @@ -859,7 +859,7 @@ describe('HomeAssistant extension', () => { expect(MQTT.publish).toHaveBeenCalledTimes(1); expect(MQTT.publish).toHaveBeenCalledWith( 'zigbee2mqtt/bulb_color', - stringify({"linkquality": null,"state": "ON", "update_available": null}), + stringify({"linkquality": null,"state": "ON", "update_available": null, "update": {"state": null}}), { retain: false, qos: 0 }, expect.any(Function), ); @@ -934,13 +934,13 @@ describe('HomeAssistant extension', () => { await flushPromises(); expect(MQTT.publish).toHaveBeenCalledWith( 'zigbee2mqtt/bulb', - stringify({"state":"ON","brightness":50,"color_temp":370,"linkquality":99,"power_on_behavior":null, "update_available": null}), + stringify({"state":"ON","brightness":50,"color_temp":370,"linkquality":99,"power_on_behavior":null, "update_available": null, "update": {"state": null}}), { retain: true, qos: 0 }, expect.any(Function) ); expect(MQTT.publish).toHaveBeenCalledWith( 'zigbee2mqtt/remote', - stringify({"action":null,"battery":null,"brightness":255,"linkquality":null, "update_available": null}), + stringify({"action":null,"battery":null,"brightness":255,"linkquality":null, "update_available": null, "update": {"state": null}}), { retain: true, qos: 0 }, expect.any(Function) ); @@ -958,13 +958,13 @@ describe('HomeAssistant extension', () => { await flushPromises(); expect(MQTT.publish).toHaveBeenCalledWith( 'zigbee2mqtt/bulb', - stringify({"state":"ON","brightness":50,"color_temp":370,"linkquality":99,"power_on_behavior":null, "update_available": null}), + stringify({"state":"ON","brightness":50,"color_temp":370,"linkquality":99,"power_on_behavior":null, "update_available": null, "update": {"state": null}}), { retain: true, qos: 0 }, expect.any(Function) ); expect(MQTT.publish).toHaveBeenCalledWith( 'zigbee2mqtt/remote', - stringify({"action":null,"battery":null,"brightness":255,"linkquality":null, "update_available": null}), + stringify({"action":null,"battery":null,"brightness":255,"linkquality":null, "update_available": null, "update": {"state": null}}), { retain: true, qos: 0 }, expect.any(Function) );