diff --git a/README.md b/README.md index e37829f..771b940 100644 --- a/README.md +++ b/README.md @@ -42,7 +42,7 @@ Some services are more common than others; some are deprecated entirely. | Media player | [media_player](https://github.com/adrianjagielak/home-assistant-futurehome/blob/master/futurehome/src/services/media_player.ts) | | ✅ | [Select](https://www.home-assistant.io/integrations/select/), [Number](https://www.home-assistant.io/integrations/number/), [Switch](https://www.home-assistant.io/integrations/switch/), [Image](https://www.home-assistant.io/integrations/image/), [Sensor](https://www.home-assistant.io/integrations/sensor/) | | Meter | [meter_elec](https://github.com/adrianjagielak/home-assistant-futurehome/blob/master/futurehome/src/services/_meter.ts), [meter_gas](https://github.com/adrianjagielak/home-assistant-futurehome/blob/master/futurehome/src/services/_meter.ts), [meter_water](https://github.com/adrianjagielak/home-assistant-futurehome/blob/master/futurehome/src/services/_meter.ts), [meter_heating](https://github.com/adrianjagielak/home-assistant-futurehome/blob/master/futurehome/src/services/_meter.ts), [meter_cooling](https://github.com/adrianjagielak/home-assistant-futurehome/blob/master/futurehome/src/services/_meter.ts) | [HAN-Sensor](https://www.futurehome.io/en/shop/han-sensor) | ✅ | [Sensor](https://www.home-assistant.io/integrations/sensor/), [Button](https://www.home-assistant.io/integrations/button/) | | Binary switch | [out_bin_switch](https://github.com/adrianjagielak/home-assistant-futurehome/blob/master/futurehome/src/services/out_bin_switch.ts) | [16A Puck Relé](https://www.futurehome.io/en_no/shop/puck-relay-16a) | ✅ | [Switch](https://www.home-assistant.io/integrations/switch/) | -| Level switch | [out_lvl_switch](https://github.com/adrianjagielak/home-assistant-futurehome/blob/master/futurehome/src/services/out_lvl_switch.ts) | [Smart LED Dimmer](https://www.futurehome.io/en_no/shop/smart-led-dimmer-polar-white) | ✅ | [Number](https://www.home-assistant.io/integrations/number/) | +| Level switch | [out_lvl_switch](https://github.com/adrianjagielak/home-assistant-futurehome/blob/master/futurehome/src/services/out_lvl_switch.ts) | [Smart LED Dimmer](https://www.futurehome.io/en_no/shop/smart-led-dimmer-polar-white) | ✅ | [Light](https://www.home-assistant.io/integrations/light/), [Number](https://www.home-assistant.io/integrations/number/) | | Button | [scene_ctrl](https://github.com/adrianjagielak/home-assistant-futurehome/blob/master/futurehome/src/services/scene_ctrl.ts) | [Modusbryter](https://www.futurehome.io/en_no/shop/modeswitch-white) | ✅ | [Sensor](https://www.home-assistant.io/integrations/sensor/), [Select](https://www.home-assistant.io/integrations/select/) | | Schedule entry | [schedule_entry](https://github.com/adrianjagielak/home-assistant-futurehome/blob/master/futurehome/src/services/schedule_entry.ts) | | ✅ | [Number](https://www.home-assistant.io/integrations/number/), [Button](https://www.home-assistant.io/integrations/button/), [Binary sensor](https://www.home-assistant.io/integrations/binary_sensor/), [Sensor](https://www.home-assistant.io/integrations/sensor/) | | Binary sensor | [sensor_contact](https://github.com/adrianjagielak/home-assistant-futurehome/blob/master/futurehome/src/services/_sensor_binary.ts), [sensor_presence](https://github.com/adrianjagielak/home-assistant-futurehome/blob/master/futurehome/src/services/_sensor_binary.ts) | | ✅ | [Binary sensor](https://www.home-assistant.io/integrations/binary_sensor/) | diff --git a/futurehome/README.md b/futurehome/README.md index b9ae54a..b71a7a1 100644 --- a/futurehome/README.md +++ b/futurehome/README.md @@ -41,7 +41,7 @@ Some services are more common than others; some are deprecated entirely. | Media player | [media_player](https://github.com/adrianjagielak/home-assistant-futurehome/blob/master/futurehome/src/services/media_player.ts) | | ✅ | [Select](https://www.home-assistant.io/integrations/select/), [Number](https://www.home-assistant.io/integrations/number/), [Switch](https://www.home-assistant.io/integrations/switch/), [Image](https://www.home-assistant.io/integrations/image/), [Sensor](https://www.home-assistant.io/integrations/sensor/) | | Meter | [meter_elec](https://github.com/adrianjagielak/home-assistant-futurehome/blob/master/futurehome/src/services/_meter.ts), [meter_gas](https://github.com/adrianjagielak/home-assistant-futurehome/blob/master/futurehome/src/services/_meter.ts), [meter_water](https://github.com/adrianjagielak/home-assistant-futurehome/blob/master/futurehome/src/services/_meter.ts), [meter_heating](https://github.com/adrianjagielak/home-assistant-futurehome/blob/master/futurehome/src/services/_meter.ts), [meter_cooling](https://github.com/adrianjagielak/home-assistant-futurehome/blob/master/futurehome/src/services/_meter.ts) | [HAN-Sensor](https://www.futurehome.io/en/shop/han-sensor) | ✅ | [Sensor](https://www.home-assistant.io/integrations/sensor/), [Button](https://www.home-assistant.io/integrations/button/) | | Binary switch | [out_bin_switch](https://github.com/adrianjagielak/home-assistant-futurehome/blob/master/futurehome/src/services/out_bin_switch.ts) | [16A Puck Relé](https://www.futurehome.io/en_no/shop/puck-relay-16a) | ✅ | [Switch](https://www.home-assistant.io/integrations/switch/) | -| Level switch | [out_lvl_switch](https://github.com/adrianjagielak/home-assistant-futurehome/blob/master/futurehome/src/services/out_lvl_switch.ts) | [Smart LED Dimmer](https://www.futurehome.io/en_no/shop/smart-led-dimmer-polar-white) | ✅ | [Number](https://www.home-assistant.io/integrations/number/) | +| Level switch | [out_lvl_switch](https://github.com/adrianjagielak/home-assistant-futurehome/blob/master/futurehome/src/services/out_lvl_switch.ts) | [Smart LED Dimmer](https://www.futurehome.io/en_no/shop/smart-led-dimmer-polar-white) | ✅ | [Light](https://www.home-assistant.io/integrations/light/), [Number](https://www.home-assistant.io/integrations/number/) | | Button | [scene_ctrl](https://github.com/adrianjagielak/home-assistant-futurehome/blob/master/futurehome/src/services/scene_ctrl.ts) | [Modusbryter](https://www.futurehome.io/en_no/shop/modeswitch-white) | ✅ | [Sensor](https://www.home-assistant.io/integrations/sensor/), [Select](https://www.home-assistant.io/integrations/select/) | | Schedule entry | [schedule_entry](https://github.com/adrianjagielak/home-assistant-futurehome/blob/master/futurehome/src/services/schedule_entry.ts) | | ✅ | [Number](https://www.home-assistant.io/integrations/number/), [Button](https://www.home-assistant.io/integrations/button/), [Binary sensor](https://www.home-assistant.io/integrations/binary_sensor/), [Sensor](https://www.home-assistant.io/integrations/sensor/) | | Binary sensor | [sensor_contact](https://github.com/adrianjagielak/home-assistant-futurehome/blob/master/futurehome/src/services/_sensor_binary.ts), [sensor_presence](https://github.com/adrianjagielak/home-assistant-futurehome/blob/master/futurehome/src/services/_sensor_binary.ts) | | ✅ | [Binary sensor](https://www.home-assistant.io/integrations/binary_sensor/) | diff --git a/futurehome/src/index.ts b/futurehome/src/index.ts index 7864b64..90a6c9a 100644 --- a/futurehome/src/index.ts +++ b/futurehome/src/index.ts @@ -80,9 +80,7 @@ import { pollVinculum } from './fimp/vinculum'; const hubId = house.val.param.house.hubId; const devices = await pollVinculum('device'); - log.debug( - `FIMP devices:\n${JSON.stringify(devices, null, 0)}`, - ); + log.debug(`FIMP devices:\n${JSON.stringify(devices, null, 0)}`); const haConfig = retainedMessages.filter((msg) => msg.topic.endsWith('/config'), @@ -191,7 +189,11 @@ import { pollVinculum } from './fimp/vinculum'; log.error('Failed publishing device', device, e); } } - if (demoMode || thingsplexAllowEmpty || (thingsplexUsername && thingsplexPassword)) { + if ( + demoMode || + thingsplexAllowEmpty || + (thingsplexUsername && thingsplexPassword) + ) { Object.assign( commandHandlers, exposeSmarthubTools({ diff --git a/futurehome/src/mqtt/demo_data/device.json b/futurehome/src/mqtt/demo_data/device.json index 77955d3..1be43b5 100644 --- a/futurehome/src/mqtt/demo_data/device.json +++ b/futurehome/src/mqtt/demo_data/device.json @@ -2518,5 +2518,128 @@ "props": { "sup_units": ["km/h"] } } } + }, + { + "client": { "name": "Spisestue" }, + "fimp": { "adapter": "zwave-ad", "address": "6", "group": "ch_0" }, + "functionality": "lighting", + "id": 1011, + "lrn": true, + "model": "zw_411_4_8705", + "param": { + "dimValue": 3, + "energy": 49.6100006103516, + "power": "off", + "timestamp": "2025-09-26 18:32:21 +0200", + "wattage": 0, + "zwaveConfigParameters": [] + }, + "problem": false, + "room": 1, + "services": { + "dev_sys": { + "addr": "/rt:dev/rn:zw/ad:1/sv:dev_sys/ad:1011_0", + "enabled": true, + "intf": [ + "cmd.config.get_report", + "cmd.config.set", + "cmd.group.add_members", + "cmd.group.delete_members", + "cmd.group.get_members", + "cmd.ping.send", + "evt.config.report", + "evt.group.members_report", + "evt.ping.report" + ], + "props": { "is_secure": false, "is_unsecure": true } + }, + "indicator_ctrl": { + "addr": "/rt:dev/rn:zw/ad:1/sv:indicator_ctrl/ad:1011_0", + "enabled": true, + "intf": [ + "cmd.indicator.identify", + "cmd.indicator.set_visual_element" + ], + "props": { "is_secure": false, "is_unsecure": true } + }, + "meter_elec": { + "addr": "/rt:dev/rn:zw/ad:1/sv:meter_elec/ad:1011_0", + "enabled": true, + "intf": [ + "cmd.meter.get_report", + "cmd.meter.reset", + "evt.meter.report" + ], + "props": { + "is_secure": false, + "is_unsecure": true, + "sup_export_units": [], + "sup_units": ["kWh", "W"] + } + }, + "out_lvl_switch": { + "addr": "/rt:dev/rn:zw/ad:1/sv:out_lvl_switch/ad:1011_0", + "enabled": true, + "intf": [ + "cmd.binary.set", + "cmd.lvl.get_report", + "cmd.lvl.set", + "cmd.lvl.start", + "cmd.lvl.stop", + "evt.binary.report", + "evt.lvl.report" + ], + "props": { + "is_secure": false, + "is_unsecure": true, + "max_lvl": 100, + "min_lvl": 0 + } + }, + "scene_ctrl": { + "addr": "/rt:dev/rn:zw/ad:1/sv:scene_ctrl/ad:1011_0", + "enabled": true, + "intf": ["evt.scene.report"], + "props": { + "is_secure": false, + "is_unsecure": true, + "sup_scenes": [ + "1.key_pressed_1_time", + "1.key_released", + "1.key_held_down", + "1.key_pressed_2_times", + "1.key_pressed_3_times", + "1.key_pressed_4_times", + "1.key_pressed_5_times", + "2.key_pressed_1_time", + "2.key_released", + "2.key_held_down", + "2.key_pressed_2_times", + "2.key_pressed_3_times", + "2.key_pressed_4_times", + "2.key_pressed_5_times" + ] + } + }, + "version": { + "addr": "/rt:dev/rn:zw/ad:1/sv:version/ad:1011_0", + "enabled": true, + "intf": ["cmd.version.get_report", "evt.version.report"], + "props": { "is_secure": false, "is_unsecure": true } + } + }, + "supports": ["clear", "poll"], + "thing": 1, + "type": { + "subtype": null, + "supported": { + "appliance": [], + "blinds": [], + "fan": [], + "heater": [], + "light": [] + }, + "type": "light" + } } ] diff --git a/futurehome/src/mqtt/demo_data/state.json b/futurehome/src/mqtt/demo_data/state.json index df53a3f..6d45862 100644 --- a/futurehome/src/mqtt/demo_data/state.json +++ b/futurehome/src/mqtt/demo_data/state.json @@ -2380,5 +2380,79 @@ "name": "sensor_wind" } ] + }, + { + "id": 1011, + "services": [ + { + "addr": "/rt:dev/rn:zw/ad:1/sv:out_lvl_switch/ad:1011_0", + "attributes": [ + { + "name": "lvl", + "values": [ + { + "ts": "2025-09-26 12:39:19 +0200", + "val": 22, + "val_t": "int" + } + ] + }, + { + "name": "binary", + "values": [ + { + "ts": "2025-09-26 12:39:19 +0200", + "val": true, + "val_t": "bool" + } + ] + } + ], + "name": "out_lvl_switch" + }, + { + "addr": "/rt:dev/rn:zw/ad:1/sv:dev_sys/ad:1011_0", + "attributes": [ + { + "name": "state", + "values": [ + { + "ts": "2025-09-08 21:06:32 +0200", + "val": "UP", + "val_t": "string" + } + ] + } + ], + "name": "dev_sys" + }, + { + "addr": "/rt:dev/rn:zw/ad:1/sv:meter_elec/ad:1011_0", + "attributes": [ + { + "name": "meter", + "values": [ + { + "props": { + "delta_t": "774", + "prv_data": "31.290001", + "unit": "kWh" + }, + "ts": "2025-09-26 18:32:21 +0200", + "val": 49.6100006103516, + "val_t": "float" + }, + { + "props": { "unit": "W" }, + "ts": "2025-09-26 18:32:21 +0200", + "val": 0, + "val_t": "float" + } + ] + } + ], + "name": "meter_elec" + } + ] } ] diff --git a/futurehome/src/services/out_lvl_switch.ts b/futurehome/src/services/out_lvl_switch.ts index 469796b..6d87ba4 100644 --- a/futurehome/src/services/out_lvl_switch.ts +++ b/futurehome/src/services/out_lvl_switch.ts @@ -4,6 +4,7 @@ import { VinculumPd7Service, } from '../fimp/vinculum_pd7_device'; import { ServiceComponentsCreationResult } from '../ha/publish_device'; +import { haGetCachedState } from '../ha/update_state'; export function out_lvl_switch__components( topicPrefix: string, @@ -11,41 +12,123 @@ export function out_lvl_switch__components( svc: VinculumPd7Service, _svcName: string, ): ServiceComponentsCreationResult | undefined { - const commandTopic = `${topicPrefix}${svc.addr}/command`; + const lvlCommandTopic = `${topicPrefix}${svc.addr}/command`; + const binaryCommandTopic = `${topicPrefix}${svc.addr}/binary/command`; + const stateTopic = `${topicPrefix}/state`; const minLvl = svc.props?.min_lvl ?? 0; const maxLvl = svc.props?.max_lvl ?? 100; - return { - components: { - [svc.addr]: { - unique_id: svc.addr, - platform: 'number', - name: 'Level Switch', - min: minLvl, - max: maxLvl, - step: 1, - command_topic: commandTopic, - optimistic: false, - value_template: `{{ value_json['${svc.addr}'].lvl }}`, - }, - }, + const isLightDevice = device.type?.type === 'light'; - commandHandlers: { - [commandTopic]: async (payload: string) => { - const lvl = parseInt(payload, 10); - if (Number.isNaN(lvl)) { - return; - } - - await sendFimpMsg({ - address: svc.addr!, - service: 'out_lvl_switch', - cmd: 'cmd.lvl.set', - val: lvl, - val_t: 'int', - }); + if (isLightDevice) { + // Use light component for light devices + return { + components: { + [`${svc.addr}_light`]: { + unique_id: `${svc.addr}_light`, + platform: 'light', + name: 'Light', + brightness: true, + brightness_scale: maxLvl, + command_topic: binaryCommandTopic, + brightness_command_topic: lvlCommandTopic, + optimistic: false, + state_topic: stateTopic, + state_value_template: `{{ (value_json['${svc.addr}'].lvl > 0) | iif('ON', 'OFF') }}`, + brightness_state_topic: stateTopic, + brightness_value_template: `{{ value_json['${svc.addr}'].lvl }}`, + }, }, - }, - }; + commandHandlers: { + [lvlCommandTopic]: async (payload: string) => { + const lvl = parseInt(payload, 10); + if (Number.isNaN(lvl)) { + return; + } + + await sendFimpMsg({ + address: svc.addr!, + service: 'out_lvl_switch', + cmd: 'cmd.lvl.set', + val: lvl, + val_t: 'int', + }); + }, + [binaryCommandTopic]: async (payload: string) => { + if (payload === 'ON') { + // Skip setting to max brightness if the device is already on, because Home Assistant also sends "ON" when only changing brightness. + const currentState = haGetCachedState({ + topic: `${topicPrefix}/state`, + })?.[svc.addr]; + if (currentState.lvl > 0) { + return; + } + + if (svc.intf?.includes('cmd.binary.set')) { + // Set level to the last known non-zero value (not supported in add-on demo mode) + await sendFimpMsg({ + address: svc.addr!, + service: 'out_lvl_switch', + cmd: 'cmd.binary.set', + val: payload === 'ON', + val_t: 'bool', + }); + } else { + // Set level to max brightness + await sendFimpMsg({ + address: svc.addr!, + service: 'out_lvl_switch', + cmd: 'cmd.lvl.set', + val: maxLvl, + val_t: 'int', + }); + } + } else { + await sendFimpMsg({ + address: svc.addr!, + service: 'out_lvl_switch', + cmd: 'cmd.lvl.set', + val: minLvl, + val_t: 'int', + }); + } + }, + }, + }; + } else { + // Use number component for non-light devices + return { + components: { + [svc.addr]: { + unique_id: svc.addr, + platform: 'number', + name: 'Level Switch', + min: minLvl, + max: maxLvl, + step: 1, + command_topic: lvlCommandTopic, + optimistic: false, + value_template: `{{ value_json['${svc.addr}'].lvl }}`, + }, + }, + + commandHandlers: { + [lvlCommandTopic]: async (payload: string) => { + const lvl = parseInt(payload, 10); + if (Number.isNaN(lvl)) { + return; + } + + await sendFimpMsg({ + address: svc.addr!, + service: 'out_lvl_switch', + cmd: 'cmd.lvl.set', + val: lvl, + val_t: 'int', + }); + }, + }, + }; + } }