diff --git a/README.md b/README.md index 0355583..4d83ec5 100644 --- a/README.md +++ b/README.md @@ -65,8 +65,9 @@ todo add info about factory reset hub to restore 30 day trial | Name | Service | Implementation status | Home Assistant entity | | --- | --- | --- | --- | -| Identify device | [indicator_ctrl](https://github.com/adrianjagielak/home-assistant-futurehome/blob/master/futurehome/src/services/indicator_ctrl.ts) | | ✅ | [Button](https://www.home-assistant.io/integrations/button/) | | Reboot device | [dev_sys](https://github.com/adrianjagielak/home-assistant-futurehome/blob/master/futurehome/src/services/dev_sys.ts) | | ✅ | [Button](https://www.home-assistant.io/integrations/button/) | +| Identify device | [indicator_ctrl](https://github.com/adrianjagielak/home-assistant-futurehome/blob/master/futurehome/src/services/indicator_ctrl.ts) | | ✅ | [Button](https://www.home-assistant.io/integrations/button/) | +| Advanced configuration of a device | [parameters](https://github.com/adrianjagielak/home-assistant-futurehome/blob/master/futurehome/src/services/parameters.ts) | | ✅ | [Button](https://www.home-assistant.io/integrations/button/), [Sensor](https://www.home-assistant.io/integrations/sensor/), [Number](https://www.home-assistant.io/integrations/number/), [Select](https://www.home-assistant.io/integrations/select/), [Text](https://www.home-assistant.io/integrations/text/) | ## Other system, meta, unused, or not essential services @@ -80,10 +81,9 @@ todo add info about factory reset hub to restore 30 day trial | inverter_grid_conn | No devices or hub support this stub service. | | inverter_solar_conn | No devices or hub support this stub service. | | ota | Managing OTA updates of devices | -| parameters | | -| Power regulator | [power_regulator](https://github.com/adrianjagielak/home-assistant-futurehome/blob/master/futurehome/src/services/power_regulator.ts) | [16A Puck Relé](https://www.futurehome.io/en_no/shop/puck-relay-16a) | | | +| power_regulator | Automation. Easily reproducible using Home Assistant's built-in configuration. | | schedule | No devices or hub support this stub service. | -| technology_specific | | +| technology_specific | Should be covered by other services | | time | Z-wave service for setting and reading time | | time_parameters | Z-wave service for setting and reading time | | version | Device hardware and software versions. Exposed through other means. | diff --git a/futurehome/CHANGELOG.md b/futurehome/CHANGELOG.md index 170f707..c62ac59 100644 --- a/futurehome/CHANGELOG.md +++ b/futurehome/CHANGELOG.md @@ -12,6 +12,7 @@ - Added support for 'doorman' service (Yale door locks). - Added support for 'complex_alarm_system' service (part of alarm sirens control). - Added support for 'dev_sys' service (reboot device). +- Added support for 'parameters' service (advanced configuration of a device). ## 0.1.5 (25.07.2025) diff --git a/futurehome/README.md b/futurehome/README.md index e705de3..1701b67 100644 --- a/futurehome/README.md +++ b/futurehome/README.md @@ -64,8 +64,9 @@ todo add info about factory reset hub to restore 30 day trial | Name | Service | Implementation status | Home Assistant entity | | --- | --- | --- | --- | -| Identify device | [indicator_ctrl](https://github.com/adrianjagielak/home-assistant-futurehome/blob/master/futurehome/src/services/indicator_ctrl.ts) | | ✅ | [Button](https://www.home-assistant.io/integrations/button/) | | Reboot device | [dev_sys](https://github.com/adrianjagielak/home-assistant-futurehome/blob/master/futurehome/src/services/dev_sys.ts) | | ✅ | [Button](https://www.home-assistant.io/integrations/button/) | +| Identify device | [indicator_ctrl](https://github.com/adrianjagielak/home-assistant-futurehome/blob/master/futurehome/src/services/indicator_ctrl.ts) | | ✅ | [Button](https://www.home-assistant.io/integrations/button/) | +| Advanced configuration of a device | [parameters](https://github.com/adrianjagielak/home-assistant-futurehome/blob/master/futurehome/src/services/parameters.ts) | | ✅ | [Button](https://www.home-assistant.io/integrations/button/), [Sensor](https://www.home-assistant.io/integrations/sensor/), [Number](https://www.home-assistant.io/integrations/number/), [Select](https://www.home-assistant.io/integrations/select/), [Text](https://www.home-assistant.io/integrations/text/) | ## Other system, meta, unused, or not essential services @@ -79,10 +80,9 @@ todo add info about factory reset hub to restore 30 day trial | inverter_grid_conn | No devices or hub support this stub service. | | inverter_solar_conn | No devices or hub support this stub service. | | ota | Managing OTA updates of devices | -| parameters | | -| Power regulator | [power_regulator](https://github.com/adrianjagielak/home-assistant-futurehome/blob/master/futurehome/src/services/power_regulator.ts) | [16A Puck Relé](https://www.futurehome.io/en_no/shop/puck-relay-16a) | | | +| power_regulator | Automation. Easily reproducible using Home Assistant's built-in configuration. | | schedule | No devices or hub support this stub service. | -| technology_specific | | +| technology_specific | Should be covered by other services | | time | Z-wave service for setting and reading time | | time_parameters | Z-wave service for setting and reading time | | version | Device hardware and software versions. Exposed through other means. | diff --git a/futurehome/src/ha/publish_device.ts b/futurehome/src/ha/publish_device.ts index 894bb64..bc0d449 100644 --- a/futurehome/src/ha/publish_device.ts +++ b/futurehome/src/ha/publish_device.ts @@ -22,6 +22,7 @@ import { indicator_ctrl__components } from '../services/indicator_ctrl'; import { media_player__components } from '../services/media_player'; import { out_bin_switch__components } from '../services/out_bin_switch'; import { out_lvl_switch__components } from '../services/out_lvl_switch'; +import { parameters__components } from '../services/parameters'; import { scene_ctrl__components } from '../services/scene_ctrl'; import { schedule_entry__components } from '../services/schedule_entry'; import { siren_ctrl__components } from '../services/siren_ctrl'; @@ -169,6 +170,7 @@ const serviceHandlers: { meter_cooling: _meter__components, out_bin_switch: out_bin_switch__components, out_lvl_switch: out_lvl_switch__components, + parameters: parameters__components, scene_ctrl: scene_ctrl__components, schedule_entry: schedule_entry__components, sensor_accelx: _sensor_numeric__components, @@ -301,9 +303,6 @@ export function haPublishDevice(parameters: { svcName, ); if (!result) { - log.error( - `Invalid service data prevented component creation: ${parameters.vinculumDeviceData} ${svc}`, - ); continue; } diff --git a/futurehome/src/ha/update_state.ts b/futurehome/src/ha/update_state.ts index 5eb4371..19abe9c 100644 --- a/futurehome/src/ha/update_state.ts +++ b/futurehome/src/ha/update_state.ts @@ -179,6 +179,7 @@ const attributeTypeKeyMap: Record = { alarm: 'event', meter: 'props.unit', meter_export: 'props.unit', + param: 'parameter_id', }; function getNestedValue(obj: any, path: string): any { @@ -211,10 +212,21 @@ function processAttributeValues(values: any[], attrName?: string): any { return undefined; } - // Sort by timestamp to get the latest values first + // Special handling for "param" attributes + if (attrName === 'param') { + const paramMap: Record = {}; + for (const entry of values) { + if (entry.parameter_id) { + paramMap[entry.parameter_id] = { ...entry }; + } + } + return paramMap; + } + + // Sort by timestamp to get the latest values first (only if ts exists) const sortedValues = [...values].sort((a, b) => { - const tsA = new Date(a.ts).getTime(); - const tsB = new Date(b.ts).getTime(); + const tsA = a.ts ? new Date(a.ts).getTime() : 0; + const tsB = b.ts ? new Date(b.ts).getTime() : 0; return tsB - tsA; // Latest first }); diff --git a/futurehome/src/mqtt/demo_data/device.json b/futurehome/src/mqtt/demo_data/device.json index 8b80f8c..c662499 100644 --- a/futurehome/src/mqtt/demo_data/device.json +++ b/futurehome/src/mqtt/demo_data/device.json @@ -2303,9 +2303,9 @@ "type": "meter" }, "services": { - "meter_heat": { - "name": "meter_heat", - "addr": "/rt:dev/rn:hoiax/ad:1/sv:meter_heat/ad:298", + "meter_heating": { + "name": "meter_heating", + "addr": "/rt:dev/rn:hoiax/ad:1/sv:meter_heating/ad:298", "enabled": true, "props": { "sup_units": ["kWh", "W"] diff --git a/futurehome/src/mqtt/demo_data/state.json b/futurehome/src/mqtt/demo_data/state.json index 4774856..102d242 100644 --- a/futurehome/src/mqtt/demo_data/state.json +++ b/futurehome/src/mqtt/demo_data/state.json @@ -1091,6 +1091,26 @@ } ], "name": "meter_elec" + }, + { + "addr": "/rt:dev/rn:cloud_adapter/ad:1/sv:parameters/ad:3_1", + "attributes": [ + { + "name": "param", + "values": [ + { + "parameter_id": "cable_always_locked", + "value_type": "bool", + "value": false + }, + { + "parameter_id": "free_charging", + "value_type": "bool", + "value": true + } + ] + } + ] } ] }, @@ -1668,7 +1688,7 @@ "id": 298, "services": [ { - "addr": "/rt:dev/rn:hoiax/ad:1/sv:meter_heat/ad:298", + "addr": "/rt:dev/rn:hoiax/ad:1/sv:meter_heating/ad:298", "attributes": [ { "name": "meter", @@ -1692,7 +1712,7 @@ ] } ], - "name": "meter_heat" + "name": "meter_heating" } ] }, diff --git a/futurehome/src/services/parameters.ts b/futurehome/src/services/parameters.ts new file mode 100644 index 0000000..eddac7f --- /dev/null +++ b/futurehome/src/services/parameters.ts @@ -0,0 +1,100 @@ +import { sendFimpMsg } from '../fimp/fimp'; +import { + VinculumPd7Device, + VinculumPd7Service, +} from '../fimp/vinculum_pd7_device'; +import { haGetCachedState } from '../ha/update_state'; +import { + ServiceComponentsCreationResult, + CommandHandlers, +} from '../ha/publish_device'; +import { HaMqttComponent } from '../ha/mqtt_components/_component'; + +export function parameters__components( + topicPrefix: string, + device: VinculumPd7Device, + svc: VinculumPd7Service, + _svcName: string, +): ServiceComponentsCreationResult | undefined { + const components: Record = {}; + const commandHandlers: CommandHandlers = {}; + const stateTopic = `${topicPrefix}/state`; + + // Fetch cached state for this service to discover known parameters :contentReference[oaicite:2]{index=2} + const currentState = haGetCachedState({ topic: stateTopic })?.[svc.addr]; + const paramMap = currentState?.param; + if (!paramMap) { + // No parameters known → nothing to expose + return; + } + + // Single MQTT topic for setting any parameter + const setParamTopic = `${topicPrefix}${svc.addr}/set_param/command`; + + // Iterate over each parameter in the cached state :contentReference[oaicite:3]{index=3} + for (const [paramId, param] of Object.entries(paramMap)) { + const valueType = (param as any).value_type as string; + const uniqueId = `${svc.addr}_${paramId}`; + const name = paramId.replace(/_/g, ' '); + const valueTemplate = `{{ value_json['${svc.addr}'].param.${paramId}.value }}`; + + if (valueType === 'bool') { + components[uniqueId] = { + unique_id: uniqueId, + platform: 'switch', + name, + state_topic: stateTopic, + value_template: valueTemplate, + command_topic: setParamTopic, + payload_on: `{"parameter_id":"${paramId}","value_type":"${valueType}","value":true}`, + payload_off: `{"parameter_id":"${paramId}","value_type":"${valueType}","value":false}`, + }; + } else if ( + ['int', 'float', 'double', 'uint8', 'uint16', 'uint32'].includes( + valueType, + ) + ) { + components[uniqueId] = { + unique_id: uniqueId, + platform: 'number', + name, + state_topic: stateTopic, + value_template: valueTemplate, + command_topic: setParamTopic, + command_template: `{"parameter_id":"${paramId}","value_type":"${valueType}","value":{{ value }}}`, + }; + } else { + components[uniqueId] = { + unique_id: uniqueId, + platform: 'text', + name, + state_topic: stateTopic, + value_template: valueTemplate, + command_topic: setParamTopic, + }; + } + } + + // Single handler for all set_param commands + commandHandlers[setParamTopic] = async (payload: string) => { + let valObj: any; + try { + valObj = JSON.parse(payload); + } catch { + // Invalid JSON: ignore + return; + } + await sendFimpMsg({ + address: svc.addr!, + service: 'parameters', + cmd: 'cmd.param.set', + val: valObj, + val_t: 'object', + }); + }; + + return { + components, + commandHandlers, + }; +}