diff --git a/README.md b/README.md index 771b940..a7a3bee 100644 --- a/README.md +++ b/README.md @@ -16,6 +16,7 @@ This add-on: * Fetches all device metadata from the Futurehome hub and maps them to Home Assistant devices/entities. * Fetches and updates device states and availability in real time. * Supports interaction with devices comparable to the official Futurehome app. +* Exposes the Futurehome house mode (Home/Away/Sleep/Vacation) — the mode changed by the [Futurehome Modeswitch](https://www.futurehome.io/en_no/shop/modeswitch-white) — as a `Mode` selector on the Smarthub device, and keeps it in sync in real time. * Supports pairing and unpairing devices. ## Installation Guide @@ -43,7 +44,7 @@ Some services are more common than others; some are deprecated entirely. | 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) | ✅ | [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/) | +| 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) | ✅ | [Event](https://www.home-assistant.io/integrations/event/), [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/) | | Numeric sensor | [sensor_accelx](https://github.com/adrianjagielak/home-assistant-futurehome/blob/master/futurehome/src/services/_sensor_numeric.ts), [sensor_accely](https://github.com/adrianjagielak/home-assistant-futurehome/blob/master/futurehome/src/services/_sensor_numeric.ts), [sensor_accelz](https://github.com/adrianjagielak/home-assistant-futurehome/blob/master/futurehome/src/services/_sensor_numeric.ts), [sensor_airflow](https://github.com/adrianjagielak/home-assistant-futurehome/blob/master/futurehome/src/services/_sensor_numeric.ts), [sensor_airq](https://github.com/adrianjagielak/home-assistant-futurehome/blob/master/futurehome/src/services/_sensor_numeric.ts), [sensor_anglepos](https://github.com/adrianjagielak/home-assistant-futurehome/blob/master/futurehome/src/services/_sensor_numeric.ts), [sensor_atmo](https://github.com/adrianjagielak/home-assistant-futurehome/blob/master/futurehome/src/services/_sensor_numeric.ts), [sensor_baro](https://github.com/adrianjagielak/home-assistant-futurehome/blob/master/futurehome/src/services/_sensor_numeric.ts), [sensor_co](https://github.com/adrianjagielak/home-assistant-futurehome/blob/master/futurehome/src/services/_sensor_numeric.ts), [sensor_co2](https://github.com/adrianjagielak/home-assistant-futurehome/blob/master/futurehome/src/services/_sensor_numeric.ts), [sensor_current](https://github.com/adrianjagielak/home-assistant-futurehome/blob/master/futurehome/src/services/_sensor_numeric.ts), [sensor_dew](https://github.com/adrianjagielak/home-assistant-futurehome/blob/master/futurehome/src/services/_sensor_numeric.ts), [sensor_direct](https://github.com/adrianjagielak/home-assistant-futurehome/blob/master/futurehome/src/services/_sensor_numeric.ts), [sensor_distance](https://github.com/adrianjagielak/home-assistant-futurehome/blob/master/futurehome/src/services/_sensor_numeric.ts), [sensor_elresist](https://github.com/adrianjagielak/home-assistant-futurehome/blob/master/futurehome/src/services/_sensor_numeric.ts), [sensor_freq](https://github.com/adrianjagielak/home-assistant-futurehome/blob/master/futurehome/src/services/_sensor_numeric.ts), [sensor_gp](https://github.com/adrianjagielak/home-assistant-futurehome/blob/master/futurehome/src/services/_sensor_numeric.ts), [sensor_gust](https://github.com/adrianjagielak/home-assistant-futurehome/blob/master/futurehome/src/services/_sensor_numeric.ts), [sensor_humid](https://github.com/adrianjagielak/home-assistant-futurehome/blob/master/futurehome/src/services/_sensor_numeric.ts), [sensor_lumin](https://github.com/adrianjagielak/home-assistant-futurehome/blob/master/futurehome/src/services/_sensor_numeric.ts), [sensor_moist](https://github.com/adrianjagielak/home-assistant-futurehome/blob/master/futurehome/src/services/_sensor_numeric.ts), [sensor_noise](https://github.com/adrianjagielak/home-assistant-futurehome/blob/master/futurehome/src/services/_sensor_numeric.ts), [sensor_power](https://github.com/adrianjagielak/home-assistant-futurehome/blob/master/futurehome/src/services/_sensor_numeric.ts), [sensor_rain](https://github.com/adrianjagielak/home-assistant-futurehome/blob/master/futurehome/src/services/_sensor_numeric.ts), [sensor_rotation](https://github.com/adrianjagielak/home-assistant-futurehome/blob/master/futurehome/src/services/_sensor_numeric.ts), [sensor_seismicint](https://github.com/adrianjagielak/home-assistant-futurehome/blob/master/futurehome/src/services/_sensor_numeric.ts), [sensor_seismicmag](https://github.com/adrianjagielak/home-assistant-futurehome/blob/master/futurehome/src/services/_sensor_numeric.ts), [sensor_solarrad](https://github.com/adrianjagielak/home-assistant-futurehome/blob/master/futurehome/src/services/_sensor_numeric.ts), [sensor_tank](https://github.com/adrianjagielak/home-assistant-futurehome/blob/master/futurehome/src/services/_sensor_numeric.ts), [sensor_temp](https://github.com/adrianjagielak/home-assistant-futurehome/blob/master/futurehome/src/services/_sensor_numeric.ts), [sensor_tidelvl](https://github.com/adrianjagielak/home-assistant-futurehome/blob/master/futurehome/src/services/_sensor_numeric.ts), [sensor_uv](https://github.com/adrianjagielak/home-assistant-futurehome/blob/master/futurehome/src/services/_sensor_numeric.ts), [sensor_veloc](https://github.com/adrianjagielak/home-assistant-futurehome/blob/master/futurehome/src/services/_sensor_numeric.ts), [sensor_voltage](https://github.com/adrianjagielak/home-assistant-futurehome/blob/master/futurehome/src/services/_sensor_numeric.ts), [sensor_watflow](https://github.com/adrianjagielak/home-assistant-futurehome/blob/master/futurehome/src/services/_sensor_numeric.ts), [sensor_watpressure](https://github.com/adrianjagielak/home-assistant-futurehome/blob/master/futurehome/src/services/_sensor_numeric.ts), [sensor_wattemp](https://github.com/adrianjagielak/home-assistant-futurehome/blob/master/futurehome/src/services/_sensor_numeric.ts), [sensor_weight](https://github.com/adrianjagielak/home-assistant-futurehome/blob/master/futurehome/src/services/_sensor_numeric.ts), [sensor_wind](https://github.com/adrianjagielak/home-assistant-futurehome/blob/master/futurehome/src/services/_sensor_numeric.ts) | [Brannvarsler](https://www.futurehome.io/en_no/shop/brannvarsler-230v) | ✅ | [Sensor](https://www.home-assistant.io/integrations/sensor/) | diff --git a/futurehome/CHANGELOG.md b/futurehome/CHANGELOG.md index e705957..27cd32b 100644 --- a/futurehome/CHANGELOG.md +++ b/futurehome/CHANGELOG.md @@ -1,5 +1,10 @@ +## 1.7.0 (01.07.2026) + +- Added support for the Futurehome house mode (Home/Away/Sleep/Vacation). The current mode is exposed as a `Mode` selector on the Smarthub device, updates in real time (e.g. when a Futurehome Modeswitch button is pressed), and can be changed from Home Assistant. +- Scene controllers (`scene_ctrl`, e.g. the Futurehome Modeswitch) now also expose an [`Event`](https://www.home-assistant.io/integrations/event/) entity, so momentary button presses can trigger automations (the previous `Scene` sensor was stuck on `unknown` and could not react to repeated presses). + ## 1.6.2 (16.05.2026) - Fix invalid device_class + unit_of_measurement combinations for HA 2026.5 (#32). diff --git a/futurehome/README.md b/futurehome/README.md index b71a7a1..6a325cc 100644 --- a/futurehome/README.md +++ b/futurehome/README.md @@ -15,6 +15,7 @@ This add-on: * Fetches all device metadata from the Futurehome hub and maps them to Home Assistant devices/entities. * Fetches and updates device states and availability in real time. * Supports interaction with devices comparable to the official Futurehome app. +* Exposes the Futurehome house mode (Home/Away/Sleep/Vacation) — the mode changed by the [Futurehome Modeswitch](https://www.futurehome.io/en_no/shop/modeswitch-white) — as a `Mode` selector on the Smarthub device, and keeps it in sync in real time. * Supports pairing and unpairing devices. ## Installation Guide @@ -42,7 +43,7 @@ Some services are more common than others; some are deprecated entirely. | 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) | ✅ | [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/) | +| 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) | ✅ | [Event](https://www.home-assistant.io/integrations/event/), [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/) | | Numeric sensor | [sensor_accelx](https://github.com/adrianjagielak/home-assistant-futurehome/blob/master/futurehome/src/services/_sensor_numeric.ts), [sensor_accely](https://github.com/adrianjagielak/home-assistant-futurehome/blob/master/futurehome/src/services/_sensor_numeric.ts), [sensor_accelz](https://github.com/adrianjagielak/home-assistant-futurehome/blob/master/futurehome/src/services/_sensor_numeric.ts), [sensor_airflow](https://github.com/adrianjagielak/home-assistant-futurehome/blob/master/futurehome/src/services/_sensor_numeric.ts), [sensor_airq](https://github.com/adrianjagielak/home-assistant-futurehome/blob/master/futurehome/src/services/_sensor_numeric.ts), [sensor_anglepos](https://github.com/adrianjagielak/home-assistant-futurehome/blob/master/futurehome/src/services/_sensor_numeric.ts), [sensor_atmo](https://github.com/adrianjagielak/home-assistant-futurehome/blob/master/futurehome/src/services/_sensor_numeric.ts), [sensor_baro](https://github.com/adrianjagielak/home-assistant-futurehome/blob/master/futurehome/src/services/_sensor_numeric.ts), [sensor_co](https://github.com/adrianjagielak/home-assistant-futurehome/blob/master/futurehome/src/services/_sensor_numeric.ts), [sensor_co2](https://github.com/adrianjagielak/home-assistant-futurehome/blob/master/futurehome/src/services/_sensor_numeric.ts), [sensor_current](https://github.com/adrianjagielak/home-assistant-futurehome/blob/master/futurehome/src/services/_sensor_numeric.ts), [sensor_dew](https://github.com/adrianjagielak/home-assistant-futurehome/blob/master/futurehome/src/services/_sensor_numeric.ts), [sensor_direct](https://github.com/adrianjagielak/home-assistant-futurehome/blob/master/futurehome/src/services/_sensor_numeric.ts), [sensor_distance](https://github.com/adrianjagielak/home-assistant-futurehome/blob/master/futurehome/src/services/_sensor_numeric.ts), [sensor_elresist](https://github.com/adrianjagielak/home-assistant-futurehome/blob/master/futurehome/src/services/_sensor_numeric.ts), [sensor_freq](https://github.com/adrianjagielak/home-assistant-futurehome/blob/master/futurehome/src/services/_sensor_numeric.ts), [sensor_gp](https://github.com/adrianjagielak/home-assistant-futurehome/blob/master/futurehome/src/services/_sensor_numeric.ts), [sensor_gust](https://github.com/adrianjagielak/home-assistant-futurehome/blob/master/futurehome/src/services/_sensor_numeric.ts), [sensor_humid](https://github.com/adrianjagielak/home-assistant-futurehome/blob/master/futurehome/src/services/_sensor_numeric.ts), [sensor_lumin](https://github.com/adrianjagielak/home-assistant-futurehome/blob/master/futurehome/src/services/_sensor_numeric.ts), [sensor_moist](https://github.com/adrianjagielak/home-assistant-futurehome/blob/master/futurehome/src/services/_sensor_numeric.ts), [sensor_noise](https://github.com/adrianjagielak/home-assistant-futurehome/blob/master/futurehome/src/services/_sensor_numeric.ts), [sensor_power](https://github.com/adrianjagielak/home-assistant-futurehome/blob/master/futurehome/src/services/_sensor_numeric.ts), [sensor_rain](https://github.com/adrianjagielak/home-assistant-futurehome/blob/master/futurehome/src/services/_sensor_numeric.ts), [sensor_rotation](https://github.com/adrianjagielak/home-assistant-futurehome/blob/master/futurehome/src/services/_sensor_numeric.ts), [sensor_seismicint](https://github.com/adrianjagielak/home-assistant-futurehome/blob/master/futurehome/src/services/_sensor_numeric.ts), [sensor_seismicmag](https://github.com/adrianjagielak/home-assistant-futurehome/blob/master/futurehome/src/services/_sensor_numeric.ts), [sensor_solarrad](https://github.com/adrianjagielak/home-assistant-futurehome/blob/master/futurehome/src/services/_sensor_numeric.ts), [sensor_tank](https://github.com/adrianjagielak/home-assistant-futurehome/blob/master/futurehome/src/services/_sensor_numeric.ts), [sensor_temp](https://github.com/adrianjagielak/home-assistant-futurehome/blob/master/futurehome/src/services/_sensor_numeric.ts), [sensor_tidelvl](https://github.com/adrianjagielak/home-assistant-futurehome/blob/master/futurehome/src/services/_sensor_numeric.ts), [sensor_uv](https://github.com/adrianjagielak/home-assistant-futurehome/blob/master/futurehome/src/services/_sensor_numeric.ts), [sensor_veloc](https://github.com/adrianjagielak/home-assistant-futurehome/blob/master/futurehome/src/services/_sensor_numeric.ts), [sensor_voltage](https://github.com/adrianjagielak/home-assistant-futurehome/blob/master/futurehome/src/services/_sensor_numeric.ts), [sensor_watflow](https://github.com/adrianjagielak/home-assistant-futurehome/blob/master/futurehome/src/services/_sensor_numeric.ts), [sensor_watpressure](https://github.com/adrianjagielak/home-assistant-futurehome/blob/master/futurehome/src/services/_sensor_numeric.ts), [sensor_wattemp](https://github.com/adrianjagielak/home-assistant-futurehome/blob/master/futurehome/src/services/_sensor_numeric.ts), [sensor_weight](https://github.com/adrianjagielak/home-assistant-futurehome/blob/master/futurehome/src/services/_sensor_numeric.ts), [sensor_wind](https://github.com/adrianjagielak/home-assistant-futurehome/blob/master/futurehome/src/services/_sensor_numeric.ts) | [Brannvarsler](https://www.futurehome.io/en_no/shop/brannvarsler-230v) | ✅ | [Sensor](https://www.home-assistant.io/integrations/sensor/) | diff --git a/futurehome/config.yaml b/futurehome/config.yaml index 3e185b7..f17913a 100644 --- a/futurehome/config.yaml +++ b/futurehome/config.yaml @@ -1,6 +1,6 @@ # https://developers.home-assistant.io/docs/add-ons/configuration#add-on-config name: Futurehome -version: '1.6.2' +version: '1.7.0' slug: futurehome description: Local Futurehome Smarthub integration url: 'https://github.com/adrianjagielak/home-assistant-futurehome' diff --git a/futurehome/src/fimp/vinculum.ts b/futurehome/src/fimp/vinculum.ts index 63dc232..3f1567e 100644 --- a/futurehome/src/fimp/vinculum.ts +++ b/futurehome/src/fimp/vinculum.ts @@ -1,7 +1,7 @@ import { FimpResponse, sendFimpMsg } from './fimp'; export async function pollVinculum( - component: 'device' | 'house' | 'state', + component: 'device' | 'house' | 'state' | 'mode', ): Promise { return await sendFimpMsg({ address: '/rt:app/rn:vinculum/ad:1', @@ -12,3 +12,20 @@ export async function pollVinculum( timeoutMs: 30000, }); } + +/** + * Changes the current Futurehome house mode (e.g. "home", "away", "sleep", + * "vacation") through the Vinculum service. + * + * Mirrors the reference `primefimp` `ChangeMode` implementation, which issues a + * `cmd.pd7.request` with `{ cmd: "set", component: "mode", id: }`. + */ +export async function setVinculumMode(mode: string): Promise { + return await sendFimpMsg({ + address: '/rt:app/rn:vinculum/ad:1', + service: 'vinculum', + cmd: 'cmd.pd7.request', + val: { cmd: 'set', component: 'mode', id: mode }, + val_t: 'object', + }); +} diff --git a/futurehome/src/ha/admin.ts b/futurehome/src/ha/admin.ts index eacae46..9f43760 100644 --- a/futurehome/src/ha/admin.ts +++ b/futurehome/src/ha/admin.ts @@ -1,5 +1,6 @@ import { CommandHandlers } from './publish_device'; import { HaDeviceConfig } from './ha_device_config'; +import { HaMqttComponent } from './mqtt_components/_component'; import { ha } from './globals'; import { log } from '../logger'; import { abbreviateHaMqttKeys } from './abbreviate_ha_mqtt_keys'; @@ -9,6 +10,7 @@ import { loginToThingsplex, } from '../thingsplex/thingsplex'; import { pollVinculum } from '../fimp/vinculum'; +import { hubModeCommandHandlers, hubModeSelectComponent } from './hub_mode'; const inclusionExclusionNotRunningValues = [ 'Ready', @@ -41,38 +43,39 @@ export function exposeSmarthubTools(parameters: { hubIp: string; thingsplexUsername: string; thingsplexPassword: string; + // Available Futurehome house modes (e.g. ["home", "away", "sleep", "vacation"]). + modeIds: string[]; + // Inclusion/exclusion tooling requires Thingsplex credentials, so it is only + // exposed when those are available. The house `Mode` select is always exposed. + includeInclusionExclusionTools: boolean; }): { commandHandlers: CommandHandlers; } { // e.g. "homeassistant/device/futurehome_123456_hub" const topicPrefix = `homeassistant/device/futurehome_${parameters.hubId}_hub`; - if (!initializedState) { - ha?.publish(`${topicPrefix}/inclusion_exclusion_status/state`, 'Ready', { - retain: true, - qos: 2, - }); - initializedState = true; - } - const configTopic = `${topicPrefix}/config`; const deviceId = `futurehome_${parameters.hubId}_hub`; - const config: HaDeviceConfig = { - device: { - identifiers: deviceId, - name: 'Futurehome Smarthub', - manufacturer: 'Futurehome', - model: 'Smarthub', - serial_number: parameters.hubId, - }, - origin: { - name: 'futurehome', - support_url: - 'https://github.com/adrianjagielak/home-assistant-futurehome', - }, - components: { + const components: { [key: string]: HaMqttComponent } = { + [`${deviceId}_mode`]: hubModeSelectComponent({ + hubId: parameters.hubId, + deviceId, + modeIds: parameters.modeIds, + }), + }; + + if (parameters.includeInclusionExclusionTools) { + if (!initializedState) { + ha?.publish(`${topicPrefix}/inclusion_exclusion_status/state`, 'Ready', { + retain: true, + qos: 2, + }); + initializedState = true; + } + + Object.assign(components, { [`${deviceId}_inclusion_exclusion_status`]: { unique_id: `${deviceId}_inclusion_exclusion_status`, platform: 'sensor', @@ -124,7 +127,23 @@ export function exposeSmarthubTools(parameters: { availability_topic: `${topicPrefix}/inclusion_exclusion_status/state`, availability_template: `{% if ${[...inclusionExclusionNotRunningValues, ...inclusionExclusionStartingStoppingValues].map((v) => `value == "${v}"`).join(' or ')} %}offline{% else %}online{% endif %}`, } as any, + }); + } + + const config: HaDeviceConfig = { + device: { + identifiers: deviceId, + name: 'Futurehome Smarthub', + manufacturer: 'Futurehome', + model: 'Smarthub', + serial_number: parameters.hubId, }, + origin: { + name: 'futurehome', + support_url: + 'https://github.com/adrianjagielak/home-assistant-futurehome', + }, + components, qos: 2, }; @@ -135,6 +154,11 @@ export function exposeSmarthubTools(parameters: { }); const handlers: CommandHandlers = { + ...hubModeCommandHandlers({ + hubId: parameters.hubId, + demoMode: parameters.demoMode, + modeIds: parameters.modeIds, + }), [`${topicPrefix}/start_inclusion/command`]: async (payload) => { if (parameters.demoMode) { ha?.publish( diff --git a/futurehome/src/ha/hub_mode.ts b/futurehome/src/ha/hub_mode.ts new file mode 100644 index 0000000..92a2955 --- /dev/null +++ b/futurehome/src/ha/hub_mode.ts @@ -0,0 +1,176 @@ +// Maps the Futurehome "house mode" (Home / Away / Sleep / Vacation) to a Home +// Assistant `select` entity on the Smarthub device. +// ───────────────────────────────────────────────────────────── +// The house mode is a hub-level (Vinculum) concept, not a device service. +// It is what the physical Futurehome *Modeswitch* changes: tapping a button +// switches the whole household between its modes. Because it lives on the hub +// and not on a Z-Wave/Zigbee device, it never shows up as a device +// `scene_ctrl` event – which is why a Modeswitch previously appeared in Home +// Assistant only as a `Scene` sensor stuck on `unknown`. +// +// FIMP ➞ HA +// • current mode – Vinculum `house` component → `param.house.mode` +// • mode changed – `evt.pd7.notify` (component "mode") → `param.current` +// • mode list – Vinculum `mode` component → `param.mode[].id` +// +// HA ➞ FIMP +// /mode/command → cmd.pd7.request { cmd:"set", component:"mode", id } +// ───────────────────────────────────────────────────────────── + +import { FimpResponse } from '../fimp/fimp'; +import { setVinculumMode } from '../fimp/vinculum'; +import { log } from '../logger'; +import { ha } from './globals'; +import { SelectComponent } from './mqtt_components/select'; +import { CommandHandlers } from './publish_device'; + +/** + * Default Futurehome house modes, used when the hub does not report its mode + * list (e.g. the `mode` component request fails or comes back empty). + */ +export const FALLBACK_MODE_IDS = ['home', 'away', 'sleep', 'vacation']; + +function hubTopicPrefix(hubId: string): string { + // e.g. "homeassistant/device/futurehome_123456_hub" + return `homeassistant/device/futurehome_${hubId}_hub`; +} + +export function hubModeStateTopic(hubId: string): string { + return `${hubTopicPrefix(hubId)}/mode/state`; +} + +export function hubModeCommandTopic(hubId: string): string { + return `${hubTopicPrefix(hubId)}/mode/command`; +} + +/** + * Extracts the available house mode ids from a Vinculum `mode` component + * response (`param.mode` → `[{ id, action }, …]`). + */ +export function extractModeIds(response: FimpResponse | undefined): string[] { + const modes = response?.val?.param?.mode; + if (!Array.isArray(modes)) { + return []; + } + return modes + .map((m: { id?: unknown }) => m?.id) + .filter((id): id is string => typeof id === 'string' && !!id); +} + +/** + * Builds the "Mode" select entity for the Smarthub device, letting the user + * see and change the current Futurehome house mode from Home Assistant. + */ +export function hubModeSelectComponent(parameters: { + hubId: string; + deviceId: string; + modeIds: string[]; +}): SelectComponent { + return { + unique_id: `${parameters.deviceId}_mode`, + platform: 'select', + name: 'Mode', + icon: 'mdi:home-switch', + options: parameters.modeIds, + state_topic: hubModeStateTopic(parameters.hubId), + command_topic: hubModeCommandTopic(parameters.hubId), + optimistic: false, + }; +} + +/** + * Command handler that sets the Futurehome house mode when the user changes the + * "Mode" select in Home Assistant. + */ +export function hubModeCommandHandlers(parameters: { + hubId: string; + demoMode: boolean; + modeIds: string[]; +}): CommandHandlers { + return { + [hubModeCommandTopic(parameters.hubId)]: async (payload: string) => { + if (!parameters.modeIds.includes(payload)) { + return; // ignore unknown modes + } + + // Optimistically reflect the change in Home Assistant right away; the + // hub's `evt.pd7.notify` and the periodic house poll will confirm it. + publishHubModeState({ hubId: parameters.hubId, mode: payload }); + + if (parameters.demoMode) { + return; + } + + try { + await setVinculumMode(payload); + } catch (e) { + log.error('Failed to set Futurehome mode', e); + } + }, + }; +} + +/** Publishes the current house mode to Home Assistant (retained). */ +export function publishHubModeState(parameters: { + hubId: string; + mode: string; +}): void { + ha?.publish(hubModeStateTopic(parameters.hubId), parameters.mode, { + retain: true, + qos: 2, + }); +} + +/** + * Reads the current house mode out of a Vinculum `house` component response + * (`param.house.mode`) and, when present, publishes it to Home Assistant. + */ +export function publishHubModeFromHouseResponse(parameters: { + hubId: string; + houseResponse: FimpResponse | undefined; +}): void { + const mode = parameters.houseResponse?.val?.param?.house?.mode; + if (typeof mode === 'string' && mode) { + publishHubModeState({ hubId: parameters.hubId, mode }); + } +} + +/** + * Handles a Vinculum `evt.pd7.notify` message and, when it reports a house mode + * change, updates the Home Assistant "Mode" entity. This is what reflects a + * Futurehome Modeswitch button press (or an app / automation mode change) in + * Home Assistant in real time. + * + * A mode change is broadcast on the hub component (`component: "hub"`, + * `id: "mode"`). The new value has been observed both nested as + * `param.mode.current` (see `primefimp` `Hub`/`HubMode`) and flat as + * `param.current` (see the `edge-iqcontrols` / `tpflow` references), so both + * are handled; the defensive `component: "mode"` case is handled too. + */ +export function handleModeNotify(parameters: { + hubId: string; + msg: FimpResponse; +}): void { + const notify = parameters.msg.val; + if (!notify) { + return; + } + if (notify.component !== 'hub' && notify.component !== 'mode') { + return; + } + + const param = notify.param ?? {}; + const current = + (typeof param.mode === 'object' && + param.mode && + typeof param.mode.current === 'string' && + param.mode.current) || + (typeof param.current === 'string' && param.current) || + (typeof param.mode === 'string' && param.mode) || + undefined; + + if (current) { + log.debug(`Futurehome house mode changed to "${current}"`); + publishHubModeState({ hubId: parameters.hubId, mode: current }); + } +} diff --git a/futurehome/src/ha/scene_events.ts b/futurehome/src/ha/scene_events.ts new file mode 100644 index 0000000..5f5e7e3 --- /dev/null +++ b/futurehome/src/ha/scene_events.ts @@ -0,0 +1,97 @@ +// Publishes Futurehome `scene_ctrl` reports as Home Assistant `event` entities. +// ───────────────────────────────────────────────────────────── +// A scene controller (button, remote, Fibaro/Namron scene switch, …) is a +// momentary, stateless device: it fires a scene when pressed and has no +// meaningful "current" value. Representing it as a plain sensor means the +// entity is stuck on `unknown` until the first press and then latches on the +// last value forever – so repeated presses of the same button never change the +// state and cannot drive automations. +// +// Home Assistant's `event` platform exists exactly for this: every received +// message fires the entity's event trigger, even for repeated identical +// presses. We publish each `evt.scene.report` to a dedicated, non-retained +// topic as `{ "event_type": }`. +// ───────────────────────────────────────────────────────────── + +import { ha } from './globals'; + +/** + * A best-effort list of scene values commonly reported by Z-Wave Central Scene + * and Zigbee scene controllers. Home Assistant discards events whose + * `event_type` is not in the entity's `event_types`, so this is merged with the + * device-reported `sup_scenes` to give a working entity out of the box. + */ +export const DEFAULT_SCENE_EVENT_TYPES: string[] = (() => { + const types = ['on', 'off', 'toggle']; + const actions = [ + 'key_pressed_1_time', + 'key_released', + 'key_held_down', + 'key_pressed_2_times', + 'key_pressed_3_times', + 'key_pressed_4_times', + 'key_pressed_5_times', + ]; + for (let button = 1; button <= 8; button++) { + types.push(`${button}`, `${button}.0`); + for (const action of actions) { + types.push(`${button}.${action}`); + } + } + return types; +})(); + +export function sceneEventStateTopic(topicPrefix: string, addr: string): string { + return `${topicPrefix}${addr}/scene/event`; +} + +/** Merges the device-reported supported scenes with the default list. */ +export function buildSceneEventTypes( + supScenes: string[] | null | undefined, +): string[] { + const types = new Set(DEFAULT_SCENE_EVENT_TYPES); + for (const scene of supScenes ?? []) { + if (typeof scene === 'string' && scene) { + types.add(scene); + } + } + return [...types]; +} + +// Maps a scene service address to its dedicated Home Assistant event topic. +const sceneEventTopics: Record = {}; + +export function registerSceneEventTopic(addr: string, topic: string): void { + sceneEventTopics[addr] = topic; +} + +/** + * Publishes a scene report as a Home Assistant event. Called for every + * `evt.scene.report`; the `addr` is the service address (the FIMP event topic + * without its `pt:j1/mt:evt` envelope). + */ +export function publishSceneEvent(parameters: { + addr: string; + value: unknown; +}): void { + const topic = sceneEventTopics[parameters.addr]; + if (!topic) { + return; + } + + if ( + typeof parameters.value !== 'string' && + typeof parameters.value !== 'number' + ) { + return; + } + const eventType = String(parameters.value); + if (!eventType) { + return; + } + + ha?.publish(topic, JSON.stringify({ event_type: eventType }), { + retain: false, + qos: 2, + }); +} diff --git a/futurehome/src/index.ts b/futurehome/src/index.ts index dd9eaf7..300986f 100644 --- a/futurehome/src/index.ts +++ b/futurehome/src/index.ts @@ -15,6 +15,13 @@ import { handleInclusionStatusReport, } from './ha/admin'; import { pollVinculum } from './fimp/vinculum'; +import { + FALLBACK_MODE_IDS, + extractModeIds, + handleModeNotify, + publishHubModeFromHouseResponse, +} from './ha/hub_mode'; +import { publishSceneEvent } from './ha/scene_events'; (async () => { const hubIp = process.env.FH_HUB_IP || 'futurehome-smarthub.local'; @@ -86,6 +93,19 @@ import { pollVinculum } from './fimp/vinculum'; const house = await pollVinculum('house'); const hubId = house.val.param.house.hubId; + // Discover the available house modes (e.g. Home/Away/Sleep/Vacation) and + // publish the current one, so the hub's "Mode" select entity can be exposed + // and kept in sync. This is what the physical Futurehome Modeswitch controls. + const modesResponse = await pollVinculum('mode').catch((e) => { + log.warn('Failed to request house modes', e); + return undefined; + }); + const discoveredModeIds = extractModeIds(modesResponse); + const modeIds = discoveredModeIds.length + ? discoveredModeIds + : FALLBACK_MODE_IDS; + publishHubModeFromHouseResponse({ hubId, houseResponse: house }); + const devices = await pollVinculum('device'); log.debug(`FIMP devices:\n${JSON.stringify(devices, null, 0)}`); @@ -196,22 +216,26 @@ import { pollVinculum } from './fimp/vinculum'; log.error('Failed publishing device', device, e); } } - if ( + // Always publish the Smarthub device (so the house "Mode" select is + // available); the inclusion/exclusion tooling additionally requires + // Thingsplex credentials. + const includeInclusionExclusionTools = !!( demoMode || thingsplexAllowEmpty || (thingsplexUsername && thingsplexPassword) - ) { - Object.assign( - commandHandlers, - exposeSmarthubTools({ - hubId, - demoMode, - hubIp, - thingsplexUsername, - thingsplexPassword, - }).commandHandlers, - ); - } + ); + Object.assign( + commandHandlers, + exposeSmarthubTools({ + hubId, + demoMode, + hubIp, + thingsplexUsername, + thingsplexPassword, + modeIds, + includeInclusionExclusionTools, + }).commandHandlers, + ); setHaCommandHandlers(commandHandlers); }; vinculumDevicesToHa(devices); @@ -317,14 +341,32 @@ import { pollVinculum } from './fimp/vinculum'; break; } + case 'evt.pd7.notify': { + // House mode changes (e.g. from a Futurehome Modeswitch) are + // broadcast as Vinculum notifications. + handleModeNotify({ hubId, msg }); + break; + } + default: { // Handle any event that matches the pattern: evt..report if (/^evt\..+\.report$/.test(msg.type ?? '')) { + const attrName = msg.type!.split('.')[1]; haUpdateStateValueReport({ topic, value: msg.val, - attrName: msg.type!.split('.')[1], + attrName, }); + + // Momentary scene controllers (buttons, remotes, the Futurehome + // Modeswitch, …) are also surfaced as Home Assistant `event` + // entities so every press can drive automations. + if (attrName === 'scene') { + publishSceneEvent({ + addr: topic.replace(/^pt:j1\/mt:evt/, ''), + value: msg.val, + }); + } } } } @@ -345,6 +387,19 @@ import { pollVinculum } from './fimp/vinculum'; setInterval(pollState, 30 * 1000); } + const pollHubMode = () => { + pollVinculum('house') + .then((houseResponse) => + publishHubModeFromHouseResponse({ hubId, houseResponse }), + ) + .catch((e) => log.warn('Failed to refresh house mode', e)); + }; + // Keep the house "Mode" entity in sync even if a change notification is + // missed (the Modeswitch may not emit a device-level event we can observe). + if (!demoMode) { + setInterval(pollHubMode, 30 * 1000); + } + const pollDevices = () => { log.debug('Refreshing Vinculum devices after 30 minutes...'); diff --git a/futurehome/src/mqtt/demo_client.ts b/futurehome/src/mqtt/demo_client.ts index d52b813..52e9d8a 100644 --- a/futurehome/src/mqtt/demo_client.ts +++ b/futurehome/src/mqtt/demo_client.ts @@ -61,7 +61,25 @@ export class DemoFimpMqttClient implements IMqttClient { ) { sendResponse({ type: 'evt.pd7.response', - val: { param: { house: { hubId: '000000004c38b232' } } }, + val: { param: { house: { hubId: '000000004c38b232', mode: 'home' } } }, + }); + } else if ( + msg.serv == 'vinculum' && + msg.type == 'cmd.pd7.request' && + msg.val?.param?.components?.includes('mode') + ) { + sendResponse({ + type: 'evt.pd7.response', + val: { + param: { + mode: [ + { id: 'home' }, + { id: 'away' }, + { id: 'sleep' }, + { id: 'vacation' }, + ], + }, + }, }); } else if ( msg.serv == 'vinculum' && diff --git a/futurehome/src/services/scene_ctrl.ts b/futurehome/src/services/scene_ctrl.ts index 2a2636e..6af6497 100644 --- a/futurehome/src/services/scene_ctrl.ts +++ b/futurehome/src/services/scene_ctrl.ts @@ -1,8 +1,8 @@ // Maps a Futurehome “scene_ctrl” service to MQTT entities // ───────────────────────────────────────────────────────────── -// FIMP ➞ HA state paths used by the value templates +// FIMP ➞ HA +// evt.scene.report → /scene/event (Home Assistant event) // value_json[svc.addr].scene – last reported scene name (string) -// value_json[svc.addr].lvl – last reported level (int) // // HA ➞ FIMP commands // /scene/command → cmd.scene.set @@ -14,6 +14,11 @@ import { VinculumPd7Service, } from '../fimp/vinculum_pd7_device'; import { HaMqttComponent } from '../ha/mqtt_components/_component'; +import { + buildSceneEventTypes, + registerSceneEventTopic, + sceneEventStateTopic, +} from '../ha/scene_events'; import { CommandHandlers, ServiceComponentsCreationResult, @@ -31,19 +36,37 @@ export function scene_ctrl__components( const components: Record = {}; const commandHandlers: CommandHandlers = {}; - // ───────────── read-only entities ───────────── + const supScenes: string[] = svc.props?.sup_scenes ?? []; + if (svc.intf?.includes('evt.scene.report')) { + // Scene controllers are momentary/stateless, so a plain sensor is stuck on + // `unknown` and can't drive automations on repeated presses. The `event` + // platform fires the entity's trigger on every reported scene instead, so + // it's the entity to use in automations. The sensor is kept for backwards + // compatibility (it shows the last reported scene). + const eventStateTopic = sceneEventStateTopic(topicPrefix, svc.addr); + registerSceneEventTopic(svc.addr, eventStateTopic); + + components[`${svc.addr}_scene_event`] = { + unique_id: `${svc.addr}_scene_event`, + platform: 'event', + name: 'Scene', + icon: 'mdi:gesture-tap-button', + state_topic: eventStateTopic, + event_types: buildSceneEventTypes(supScenes), + }; + components[`${svc.addr}_scene`] = { unique_id: `${svc.addr}_scene`, platform: 'sensor', - name: 'Scene', + entity_category: 'diagnostic', + name: 'Scene (last value)', unit_of_measurement: '', value_template: `{{ value_json['${svc.addr}'].scene }}`, }; } // ───────────── writeable “select” (scene activator) ───────────── - const supScenes: string[] = svc.props?.sup_scenes ?? []; if (svc.intf?.includes('cmd.scene.set') && supScenes.length) { const commandTopic = `${topicPrefix}${svc.addr}/scene/command`;