From dc4e676d38875d35acab25327e79c29cec1844b0 Mon Sep 17 00:00:00 2001 From: Adrian Jagielak Date: Fri, 25 Jul 2025 21:10:57 +0200 Subject: [PATCH] Add support for 'siren_ctrl' service --- README.md | 2 +- futurehome/CHANGELOG.md | 1 + futurehome/README.md | 2 +- futurehome/src/ha/publish_device.ts | 2 + futurehome/src/mqtt/demo_data/device.json | 170 ++++++++++++++++++++++ futurehome/src/mqtt/demo_data/state.json | 159 ++++++++++++++++++++ futurehome/src/services/siren_ctrl.ts | 119 +++++++++++++++ 7 files changed, 453 insertions(+), 2 deletions(-) create mode 100644 futurehome/src/services/siren_ctrl.ts diff --git a/README.md b/README.md index 71c5d71..3314fda 100644 --- a/README.md +++ b/README.md @@ -118,7 +118,7 @@ todo: links to the .ts service implementations below | sensor_wattemp | | ✅ | | sensor_weight | | ✅ | | sensor_wind | | ✅ | -| siren_ctrl | | | +| siren_ctrl | [Brannvarsler](https://www.futurehome.io/en_no/shop/brannvarsler-230v) | ✅ | | thermostat | [Thermostat](https://www.futurehome.io/en_no/shop/thermostat-w) | ✅ | | user_code | | | | water_heater | | ✅ | diff --git a/futurehome/CHANGELOG.md b/futurehome/CHANGELOG.md index 26017df..1ba4dd8 100644 --- a/futurehome/CHANGELOG.md +++ b/futurehome/CHANGELOG.md @@ -6,6 +6,7 @@ - Added support for 'media_player' service. - Removed demo mode 'optimistic' override causing switches to look weird. - Updated demo mode fake state handling. +- Added support for 'siren_ctrl' service (alarm sirens). ## 0.1.3 (25.07.2025) diff --git a/futurehome/README.md b/futurehome/README.md index e9a0a2a..1a52243 100644 --- a/futurehome/README.md +++ b/futurehome/README.md @@ -117,7 +117,7 @@ todo: links to the .ts service implementations below | sensor_wattemp | | ✅ | | sensor_weight | | ✅ | | sensor_wind | | ✅ | -| siren_ctrl | | | +| siren_ctrl | [Brannvarsler](https://www.futurehome.io/en_no/shop/brannvarsler-230v) | ✅ | | thermostat | [Thermostat](https://www.futurehome.io/en_no/shop/thermostat-w) | ✅ | | user_code | | | | water_heater | | ✅ | diff --git a/futurehome/src/ha/publish_device.ts b/futurehome/src/ha/publish_device.ts index d2a72f7..fb90da9 100644 --- a/futurehome/src/ha/publish_device.ts +++ b/futurehome/src/ha/publish_device.ts @@ -56,6 +56,7 @@ import { sensor_watpressure__components } from '../services/sensor_watpressure'; import { sensor_wattemp__components } from '../services/sensor_wattemp'; import { sensor_weight__components } from '../services/sensor_weight'; import { sensor_wind__components } from '../services/sensor_wind'; +import { siren_ctrl__components } from '../services/siren_ctrl'; import { thermostat__components } from '../services/thermostat'; import { water_heater__components } from '../services/water_heater'; import { abbreviateHaMqttKeys } from './abbreviate_ha_mqtt_keys'; @@ -215,6 +216,7 @@ const serviceHandlers: { sensor_wattemp: sensor_wattemp__components, sensor_weight: sensor_weight__components, sensor_wind: sensor_wind__components, + siren_ctrl: siren_ctrl__components, thermostat: thermostat__components, water_heater: water_heater__components, }; diff --git a/futurehome/src/mqtt/demo_data/device.json b/futurehome/src/mqtt/demo_data/device.json index 038b075..f144725 100644 --- a/futurehome/src/mqtt/demo_data/device.json +++ b/futurehome/src/mqtt/demo_data/device.json @@ -1995,5 +1995,175 @@ } }, "metadata": null + }, + { + "client": { + "name": "Futurehome SDCO Alarm" + }, + "fimp": { + "adapter": "zwave-ad", + "address": "86", + "group": "ch_0" + }, + "functionality": null, + "id": 73, + "lrn": true, + "model": "Futurehome SDCO Alarm", + "modelAlias": "Futurehome SDCO Alarm", + "param": { + "alarms": { + "fire": ["smoke_test"] + }, + "batteryLevel": "ok", + "batteryPercentage": 80, + "humidity": 38, + "siren": "silence", + "smoke": false, + "supportedAlarms": { + "burglar": ["tamper_removed_cover"], + "fire": ["smoke", "smoke_test"], + "gas": ["CO"], + "heat": ["overheat"] + }, + "temperature": 22.9799995422363, + "timestamp": "2020-01-30 07:23:39 +0100" + }, + "problem": false, + "room": null, + "services": { + "alarm_burglar": { + "addr": "/rt:dev/rn:zw/ad:1/sv:alarm_burglar/ad:86_0", + "enabled": true, + "intf": ["cmd.alarm.get_report", "evt.alarm.report"], + "props": { + "is_secure": true, + "is_unsecure": false, + "sup_events": ["tamper_removed_cover"] + } + }, + "alarm_fire": { + "addr": "/rt:dev/rn:zw/ad:1/sv:alarm_fire/ad:86_0", + "enabled": true, + "intf": ["cmd.alarm.get_report", "evt.alarm.report"], + "props": { + "is_secure": true, + "is_unsecure": false, + "sup_events": ["smoke", "smoke_test"] + } + }, + "alarm_gas": { + "addr": "/rt:dev/rn:zw/ad:1/sv:alarm_gas/ad:86_0", + "enabled": true, + "intf": ["cmd.alarm.get_report", "evt.alarm.report"], + "props": { + "is_secure": true, + "is_unsecure": false, + "sup_events": ["CO"] + } + }, + "alarm_heat": { + "addr": "/rt:dev/rn:zw/ad:1/sv:alarm_heat/ad:86_0", + "enabled": true, + "intf": ["cmd.alarm.get_report", "evt.alarm.report"], + "props": { + "is_secure": true, + "is_unsecure": false, + "sup_events": ["overheat"] + } + }, + "battery": { + "addr": "/rt:dev/rn:zw/ad:1/sv:battery/ad:86_0", + "enabled": true, + "intf": ["cmd.lvl.get_report", "evt.alarm.report", "evt.lvl.report"], + "props": { + "is_secure": true, + "is_unsecure": false + } + }, + "complex_alarm_system": { + "addr": "/rt:dev/rn:zw/ad:1/sv:complex_alarm_system/ad:86_0", + "enabled": true, + "intf": ["cmd.alarm.silence", "evt.alarm.silence"], + "props": { + "is_secure": true, + "is_unsecure": false + } + }, + "dev_sys": { + "addr": "/rt:dev/rn:zw/ad:1/sv:dev_sys/ad:86_0", + "enabled": true, + "intf": [ + "cmd.group.add_members", + "cmd.group.delete_members", + "cmd.group.get_members", + "cmd.ping.send", + "evt.group.members_report", + "evt.ping.report" + ], + "props": { + "is_secure": true, + "is_unsecure": false + } + }, + "indicator_ctrl": { + "addr": "/rt:dev/rn:zw/ad:1/sv:indicator_ctrl/ad:86_0", + "enabled": true, + "intf": ["cmd.indicator.set_visual_element"], + "props": { + "duration": "", + "is_secure": true, + "is_unsecure": false + } + }, + "scene_ctrl": { + "addr": "/rt:dev/rn:zw/ad:1/sv:scene_ctrl/ad:86_0", + "enabled": true, + "intf": ["cmd.scene.get_report", "cmd.scene.set", "evt.scene.report"], + "props": { + "is_secure": true, + "is_unsecure": false, + "sup_modes": ["on", "off"] + } + }, + "sensor_humid": { + "addr": "/rt:dev/rn:zw/ad:1/sv:sensor_humid/ad:86_0", + "enabled": true, + "intf": ["cmd.sensor.get_report", "evt.sensor.report"], + "props": { + "is_secure": true, + "is_unsecure": false, + "sup_units": ["g/m3", "%"] + } + }, + "sensor_temp": { + "addr": "/rt:dev/rn:zw/ad:1/sv:sensor_temp/ad:86_0", + "enabled": true, + "intf": ["cmd.sensor.get_report", "evt.sensor.report"], + "props": { + "is_secure": true, + "is_unsecure": false, + "sup_units": ["C", "F"] + } + }, + "siren_ctrl": { + "addr": "/rt:dev/rn:zw/ad:1/sv:siren_ctrl/ad:86_0", + "enabled": true, + "intf": ["cmd.mode.get_report", "cmd.mode.set", "evt.mode.report"], + "props": { + "is_secure": true, + "is_unsecure": false, + "sup_modes": ["on", "off", "fire", "CO"] + } + } + }, + "supports": ["clear", "poll"], + "thing": 56, + "type": { + "subtype": null, + "supported": { + "fire_detector": [] + }, + "type": null + } } ] diff --git a/futurehome/src/mqtt/demo_data/state.json b/futurehome/src/mqtt/demo_data/state.json index 9748ea8..724fb82 100644 --- a/futurehome/src/mqtt/demo_data/state.json +++ b/futurehome/src/mqtt/demo_data/state.json @@ -1314,5 +1314,164 @@ "name": "media_player" } ] + }, + { + "id": 73, + "services": [ + { + "addr": "/rt:dev/rn:zw/ad:1/sv:alarm_burglar/ad:86_0", + "attributes": [ + { + "name": "alarm", + "values": [ + { + "ts": "2023-06-22 10:31:28 +0200", + "val": { + "event": "tamper_removed_cover", + "status": "deactiv" + }, + "val_t": "str_map" + } + ] + } + ], + "name": "alarm_burglar" + }, + { + "addr": "/rt:dev/rn:zw/ad:1/sv:technology_specific/ad:86_0", + "attributes": [ + { + "name": "notification", + "values": [ + { + "ts": "2023-06-25 22:04:42 +0200", + "val": { + "category": "smoke_alarm", + "domain": "zwave", + "subject": "", + "type": "state", + "value": "unknown_event_state" + }, + "val_t": "str_map" + } + ] + } + ], + "name": "technology_specific" + }, + { + "addr": "/rt:dev/rn:zw/ad:1/sv:battery/ad:86_0", + "attributes": [ + { + "name": "lvl", + "values": [ + { + "ts": "2023-06-29 09:00:25 +0200", + "val": 80, + "val_t": "int" + } + ] + } + ], + "name": "battery" + }, + { + "addr": "/rt:dev/rn:zw/ad:1/sv:alarm_fire/ad:86_0", + "attributes": [ + { + "name": "alarm", + "values": [ + { + "ts": "2023-05-17 10:37:23 +0200", + "val": { + "event": "smoke_test", + "status": "deactiv" + }, + "val_t": "str_map" + }, + { + "ts": "2023-06-22 10:28:58 +0200", + "val": { + "event": "smoke", + "status": "deactiv" + }, + "val_t": "str_map" + }, + { + "ts": "2023-03-08 12:37:12 +0100", + "val": { + "event": "CO", + "status": "deactiv" + }, + "val_t": "str_map" + }, + { + "props": { + "silenced_by": "command" + }, + "ts": "2023-06-22 17:53:19 +0200", + "val": { + "event": "silenced", + "status": "activ" + }, + "val_t": "str_map" + } + ] + } + ], + "name": "alarm_fire" + }, + { + "addr": "/rt:dev/rn:zw/ad:1/sv:dev_sys/ad:86_0", + "attributes": [ + { + "name": "state", + "values": [ + { + "ts": "2023-06-22 16:35:24 +0200", + "val": "UP", + "val_t": "string" + } + ] + } + ], + "name": "dev_sys" + }, + { + "addr": "/rt:dev/rn:zw/ad:1/sv:siren_ctrl/ad:86_0", + "attributes": [ + { + "name": "mode", + "values": [ + { + "ts": "2023-06-22 15:49:51 +0200", + "val": "on", + "val_t": "string" + } + ] + } + ], + "nam,e": "siren_ctrl" + }, + { + "addr": "/rt:dev/rn:zw/ad:1/sv:alarm_heat/ad:86_0", + "attributes": [ + { + "name": "alarm", + "values": [ + { + "ts": "2022-10-14 17:45:52 +0200", + "val": { + "event": "overheat", + "status": "deactiv" + }, + "val_t": "str_map" + } + ] + } + ], + "name": "alarm_heat" + } + ] } ] diff --git a/futurehome/src/services/siren_ctrl.ts b/futurehome/src/services/siren_ctrl.ts new file mode 100644 index 0000000..f368b68 --- /dev/null +++ b/futurehome/src/services/siren_ctrl.ts @@ -0,0 +1,119 @@ +import { sendFimpMsg } from '../fimp/fimp'; +import { + VinculumPd7Device, + VinculumPd7Service, +} from '../fimp/vinculum_pd7_device'; +import { HaMqttComponent } from '../ha/mqtt_components/_component'; +import { SirenComponent } from '../ha/mqtt_components/siren'; +import { + CommandHandlers, + ServiceComponentsCreationResult, +} from '../ha/publish_device'; + +export function siren_ctrl__components( + topicPrefix: string, + device: VinculumPd7Device, + svc: VinculumPd7Service, +): ServiceComponentsCreationResult | undefined { + const components: Record = {}; + const commandHandlers: CommandHandlers = {}; + + // Extract supported modes from service properties + const supModes = svc.props?.sup_modes || []; + + if (supModes.length === 0) { + // If no supported modes are defined, we can't create a functional siren + return undefined; + } + + // Main siren component + const commandTopic = `${topicPrefix}${svc.addr}/command`; + + // Determine available tones based on supported modes + // Filter out 'off' as it's handled separately by Home Assistant + const availableTones = supModes.filter((mode: string) => mode !== 'off'); + + const sirenComponent: SirenComponent = { + unique_id: svc.addr, + platform: 'siren', + name: 'Siren', + command_topic: commandTopic, + optimistic: false, + state_value_template: `{{ (value_json['${svc.addr}'].mode != "off") | iif('ON', 'OFF') }}`, + support_duration: false, + support_volume_set: false, + }; + + // Add available tones if there are specific tone modes + if (availableTones.length > 0) { + sirenComponent.available_tones = availableTones; + // Use command template to handle tone selection + sirenComponent.command_template = `{% if value == "ON" %}{% if tone is defined %}{{ tone }}{% else %}{{ available_tones[0] if available_tones else "on" }}{% endif %}{% else %}off{% endif %}`; + } + + // Map Home Assistant state values to display values + sirenComponent.state_value_template = `{% set mode = value_json['${svc.addr}'].mode | default('off') %}{% if mode == 'off' %}OFF{% else %}ON{% endif %}`; + + components[svc.addr] = sirenComponent; + + // Command handler + commandHandlers[commandTopic] = async (payload: string) => { + let targetMode: string; + + // Handle different payload formats + try { + // Try to parse as JSON first (for tone commands) + const jsonPayload = JSON.parse(payload); + + if (jsonPayload.state === 'ON' || jsonPayload.state === true) { + // If tone is specified and supported, use it + if (jsonPayload.tone && supModes.includes(jsonPayload.tone)) { + targetMode = jsonPayload.tone; + } else { + // Use first available non-off mode or 'on' as fallback + targetMode = availableTones.length > 0 ? availableTones[0] : 'on'; + } + } else { + targetMode = 'off'; + } + } catch { + // Handle simple string payloads + switch (payload) { + case 'ON': + case 'on': + // Use first available tone or 'on' as fallback + targetMode = availableTones.length > 0 ? availableTones[0] : 'on'; + break; + case 'OFF': + case 'off': + targetMode = 'off'; + break; + default: + // Check if payload is a supported mode/tone + if (supModes.includes(payload)) { + targetMode = payload; + } else { + return; // Unsupported payload + } + } + } + + // Only send command if the target mode is supported + if (!supModes.includes(targetMode)) { + return; + } + + await sendFimpMsg({ + address: svc.addr, + service: 'siren_ctrl', + cmd: 'cmd.mode.set', + val_t: 'string', + val: targetMode, + }); + }; + + return { + components, + commandHandlers, + }; +}