Format the codebase using prettier

This commit is contained in:
Adrian Jagielak 2025-07-24 16:34:58 +02:00
parent 7b4309a596
commit 3c56a30d01
No known key found for this signature in database
GPG Key ID: 0818CF7AF6C62BFB
69 changed files with 984 additions and 895 deletions

View File

@ -1,6 +1,6 @@
# https://developers.home-assistant.io/docs/add-ons/configuration#add-on-config
name: Futurehome
version: "0.0.33"
version: "0.0.34"
slug: futurehome
description: Local Futurehome Smarthub integration
url: "https://github.com/adrianjagielak/home-assistant-futurehome"

View File

@ -1,53 +1,56 @@
import { v4 as uuidv4 } from "uuid";
import { IMqttClient } from "./mqtt/interface";
import { v4 as uuidv4 } from 'uuid';
import { IMqttClient } from './mqtt/interface';
export function exposeSmarthubTools(
ha: IMqttClient,
fimp: IMqttClient,
hubAddr = "pt:j1/mt:cmd/rt:app/rn:zb/ad:1"
hubAddr = 'pt:j1/mt:cmd/rt:app/rn:zb/ad:1',
) {
const base = "homeassistant/switch/fh_zb_pairing";
const base = 'homeassistant/switch/fh_zb_pairing';
const device = {
identifiers: ["futurehome_hub"],
name: "Futurehome Hub",
model: "Smarthub",
identifiers: ['futurehome_hub'],
name: 'Futurehome Hub',
model: 'Smarthub',
};
ha.publish(
`${base}/config`,
JSON.stringify({
name: "Zigbee Pairing",
uniq_id: "fh_zb_pairing",
name: 'Zigbee Pairing',
uniq_id: 'fh_zb_pairing',
cmd_t: `${base}/set`,
stat_t: `${base}/state`,
device,
}),
{ retain: true, qos: 2 }
{ retain: true, qos: 2 },
);
// // keep last known state locally
// let pairingOn = false;
ha.subscribe(`${base}/set`);
ha.on("message", (topic, payload) => {
ha.on('message', (topic, payload) => {
if (topic !== `${base}/set`) return;
const turnOn = payload.toString() === "ON";
const turnOn = payload.toString() === 'ON';
// // optimistic update so the UI flips instantly
// pairingOn = turnOn;
ha.publish(`${base}/state`, turnOn ? "ON" : "OFF", { retain: true, qos: 2 });
ha.publish(`${base}/state`, turnOn ? 'ON' : 'OFF', {
retain: true,
qos: 2,
});
// placeholder FIMP message adjust to real API if different
fimp.publish(
hubAddr,
JSON.stringify({
type: "cmd.pairing_mode.set",
service: "zigbee",
type: 'cmd.pairing_mode.set',
service: 'zigbee',
uid: uuidv4(),
val_t: "str",
val: turnOn ? "start" : "stop",
val_t: 'str',
val: turnOn ? 'start' : 'stop',
}),
{ qos: 1 }
{ qos: 1 },
);
});

View File

@ -1,26 +1,48 @@
import { DemoFimpMqttClient } from "./mqtt/demo_client";
import { IMqttClient } from "./mqtt/interface";
import { RealMqttClient } from "./mqtt/real_client";
import { DemoFimpMqttClient } from './mqtt/demo_client';
import { IMqttClient } from './mqtt/interface';
import { RealMqttClient } from './mqtt/real_client';
export function connectHub(opts: { hubIp: string; username: string; password: string; demo: boolean; }): Promise<IMqttClient> {
export function connectHub(opts: {
hubIp: string;
username: string;
password: string;
demo: boolean;
}): Promise<IMqttClient> {
const url = `mqtt://${opts.hubIp}`;
return makeClient(url, 1884, opts.username, opts.password, opts.demo);
}
export async function connectHA(opts: { mqttHost: string; mqttPort: number; mqttUsername: string; mqttPassword: string; }): Promise<{ ha: IMqttClient; retainedMessages: RetainedMessage[] }> {
export async function connectHA(opts: {
mqttHost: string;
mqttPort: number;
mqttUsername: string;
mqttPassword: string;
}): Promise<{ ha: IMqttClient; retainedMessages: RetainedMessage[] }> {
const url = `mqtt://${opts.mqttHost}`;
const ha = await makeClient(url, opts.mqttPort, opts.mqttUsername, opts.mqttPassword, false);
const retainedMessages = await waitForHARetainedMessages(ha)
const ha = await makeClient(
url,
opts.mqttPort,
opts.mqttUsername,
opts.mqttPassword,
false,
);
const retainedMessages = await waitForHARetainedMessages(ha);
return { ha, retainedMessages };
}
function makeClient(url: string, port: number, username: string, password: string, demo: boolean): Promise<IMqttClient> {
function makeClient(
url: string,
port: number,
username: string,
password: string,
demo: boolean,
): Promise<IMqttClient> {
return new Promise((resolve, reject) => {
const client = demo ? new DemoFimpMqttClient() : new RealMqttClient();
client.connect(url, { port, username, password, protocolVersion: 4 });
client.once("connect", () => resolve(client));
client.once("error", reject);
client.once('connect', () => resolve(client));
client.once('error', reject);
});
}
@ -28,14 +50,18 @@ export type RetainedMessage = { topic: string; message: string };
async function waitForHARetainedMessages(
client: IMqttClient,
timeoutMs = 3000
timeoutMs = 3000,
): Promise<RetainedMessage[]> {
const topicPattern = /^homeassistant\/device\/futurehome.*$/;
return new Promise((resolve, reject) => {
const retainedMessages: RetainedMessage[] = [];
const messageHandler = (topic: string, message: Buffer, packet: { retain?: boolean }) => {
const messageHandler = (
topic: string,
message: Buffer,
packet: { retain?: boolean },
) => {
if (packet.retain && topicPattern.test(topic)) {
retainedMessages.push({ topic, message: message.toString() });
}

View File

@ -1,6 +1,6 @@
import { v4 as uuidv4 } from "uuid";
import { log } from "../logger";
import { IMqttClient } from "../mqtt/interface";
import { v4 as uuidv4 } from 'uuid';
import { log } from '../logger';
import { IMqttClient } from '../mqtt/interface';
let fimp: IMqttClient | undefined = undefined;
@ -21,7 +21,21 @@ export type FimpResponse = {
ver?: any;
};
type FimpValueType = 'string' | 'int' | 'float' | 'bool' | 'null' | 'str_array' | 'int_array' | 'float_array' | 'str_map' | 'int_map' | 'float_map' | 'bool_map' | 'object' | 'bin';
type FimpValueType =
| 'string'
| 'int'
| 'float'
| 'bool'
| 'null'
| 'str_array'
| 'int_array'
| 'float_array'
| 'str_map'
| 'int_map'
| 'float_map'
| 'bool_map'
| 'object'
| 'bin';
export async function sendFimpMsg({
address,
@ -40,27 +54,27 @@ export async function sendFimpMsg({
}): Promise<FimpResponse> {
const uid = uuidv4();
const topic = `pt:j1/mt:cmd${address}`;
const message = JSON.stringify(
{
corid: null,
ctime: new Date().toISOString(),
props: {},
resp_to: 'pt:j1/mt:rsp/rt:app/rn:ha-futurehome/ad:addon',
serv: service,
src: 'ha-futurehome',
tags: [],
'type': cmd,
uid: uid,
val: val,
val_t: val_t,
ver: '1',
},
);
const message = JSON.stringify({
corid: null,
ctime: new Date().toISOString(),
props: {},
resp_to: 'pt:j1/mt:rsp/rt:app/rn:ha-futurehome/ad:addon',
serv: service,
src: 'ha-futurehome',
tags: [],
type: cmd,
uid: uid,
val: val,
val_t: val_t,
ver: '1',
});
return new Promise((resolve, reject) => {
const timeout = setTimeout(() => {
fimp?.removeListener('message', onResponse);
const error = new Error(`Timeout waiting for FIMP response (uid: ${uid}, service: ${service}, cmd: ${cmd})`);
const error = new Error(
`Timeout waiting for FIMP response (uid: ${uid}, service: ${service}, cmd: ${cmd})`,
);
log.warn(error.message, error.stack);
reject(error);
}, timeoutMs);
@ -72,13 +86,17 @@ export async function sendFimpMsg({
if (msg.type === 'evt.error.report') {
fimp?.removeListener('message', onResponse);
const error = new Error(`Received FIMP response for message ${uid}: error (evt.error.report) (matched using uid)`);
const error = new Error(
`Received FIMP response for message ${uid}: error (evt.error.report) (matched using uid)`,
);
log.warn(error.message, error.stack);
reject(error);
return;
}
log.debug(`Received FIMP response for message ${uid} (matched using uid).`);
log.debug(
`Received FIMP response for message ${uid} (matched using uid).`,
);
clearTimeout(timeout);
fimp?.removeListener('message', onResponse);
@ -90,13 +108,17 @@ export async function sendFimpMsg({
if (msg.type === 'evt.error.report') {
fimp?.removeListener('message', onResponse);
const error = new Error(`Received FIMP response for message ${uid}: error (evt.error.report) (matched using topic)`);
const error = new Error(
`Received FIMP response for message ${uid}: error (evt.error.report) (matched using topic)`,
);
log.warn(error.message, error.stack);
reject(error);
return;
}
log.debug(`Received FIMP response for message ${uid} (matched using topic).`);
log.debug(
`Received FIMP response for message ${uid} (matched using topic).`,
);
clearTimeout(timeout);
fimp?.removeListener('message', onResponse);
@ -107,13 +129,23 @@ export async function sendFimpMsg({
const hasValidType = msg.type != null && msg.type.startsWith('evt.');
const reqCmdParts = cmd.split('.');
const resCmdParts = msg.type?.split('.') ?? [];
const hasThreeParts = resCmdParts.length === 3 && reqCmdParts.length === 3;
const hasThreeParts =
resCmdParts.length === 3 && reqCmdParts.length === 3;
const middlePartMatches = resCmdParts[1] === reqCmdParts[1];
const endsWithLastPart = cmd.endsWith(resCmdParts.at(-1)!);
const reqEndsWithSetAndResEndsWithReport = reqCmdParts[2] === 'set' && resCmdParts[2] === 'report'
const reqEndsWithSetAndResEndsWithReport =
reqCmdParts[2] === 'set' && resCmdParts[2] === 'report';
const sameService = msg.serv === service;
if (hasValidType && hasThreeParts && middlePartMatches && (endsWithLastPart || reqEndsWithSetAndResEndsWithReport) && sameService) {
log.debug(`Received FIMP response for message ${uid} (matched using event name).`);
if (
hasValidType &&
hasThreeParts &&
middlePartMatches &&
(endsWithLastPart || reqEndsWithSetAndResEndsWithReport) &&
sameService
) {
log.debug(
`Received FIMP response for message ${uid} (matched using event name).`,
);
clearTimeout(timeout);
fimp?.removeListener('message', onResponse);

View File

@ -1,10 +1,12 @@
/// Returns the adapter address of the device associated with the given service address.
/// The service address may belong to any service on the device.
export function adapterAddressFromServiceAddress(serviceAddress: string): string {
export function adapterAddressFromServiceAddress(
serviceAddress: string,
): string {
const parts = serviceAddress.split('/');
if (parts.length < 4) {
throw new Error("Invalid address format");
throw new Error('Invalid address format');
}
const adapterPart = parts[2]; // e.g., "rn:zigbee"
@ -16,11 +18,13 @@ export function adapterAddressFromServiceAddress(serviceAddress: string): string
/// Returns the adapter service name of the device associated with the given service address.
/// The service address may belong to any service on the device.
export function adapterServiceFromServiceAddress(serviceAddress: string): string {
export function adapterServiceFromServiceAddress(
serviceAddress: string,
): string {
const parts = serviceAddress.split('/');
if (parts.length < 3) {
throw new Error("Invalid address format");
throw new Error('Invalid address format');
}
const adapterPart = parts[2]; // e.g., "rn:zigbee"
@ -31,5 +35,4 @@ export function adapterServiceFromServiceAddress(serviceAddress: string): string
}
return adapterName;
}

View File

@ -1,27 +1,31 @@
import { log } from "../logger";
import { sendFimpMsg } from "./fimp";
import { log } from '../logger';
import { sendFimpMsg } from './fimp';
export type InclusionReport = {
address?: string | null; // Household device ID
product_name?: string | null; // e.g. "SWITCH-ZR03-1"
product_hash?: string | null; // e.g. "zb - eWeLink - SWITCH-ZR03-1"
product_id?: string | null; // e.g. "SWITCH-ZR03-1"
manufacturer_id?: string | null; // e.g. "eWeLink"
device_id?: string | null; // e.g. "b4:0e:cf:d1:bc:2a:00:00"
hw_ver?: string | null; // e.g. "0"
sw_ver?: string | null; // e.g. "0x0"
comm_tech?: string | null; // e.g. "zigbee"
power_source?: string | null; // e.g. "battery"
product_hash?: string | null; // e.g. "zb - eWeLink - SWITCH-ZR03-1"
product_id?: string | null; // e.g. "SWITCH-ZR03-1"
manufacturer_id?: string | null; // e.g. "eWeLink"
device_id?: string | null; // e.g. "b4:0e:cf:d1:bc:2a:00:00"
hw_ver?: string | null; // e.g. "0"
sw_ver?: string | null; // e.g. "0x0"
comm_tech?: string | null; // e.g. "zigbee"
power_source?: string | null; // e.g. "battery"
services?: InclusionReportService[] | null;
};
export type InclusionReportService = {
name?: string | null;
address?: string | null;
props?: { [key: string]: any } | null
props?: { [key: string]: any } | null;
};
export async function getInclusionReport(parameters: { adapterAddress: string; adapterService: string; deviceId: string }): Promise<InclusionReport | undefined> {
export async function getInclusionReport(parameters: {
adapterAddress: string;
adapterService: string;
deviceId: string;
}): Promise<InclusionReport | undefined> {
try {
const inclusionReport = await sendFimpMsg({
address: parameters.adapterAddress,
@ -33,6 +37,9 @@ export async function getInclusionReport(parameters: { adapterAddress: string; a
return inclusionReport.val;
} catch (e) {
log.error(`Failed getting inclusion report for adapterAddress: ${parameters.adapterAddress}, adapterService: ${parameters.adapterService}, deviceId: ${parameters.deviceId}`, e)
log.error(
`Failed getting inclusion report for adapterAddress: ${parameters.adapterAddress}, adapterService: ${parameters.adapterService}, deviceId: ${parameters.deviceId}`,
e,
);
}
}

View File

@ -1,20 +1,34 @@
export type DeviceState = {
id?: number | null;
services?: DeviceStateService[] | null
services?: DeviceStateService[] | null;
};
export type DeviceStateService = {
addr?: string;
attributes?: Attribute[];
name?: string;
}
};
export type Attribute = {
name: string;
values: AttributeValue[];
}
};
export type AttributeValue = StringValue | IntValue | FloatValue | BoolValue | NullValue | StrArrayValue | IntArrayValue | FloatArrayValue | StrMapValue | IntMapValue | FloatMapValue | BoolMapValue | ObjectValue | BinValue;
export type AttributeValue =
| StringValue
| IntValue
| FloatValue
| BoolValue
| NullValue
| StrArrayValue
| IntArrayValue
| FloatArrayValue
| StrMapValue
| IntMapValue
| FloatMapValue
| BoolMapValue
| ObjectValue
| BinValue;
export type Timestamp = string;
@ -22,49 +36,49 @@ export type StringValue = {
ts: Timestamp;
val: string;
val_t: 'string';
}
};
export type IntValue = {
ts: Timestamp;
val: number;
val_t: 'int';
}
};
export type FloatValue = {
ts: Timestamp;
val: number;
val_t: 'float';
}
};
export type BoolValue = {
ts: Timestamp;
val: boolean;
val_t: 'bool';
}
};
export type NullValue = {
ts: Timestamp;
val?: null;
val_t: 'null';
}
};
export type StrArrayValue = {
ts: Timestamp;
val: string[];
val_t: 'str_array';
}
};
export type IntArrayValue = {
ts: Timestamp;
val: number[];
val_t: 'int_array';
}
};
export type FloatArrayValue = {
ts: Timestamp;
val: number[];
val_t: 'float_array';
}
};
export type StrMapValue = {
ts: Timestamp;
@ -72,7 +86,7 @@ export type StrMapValue = {
[key: string]: string;
};
val_t: 'str_map';
}
};
export type IntMapValue = {
ts: Timestamp;
@ -80,7 +94,7 @@ export type IntMapValue = {
[key: string]: number;
};
val_t: 'int_map';
}
};
export type FloatMapValue = {
ts: Timestamp;
@ -88,7 +102,7 @@ export type FloatMapValue = {
[key: string]: number;
};
val_t: 'float_map';
}
};
export type BoolMapValue = {
ts: Timestamp;
@ -96,7 +110,7 @@ export type BoolMapValue = {
[key: string]: boolean;
};
val_t: 'bool_map';
}
};
export type ObjectValue = {
ts: Timestamp;
@ -104,10 +118,10 @@ export type ObjectValue = {
[key: string]: any;
};
val_t: 'object';
}
};
export type BinValue = {
ts: Timestamp;
val: string;
val_t: 'bin';
}
};

View File

@ -8,7 +8,20 @@ export type VinculumPd7Device = {
model?: string | null;
// "Model alias", e.g. "TS0202"
modelAlias?: string | null;
functionality?: 'appliance' | 'climate' | 'energy' | 'ev_charger' | 'lighting' | 'media' | 'other' | 'power' | 'safety' | 'security' | 'shading' | string | null;
functionality?:
| 'appliance'
| 'climate'
| 'energy'
| 'ev_charger'
| 'lighting'
| 'media'
| 'other'
| 'power'
| 'safety'
| 'security'
| 'shading'
| string
| null;
services?: Record<string, VinculumPd7Service> | null;
type?: {
// User-defined device type (e.g. "sensor", "chargepoint", or "light")

View File

@ -1,5 +1,5 @@
import { IMqttClient } from "../mqtt/interface";
import { CommandHandlers } from "./publish_device";
import { IMqttClient } from '../mqtt/interface';
import { CommandHandlers } from './publish_device';
export let ha: IMqttClient | undefined = undefined;

View File

@ -274,7 +274,8 @@ export function haPublishDevice(parameters: {
},
origin: {
name: 'futurehome',
support_url: 'https://github.com/adrianjagielak/home-assistant-futurehome',
support_url:
'https://github.com/adrianjagielak/home-assistant-futurehome',
},
components: components,
state_topic: stateTopic,
@ -283,7 +284,10 @@ export function haPublishDevice(parameters: {
};
log.debug(`Publishing HA device "${configTopic}"`);
ha?.publish(configTopic, JSON.stringify(abbreviateHaMqttKeys(config)), { retain: true, qos: 2 });
ha?.publish(configTopic, JSON.stringify(abbreviateHaMqttKeys(config)), {
retain: true,
qos: 2,
});
return { commandHandlers: handlers };
}

View File

@ -1,5 +1,5 @@
import { log } from "../logger";
import { ha } from "./globals";
import { log } from '../logger';
import { ha } from './globals';
/**
* Example raw FIMP availaility (from evt.network.all_nodes_report) input:
@ -20,11 +20,15 @@ topic: homeassistant/device/futurehome_123456_1/availability
online
```
*/
export function haUpdateAvailability(parameters: { hubId: string, deviceAvailability: any }) {
const availabilityTopic = `homeassistant/device/futurehome_${parameters.hubId}_${parameters.deviceAvailability.address?.toString()}/availability`
export function haUpdateAvailability(parameters: {
hubId: string;
deviceAvailability: any;
}) {
const availabilityTopic = `homeassistant/device/futurehome_${parameters.hubId}_${parameters.deviceAvailability.address?.toString()}/availability`;
const availability = parameters.deviceAvailability?.status === "UP" ? "online" : "offline";
const availability =
parameters.deviceAvailability?.status === 'UP' ? 'online' : 'offline';
log.debug(`Publishing HA availability "${availabilityTopic}"`)
log.debug(`Publishing HA availability "${availabilityTopic}"`);
ha?.publish(availabilityTopic, availability, { retain: true, qos: 2 });
}

View File

@ -1,6 +1,6 @@
import { DeviceState } from "../fimp/state";
import { log } from "../logger";
import { ha } from "./globals";
import { DeviceState } from '../fimp/state';
import { log } from '../logger';
import { ha } from './globals';
/**
* Example raw FIMP state input:
@ -77,8 +77,8 @@ topic: homeassistant/device/futurehome_123456_1/state
*/
const haStateCache: Record<
string, // state topic
Record<string, Record<string, any>> // payload (addr → { attr → value })
string, // state topic
Record<string, Record<string, any>> // payload (addr → { attr → value })
> = {};
/**
@ -88,7 +88,10 @@ const haStateCache: Record<
* Example MQTT topic produced for hub 123456 and device id 1:
* homeassistant/device/futurehome_123456_1/state
*/
export function haUpdateState(parameters: { hubId: string; deviceState: DeviceState }) {
export function haUpdateState(parameters: {
hubId: string;
deviceState: DeviceState;
}) {
const stateTopic = `homeassistant/device/futurehome_${parameters.hubId}_${parameters.deviceState.id?.toString()}/state`;
const haState: Record<string, Record<string, any>> = {};
@ -126,9 +129,13 @@ export function haUpdateState(parameters: { hubId: string; deviceState: DeviceSt
* The prefix "pt:j1/mt:evt" is removed before matching so that the remainder
* exactly matches the address keys stored in the cached HA payloads.
*/
export function haUpdateStateSensorReport(parameters: { topic: string; value: any, attrName: string }) {
export function haUpdateStateSensorReport(parameters: {
topic: string;
value: any;
attrName: string;
}) {
// Strip the FIMP envelope so we end up with "/rt:dev/…/ad:x_y"
const sensorAddr = parameters.topic.replace(/^pt:j1\/mt:evt/, "");
const sensorAddr = parameters.topic.replace(/^pt:j1\/mt:evt/, '');
for (const [stateTopic, payload] of Object.entries(haStateCache)) {
if (!payload[sensorAddr]) continue;
@ -136,7 +143,9 @@ export function haUpdateStateSensorReport(parameters: { topic: string; value: an
// Update the reading inplace
payload[sensorAddr][parameters.attrName] = parameters.value;
log.debug(`Publishing updated sensor value for "${sensorAddr}" to "${stateTopic}"`);
log.debug(
`Publishing updated sensor value for "${sensorAddr}" to "${stateTopic}"`,
);
ha?.publish(stateTopic, JSON.stringify(payload), { retain: true, qos: 2 });
haStateCache[stateTopic] = payload;

View File

@ -1,15 +1,15 @@
import { connectHub, connectHA, RetainedMessage } from "./client";
import { log } from "./logger";
import { FimpResponse, sendFimpMsg, setFimp } from "./fimp/fimp";
import { haCommandHandlers, setHa, setHaCommandHandlers } from "./ha/globals";
import { CommandHandlers, haPublishDevice } from "./ha/publish_device";
import { haUpdateState, haUpdateStateSensorReport } from "./ha/update_state";
import { VinculumPd7Device } from "./fimp/vinculum_pd7_device";
import { haUpdateAvailability } from "./ha/update_availability";
import { delay } from "./utils";
import { connectHub, connectHA, RetainedMessage } from './client';
import { log } from './logger';
import { FimpResponse, sendFimpMsg, setFimp } from './fimp/fimp';
import { haCommandHandlers, setHa, setHaCommandHandlers } from './ha/globals';
import { CommandHandlers, haPublishDevice } from './ha/publish_device';
import { haUpdateState, haUpdateStateSensorReport } from './ha/update_state';
import { VinculumPd7Device } from './fimp/vinculum_pd7_device';
import { haUpdateAvailability } from './ha/update_availability';
import { delay } from './utils';
(async () => {
const hubIp = process.env.FH_HUB_IP || "futurehome-smarthub.local";
const hubIp = process.env.FH_HUB_IP || 'futurehome-smarthub.local';
const hubUsername = process.env.FH_USERNAME || '';
const hubPassword = process.env.FH_PASSWORD || '';
const demoMode = (process.env.DEMO_MODE || '').toLowerCase().includes('true');
@ -20,13 +20,20 @@ import { delay } from "./utils";
const mqttPassword = process.env.MQTT_PWD || '';
// 1) Connect to HA broker (for discovery + state + availability + commands)
log.info("Connecting to HA broker...");
const { ha, retainedMessages } = await connectHA({ mqttHost, mqttPort, mqttUsername, mqttPassword, });
log.info('Connecting to HA broker...');
const { ha, retainedMessages } = await connectHA({
mqttHost,
mqttPort,
mqttUsername,
mqttPassword,
});
setHa(ha);
log.info("Connected to HA broker");
log.info('Connected to HA broker');
if (!demoMode && (!hubUsername || !hubPassword)) {
log.info("Empty username or password in non-demo mode. Removing all Futurehome devices from Home Assistant...");
log.info(
'Empty username or password in non-demo mode. Removing all Futurehome devices from Home Assistant...',
);
const publishWithDelay = (messages: RetainedMessage[], index = 0) => {
if (index >= messages.length) return;
@ -42,17 +49,22 @@ import { delay } from "./utils";
}
// 2) Connect to Futurehome hub (FIMP traffic)
log.info("Connecting to Futurehome hub...");
const fimp = await connectHub({ hubIp, username: hubUsername, password: hubPassword, demo: demoMode });
fimp.subscribe("#");
log.info('Connecting to Futurehome hub...');
const fimp = await connectHub({
hubIp,
username: hubUsername,
password: hubPassword,
demo: demoMode,
});
fimp.subscribe('#');
setFimp(fimp);
log.info("Connected to Futurehome hub");
log.info('Connected to Futurehome hub');
const house = await sendFimpMsg({
address: '/rt:app/rn:vinculum/ad:1',
service: 'vinculum',
cmd: 'cmd.pd7.request',
val: { cmd: "get", component: null, param: { components: ['house'] } },
val: { cmd: 'get', component: null, param: { components: ['house'] } },
val_t: 'object',
timeoutMs: 30000,
});
@ -62,16 +74,20 @@ import { delay } from "./utils";
address: '/rt:app/rn:vinculum/ad:1',
service: 'vinculum',
cmd: 'cmd.pd7.request',
val: { cmd: "get", component: null, param: { components: ['device'] } },
val: { cmd: 'get', component: null, param: { components: ['device'] } },
val_t: 'object',
timeoutMs: 30000,
});
const haConfig = retainedMessages.filter(msg => msg.topic.endsWith("/config"));
const haConfig = retainedMessages.filter((msg) =>
msg.topic.endsWith('/config'),
);
const regex = new RegExp(`^homeassistant/device/futurehome_${hubId}_([a-zA-Z0-9]+)/config$`);
const regex = new RegExp(
`^homeassistant/device/futurehome_${hubId}_([a-zA-Z0-9]+)/config$`,
);
for (const haDevice of haConfig) {
log.debug('Found existing HA device', haDevice.topic)
log.debug('Found existing HA device', haDevice.topic);
const match = haDevice.topic.match(regex);
@ -80,15 +96,18 @@ import { delay } from "./utils";
const idNumber = Number(deviceId);
if (!isNaN(idNumber)) {
const basicDeviceData: { services?: { [key: string]: any } } = devices.val.param.device.find((d: any) => d?.id === idNumber);
const firstServiceAddr = basicDeviceData?.services ? Object.values(basicDeviceData.services)[0]?.addr : undefined;;
const basicDeviceData: { services?: { [key: string]: any } } =
devices.val.param.device.find((d: any) => d?.id === idNumber);
const firstServiceAddr = basicDeviceData?.services
? Object.values(basicDeviceData.services)[0]?.addr
: undefined;
if (!basicDeviceData || !firstServiceAddr) {
log.debug('Device was removed, removing from HA.');
ha?.publish(haDevice.topic, '', { retain: true, qos: 2 });
await delay(50);
}
} else if (deviceId.toLowerCase() === "hub") {
} else if (deviceId.toLowerCase() === 'hub') {
// Hub admin tools, ignore
} else {
log.debug('Invalid format, removing.');
@ -106,11 +125,15 @@ import { delay } from "./utils";
const commandHandlers: CommandHandlers = {};
for (const device of devices.val.param.device) {
try {
const vinculumDeviceData: VinculumPd7Device = device
const deviceId = vinculumDeviceData.id.toString()
const firstServiceAddr = vinculumDeviceData.services ? Object.values(vinculumDeviceData.services)[0]?.addr : undefined;;
const vinculumDeviceData: VinculumPd7Device = device;
const deviceId = vinculumDeviceData.id.toString();
const firstServiceAddr = vinculumDeviceData.services
? Object.values(vinculumDeviceData.services)[0]?.addr
: undefined;
if (!firstServiceAddr) { continue; }
if (!firstServiceAddr) {
continue;
}
// This is problematic when the adapter doesn't respond, so we are not getting the inclusion report for now. I'm leaving it here since we might want it in the future.
// // Get additional metadata like manufacutrer or sw/hw version directly from the adapter
@ -119,14 +142,27 @@ import { delay } from "./utils";
// const deviceInclusionReport = await getInclusionReport({ adapterAddress, adapterService, deviceId });
const deviceInclusionReport = undefined;
const result = haPublishDevice({ hubId, vinculumDeviceData, deviceInclusionReport });
const result = haPublishDevice({
hubId,
vinculumDeviceData,
deviceInclusionReport,
});
await delay(50);
Object.assign(commandHandlers, result.commandHandlers);
if (!retainedMessages.some(msg => msg.topic === `homeassistant/device/futurehome_${hubId}_${deviceId}/availability`)) {
if (
!retainedMessages.some(
(msg) =>
msg.topic ===
`homeassistant/device/futurehome_${hubId}_${deviceId}/availability`,
)
) {
// Set initial availability
haUpdateAvailability({ hubId, deviceAvailability: { address: deviceId, status: 'UP' } });
haUpdateAvailability({
hubId,
deviceAvailability: { address: deviceId, status: 'UP' },
});
await delay(50);
}
} catch (e) {
@ -142,10 +178,12 @@ import { delay } from "./utils";
// todo
// exposeSmarthubTools();
fimp.on("message", async (topic, buf) => {
fimp.on('message', async (topic, buf) => {
try {
const msg: FimpResponse = JSON.parse(buf.toString());
log.debug(`Received FIMP message on topic "${topic}":\n${JSON.stringify(msg, null, 0)}`);
log.debug(
`Received FIMP message on topic "${topic}":\n${JSON.stringify(msg, null, 0)}`,
);
switch (msg.type) {
case 'evt.pd7.response': {
@ -163,8 +201,12 @@ import { delay } from "./utils";
if (devices) {
const newDeviceIds = new Set(devices.map((d: any) => d?.id));
const addedDeviceIds = [...newDeviceIds].filter(id => !knownDeviceIds.has(id));
const removedDeviceIds = [...knownDeviceIds].filter(id => !newDeviceIds.has(id));
const addedDeviceIds = [...newDeviceIds].filter(
(id) => !knownDeviceIds.has(id),
);
const removedDeviceIds = [...knownDeviceIds].filter(
(id) => !newDeviceIds.has(id),
);
log.info(`Added devices: ${addedDeviceIds}`);
log.info(`Removed devices: ${removedDeviceIds}`);
@ -190,14 +232,19 @@ import { delay } from "./utils";
case 'evt.open.report':
case 'evt.lvl.report':
case 'evt.alarm.report':
case 'evt.binary.report':
{
haUpdateStateSensorReport({ topic, value: msg.val, attrName: msg.type.split('.')[1] })
break;
}
case 'evt.binary.report': {
haUpdateStateSensorReport({
topic,
value: msg.val,
attrName: msg.type.split('.')[1],
});
break;
}
case 'evt.network.all_nodes_report': {
const devicesAvailability = msg.val;
if (!devicesAvailability) { return; }
if (!devicesAvailability) {
return;
}
for (const deviceAvailability of devicesAvailability) {
haUpdateAvailability({ hubId, deviceAvailability });
await delay(50);
@ -206,50 +253,59 @@ import { delay } from "./utils";
}
}
} catch (e) {
log.warn("Bad FIMP JSON", e, topic, buf);
log.warn('Bad FIMP JSON', e, topic, buf);
}
});
const pollState = () => {
log.debug("Refreshing Vinculum state after 30 seconds...");
log.debug('Refreshing Vinculum state after 30 seconds...');
sendFimpMsg({
address: '/rt:app/rn:vinculum/ad:1',
service: 'vinculum',
cmd: 'cmd.pd7.request',
val: { cmd: "get", component: null, param: { components: ['state'] } },
val: { cmd: 'get', component: null, param: { components: ['state'] } },
val_t: 'object',
timeoutMs: 30000,
}).catch(e => log.warn("Failed to request state", e));
}).catch((e) => log.warn('Failed to request state', e));
};
// Request initial state
pollState();
// Then poll every 30 seconds
if (!demoMode) { setInterval(pollState, 30 * 1000); }
if (!demoMode) {
setInterval(pollState, 30 * 1000);
}
const pollDevices = () => {
log.debug("Refreshing Vinculum devices after 30 minutes...");
log.debug('Refreshing Vinculum devices after 30 minutes...');
sendFimpMsg({
address: '/rt:app/rn:vinculum/ad:1',
service: 'vinculum',
cmd: 'cmd.pd7.request',
val: { cmd: "get", component: null, param: { components: ['device'] } },
val: { cmd: 'get', component: null, param: { components: ['device'] } },
val_t: 'object',
timeoutMs: 30000,
}).catch(e => log.warn("Failed to request state", e));
}).catch((e) => log.warn('Failed to request state', e));
};
// Poll devices every 30 minutes (1800000 ms)
if (!demoMode) { setInterval(pollDevices, 30 * 60 * 1000); }
if (!demoMode) {
setInterval(pollDevices, 30 * 60 * 1000);
}
ha.on('message', (topic, buf) => {
// Handle Home Assistant command messages
const handler = haCommandHandlers?.[topic];
if (handler) {
log.debug(`Handling Home Assistant command topic: ${topic}, payload: ${buf.toString()}`);
log.debug(
`Handling Home Assistant command topic: ${topic}, payload: ${buf.toString()}`,
);
handler(buf.toString()).catch((e) => {
log.warn(`Failed executing handler for topic: ${topic}, payload: ${buf.toString()}`, e);
log.warn(
`Failed executing handler for topic: ${topic}, payload: ${buf.toString()}`,
e,
);
});
}
})
});
})();

View File

@ -11,26 +11,37 @@ export class DemoFimpMqttClient implements IMqttClient {
private onceConnectHandlers: (() => void)[] = [];
private onceErrorHandlers: OnErrorCallback[] = [];
connect(_url: string, _options: {
port: number;
username: string;
password: string;
protocolVersion: 4;
}): void {
connect(
_url: string,
_options: {
port: number;
username: string;
password: string;
protocolVersion: 4;
},
): void {
setTimeout(() => {
this.onceConnectHandlers.forEach((h) => h());
}, 100);
}
subscribe(topicObject: string, opts?: { qos: 0 | 1 | 2 }, callback?: (err: Error | null) => void): void;
subscribe(_topic: string, _opts?: any, _callback?: any): void { }
subscribe(
topicObject: string,
opts?: { qos: 0 | 1 | 2 },
callback?: (err: Error | null) => void,
): void;
subscribe(_topic: string, _opts?: any, _callback?: any): void {}
publish(topic: string, value: string, _options: {
retain?: boolean;
qos: 0 | 1 | 2;
}): void {
publish(
topic: string,
value: string,
_options: {
retain?: boolean;
qos: 0 | 1 | 2;
},
): void {
setTimeout(() => {
const msg = JSON.parse(value)
const msg = JSON.parse(value);
const sendResponse = (response: FimpResponse) => {
response.corid = response.corid ?? msg.uid;
@ -38,14 +49,35 @@ export class DemoFimpMqttClient implements IMqttClient {
for (const handler of this.messageHandlers) {
handler(topic, buffer, { retain: false } as any);
}
}
};
if (msg.serv == 'vinculum' && msg.type == 'cmd.pd7.request' && msg.val?.param?.components?.includes('house')) {
sendResponse({ type: 'evt.pd7.response', val: { param: { house: { hubId: '000000004c38b232' } } } })
} else if (msg.serv == 'vinculum' && msg.type == 'cmd.pd7.request' && msg.val?.param?.components?.includes('device')) {
sendResponse({ type: 'evt.pd7.response', val: { param: { device: demo_data__device } } });
} else if (msg.serv == 'vinculum' && msg.type == 'cmd.pd7.request' && msg.val?.param?.components?.includes('state')) {
sendResponse({ type: 'evt.pd7.response', val: { param: { state: { devices: demo_data__state } } } })
if (
msg.serv == 'vinculum' &&
msg.type == 'cmd.pd7.request' &&
msg.val?.param?.components?.includes('house')
) {
sendResponse({
type: 'evt.pd7.response',
val: { param: { house: { hubId: '000000004c38b232' } } },
});
} else if (
msg.serv == 'vinculum' &&
msg.type == 'cmd.pd7.request' &&
msg.val?.param?.components?.includes('device')
) {
sendResponse({
type: 'evt.pd7.response',
val: { param: { device: demo_data__device } },
});
} else if (
msg.serv == 'vinculum' &&
msg.type == 'cmd.pd7.request' &&
msg.val?.param?.components?.includes('state')
) {
sendResponse({
type: 'evt.pd7.response',
val: { param: { state: { devices: demo_data__state } } },
});
}
}, 100);
}

File diff suppressed because it is too large Load Diff

View File

@ -474,9 +474,7 @@
{
"ts": "2022-02-19 16:19:10 +0100",
"val": {
"members": [
"3_1"
]
"members": ["3_1"]
},
"val_t": "object"
}
@ -972,9 +970,7 @@
{
"ts": "2023-01-05 00:31:29 +0100",
"val": {
"members": [
"3_1"
]
"members": ["3_1"]
},
"val_t": "object"
}

View File

@ -1,20 +1,31 @@
import { IPublishPacket, OnErrorCallback, OnMessageCallback } from "mqtt/*";
import { IPublishPacket, OnErrorCallback, OnMessageCallback } from 'mqtt/*';
export interface IMqttClient {
connect(url: string, options: {
port: number;
username: string;
password: string;
protocolVersion: 4;
}): void;
connect(
url: string,
options: {
port: number;
username: string;
password: string;
protocolVersion: 4;
},
): void;
subscribe(topic: string): void;
subscribe(topicObject: string, opts?: { qos: 0 | 1 | 2 }, callback?: (err: Error | null) => void): void;
subscribe(
topicObject: string,
opts?: { qos: 0 | 1 | 2 },
callback?: (err: Error | null) => void,
): void;
publish(topic: string, value: string, options: {
retain?: boolean;
qos: 0 | 1 | 2;
}): void;
publish(
topic: string,
value: string,
options: {
retain?: boolean;
qos: 0 | 1 | 2;
},
): void;
on(event: 'message', handler: OnMessageCallback): void;
on(event: 'error', handler: OnErrorCallback): void;
@ -22,7 +33,10 @@ export interface IMqttClient {
off(event: 'message', handler: OnMessageCallback): void;
off(event: 'error', handler: OnErrorCallback): void;
removeListener(event: 'message', handler: (topic: string, payload: Buffer, packet: IPublishPacket) => void): void;
removeListener(
event: 'message',
handler: (topic: string, payload: Buffer, packet: IPublishPacket) => void,
): void;
once(event: 'connect', handler: () => void): void;
once(event: 'error', handler: OnErrorCallback): void;

View File

@ -8,16 +8,23 @@ export class RealMqttClient implements IMqttClient {
this.client = {} as MqttClient; // gets initialized in connect()
}
connect(url: string, options: {
port: number;
username: string;
password: string;
protocolVersion: 4;
}): void {
connect(
url: string,
options: {
port: number;
username: string;
password: string;
protocolVersion: 4;
},
): void {
this.client = connect(url, options);
}
subscribe(topicObject: string, opts?: { qos: 0 | 1 | 2 }, callback?: (err: Error | null) => void): void;
subscribe(
topicObject: string,
opts?: { qos: 0 | 1 | 2 },
callback?: (err: Error | null) => void,
): void;
subscribe(topic: string, opts?: any, callback?: any): void {
if (opts) {
this.client.subscribe(topic, opts, callback);
@ -26,10 +33,14 @@ export class RealMqttClient implements IMqttClient {
}
}
publish(topic: string, value: string, options: {
retain?: boolean;
qos: 0 | 1 | 2;
}): void {
publish(
topic: string,
value: string,
options: {
retain?: boolean;
qos: 0 | 1 | 2;
},
): void {
this.client.publish(topic, value, options);
}

View File

@ -7,14 +7,17 @@
// command_topic → cmd.lvl.set
// ───────────────────────────────────────────────────────────────
import { sendFimpMsg } from "../fimp/fimp";
import { VinculumPd7Device, VinculumPd7Service } from "../fimp/vinculum_pd7_device";
import { ServiceComponentsCreationResult } from "../ha/publish_device";
import { sendFimpMsg } from '../fimp/fimp';
import {
VinculumPd7Device,
VinculumPd7Service,
} from '../fimp/vinculum_pd7_device';
import { ServiceComponentsCreationResult } from '../ha/publish_device';
export function basic__components(
topicPrefix: string,
_device: VinculumPd7Device,
svc: VinculumPd7Service
svc: VinculumPd7Service,
): ServiceComponentsCreationResult | undefined {
// MQTT topic that HA will publish commands to
const cmdTopic = `${topicPrefix}${svc.addr}/command`;
@ -45,9 +48,9 @@ export function basic__components(
await sendFimpMsg({
address: svc.addr!,
service: "basic",
cmd: "cmd.lvl.set",
val_t: "int",
service: 'basic',
cmd: 'cmd.lvl.set',
val_t: 'int',
val: lvl,
});
},

View File

@ -1,11 +1,14 @@
import { VinculumPd7Device, VinculumPd7Service } from "../fimp/vinculum_pd7_device";
import { HaMqttComponent } from "../ha/mqtt_components/_component";
import { ServiceComponentsCreationResult } from "../ha/publish_device";
import {
VinculumPd7Device,
VinculumPd7Service,
} from '../fimp/vinculum_pd7_device';
import { HaMqttComponent } from '../ha/mqtt_components/_component';
import { ServiceComponentsCreationResult } from '../ha/publish_device';
export function battery__components(
topicPrefix: string,
device: VinculumPd7Device,
svc: VinculumPd7Service
svc: VinculumPd7Service,
): ServiceComponentsCreationResult | undefined {
const components: Record<string, HaMqttComponent> = {};

View File

@ -1,12 +1,18 @@
import { sendFimpMsg } from "../fimp/fimp";
import { VinculumPd7Device, VinculumPd7Service } from "../fimp/vinculum_pd7_device";
import { LightComponent } from "../ha/mqtt_components/light";
import { ServiceComponentsCreationResult, CommandHandlers } from "../ha/publish_device";
import { sendFimpMsg } from '../fimp/fimp';
import {
VinculumPd7Device,
VinculumPd7Service,
} from '../fimp/vinculum_pd7_device';
import { LightComponent } from '../ha/mqtt_components/light';
import {
ServiceComponentsCreationResult,
CommandHandlers,
} from '../ha/publish_device';
export function color_ctrl__components(
topicPrefix: string,
device: VinculumPd7Device,
svc: VinculumPd7Service
svc: VinculumPd7Service,
): ServiceComponentsCreationResult | undefined {
const supComponents: string[] = svc.props?.sup_components ?? [];
@ -15,7 +21,10 @@ export function color_ctrl__components(
}
// Check if we have RGB support (minimum requirement for a useful light)
const hasRgb = supComponents.includes('red') && supComponents.includes('green') && supComponents.includes('blue');
const hasRgb =
supComponents.includes('red') &&
supComponents.includes('green') &&
supComponents.includes('blue');
if (!hasRgb) {
return undefined; // No RGB support, skip this service
}
@ -76,7 +85,10 @@ export function color_ctrl__components(
lightComponent.color_temp_value_template = `{{ (1000000 / value_json['${svc.addr}'].temp) | round(0) }}`; // Convert Kelvin to mireds
lightComponent.min_mireds = 153; // ~6500K
lightComponent.max_mireds = 370; // ~2700K
} else if (supComponents.includes('warm_w') && supComponents.includes('cold_w')) {
} else if (
supComponents.includes('warm_w') &&
supComponents.includes('cold_w')
) {
// Z-Wave style - warm/cold white mix
lightComponent.color_temp_command_topic = colorTempCommandTopic;
lightComponent.color_temp_state_topic = stateTopic;
@ -101,23 +113,23 @@ export function color_ctrl__components(
await sendFimpMsg({
address: svc.addr!,
service: "color_ctrl",
cmd: "cmd.color.set",
val_t: "int_map",
service: 'color_ctrl',
cmd: 'cmd.color.set',
val_t: 'int_map',
val: colorMap,
});
} else if (payload === 'OFF') {
// Turn off (all components to 0)
const colorMap: Record<string, number> = {};
supComponents.forEach(component => {
supComponents.forEach((component) => {
colorMap[component] = 0;
});
await sendFimpMsg({
address: svc.addr!,
service: "color_ctrl",
cmd: "cmd.color.set",
val_t: "int_map",
service: 'color_ctrl',
cmd: 'cmd.color.set',
val_t: 'int_map',
val: colorMap,
});
}
@ -132,8 +144,17 @@ export function color_ctrl__components(
const green = parseInt(parts[1], 10);
const blue = parseInt(parts[2], 10);
if (Number.isNaN(red) || Number.isNaN(green) || Number.isNaN(blue)) return;
if (red < 0 || red > 255 || green < 0 || green > 255 || blue < 0 || blue > 255) return;
if (Number.isNaN(red) || Number.isNaN(green) || Number.isNaN(blue))
return;
if (
red < 0 ||
red > 255 ||
green < 0 ||
green > 255 ||
blue < 0 ||
blue > 255
)
return;
const colorMap: Record<string, number> = {
red,
@ -143,9 +164,9 @@ export function color_ctrl__components(
await sendFimpMsg({
address: svc.addr!,
service: "color_ctrl",
cmd: "cmd.color.set",
val_t: "int_map",
service: 'color_ctrl',
cmd: 'cmd.color.set',
val_t: 'int_map',
val: colorMap,
});
},
@ -167,12 +188,15 @@ export function color_ctrl__components(
await sendFimpMsg({
address: svc.addr!,
service: "color_ctrl",
cmd: "cmd.color.set",
val_t: "int_map",
service: 'color_ctrl',
cmd: 'cmd.color.set',
val_t: 'int_map',
val: colorMap,
});
} else if (supComponents.includes('warm_w') && supComponents.includes('cold_w')) {
} else if (
supComponents.includes('warm_w') &&
supComponents.includes('cold_w')
) {
// Z-Wave style - convert mireds to warm/cold white mix
// Linear interpolation between cold (153 mireds) and warm (370 mireds)
const warmRatio = (mireds - 153) / (370 - 153);
@ -189,9 +213,9 @@ export function color_ctrl__components(
await sendFimpMsg({
address: svc.addr!,
service: "color_ctrl",
cmd: "cmd.color.set",
val_t: "int_map",
service: 'color_ctrl',
cmd: 'cmd.color.set',
val_t: 'int_map',
val: colorMap,
});
}

View File

@ -1,11 +1,14 @@
import { sendFimpMsg } from "../fimp/fimp";
import { VinculumPd7Device, VinculumPd7Service } from "../fimp/vinculum_pd7_device";
import { ServiceComponentsCreationResult } from "../ha/publish_device";
import { sendFimpMsg } from '../fimp/fimp';
import {
VinculumPd7Device,
VinculumPd7Service,
} from '../fimp/vinculum_pd7_device';
import { ServiceComponentsCreationResult } from '../ha/publish_device';
export function fan_ctrl__components(
topicPrefix: string,
device: VinculumPd7Device,
svc: VinculumPd7Service
svc: VinculumPd7Service,
): ServiceComponentsCreationResult | undefined {
const supModes: string[] = svc.props?.sup_modes ?? [];
@ -38,10 +41,10 @@ export function fan_ctrl__components(
if (supModes.includes(mode) || mode === 'off') {
await sendFimpMsg({
address: svc.addr!,
service: "fan_ctrl",
cmd: "cmd.mode.set",
service: 'fan_ctrl',
cmd: 'cmd.mode.set',
val: mode,
val_t: "string",
val_t: 'string',
});
}
} else {
@ -49,10 +52,10 @@ export function fan_ctrl__components(
if (supModes.includes(payload)) {
await sendFimpMsg({
address: svc.addr!,
service: "fan_ctrl",
cmd: "cmd.mode.set",
service: 'fan_ctrl',
cmd: 'cmd.mode.set',
val: payload,
val_t: "string",
val_t: 'string',
});
}
}

View File

@ -1,11 +1,14 @@
import { sendFimpMsg } from "../fimp/fimp";
import { VinculumPd7Device, VinculumPd7Service } from "../fimp/vinculum_pd7_device";
import { ServiceComponentsCreationResult } from "../ha/publish_device";
import { sendFimpMsg } from '../fimp/fimp';
import {
VinculumPd7Device,
VinculumPd7Service,
} from '../fimp/vinculum_pd7_device';
import { ServiceComponentsCreationResult } from '../ha/publish_device';
export function out_bin_switch__components(
topicPrefix: string,
device: VinculumPd7Device,
svc: VinculumPd7Service
svc: VinculumPd7Service,
): ServiceComponentsCreationResult | undefined {
const commandTopic = `${topicPrefix}${svc.addr}/command`;
@ -29,6 +32,6 @@ export function out_bin_switch__components(
val_t: 'bool',
});
},
}
},
};
}

View File

@ -1,11 +1,14 @@
import { sendFimpMsg } from "../fimp/fimp";
import { VinculumPd7Device, VinculumPd7Service } from "../fimp/vinculum_pd7_device";
import { ServiceComponentsCreationResult } from "../ha/publish_device";
import { sendFimpMsg } from '../fimp/fimp';
import {
VinculumPd7Device,
VinculumPd7Service,
} from '../fimp/vinculum_pd7_device';
import { ServiceComponentsCreationResult } from '../ha/publish_device';
export function out_lvl_switch__components(
topicPrefix: string,
device: VinculumPd7Device,
svc: VinculumPd7Service
svc: VinculumPd7Service,
): ServiceComponentsCreationResult | undefined {
const commandTopic = `${topicPrefix}${svc.addr}/command`;
@ -35,10 +38,10 @@ export function out_lvl_switch__components(
await sendFimpMsg({
address: svc.addr!,
service: "out_lvl_switch",
cmd: "cmd.lvl.set",
service: 'out_lvl_switch',
cmd: 'cmd.lvl.set',
val: lvl,
val_t: "int",
val_t: 'int',
});
},
},

View File

@ -8,13 +8,16 @@
// <topicPrefix><addr>/scene/command → cmd.scene.set
// ─────────────────────────────────────────────────────────────
import { sendFimpMsg } from "../fimp/fimp";
import { VinculumPd7Device, VinculumPd7Service } from "../fimp/vinculum_pd7_device";
import { HaMqttComponent } from "../ha/mqtt_components/_component";
import { sendFimpMsg } from '../fimp/fimp';
import {
VinculumPd7Device,
VinculumPd7Service,
} from '../fimp/vinculum_pd7_device';
import { HaMqttComponent } from '../ha/mqtt_components/_component';
import {
CommandHandlers,
ServiceComponentsCreationResult,
} from "../ha/publish_device";
} from '../ha/publish_device';
/**
* Creates MQTT components for a single *scene_ctrl* service.
@ -22,33 +25,33 @@ import {
export function scene_ctrl__components(
topicPrefix: string,
_device: VinculumPd7Device,
svc: VinculumPd7Service
svc: VinculumPd7Service,
): ServiceComponentsCreationResult | undefined {
const components: Record<string, HaMqttComponent> = {};
const handlers: CommandHandlers = {};
// ───────────── read-only entities ─────────────
if (svc.intf?.includes("evt.scene.report")) {
if (svc.intf?.includes('evt.scene.report')) {
components[`${svc.addr}_scene`] = {
unique_id: `${svc.addr}_scene`,
platform: 'sensor',
unit_of_measurement: "",
unit_of_measurement: '',
value_template: `{{ value_json['${svc.addr}'].scene }}`,
};
}
if (svc.intf?.includes("evt.lvl.report")) {
if (svc.intf?.includes('evt.lvl.report')) {
components[`${svc.addr}_lvl`] = {
unique_id: `${svc.addr}_lvl`,
platform: 'sensor',
unit_of_measurement: "",
unit_of_measurement: '',
value_template: `{{ value_json['${svc.addr}'].lvl }}`,
};
}
// ───────────── writeable “select” (scene activator) ─────────────
const supScenes: string[] = svc.props?.sup_scenes ?? [];
if (svc.intf?.includes("cmd.scene.set") && supScenes.length) {
if (svc.intf?.includes('cmd.scene.set') && supScenes.length) {
const commandTopic = `${topicPrefix}${svc.addr}/scene/command`;
components[`${svc.addr}_select`] = {
@ -65,9 +68,9 @@ export function scene_ctrl__components(
await sendFimpMsg({
address: svc.addr!,
service: "scene_ctrl",
cmd: "cmd.scene.set",
val_t: "string",
service: 'scene_ctrl',
cmd: 'cmd.scene.set',
val_t: 'string',
val: payload,
});
};

View File

@ -1,10 +1,13 @@
import { VinculumPd7Device, VinculumPd7Service } from "../fimp/vinculum_pd7_device";
import { ServiceComponentsCreationResult } from "../ha/publish_device";
import {
VinculumPd7Device,
VinculumPd7Service,
} from '../fimp/vinculum_pd7_device';
import { ServiceComponentsCreationResult } from '../ha/publish_device';
export function sensor_accelx__components(
topicPrefix: string,
device: VinculumPd7Device,
svc: VinculumPd7Service
svc: VinculumPd7Service,
): ServiceComponentsCreationResult | undefined {
const device_class = undefined;
const unit = svc.props?.sup_units?.[0] ?? 'm/s2';
@ -18,6 +21,6 @@ export function sensor_accelx__components(
unit_of_measurement: unit,
value_template: `{{ value_json['${svc.addr}'].sensor }}`,
},
}
},
};
}

View File

@ -1,10 +1,13 @@
import { VinculumPd7Device, VinculumPd7Service } from "../fimp/vinculum_pd7_device";
import { ServiceComponentsCreationResult } from "../ha/publish_device";
import {
VinculumPd7Device,
VinculumPd7Service,
} from '../fimp/vinculum_pd7_device';
import { ServiceComponentsCreationResult } from '../ha/publish_device';
export function sensor_accely__components(
topicPrefix: string,
device: VinculumPd7Device,
svc: VinculumPd7Service
svc: VinculumPd7Service,
): ServiceComponentsCreationResult | undefined {
const device_class = undefined;
const unit = svc.props?.sup_units?.[0] ?? 'm/s2';
@ -18,6 +21,6 @@ export function sensor_accely__components(
unit_of_measurement: unit,
value_template: `{{ value_json['${svc.addr}'].sensor }}`,
},
}
},
};
}

View File

@ -1,10 +1,13 @@
import { VinculumPd7Device, VinculumPd7Service } from "../fimp/vinculum_pd7_device";
import { ServiceComponentsCreationResult } from "../ha/publish_device";
import {
VinculumPd7Device,
VinculumPd7Service,
} from '../fimp/vinculum_pd7_device';
import { ServiceComponentsCreationResult } from '../ha/publish_device';
export function sensor_accelz__components(
topicPrefix: string,
device: VinculumPd7Device,
svc: VinculumPd7Service
svc: VinculumPd7Service,
): ServiceComponentsCreationResult | undefined {
const device_class = undefined;
const unit = svc.props?.sup_units?.[0] ?? 'm/s2';
@ -18,6 +21,6 @@ export function sensor_accelz__components(
unit_of_measurement: unit,
value_template: `{{ value_json['${svc.addr}'].sensor }}`,
},
}
},
};
}

View File

@ -1,10 +1,13 @@
import { VinculumPd7Device, VinculumPd7Service } from "../fimp/vinculum_pd7_device";
import { ServiceComponentsCreationResult } from "../ha/publish_device";
import {
VinculumPd7Device,
VinculumPd7Service,
} from '../fimp/vinculum_pd7_device';
import { ServiceComponentsCreationResult } from '../ha/publish_device';
export function sensor_airflow__components(
topicPrefix: string,
device: VinculumPd7Device,
svc: VinculumPd7Service
svc: VinculumPd7Service,
): ServiceComponentsCreationResult | undefined {
const device_class = undefined;
const unit = svc.props?.sup_units?.[0] ?? 'm3/h';
@ -18,6 +21,6 @@ export function sensor_airflow__components(
unit_of_measurement: unit,
value_template: `{{ value_json['${svc.addr}'].sensor }}`,
},
}
},
};
}

View File

@ -1,10 +1,13 @@
import { VinculumPd7Device, VinculumPd7Service } from "../fimp/vinculum_pd7_device";
import { ServiceComponentsCreationResult } from "../ha/publish_device";
import {
VinculumPd7Device,
VinculumPd7Service,
} from '../fimp/vinculum_pd7_device';
import { ServiceComponentsCreationResult } from '../ha/publish_device';
export function sensor_airq__components(
topicPrefix: string,
device: VinculumPd7Device,
svc: VinculumPd7Service
svc: VinculumPd7Service,
): ServiceComponentsCreationResult | undefined {
const device_class = 'aqi';
const unit = svc.props?.sup_units?.[0] ?? 'pm25';
@ -18,6 +21,6 @@ export function sensor_airq__components(
unit_of_measurement: unit,
value_template: `{{ value_json['${svc.addr}'].sensor }}`,
},
}
},
};
}

View File

@ -1,10 +1,13 @@
import { VinculumPd7Device, VinculumPd7Service } from "../fimp/vinculum_pd7_device";
import { ServiceComponentsCreationResult } from "../ha/publish_device";
import {
VinculumPd7Device,
VinculumPd7Service,
} from '../fimp/vinculum_pd7_device';
import { ServiceComponentsCreationResult } from '../ha/publish_device';
export function sensor_anglepos__components(
topicPrefix: string,
device: VinculumPd7Device,
svc: VinculumPd7Service
svc: VinculumPd7Service,
): ServiceComponentsCreationResult | undefined {
const device_class = undefined;
const unit = svc.props?.sup_units?.[0] ?? '%';
@ -18,6 +21,6 @@ export function sensor_anglepos__components(
unit_of_measurement: unit,
value_template: `{{ value_json['${svc.addr}'].sensor }}`,
},
}
},
};
}

View File

@ -1,10 +1,13 @@
import { VinculumPd7Device, VinculumPd7Service } from "../fimp/vinculum_pd7_device";
import { ServiceComponentsCreationResult } from "../ha/publish_device";
import {
VinculumPd7Device,
VinculumPd7Service,
} from '../fimp/vinculum_pd7_device';
import { ServiceComponentsCreationResult } from '../ha/publish_device';
export function sensor_atmo__components(
topicPrefix: string,
device: VinculumPd7Device,
svc: VinculumPd7Service
svc: VinculumPd7Service,
): ServiceComponentsCreationResult | undefined {
const device_class = 'atmospheric_pressure';
const unit = svc.props?.sup_units?.[0] ?? 'kPa';
@ -18,6 +21,6 @@ export function sensor_atmo__components(
unit_of_measurement: unit,
value_template: `{{ value_json['${svc.addr}'].sensor }}`,
},
}
},
};
}

View File

@ -1,10 +1,13 @@
import { VinculumPd7Device, VinculumPd7Service } from "../fimp/vinculum_pd7_device";
import { ServiceComponentsCreationResult } from "../ha/publish_device";
import {
VinculumPd7Device,
VinculumPd7Service,
} from '../fimp/vinculum_pd7_device';
import { ServiceComponentsCreationResult } from '../ha/publish_device';
export function sensor_baro__components(
topicPrefix: string,
device: VinculumPd7Device,
svc: VinculumPd7Service
svc: VinculumPd7Service,
): ServiceComponentsCreationResult | undefined {
const device_class = 'atmospheric_pressure';
const unit = svc.props?.sup_units?.[0] ?? 'kPa';
@ -18,6 +21,6 @@ export function sensor_baro__components(
unit_of_measurement: unit,
value_template: `{{ value_json['${svc.addr}'].sensor }}`,
},
}
},
};
}

View File

@ -1,10 +1,13 @@
import { VinculumPd7Device, VinculumPd7Service } from "../fimp/vinculum_pd7_device";
import { ServiceComponentsCreationResult } from "../ha/publish_device";
import {
VinculumPd7Device,
VinculumPd7Service,
} from '../fimp/vinculum_pd7_device';
import { ServiceComponentsCreationResult } from '../ha/publish_device';
export function sensor_co__components(
topicPrefix: string,
device: VinculumPd7Device,
svc: VinculumPd7Service
svc: VinculumPd7Service,
): ServiceComponentsCreationResult | undefined {
const device_class = 'carbon_monoxide';
const unit = svc.props?.sup_units?.[0] ?? 'mol/m3';
@ -18,6 +21,6 @@ export function sensor_co__components(
unit_of_measurement: unit,
value_template: `{{ value_json['${svc.addr}'].sensor }}`,
},
}
},
};
}

View File

@ -1,10 +1,13 @@
import { VinculumPd7Device, VinculumPd7Service } from "../fimp/vinculum_pd7_device";
import { ServiceComponentsCreationResult } from "../ha/publish_device";
import {
VinculumPd7Device,
VinculumPd7Service,
} from '../fimp/vinculum_pd7_device';
import { ServiceComponentsCreationResult } from '../ha/publish_device';
export function sensor_co2__components(
topicPrefix: string,
device: VinculumPd7Device,
svc: VinculumPd7Service
svc: VinculumPd7Service,
): ServiceComponentsCreationResult | undefined {
const device_class = 'carbon_dioxide';
const unit = svc.props?.sup_units?.[0] ?? 'ppm';
@ -18,6 +21,6 @@ export function sensor_co2__components(
unit_of_measurement: unit,
value_template: `{{ value_json['${svc.addr}'].sensor }}`,
},
}
},
};
}

View File

@ -1,12 +1,15 @@
import { VinculumPd7Device, VinculumPd7Service } from "../fimp/vinculum_pd7_device";
import { ServiceComponentsCreationResult } from "../ha/publish_device";
import {
VinculumPd7Device,
VinculumPd7Service,
} from '../fimp/vinculum_pd7_device';
import { ServiceComponentsCreationResult } from '../ha/publish_device';
export function sensor_contact__components(
topicPrefix: string,
device: VinculumPd7Device,
svc: VinculumPd7Service
svc: VinculumPd7Service,
): ServiceComponentsCreationResult | undefined {
const device_class = 'opening'
const device_class = 'opening';
return {
components: {

View File

@ -1,10 +1,13 @@
import { VinculumPd7Device, VinculumPd7Service } from "../fimp/vinculum_pd7_device";
import { ServiceComponentsCreationResult } from "../ha/publish_device";
import {
VinculumPd7Device,
VinculumPd7Service,
} from '../fimp/vinculum_pd7_device';
import { ServiceComponentsCreationResult } from '../ha/publish_device';
export function sensor_current__components(
topicPrefix: string,
device: VinculumPd7Device,
svc: VinculumPd7Service
svc: VinculumPd7Service,
): ServiceComponentsCreationResult | undefined {
const device_class = 'current';
const unit = svc.props?.sup_units?.[0] ?? 'A';
@ -18,6 +21,6 @@ export function sensor_current__components(
unit_of_measurement: unit,
value_template: `{{ value_json['${svc.addr}'].sensor }}`,
},
}
},
};
}

View File

@ -1,13 +1,16 @@
import { VinculumPd7Device, VinculumPd7Service } from "../fimp/vinculum_pd7_device";
import { ServiceComponentsCreationResult } from "../ha/publish_device";
import {
VinculumPd7Device,
VinculumPd7Service,
} from '../fimp/vinculum_pd7_device';
import { ServiceComponentsCreationResult } from '../ha/publish_device';
export function sensor_dew__components(
topicPrefix: string,
device: VinculumPd7Device,
svc: VinculumPd7Service
svc: VinculumPd7Service,
): ServiceComponentsCreationResult | undefined {
const device_class = 'temperature'
let unit = svc.props?.sup_units?.[0] ?? "°C";
const device_class = 'temperature';
let unit = svc.props?.sup_units?.[0] ?? '°C';
if (unit === 'C') unit = '°C';
if (unit === 'F') unit = '°F';

View File

@ -1,10 +1,13 @@
import { VinculumPd7Device, VinculumPd7Service } from "../fimp/vinculum_pd7_device";
import { ServiceComponentsCreationResult } from "../ha/publish_device";
import {
VinculumPd7Device,
VinculumPd7Service,
} from '../fimp/vinculum_pd7_device';
import { ServiceComponentsCreationResult } from '../ha/publish_device';
export function sensor_direct__components(
topicPrefix: string,
device: VinculumPd7Device,
svc: VinculumPd7Service
svc: VinculumPd7Service,
): ServiceComponentsCreationResult | undefined {
const device_class = 'wind_direction';
const unit = svc.props?.sup_units?.[0] ?? '°';
@ -18,6 +21,6 @@ export function sensor_direct__components(
unit_of_measurement: unit,
value_template: `{{ value_json['${svc.addr}'].sensor }}`,
},
}
},
};
}

View File

@ -1,10 +1,13 @@
import { VinculumPd7Device, VinculumPd7Service } from "../fimp/vinculum_pd7_device";
import { ServiceComponentsCreationResult } from "../ha/publish_device";
import {
VinculumPd7Device,
VinculumPd7Service,
} from '../fimp/vinculum_pd7_device';
import { ServiceComponentsCreationResult } from '../ha/publish_device';
export function sensor_distance__components(
topicPrefix: string,
device: VinculumPd7Device,
svc: VinculumPd7Service
svc: VinculumPd7Service,
): ServiceComponentsCreationResult | undefined {
const device_class = 'distance';
const unit = svc.props?.sup_units?.[0] ?? 'm';
@ -18,6 +21,6 @@ export function sensor_distance__components(
unit_of_measurement: unit,
value_template: `{{ value_json['${svc.addr}'].sensor }}`,
},
}
},
};
}

View File

@ -1,10 +1,13 @@
import { VinculumPd7Device, VinculumPd7Service } from "../fimp/vinculum_pd7_device";
import { ServiceComponentsCreationResult } from "../ha/publish_device";
import {
VinculumPd7Device,
VinculumPd7Service,
} from '../fimp/vinculum_pd7_device';
import { ServiceComponentsCreationResult } from '../ha/publish_device';
export function sensor_elresist__components(
topicPrefix: string,
device: VinculumPd7Device,
svc: VinculumPd7Service
svc: VinculumPd7Service,
): ServiceComponentsCreationResult | undefined {
const device_class = undefined;
const unit = svc.props?.sup_units?.[0] ?? 'ohm/m';
@ -18,6 +21,6 @@ export function sensor_elresist__components(
unit_of_measurement: unit,
value_template: `{{ value_json['${svc.addr}'].sensor }}`,
},
}
},
};
}

View File

@ -1,10 +1,13 @@
import { VinculumPd7Device, VinculumPd7Service } from "../fimp/vinculum_pd7_device";
import { ServiceComponentsCreationResult } from "../ha/publish_device";
import {
VinculumPd7Device,
VinculumPd7Service,
} from '../fimp/vinculum_pd7_device';
import { ServiceComponentsCreationResult } from '../ha/publish_device';
export function sensor_freq__components(
topicPrefix: string,
device: VinculumPd7Device,
svc: VinculumPd7Service
svc: VinculumPd7Service,
): ServiceComponentsCreationResult | undefined {
const device_class = 'frequency';
const unit = svc.props?.sup_units?.[0] ?? 'Hz';
@ -18,6 +21,6 @@ export function sensor_freq__components(
unit_of_measurement: unit,
value_template: `{{ value_json['${svc.addr}'].sensor }}`,
},
}
},
};
}

View File

@ -1,10 +1,13 @@
import { VinculumPd7Device, VinculumPd7Service } from "../fimp/vinculum_pd7_device";
import { ServiceComponentsCreationResult } from "../ha/publish_device";
import {
VinculumPd7Device,
VinculumPd7Service,
} from '../fimp/vinculum_pd7_device';
import { ServiceComponentsCreationResult } from '../ha/publish_device';
export function sensor_gp__components(
topicPrefix: string,
device: VinculumPd7Device,
svc: VinculumPd7Service
svc: VinculumPd7Service,
): ServiceComponentsCreationResult | undefined {
const device_class = undefined;
const unit = svc.props?.sup_units?.[0] ?? '%';
@ -18,6 +21,6 @@ export function sensor_gp__components(
unit_of_measurement: unit,
value_template: `{{ value_json['${svc.addr}'].sensor }}`,
},
}
},
};
}

View File

@ -1,14 +1,17 @@
import { VinculumPd7Device, VinculumPd7Service } from "../fimp/vinculum_pd7_device";
import { ServiceComponentsCreationResult } from "../ha/publish_device";
import {
VinculumPd7Device,
VinculumPd7Service,
} from '../fimp/vinculum_pd7_device';
import { ServiceComponentsCreationResult } from '../ha/publish_device';
export function sensor_gust__components(
topicPrefix: string,
device: VinculumPd7Device,
svc: VinculumPd7Service
svc: VinculumPd7Service,
): ServiceComponentsCreationResult | undefined {
const device_class = undefined;
let unit = svc.props?.sup_units?.[0] ?? 'km/h';
if (unit === 'kph') unit = 'km/h'
if (unit === 'kph') unit = 'km/h';
return {
components: {
@ -19,6 +22,6 @@ export function sensor_gust__components(
unit_of_measurement: unit,
value_template: `{{ value_json['${svc.addr}'].sensor }}`,
},
}
},
};
}

View File

@ -1,10 +1,13 @@
import { VinculumPd7Device, VinculumPd7Service } from "../fimp/vinculum_pd7_device";
import { ServiceComponentsCreationResult } from "../ha/publish_device";
import {
VinculumPd7Device,
VinculumPd7Service,
} from '../fimp/vinculum_pd7_device';
import { ServiceComponentsCreationResult } from '../ha/publish_device';
export function sensor_humid__components(
topicPrefix: string,
device: VinculumPd7Device,
svc: VinculumPd7Service
svc: VinculumPd7Service,
): ServiceComponentsCreationResult | undefined {
const device_class = 'humidity';
const unit = svc.props?.sup_units?.[0] ?? '%';
@ -18,6 +21,6 @@ export function sensor_humid__components(
unit_of_measurement: unit,
value_template: `{{ value_json['${svc.addr}'].sensor }}`,
},
}
},
};
}

View File

@ -1,10 +1,13 @@
import { VinculumPd7Device, VinculumPd7Service } from "../fimp/vinculum_pd7_device";
import { ServiceComponentsCreationResult } from "../ha/publish_device";
import {
VinculumPd7Device,
VinculumPd7Service,
} from '../fimp/vinculum_pd7_device';
import { ServiceComponentsCreationResult } from '../ha/publish_device';
export function sensor_lumin__components(
topicPrefix: string,
device: VinculumPd7Device,
svc: VinculumPd7Service
svc: VinculumPd7Service,
): ServiceComponentsCreationResult | undefined {
const device_class = 'illuminance';
const unit = svc.props?.sup_units?.[0] ?? 'Lux';
@ -18,6 +21,6 @@ export function sensor_lumin__components(
unit_of_measurement: unit,
value_template: `{{ value_json['${svc.addr}'].sensor }}`,
},
}
},
};
}

View File

@ -1,10 +1,13 @@
import { VinculumPd7Device, VinculumPd7Service } from "../fimp/vinculum_pd7_device";
import { ServiceComponentsCreationResult } from "../ha/publish_device";
import {
VinculumPd7Device,
VinculumPd7Service,
} from '../fimp/vinculum_pd7_device';
import { ServiceComponentsCreationResult } from '../ha/publish_device';
export function sensor_moist__components(
topicPrefix: string,
device: VinculumPd7Device,
svc: VinculumPd7Service
svc: VinculumPd7Service,
): ServiceComponentsCreationResult | undefined {
const device_class = 'moisture';
const unit = svc.props?.sup_units?.[0] ?? '%';
@ -18,6 +21,6 @@ export function sensor_moist__components(
unit_of_measurement: unit,
value_template: `{{ value_json['${svc.addr}'].sensor }}`,
},
}
},
};
}

View File

@ -1,10 +1,13 @@
import { VinculumPd7Device, VinculumPd7Service } from "../fimp/vinculum_pd7_device";
import { ServiceComponentsCreationResult } from "../ha/publish_device";
import {
VinculumPd7Device,
VinculumPd7Service,
} from '../fimp/vinculum_pd7_device';
import { ServiceComponentsCreationResult } from '../ha/publish_device';
export function sensor_noise__components(
topicPrefix: string,
device: VinculumPd7Device,
svc: VinculumPd7Service
svc: VinculumPd7Service,
): ServiceComponentsCreationResult | undefined {
const device_class = 'sound_pressure';
const unit = svc.props?.sup_units?.[0] ?? 'dB';
@ -18,6 +21,6 @@ export function sensor_noise__components(
unit_of_measurement: unit,
value_template: `{{ value_json['${svc.addr}'].sensor }}`,
},
}
},
};
}

View File

@ -1,10 +1,13 @@
import { VinculumPd7Device, VinculumPd7Service } from "../fimp/vinculum_pd7_device";
import { ServiceComponentsCreationResult } from "../ha/publish_device";
import {
VinculumPd7Device,
VinculumPd7Service,
} from '../fimp/vinculum_pd7_device';
import { ServiceComponentsCreationResult } from '../ha/publish_device';
export function sensor_power__components(
topicPrefix: string,
device: VinculumPd7Device,
svc: VinculumPd7Service
svc: VinculumPd7Service,
): ServiceComponentsCreationResult | undefined {
const device_class = 'power';
const unit = svc.props?.sup_units?.[0] ?? 'W';
@ -18,6 +21,6 @@ export function sensor_power__components(
unit_of_measurement: unit,
value_template: `{{ value_json['${svc.addr}'].sensor }}`,
},
}
},
};
}

View File

@ -1,12 +1,15 @@
import { VinculumPd7Device, VinculumPd7Service } from "../fimp/vinculum_pd7_device";
import { ServiceComponentsCreationResult } from "../ha/publish_device";
import {
VinculumPd7Device,
VinculumPd7Service,
} from '../fimp/vinculum_pd7_device';
import { ServiceComponentsCreationResult } from '../ha/publish_device';
export function sensor_presence__components(
topicPrefix: string,
device: VinculumPd7Device,
svc: VinculumPd7Service
svc: VinculumPd7Service,
): ServiceComponentsCreationResult | undefined {
const device_class = 'occupancy'
const device_class = 'occupancy';
return {
components: {

View File

@ -1,10 +1,13 @@
import { VinculumPd7Device, VinculumPd7Service } from "../fimp/vinculum_pd7_device";
import { ServiceComponentsCreationResult } from "../ha/publish_device";
import {
VinculumPd7Device,
VinculumPd7Service,
} from '../fimp/vinculum_pd7_device';
import { ServiceComponentsCreationResult } from '../ha/publish_device';
export function sensor_rain__components(
topicPrefix: string,
device: VinculumPd7Device,
svc: VinculumPd7Service
svc: VinculumPd7Service,
): ServiceComponentsCreationResult | undefined {
const device_class = 'precipitation_intensity';
const unit = svc.props?.sup_units?.[0] ?? 'mm/h';
@ -18,6 +21,6 @@ export function sensor_rain__components(
unit_of_measurement: unit,
value_template: `{{ value_json['${svc.addr}'].sensor }}`,
},
}
},
};
}

View File

@ -1,10 +1,13 @@
import { VinculumPd7Device, VinculumPd7Service } from "../fimp/vinculum_pd7_device";
import { ServiceComponentsCreationResult } from "../ha/publish_device";
import {
VinculumPd7Device,
VinculumPd7Service,
} from '../fimp/vinculum_pd7_device';
import { ServiceComponentsCreationResult } from '../ha/publish_device';
export function sensor_rotation__components(
topicPrefix: string,
device: VinculumPd7Device,
svc: VinculumPd7Service
svc: VinculumPd7Service,
): ServiceComponentsCreationResult | undefined {
const device_class = undefined;
const unit = svc.props?.sup_units?.[0] ?? 'rpm';
@ -18,6 +21,6 @@ export function sensor_rotation__components(
unit_of_measurement: unit,
value_template: `{{ value_json['${svc.addr}'].sensor }}`,
},
}
},
};
}

View File

@ -1,10 +1,13 @@
import { VinculumPd7Device, VinculumPd7Service } from "../fimp/vinculum_pd7_device";
import { ServiceComponentsCreationResult } from "../ha/publish_device";
import {
VinculumPd7Device,
VinculumPd7Service,
} from '../fimp/vinculum_pd7_device';
import { ServiceComponentsCreationResult } from '../ha/publish_device';
export function sensor_seismicint__components(
topicPrefix: string,
device: VinculumPd7Device,
svc: VinculumPd7Service
svc: VinculumPd7Service,
): ServiceComponentsCreationResult | undefined {
const device_class = undefined;
const unit = svc.props?.sup_units?.[0] ?? 'EMCRO';
@ -18,6 +21,6 @@ export function sensor_seismicint__components(
unit_of_measurement: unit,
value_template: `{{ value_json['${svc.addr}'].sensor }}`,
},
}
},
};
}

View File

@ -1,10 +1,13 @@
import { VinculumPd7Device, VinculumPd7Service } from "../fimp/vinculum_pd7_device";
import { ServiceComponentsCreationResult } from "../ha/publish_device";
import {
VinculumPd7Device,
VinculumPd7Service,
} from '../fimp/vinculum_pd7_device';
import { ServiceComponentsCreationResult } from '../ha/publish_device';
export function sensor_seismicmag__components(
topicPrefix: string,
device: VinculumPd7Device,
svc: VinculumPd7Service
svc: VinculumPd7Service,
): ServiceComponentsCreationResult | undefined {
const device_class = undefined;
const unit = svc.props?.sup_units?.[0] ?? 'MB';
@ -18,6 +21,6 @@ export function sensor_seismicmag__components(
unit_of_measurement: unit,
value_template: `{{ value_json['${svc.addr}'].sensor }}`,
},
}
},
};
}

View File

@ -1,10 +1,13 @@
import { VinculumPd7Device, VinculumPd7Service } from "../fimp/vinculum_pd7_device";
import { ServiceComponentsCreationResult } from "../ha/publish_device";
import {
VinculumPd7Device,
VinculumPd7Service,
} from '../fimp/vinculum_pd7_device';
import { ServiceComponentsCreationResult } from '../ha/publish_device';
export function sensor_solarrad__components(
topicPrefix: string,
device: VinculumPd7Device,
svc: VinculumPd7Service
svc: VinculumPd7Service,
): ServiceComponentsCreationResult | undefined {
const device_class = undefined;
const unit = svc.props?.sup_units?.[0] ?? 'W/m2';
@ -18,6 +21,6 @@ export function sensor_solarrad__components(
unit_of_measurement: unit,
value_template: `{{ value_json['${svc.addr}'].sensor }}`,
},
}
},
};
}

View File

@ -1,10 +1,13 @@
import { VinculumPd7Device, VinculumPd7Service } from "../fimp/vinculum_pd7_device";
import { ServiceComponentsCreationResult } from "../ha/publish_device";
import {
VinculumPd7Device,
VinculumPd7Service,
} from '../fimp/vinculum_pd7_device';
import { ServiceComponentsCreationResult } from '../ha/publish_device';
export function sensor_tank__components(
topicPrefix: string,
device: VinculumPd7Device,
svc: VinculumPd7Service
svc: VinculumPd7Service,
): ServiceComponentsCreationResult | undefined {
const device_class = 'volume_storage';
const unit = svc.props?.sup_units?.[0] ?? 'l';
@ -18,6 +21,6 @@ export function sensor_tank__components(
unit_of_measurement: unit,
value_template: `{{ value_json['${svc.addr}'].sensor }}`,
},
}
},
};
}

View File

@ -1,13 +1,16 @@
import { VinculumPd7Device, VinculumPd7Service } from "../fimp/vinculum_pd7_device";
import { ServiceComponentsCreationResult } from "../ha/publish_device";
import {
VinculumPd7Device,
VinculumPd7Service,
} from '../fimp/vinculum_pd7_device';
import { ServiceComponentsCreationResult } from '../ha/publish_device';
export function sensor_temp__components(
topicPrefix: string,
device: VinculumPd7Device,
svc: VinculumPd7Service
svc: VinculumPd7Service,
): ServiceComponentsCreationResult | undefined {
const device_class = 'temperature'
let unit = svc.props?.sup_units?.[0] ?? "°C";
const device_class = 'temperature';
let unit = svc.props?.sup_units?.[0] ?? '°C';
if (unit === 'C') unit = '°C';
if (unit === 'F') unit = '°F';

View File

@ -1,10 +1,13 @@
import { VinculumPd7Device, VinculumPd7Service } from "../fimp/vinculum_pd7_device";
import { ServiceComponentsCreationResult } from "../ha/publish_device";
import {
VinculumPd7Device,
VinculumPd7Service,
} from '../fimp/vinculum_pd7_device';
import { ServiceComponentsCreationResult } from '../ha/publish_device';
export function sensor_tidelvl__components(
topicPrefix: string,
device: VinculumPd7Device,
svc: VinculumPd7Service
svc: VinculumPd7Service,
): ServiceComponentsCreationResult | undefined {
const device_class = undefined;
const unit = svc.props?.sup_units?.[0] ?? 'm';
@ -18,6 +21,6 @@ export function sensor_tidelvl__components(
unit_of_measurement: unit,
value_template: `{{ value_json['${svc.addr}'].sensor }}`,
},
}
},
};
}

View File

@ -1,10 +1,13 @@
import { VinculumPd7Device, VinculumPd7Service } from "../fimp/vinculum_pd7_device";
import { ServiceComponentsCreationResult } from "../ha/publish_device";
import {
VinculumPd7Device,
VinculumPd7Service,
} from '../fimp/vinculum_pd7_device';
import { ServiceComponentsCreationResult } from '../ha/publish_device';
export function sensor_uv__components(
topicPrefix: string,
device: VinculumPd7Device,
svc: VinculumPd7Service
svc: VinculumPd7Service,
): ServiceComponentsCreationResult | undefined {
const device_class = undefined;
const unit = svc.props?.sup_units?.[0] ?? 'index';
@ -18,6 +21,6 @@ export function sensor_uv__components(
unit_of_measurement: unit,
value_template: `{{ value_json['${svc.addr}'].sensor }}`,
},
}
},
};
}

View File

@ -1,10 +1,13 @@
import { VinculumPd7Device, VinculumPd7Service } from "../fimp/vinculum_pd7_device";
import { ServiceComponentsCreationResult } from "../ha/publish_device";
import {
VinculumPd7Device,
VinculumPd7Service,
} from '../fimp/vinculum_pd7_device';
import { ServiceComponentsCreationResult } from '../ha/publish_device';
export function sensor_veloc__components(
topicPrefix: string,
device: VinculumPd7Device,
svc: VinculumPd7Service
svc: VinculumPd7Service,
): ServiceComponentsCreationResult | undefined {
const device_class = undefined;
const unit = svc.props?.sup_units?.[0] ?? 'm/2';
@ -18,6 +21,6 @@ export function sensor_veloc__components(
unit_of_measurement: unit,
value_template: `{{ value_json['${svc.addr}'].sensor }}`,
},
}
},
};
}

View File

@ -1,10 +1,13 @@
import { VinculumPd7Device, VinculumPd7Service } from "../fimp/vinculum_pd7_device";
import { ServiceComponentsCreationResult } from "../ha/publish_device";
import {
VinculumPd7Device,
VinculumPd7Service,
} from '../fimp/vinculum_pd7_device';
import { ServiceComponentsCreationResult } from '../ha/publish_device';
export function sensor_voltage__components(
topicPrefix: string,
device: VinculumPd7Device,
svc: VinculumPd7Service
svc: VinculumPd7Service,
): ServiceComponentsCreationResult | undefined {
const device_class = 'voltage';
const unit = svc.props?.sup_units?.[0] ?? 'V';
@ -18,6 +21,6 @@ export function sensor_voltage__components(
unit_of_measurement: unit,
value_template: `{{ value_json['${svc.addr}'].sensor }}`,
},
}
},
};
}

View File

@ -1,10 +1,13 @@
import { VinculumPd7Device, VinculumPd7Service } from "../fimp/vinculum_pd7_device";
import { ServiceComponentsCreationResult } from "../ha/publish_device";
import {
VinculumPd7Device,
VinculumPd7Service,
} from '../fimp/vinculum_pd7_device';
import { ServiceComponentsCreationResult } from '../ha/publish_device';
export function sensor_watflow__components(
topicPrefix: string,
device: VinculumPd7Device,
svc: VinculumPd7Service
svc: VinculumPd7Service,
): ServiceComponentsCreationResult | undefined {
const device_class = 'volume_flow_rate';
const unit = svc.props?.sup_units?.[0] ?? 'l/h';
@ -18,6 +21,6 @@ export function sensor_watflow__components(
unit_of_measurement: unit,
value_template: `{{ value_json['${svc.addr}'].sensor }}`,
},
}
},
};
}

View File

@ -1,10 +1,13 @@
import { VinculumPd7Device, VinculumPd7Service } from "../fimp/vinculum_pd7_device";
import { ServiceComponentsCreationResult } from "../ha/publish_device";
import {
VinculumPd7Device,
VinculumPd7Service,
} from '../fimp/vinculum_pd7_device';
import { ServiceComponentsCreationResult } from '../ha/publish_device';
export function sensor_watpressure__components(
topicPrefix: string,
device: VinculumPd7Device,
svc: VinculumPd7Service
svc: VinculumPd7Service,
): ServiceComponentsCreationResult | undefined {
const device_class = 'pressure';
const unit = svc.props?.sup_units?.[0] ?? 'kPa';
@ -18,6 +21,6 @@ export function sensor_watpressure__components(
unit_of_measurement: unit,
value_template: `{{ value_json['${svc.addr}'].sensor }}`,
},
}
},
};
}

View File

@ -1,13 +1,16 @@
import { VinculumPd7Device, VinculumPd7Service } from "../fimp/vinculum_pd7_device";
import { ServiceComponentsCreationResult } from "../ha/publish_device";
import {
VinculumPd7Device,
VinculumPd7Service,
} from '../fimp/vinculum_pd7_device';
import { ServiceComponentsCreationResult } from '../ha/publish_device';
export function sensor_wattemp__components(
topicPrefix: string,
device: VinculumPd7Device,
svc: VinculumPd7Service
svc: VinculumPd7Service,
): ServiceComponentsCreationResult | undefined {
const device_class = 'temperature';
let unit = svc.props?.sup_units?.[0] ?? "°C";
let unit = svc.props?.sup_units?.[0] ?? '°C';
if (unit === 'C') unit = '°C';
if (unit === 'F') unit = '°F';

View File

@ -1,10 +1,13 @@
import { VinculumPd7Device, VinculumPd7Service } from "../fimp/vinculum_pd7_device";
import { ServiceComponentsCreationResult } from "../ha/publish_device";
import {
VinculumPd7Device,
VinculumPd7Service,
} from '../fimp/vinculum_pd7_device';
import { ServiceComponentsCreationResult } from '../ha/publish_device';
export function sensor_weight__components(
topicPrefix: string,
device: VinculumPd7Device,
svc: VinculumPd7Service
svc: VinculumPd7Service,
): ServiceComponentsCreationResult | undefined {
const device_class = 'weight';
const unit = svc.props?.sup_units?.[0] ?? 'kg';
@ -18,6 +21,6 @@ export function sensor_weight__components(
unit_of_measurement: unit,
value_template: `{{ value_json['${svc.addr}'].sensor }}`,
},
}
},
};
}

View File

@ -1,10 +1,13 @@
import { VinculumPd7Device, VinculumPd7Service } from "../fimp/vinculum_pd7_device";
import { ServiceComponentsCreationResult } from "../ha/publish_device";
import {
VinculumPd7Device,
VinculumPd7Service,
} from '../fimp/vinculum_pd7_device';
import { ServiceComponentsCreationResult } from '../ha/publish_device';
export function sensor_wind__components(
topicPrefix: string,
device: VinculumPd7Device,
svc: VinculumPd7Service
svc: VinculumPd7Service,
): ServiceComponentsCreationResult | undefined {
const device_class = 'wind_speed';
const unit = svc.props?.sup_units?.[0] ?? 'km/h';
@ -18,6 +21,6 @@ export function sensor_wind__components(
unit_of_measurement: unit,
value_template: `{{ value_json['${svc.addr}'].sensor }}`,
},
}
},
};
}

View File

@ -9,26 +9,29 @@
// temperature_command_topic → cmd.setpoint.set
// ─────────────────────────────────────────────────────────────────────────
import { sendFimpMsg } from "../fimp/fimp";
import { VinculumPd7Device, VinculumPd7Service } from "../fimp/vinculum_pd7_device";
import { ClimateComponent } from "../ha/mqtt_components/climate";
import { sendFimpMsg } from '../fimp/fimp';
import {
VinculumPd7Device,
VinculumPd7Service,
} from '../fimp/vinculum_pd7_device';
import { ClimateComponent } from '../ha/mqtt_components/climate';
import {
CommandHandlers,
ServiceComponentsCreationResult,
} from "../ha/publish_device";
import { haGetCachedState } from "../ha/update_state";
} from '../ha/publish_device';
import { haGetCachedState } from '../ha/update_state';
export function thermostat__components(
topicPrefix: string,
_device: VinculumPd7Device,
svc: VinculumPd7Service
svc: VinculumPd7Service,
): ServiceComponentsCreationResult | undefined {
const supModes: string[] = svc.props?.sup_modes ?? [];
const supSetpoints: string[] = svc.props?.sup_setpoints ?? [];
if (!supModes.length) return undefined; // nothing useful to expose
const defaultSpType = supSetpoints[0] ?? "heat";
const defaultSpType = supSetpoints[0] ?? 'heat';
const ranges: Record<string, { min?: number; max?: number }> =
svc.props?.sup_temperatures ?? {};
@ -82,9 +85,9 @@ export function thermostat__components(
if (!supModes.includes(payload)) return;
await sendFimpMsg({
address: svc.addr!,
service: "thermostat",
cmd: "cmd.mode.set",
val_t: "string",
service: 'thermostat',
cmd: 'cmd.mode.set',
val_t: 'string',
val: payload,
});
},
@ -95,13 +98,15 @@ export function thermostat__components(
await sendFimpMsg({
address: svc.addr!,
service: "thermostat",
cmd: "cmd.setpoint.set",
val_t: "str_map",
service: 'thermostat',
cmd: 'cmd.setpoint.set',
val_t: 'str_map',
val: {
type: haGetCachedState({ topic: `${topicPrefix}/state` })?.[svc.addr]?.mode ?? defaultSpType,
type:
haGetCachedState({ topic: `${topicPrefix}/state` })?.[svc.addr]
?.mode ?? defaultSpType,
temp: payload,
unit: "C",
unit: 'C',
},
});
},

View File

@ -1,3 +1,3 @@
export function delay(ms: number) {
return new Promise(resolve => setTimeout(resolve, ms));
return new Promise((resolve) => setTimeout(resolve, ms));
}