diff --git a/lib/extension/homeassistant.ts b/lib/extension/homeassistant.ts index d3e68f99a..1a4d40853 100644 --- a/lib/extension/homeassistant.ts +++ b/lib/extension/homeassistant.ts @@ -1220,6 +1220,61 @@ export class HomeAssistant extends Extension { case "composite": case "list": { const firstExposeTyped = firstExpose as zhc.Text | zhc.Composite | zhc.List; + + // Warning composite → HA siren entity + if (firstExposeTyped.type === "composite" && firstExposeTyped.name === "warning" && firstExposeTyped.access & ACCESS_SET) { + const warningExpose = firstExpose as zhc.Composite; + const modeFeature = warningExpose.features.filter(isEnumExpose).find((f) => f.name === "mode"); + const levelFeature = warningExpose.features.filter(isEnumExpose).find((f) => f.name === "level"); + const durationFeature = warningExpose.features.filter(isNumericExpose).find((f) => f.name === "duration"); + + const discoveryEntry: DiscoveryEntry = { + type: "siren", + object_id: endpoint ? /* v8 ignore next */ `siren_${endpoint}` : "siren", + mockProperties: [{property: warningExpose.property, value: null}], + discovery_payload: { + name: endpoint ? /* v8 ignore next */ utils.capitalize(endpoint) : null, + command_topic: true, + command_topic_prefix: endpoint, + state_topic: false, + optimistic: true, + }, + }; + + if (modeFeature) { + const tones = modeFeature.values.filter((v) => v !== "stop"); + if (tones.length) { + discoveryEntry.discovery_payload.available_tones = tones; + } + } + + if (levelFeature) { + discoveryEntry.discovery_payload.support_volume_set = true; + } + + if (durationFeature) { + discoveryEntry.discovery_payload.support_duration = true; + } + + const levelTemplate = + "{% if volume_level is defined %}" + + "{% if volume_level | float <= 0.25 %}low" + + "{% elif volume_level | float <= 0.5 %}medium" + + "{% elif volume_level | float <= 0.75 %}high" + + "{% else %}very_high{% endif %}" + + "{% else %}medium{% endif %}"; + + discoveryEntry.discovery_payload.command_template = + `{"warning": {"mode": "{{ tone | default('emergency') }}", ` + + `"level": "${levelTemplate}", ` + + `"duration": {{ duration | default(10) }}}}`; + + discoveryEntry.discovery_payload.command_off_template = '{"warning": {"mode": "stop"}}'; + + discoveryEntries.push(discoveryEntry); + break; + } + if (firstExposeTyped.type === "text" && firstExposeTyped.access & ACCESS_SET) { discoveryEntries.push({ type: "text", diff --git a/test/extensions/homeassistant.test.ts b/test/extensions/homeassistant.test.ts index 574c19799..47186234c 100644 --- a/test/extensions/homeassistant.test.ts +++ b/test/extensions/homeassistant.test.ts @@ -944,6 +944,45 @@ describe("Extension: HomeAssistant", () => { }); }); + it("Should discover siren (HS2WD-E)", () => { + const payload = { + available_tones: ["emergency"], + support_duration: true, + optimistic: true, + command_topic: "zigbee2mqtt/siren/set", + command_template: + '{"warning": {"mode": "{{ tone | default(\'emergency\') }}", ' + + '"level": "' + + "{% if volume_level is defined %}" + + "{% if volume_level | float <= 0.25 %}low" + + "{% elif volume_level | float <= 0.5 %}medium" + + "{% elif volume_level | float <= 0.75 %}high" + + "{% else %}very_high{% endif %}" + + '{% else %}medium{% endif %}", ' + + '"duration": {{ duration | default(10) }}}}', + command_off_template: '{"warning": {"mode": "stop"}}', + name: null, + object_id: "siren", + default_entity_id: "siren.siren", + unique_id: "0x0017880104e45549_siren_zigbee2mqtt", + origin: origin, + device: { + identifiers: ["zigbee2mqtt_0x0017880104e45549"], + name: "siren", + model: "Smart siren", + model_id: "HS2WD-E", + manufacturer: "Heiman", + via_device: "zigbee2mqtt_bridge_0x00124b00120144ae", + }, + availability: [{topic: "zigbee2mqtt/bridge/state", value_template: "{{ value_json.state }}"}], + }; + + expect(mockMQTTPublishAsync).toHaveBeenCalledWith("homeassistant/siren/0x0017880104e45549/siren/config", stringify(payload), { + retain: true, + qos: 1, + }); + }); + it("Should discover devices with speed-controlled fan", () => { const payload = { state_topic: "zigbee2mqtt/fanbee",