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
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
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(
|
||||
|
||||
162
futurehome/src/ha/hub_mode.ts
Normal file
162
futurehome/src/ha/hub_mode.ts
Normal file
@@ -0,0 +1,162 @@
|
||||
// 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 (`component: "mode"`, `param: { current, prev }`), 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.
|
||||
*/
|
||||
export function handleModeNotify(parameters: {
|
||||
hubId: string;
|
||||
msg: FimpResponse;
|
||||
}): void {
|
||||
const notify = parameters.msg.val;
|
||||
if (!notify || notify.component !== 'mode') {
|
||||
return;
|
||||
}
|
||||
|
||||
const current =
|
||||
(typeof notify.param?.current === 'string' && notify.param.current) ||
|
||||
(typeof notify.id === 'string' && notify.id) ||
|
||||
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