mirror of
https://github.com/adrianjagielak/home-assistant-futurehome.git
synced 2026-07-07 13:23:03 +00:00
Add Futurehome house mode support (Modeswitch) + scene event entities (#33)
* Add Futurehome house mode support (Modeswitch) + scene event entities
The Futurehome Modeswitch (Modusbryter) is a house-mode switch: its buttons
change the hub's mode (Home/Away/Sleep/Vacation), which is a hub-level Vinculum
concept broadcast via `evt.pd7.notify` (component "mode") — not a device
`scene_ctrl` event. That is why tapping it produced nothing in the device
message stream and the mapped scene sensor was stuck on `unknown`.
The add-on had no house-mode support at all. This adds it:
- Expose the current house mode as a `Mode` select on the Smarthub device.
It reflects the current mode, updates in real time via `evt.pd7.notify`
(with a 30s house-poll fallback), and can change the mode from Home Assistant
via `cmd.pd7.request {cmd:"set", component:"mode", id:<mode>}` (matching the
reference primefimp `ChangeMode`).
- Always publish the Smarthub hub device so the Mode select is available even
without Thingsplex credentials; inclusion/exclusion tooling stays gated.
- Discover available modes from the Vinculum `mode` component, falling back to
home/away/sleep/vacation.
Also improve genuine scene controllers: `scene_ctrl` now additionally exposes a
Home Assistant `event` entity so momentary button presses can drive automations
(the previous `Scene` sensor latched and could not react to repeated presses).
The sensor is kept for backwards compatibility (renamed "Scene (last value)").
Includes demo-mode support, README/CHANGELOG updates, and a version bump to
1.7.0.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FuQAyPde4K57P2XMCKbFek
* Fix real-time house-mode updates: hub-mode notify uses component "hub"
A house-mode-change evt.pd7.notify is broadcast on the hub component
(component "hub", id "mode"), not component "mode". The previous guard
`notify.component !== 'mode'` discarded every real mode-change notification, so
the Mode select only updated via the 30s poll fallback instead of in real time.
Gate on component "hub" (and, defensively, "mode") and extract the new mode
from either param.mode.current (primefimp Hub/HubMode) or param.current
(edge-iqcontrols/tpflow references), publishing only when a mode string is
found.
This commit is contained in:
@@ -1,5 +1,10 @@
|
||||
<!-- https://developers.home-assistant.io/docs/add-ons/presentation#keeping-a-changelog -->
|
||||
|
||||
## 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).
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -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'
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { FimpResponse, sendFimpMsg } from './fimp';
|
||||
|
||||
export async function pollVinculum(
|
||||
component: 'device' | 'house' | 'state',
|
||||
component: 'device' | 'house' | 'state' | 'mode',
|
||||
): Promise<FimpResponse> {
|
||||
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: <mode> }`.
|
||||
*/
|
||||
export async function setVinculumMode(mode: string): Promise<FimpResponse> {
|
||||
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',
|
||||
});
|
||||
}
|
||||
|
||||
@@ -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,12 +43,30 @@ 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`;
|
||||
|
||||
const configTopic = `${topicPrefix}/config`;
|
||||
|
||||
const deviceId = `futurehome_${parameters.hubId}_hub`;
|
||||
|
||||
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,
|
||||
@@ -55,24 +75,7 @@ export function exposeSmarthubTools(parameters: {
|
||||
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: {
|
||||
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(
|
||||
|
||||
176
futurehome/src/ha/hub_mode.ts
Normal file
176
futurehome/src/ha/hub_mode.ts
Normal file
@@ -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
|
||||
// <hubPrefix>/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 });
|
||||
}
|
||||
}
|
||||
97
futurehome/src/ha/scene_events.ts
Normal file
97
futurehome/src/ha/scene_events.ts
Normal file
@@ -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": <scene> }`.
|
||||
// ─────────────────────────────────────────────────────────────
|
||||
|
||||
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<string>(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<string, string> = {};
|
||||
|
||||
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,
|
||||
});
|
||||
}
|
||||
@@ -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,11 +216,14 @@ 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({
|
||||
@@ -209,9 +232,10 @@ import { pollVinculum } from './fimp/vinculum';
|
||||
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.<something>.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...');
|
||||
|
||||
|
||||
@@ -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' &&
|
||||
|
||||
@@ -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 → <topicPrefix><addr>/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
|
||||
// <topicPrefix><addr>/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<string, HaMqttComponent> = {};
|
||||
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`;
|
||||
|
||||
|
||||
Reference in New Issue
Block a user