2025-07-24 16:13:39 +02:00
|
|
|
|
import { InclusionReport } from '../fimp/inclusion_report';
|
|
|
|
|
import {
|
|
|
|
|
VinculumPd7Device,
|
|
|
|
|
VinculumPd7Service,
|
|
|
|
|
} from '../fimp/vinculum_pd7_device';
|
|
|
|
|
import { log } from '../logger';
|
2025-07-26 02:38:22 +02:00
|
|
|
|
import { _alarm__components } from '../services/_alarm';
|
2025-07-26 21:28:23 +02:00
|
|
|
|
import { _sensor_binary__components } from '../services/_sensor_binary';
|
|
|
|
|
import { _sensor_numeric__components } from '../services/_sensor_numeric';
|
2025-07-24 23:14:34 +02:00
|
|
|
|
import { barrier_ctrl__components } from '../services/barrier_ctrl';
|
2025-07-24 16:13:39 +02:00
|
|
|
|
import { basic__components } from '../services/basic';
|
|
|
|
|
import { battery__components } from '../services/battery';
|
2025-07-25 13:32:50 +02:00
|
|
|
|
import { chargepoint__components } from '../services/chargepoint';
|
2025-07-24 16:13:39 +02:00
|
|
|
|
import { color_ctrl__components } from '../services/color_ctrl';
|
|
|
|
|
import { fan_ctrl__components } from '../services/fan_ctrl';
|
2025-07-24 22:50:34 +02:00
|
|
|
|
import { indicator_ctrl__components } from '../services/indicator_ctrl';
|
2025-07-25 15:40:28 +02:00
|
|
|
|
import { media_player__components } from '../services/media_player';
|
2025-07-24 16:13:39 +02:00
|
|
|
|
import { out_bin_switch__components } from '../services/out_bin_switch';
|
|
|
|
|
import { out_lvl_switch__components } from '../services/out_lvl_switch';
|
|
|
|
|
import { scene_ctrl__components } from '../services/scene_ctrl';
|
2025-07-25 21:10:57 +02:00
|
|
|
|
import { siren_ctrl__components } from '../services/siren_ctrl';
|
2025-07-24 16:13:39 +02:00
|
|
|
|
import { thermostat__components } from '../services/thermostat';
|
2025-07-25 01:03:19 +02:00
|
|
|
|
import { water_heater__components } from '../services/water_heater';
|
2025-07-24 16:29:51 +02:00
|
|
|
|
import { abbreviateHaMqttKeys } from './abbreviate_ha_mqtt_keys';
|
2025-07-24 16:13:39 +02:00
|
|
|
|
import { ha } from './globals';
|
|
|
|
|
import { HaMqttComponent } from './mqtt_components/_component';
|
2025-07-22 23:21:34 +02:00
|
|
|
|
|
|
|
|
|
type HaDeviceConfig = {
|
2025-07-24 16:13:39 +02:00
|
|
|
|
/**
|
|
|
|
|
* Information about the device this sensor is a part of to tie it into the [device registry](https://developers.home-assistant.io/docs/device_registry_index/).
|
|
|
|
|
* Only works when [`unique_id`](#unique_id) is set.
|
|
|
|
|
* At least one of identifiers or connections must be present to identify the device.
|
|
|
|
|
*/
|
|
|
|
|
device?: {
|
|
|
|
|
/**
|
|
|
|
|
* A link to the webpage that can manage the configuration of this device.
|
|
|
|
|
* Can be either an `http://`, `https://` or an internal `homeassistant://` URL.
|
|
|
|
|
*/
|
|
|
|
|
configuration_url?: string;
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* A list of connections of the device to the outside world as a list of tuples `[connection_type, connection_identifier]`.
|
|
|
|
|
* For example the MAC address of a network interface:
|
|
|
|
|
* `"connections": [["mac", "02:5b:26:a8:dc:12"]]`.
|
|
|
|
|
*/
|
|
|
|
|
connections?: Array<[string, string]>;
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* The hardware version of the device.
|
|
|
|
|
*/
|
|
|
|
|
hw_version?: string;
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* A list of IDs that uniquely identify the device.
|
|
|
|
|
* For example a serial number.
|
|
|
|
|
*/
|
|
|
|
|
identifiers?: string | string[];
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* The manufacturer of the device.
|
|
|
|
|
*/
|
|
|
|
|
manufacturer?: string;
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* The model of the device.
|
|
|
|
|
*/
|
|
|
|
|
model?: string;
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* The model identifier of the device.
|
|
|
|
|
*/
|
|
|
|
|
model_id?: string;
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* The name of the device.
|
|
|
|
|
*/
|
|
|
|
|
name?: string;
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* The serial number of the device.
|
|
|
|
|
*/
|
|
|
|
|
serial_number?: string;
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Suggest an area if the device isn’t in one yet.
|
|
|
|
|
*/
|
|
|
|
|
suggested_area?: string;
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* The firmware version of the device.
|
|
|
|
|
*/
|
|
|
|
|
sw_version?: string;
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Identifier of a device that routes messages between this device and Home Assistant.
|
|
|
|
|
* Examples of such devices are hubs, or parent devices of a sub-device.
|
|
|
|
|
* This is used to show device topology in Home Assistant.
|
|
|
|
|
*/
|
|
|
|
|
via_device?: string;
|
2025-07-22 23:21:34 +02:00
|
|
|
|
};
|
2025-07-24 16:13:39 +02:00
|
|
|
|
origin: {
|
|
|
|
|
name: 'futurehome';
|
2025-07-24 16:29:51 +02:00
|
|
|
|
support_url: 'https://github.com/adrianjagielak/home-assistant-futurehome';
|
2025-07-22 23:21:34 +02:00
|
|
|
|
};
|
2025-07-24 16:13:39 +02:00
|
|
|
|
components: {
|
2025-07-24 15:54:47 +02:00
|
|
|
|
[key: string]: HaMqttComponent;
|
2025-07-24 16:13:39 +02:00
|
|
|
|
};
|
|
|
|
|
state_topic: string;
|
|
|
|
|
availability_topic: string;
|
|
|
|
|
qos: number;
|
|
|
|
|
};
|
2025-07-22 23:21:34 +02:00
|
|
|
|
|
2025-07-23 15:34:22 +02:00
|
|
|
|
export type ServiceComponentsCreationResult = {
|
2025-07-24 15:54:47 +02:00
|
|
|
|
components: { [key: string]: HaMqttComponent };
|
2025-07-23 15:34:22 +02:00
|
|
|
|
commandHandlers?: CommandHandlers;
|
2025-07-24 16:13:39 +02:00
|
|
|
|
};
|
2025-07-22 23:21:34 +02:00
|
|
|
|
|
2025-07-24 16:13:39 +02:00
|
|
|
|
export type CommandHandlers = {
|
|
|
|
|
[topic: string]: (payload: string) => Promise<void>;
|
|
|
|
|
};
|
2025-07-23 15:34:22 +02:00
|
|
|
|
|
2025-07-22 23:21:34 +02:00
|
|
|
|
const serviceHandlers: {
|
2025-07-24 16:13:39 +02:00
|
|
|
|
[name: string]: (
|
|
|
|
|
topicPrefix: string,
|
|
|
|
|
device: VinculumPd7Device,
|
|
|
|
|
svc: VinculumPd7Service,
|
2025-07-26 02:38:22 +02:00
|
|
|
|
svcName: string,
|
2025-07-24 16:13:39 +02:00
|
|
|
|
) => ServiceComponentsCreationResult | undefined;
|
2025-07-22 23:21:34 +02:00
|
|
|
|
} = {
|
2025-07-26 02:38:22 +02:00
|
|
|
|
alarm_appliance: _alarm__components,
|
|
|
|
|
alarm_burglar: _alarm__components,
|
|
|
|
|
alarm_emergency: _alarm__components,
|
|
|
|
|
alarm_fire: _alarm__components,
|
|
|
|
|
alarm_gas: _alarm__components,
|
|
|
|
|
alarm_health: _alarm__components,
|
|
|
|
|
alarm_heat: _alarm__components,
|
|
|
|
|
alarm_lock: _alarm__components,
|
|
|
|
|
alarm_power: _alarm__components,
|
|
|
|
|
alarm_siren: _alarm__components,
|
|
|
|
|
alarm_system: _alarm__components,
|
|
|
|
|
alarm_time: _alarm__components,
|
|
|
|
|
alarm_water_valve: _alarm__components,
|
|
|
|
|
alarm_water: _alarm__components,
|
|
|
|
|
alarm_weather: _alarm__components,
|
2025-07-24 23:14:34 +02:00
|
|
|
|
barrier_ctrl: barrier_ctrl__components,
|
2025-07-23 22:47:58 +02:00
|
|
|
|
basic: basic__components,
|
2025-07-23 13:06:54 +02:00
|
|
|
|
battery: battery__components,
|
2025-07-25 13:32:50 +02:00
|
|
|
|
chargepoint: chargepoint__components,
|
2025-07-24 01:59:56 +02:00
|
|
|
|
color_ctrl: color_ctrl__components,
|
2025-07-23 23:19:06 +02:00
|
|
|
|
fan_ctrl: fan_ctrl__components,
|
2025-07-24 22:50:34 +02:00
|
|
|
|
indicator_ctrl: indicator_ctrl__components,
|
2025-07-25 15:40:28 +02:00
|
|
|
|
media_player: media_player__components,
|
2025-07-23 13:06:54 +02:00
|
|
|
|
out_bin_switch: out_bin_switch__components,
|
|
|
|
|
out_lvl_switch: out_lvl_switch__components,
|
2025-07-23 23:12:15 +02:00
|
|
|
|
scene_ctrl: scene_ctrl__components,
|
2025-07-26 21:28:23 +02:00
|
|
|
|
sensor_accelx: _sensor_numeric__components,
|
|
|
|
|
sensor_accely: _sensor_numeric__components,
|
|
|
|
|
sensor_accelz: _sensor_numeric__components,
|
|
|
|
|
sensor_airflow: _sensor_numeric__components,
|
|
|
|
|
sensor_airq: _sensor_numeric__components,
|
|
|
|
|
sensor_anglepos: _sensor_numeric__components,
|
|
|
|
|
sensor_atmo: _sensor_numeric__components,
|
|
|
|
|
sensor_baro: _sensor_numeric__components,
|
|
|
|
|
sensor_co: _sensor_numeric__components,
|
|
|
|
|
sensor_co2: _sensor_numeric__components,
|
|
|
|
|
sensor_contact: _sensor_binary__components,
|
|
|
|
|
sensor_current: _sensor_numeric__components,
|
|
|
|
|
sensor_dew: _sensor_numeric__components,
|
|
|
|
|
sensor_direct: _sensor_numeric__components,
|
|
|
|
|
sensor_distance: _sensor_numeric__components,
|
|
|
|
|
sensor_elresist: _sensor_numeric__components,
|
|
|
|
|
sensor_freq: _sensor_numeric__components,
|
|
|
|
|
sensor_gp: _sensor_numeric__components,
|
|
|
|
|
sensor_gust: _sensor_numeric__components,
|
|
|
|
|
sensor_humid: _sensor_numeric__components,
|
|
|
|
|
sensor_lumin: _sensor_numeric__components,
|
|
|
|
|
sensor_moist: _sensor_numeric__components,
|
|
|
|
|
sensor_noise: _sensor_numeric__components,
|
|
|
|
|
sensor_power: _sensor_numeric__components,
|
|
|
|
|
sensor_presence: _sensor_binary__components,
|
|
|
|
|
sensor_rain: _sensor_numeric__components,
|
|
|
|
|
sensor_rotation: _sensor_numeric__components,
|
|
|
|
|
sensor_seismicint: _sensor_numeric__components,
|
|
|
|
|
sensor_seismicmag: _sensor_numeric__components,
|
|
|
|
|
sensor_solarrad: _sensor_numeric__components,
|
|
|
|
|
sensor_tank: _sensor_numeric__components,
|
|
|
|
|
sensor_temp: _sensor_numeric__components,
|
|
|
|
|
sensor_tidelvl: _sensor_numeric__components,
|
|
|
|
|
sensor_uv: _sensor_numeric__components,
|
|
|
|
|
sensor_veloc: _sensor_numeric__components,
|
|
|
|
|
sensor_voltage: _sensor_numeric__components,
|
|
|
|
|
sensor_watflow: _sensor_numeric__components,
|
|
|
|
|
sensor_watpressure: _sensor_numeric__components,
|
|
|
|
|
sensor_wattemp: _sensor_numeric__components,
|
|
|
|
|
sensor_weight: _sensor_numeric__components,
|
|
|
|
|
sensor_wind: _sensor_numeric__components,
|
2025-07-25 21:10:57 +02:00
|
|
|
|
siren_ctrl: siren_ctrl__components,
|
2025-07-23 22:32:23 +02:00
|
|
|
|
thermostat: thermostat__components,
|
2025-07-25 01:03:19 +02:00
|
|
|
|
water_heater: water_heater__components,
|
2025-07-22 23:21:34 +02:00
|
|
|
|
};
|
|
|
|
|
|
2025-07-25 01:58:13 +02:00
|
|
|
|
// Defines service exclusions based on higher-level MQTT entity types.
|
|
|
|
|
// For example, if a device has a `thermostat` service, we skip `sensor_temp`
|
|
|
|
|
// because the thermostat component itself already reads and exposes the
|
|
|
|
|
// temperature internally. Similarly, `sensor_wattemp` is skipped when
|
|
|
|
|
// `water_heater` is present to avoid creating redundant entities.
|
|
|
|
|
const serviceExclusionMap: Record<string, string[]> = {
|
|
|
|
|
sensor_temp: ['thermostat'],
|
|
|
|
|
sensor_wattemp: ['water_heater'],
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Determines whether a given service should be published as a separate entity.
|
|
|
|
|
*
|
|
|
|
|
* Certain services (e.g., `sensor_temp`) are excluded when higher-level
|
|
|
|
|
* services (e.g., `thermostat`) are present, because those higher-level
|
|
|
|
|
* services already consume the lower-level state and expose it through
|
|
|
|
|
* their own MQTT entities.
|
|
|
|
|
*
|
|
|
|
|
* @param svcName - The name of the service being evaluated.
|
|
|
|
|
* @param services - A map of all services available for the device.
|
|
|
|
|
* @returns `true` if the service should be published, `false` if it is excluded.
|
|
|
|
|
*/
|
|
|
|
|
function shouldPublishService(
|
|
|
|
|
svcName: string,
|
|
|
|
|
services: { [name: string]: VinculumPd7Service },
|
|
|
|
|
): boolean {
|
|
|
|
|
const exclusions = serviceExclusionMap[svcName];
|
|
|
|
|
if (!exclusions) return true;
|
|
|
|
|
|
|
|
|
|
return !exclusions.some((excludedService) => excludedService in services);
|
|
|
|
|
}
|
|
|
|
|
|
2025-07-24 16:13:39 +02:00
|
|
|
|
export function haPublishDevice(parameters: {
|
|
|
|
|
hubId: string;
|
2025-07-24 16:51:19 +02:00
|
|
|
|
demoMode: boolean;
|
2025-07-24 16:13:39 +02:00
|
|
|
|
vinculumDeviceData: VinculumPd7Device;
|
|
|
|
|
deviceInclusionReport: InclusionReport | undefined;
|
|
|
|
|
}): { commandHandlers: CommandHandlers } {
|
2025-07-24 15:54:47 +02:00
|
|
|
|
const components: { [key: string]: HaMqttComponent } = {};
|
2025-07-23 15:34:22 +02:00
|
|
|
|
const handlers: CommandHandlers = {};
|
|
|
|
|
|
|
|
|
|
// e.g. "homeassistant/device/futurehome_123456_1"
|
2025-07-23 20:24:14 +02:00
|
|
|
|
const topicPrefix = `homeassistant/device/futurehome_${parameters.hubId}_${parameters.vinculumDeviceData.id}`;
|
2025-07-22 23:21:34 +02:00
|
|
|
|
|
2025-07-24 16:13:39 +02:00
|
|
|
|
for (const [svcName, svc] of Object.entries(
|
|
|
|
|
parameters.vinculumDeviceData.services ?? {},
|
|
|
|
|
)) {
|
|
|
|
|
if (!svcName) {
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
if (!svc.addr) {
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
if (!svc.enabled) {
|
|
|
|
|
continue;
|
|
|
|
|
}
|
2025-07-25 01:58:13 +02:00
|
|
|
|
// Skip publishing services that are already represented by higher-level MQTT entities
|
|
|
|
|
if (
|
|
|
|
|
!shouldPublishService(
|
|
|
|
|
svcName,
|
|
|
|
|
parameters.vinculumDeviceData.services ?? {},
|
|
|
|
|
)
|
|
|
|
|
) {
|
|
|
|
|
log.debug(
|
|
|
|
|
`Skipping service ${svcName} because a higher-level service handles its data`,
|
|
|
|
|
);
|
|
|
|
|
continue;
|
|
|
|
|
}
|
2025-07-23 16:26:41 +02:00
|
|
|
|
|
2025-07-23 20:24:14 +02:00
|
|
|
|
const handler = serviceHandlers[svcName];
|
2025-07-23 16:26:41 +02:00
|
|
|
|
if (!handler) {
|
2025-07-23 20:24:14 +02:00
|
|
|
|
log.error(`No handler for service: ${svcName}`);
|
2025-07-23 16:26:41 +02:00
|
|
|
|
continue;
|
2025-07-22 23:21:34 +02:00
|
|
|
|
}
|
2025-07-23 16:26:41 +02:00
|
|
|
|
|
2025-07-26 02:38:22 +02:00
|
|
|
|
const result = handler(
|
|
|
|
|
topicPrefix,
|
|
|
|
|
parameters.vinculumDeviceData,
|
|
|
|
|
svc,
|
|
|
|
|
svcName,
|
|
|
|
|
);
|
2025-07-23 16:26:41 +02:00
|
|
|
|
if (!result) {
|
2025-07-24 16:13:39 +02:00
|
|
|
|
log.error(
|
|
|
|
|
`Invalid service data prevented component creation: ${parameters.vinculumDeviceData} ${svc}`,
|
|
|
|
|
);
|
2025-07-23 20:24:14 +02:00
|
|
|
|
continue;
|
2025-07-23 16:26:41 +02:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
Object.assign(components, result.components);
|
|
|
|
|
Object.assign(handlers, result.commandHandlers);
|
2025-07-22 23:21:34 +02:00
|
|
|
|
}
|
|
|
|
|
|
2025-07-25 01:04:46 +02:00
|
|
|
|
let vinculumManufacturer: string | undefined;
|
|
|
|
|
const parts = (parameters.vinculumDeviceData?.model ?? '').split(' - ');
|
|
|
|
|
if (parts.length === 3) {
|
|
|
|
|
vinculumManufacturer = parts[1];
|
|
|
|
|
}
|
|
|
|
|
|
2025-07-24 16:13:39 +02:00
|
|
|
|
const configTopic = `${topicPrefix}/config`;
|
|
|
|
|
const stateTopic = `${topicPrefix}/state`;
|
|
|
|
|
const availabilityTopic = `${topicPrefix}/availability`;
|
2025-07-22 23:21:34 +02:00
|
|
|
|
const config: HaDeviceConfig = {
|
2025-07-24 16:13:39 +02:00
|
|
|
|
device: {
|
|
|
|
|
identifiers: parameters.vinculumDeviceData.id.toString(),
|
|
|
|
|
name:
|
|
|
|
|
parameters.vinculumDeviceData?.client?.name ??
|
|
|
|
|
parameters.vinculumDeviceData?.modelAlias ??
|
|
|
|
|
parameters.deviceInclusionReport?.product_name ??
|
|
|
|
|
undefined,
|
|
|
|
|
manufacturer:
|
2025-07-25 01:04:46 +02:00
|
|
|
|
vinculumManufacturer ??
|
|
|
|
|
parameters.deviceInclusionReport?.manufacturer_id ??
|
|
|
|
|
undefined,
|
2025-07-24 16:13:39 +02:00
|
|
|
|
model:
|
|
|
|
|
parameters.vinculumDeviceData?.modelAlias ??
|
|
|
|
|
parameters.deviceInclusionReport?.product_id ??
|
|
|
|
|
undefined,
|
|
|
|
|
sw_version: parameters.deviceInclusionReport?.sw_ver ?? undefined,
|
|
|
|
|
serial_number:
|
|
|
|
|
parameters.deviceInclusionReport?.product_hash ?? undefined,
|
|
|
|
|
hw_version: parameters.deviceInclusionReport?.hw_ver ?? undefined,
|
|
|
|
|
via_device: 'todo_hub_id',
|
2025-07-22 23:21:34 +02:00
|
|
|
|
},
|
2025-07-24 16:13:39 +02:00
|
|
|
|
origin: {
|
2025-07-22 23:21:34 +02:00
|
|
|
|
name: 'futurehome',
|
2025-07-24 16:34:58 +02:00
|
|
|
|
support_url:
|
|
|
|
|
'https://github.com/adrianjagielak/home-assistant-futurehome',
|
2025-07-22 23:21:34 +02:00
|
|
|
|
},
|
2025-07-24 16:13:39 +02:00
|
|
|
|
components: components,
|
|
|
|
|
state_topic: stateTopic,
|
|
|
|
|
availability_topic: availabilityTopic,
|
2025-07-22 23:21:34 +02:00
|
|
|
|
qos: 2,
|
|
|
|
|
};
|
|
|
|
|
|
2025-07-23 15:34:22 +02:00
|
|
|
|
log.debug(`Publishing HA device "${configTopic}"`);
|
2025-07-24 16:34:58 +02:00
|
|
|
|
ha?.publish(configTopic, JSON.stringify(abbreviateHaMqttKeys(config)), {
|
|
|
|
|
retain: true,
|
|
|
|
|
qos: 2,
|
|
|
|
|
});
|
2025-07-23 15:34:22 +02:00
|
|
|
|
|
|
|
|
|
return { commandHandlers: handlers };
|
2025-07-24 16:13:39 +02:00
|
|
|
|
}
|