From 1aad80ed3847f744684570354a8102b4c0540315 Mon Sep 17 00:00:00 2001 From: Adrian Jagielak Date: Mon, 28 Jul 2025 16:57:57 +0200 Subject: [PATCH] Inclusion/exclusion improvements --- futurehome/src/fimp/vinculum_pd7_device.ts | 130 +++++++++++---------- futurehome/src/ha/admin.ts | 23 +--- futurehome/src/ha/publish_device.ts | 49 ++++---- futurehome/src/index.ts | 17 ++- futurehome/src/thingsplex/thingsplex.ts | 10 ++ 5 files changed, 121 insertions(+), 108 deletions(-) diff --git a/futurehome/src/fimp/vinculum_pd7_device.ts b/futurehome/src/fimp/vinculum_pd7_device.ts index 17cb97d..5002aa3 100644 --- a/futurehome/src/fimp/vinculum_pd7_device.ts +++ b/futurehome/src/fimp/vinculum_pd7_device.ts @@ -1,3 +1,61 @@ +export type DeviceFunctionality = + | 'appliance' + | 'climate' + | 'energy' + | 'ev_charger' + | 'lighting' + | 'media' + | 'other' + | 'power' + | 'safety' + | 'security' + | 'shading' + | string; + +export type DeviceType = + | 'appliance' + | 'battery' + | 'blinds' + | 'boiler' + | 'chargepoint' + | 'door_lock' + | 'energy_storage' + | 'fan' + | 'fire_detector' + | 'garage_door' + | 'gas_detector' + | 'gate' + | 'heat_detector' + | 'heat_pump' + | 'heater' + | 'input' + | 'inverter' + | 'leak_detector' + | 'light' + | 'media_player' + | 'meter' + | 'none' + | 'sensor' + | 'siren' + | 'thermostat' + | 'water_valve' + | string; + +export type DeviceSubType = + | 'car_charger' + | 'door' + | 'door_lock' + | 'garage' + | 'lock' + | 'main_elec' + | 'none' + | 'other' + | 'presence' + | 'scene' + | 'window' + | 'window_lock' + | string; + export type VinculumPd7Device = { client?: { // User-defined device name @@ -5,75 +63,29 @@ export type VinculumPd7Device = { } | null; // FIMP Device ID. id: number; + fimp?: { + adapter?: 'zigbee' | 'zwave-ad' | string | null; + // FIMP Device address (ID) in the context of its adapter. + address?: string | null; + } | null; // FIMP Thing Address (ID). - thing?: number | null; + thing?: string | null; // "Model" string, e.g. "zb - _TZ3040_bb6xaihh - TS0202" // The first one is the adapter, the second one is the manufacturer, the third one is the device model. model?: string | null; // Device model, e.g. "TS0202" modelAlias?: string | null; - functionality?: - | 'appliance' - | 'climate' - | 'energy' - | 'ev_charger' - | 'lighting' - | 'media' - | 'other' - | 'power' - | 'safety' - | 'security' - | 'shading' - | string - | null; + functionality?: DeviceFunctionality | null; services?: Record | null; type?: { // User-defined device type - type?: - | 'appliance' - | 'battery' - | 'blinds' - | 'boiler' - | 'chargepoint' - | 'door_lock' - | 'energy_storage' - | 'fan' - | 'fire_detector' - | 'garage_door' - | 'gas_detector' - | 'gate' - | 'heat_detector' - | 'heat_pump' - | 'heater' - | 'input' - | 'inverter' - | 'leak_detector' - | 'light' - | 'media_player' - | 'meter' - | 'none' - | 'sensor' - | 'siren' - | 'thermostat' - | 'water_valve' - | string - | null; + type?: DeviceType | null; // User-defined device subtype - subtype?: - | 'car_charger' - | 'door' - | 'door_lock' - | 'garage' - | 'lock' - | 'main_elec' - | 'none' - | 'other' - | 'presence' - | 'scene' - | 'window' - | 'window_lock' - | string - | null; + subtype?: DeviceSubType | null; + // Supported device types and subtypes for this device + supported?: { + [key: DeviceType]: DeviceSubType[]; + } | null; } | null; }; diff --git a/futurehome/src/ha/admin.ts b/futurehome/src/ha/admin.ts index e4b7ccc..85df416 100644 --- a/futurehome/src/ha/admin.ts +++ b/futurehome/src/ha/admin.ts @@ -340,21 +340,9 @@ export function handleInclusionStatusReport(hubId: string, msg: FimpResponse) { case 'ADD_NODE_GET_NODE_INFO': case 'ADD_NODE_PROTOCOL_DONE': localizedStatus = 'Device added successfully!'; - pollVinculum('device').catch((e) => - log.warn('Failed to request devices', e), - ); - pollVinculum('state').catch((e) => - log.warn('Failed to request state', e), - ); break; case 'ADD_NODE_DONE': localizedStatus = 'Done'; - pollVinculum('device').catch((e) => - log.warn('Failed to request devices', e), - ); - pollVinculum('state').catch((e) => - log.warn('Failed to request state', e), - ); break; case 'NET_NODE_INCL_CTRL_OP_FAILED': localizedStatus = "Operation failed. The device can't be included."; @@ -389,12 +377,6 @@ export function handleExclusionStatusReport(hubId: string, msg: FimpResponse) { break; case 'REMOVE_NODE_DONE': localizedStatus = 'Done'; - pollVinculum('device').catch((e) => - log.warn('Failed to request devices', e), - ); - pollVinculum('state').catch((e) => - log.warn('Failed to request state', e), - ); break; case 'NET_NODE_REMOVE_FAILED': localizedStatus = "Operation failed. The device can't be excluded."; @@ -415,6 +397,11 @@ export function handleExclusionStatusReport(hubId: string, msg: FimpResponse) { ); } +export function handleInclusionReport() { + pollVinculum('device').catch((e) => log.warn('Failed to request devices', e)); + pollVinculum('state').catch((e) => log.warn('Failed to request state', e)); +} + export function handleExclusionReport() { pollVinculum('device').catch((e) => log.warn('Failed to request devices', e)); pollVinculum('state').catch((e) => log.warn('Failed to request state', e)); diff --git a/futurehome/src/ha/publish_device.ts b/futurehome/src/ha/publish_device.ts index fcdf673..a6ed159 100644 --- a/futurehome/src/ha/publish_device.ts +++ b/futurehome/src/ha/publish_device.ts @@ -31,7 +31,10 @@ import { sound_switch__components } from '../services/sound_switch'; import { thermostat__components } from '../services/thermostat'; import { user_code__components } from '../services/user_code'; import { water_heater__components } from '../services/water_heater'; -import { connectThingsplexWebSocketAndSend, loginToThingsplex } from '../thingsplex/thingsplex'; +import { + connectThingsplexWebSocketAndSend, + loginToThingsplex, +} from '../thingsplex/thingsplex'; import { abbreviateHaMqttKeys } from './abbreviate_ha_mqtt_keys'; import { ha, haCommandHandlers } from './globals'; import { HaDeviceConfig } from './ha_device_config'; @@ -258,35 +261,26 @@ export function haPublishDevice(parameters: { Object.assign(handlers, result.commandHandlers); } - const firstSvcAddr = - Object.entries(parameters.vinculumDeviceData.services ?? {})?.[0]?.[1] - .addr ?? ''; if ( parameters.thingsplexUsername && parameters.thingsplexPassword && - parameters.vinculumDeviceData.thing && - (firstSvcAddr.includes('/rn:zigbee/ad:1/') || - firstSvcAddr.includes('/rn:zw/ad:1/')) + parameters.vinculumDeviceData.fimp?.address && + (parameters.vinculumDeviceData.fimp?.adapter === 'zigbee' || + parameters.vinculumDeviceData.fimp?.adapter === 'zwave-ad') ) { const deleteCommandTopic = `${topicPrefix}/delete/command`; - const availabilityTopic = `${topicPrefix}/delete/availability`; components[`${topicPrefix}_delete_button`] = { unique_id: `${topicPrefix}_delete_button`, platform: 'button', entity_category: 'diagnostic', - name: firstSvcAddr.includes('/rn:zigbee/ad:1/') - ? 'ZigBee: Unpair Device' - : 'Z-Wave: Unpair Device', + name: + parameters.vinculumDeviceData.fimp?.adapter === 'zigbee' + ? 'ZigBee: Unpair Device' + : 'Z-Wave: Unpair Device', icon: 'mdi:delete-forever', command_topic: deleteCommandTopic, - availability_topic: availabilityTopic, - availability_template: `{% if value == "online" or value == "" %}online{% else %}offline{% endif %}`, } as any; handlers[deleteCommandTopic] = async (_payload: string) => { - ha?.publish(availabilityTopic, 'offline', { - retain: true, - qos: 2, - }); try { const token = await loginToThingsplex({ host: parameters.hubIp, @@ -300,25 +294,24 @@ export function haPublishDevice(parameters: { }, [ { - address: firstSvcAddr.includes('/rn:zigbee/ad:1/') - ? 'pt:j1/mt:cmd/rt:ad/rn:zigbee/ad:1' - : 'pt:j1/mt:evt/rt:ad/rn:zw/ad:1', - service: firstSvcAddr.includes('/rn:zigbee/ad:1/') - ? 'zigbee' - : 'zwave-ad', + address: + parameters.vinculumDeviceData.fimp?.adapter === 'zigbee' + ? 'pt:j1/mt:cmd/rt:ad/rn:zigbee/ad:1' + : 'pt:j1/mt:evt/rt:ad/rn:zw/ad:1', + service: + parameters.vinculumDeviceData.fimp?.adapter === 'zigbee' + ? 'zigbee' + : 'zwave-ad', cmd: 'cmd.thing.delete', val_t: 'str_map', val: { - address: parameters.vinculumDeviceData.thing, + address: parameters.vinculumDeviceData.fimp?.address, }, }, ], ); } catch (e) { - ha?.publish(availabilityTopic, 'online', { - retain: true, - qos: 2, - }); + log.error('Failed to delete device:', e); } }; } diff --git a/futurehome/src/index.ts b/futurehome/src/index.ts index 75d1c87..e65bad0 100644 --- a/futurehome/src/index.ts +++ b/futurehome/src/index.ts @@ -7,7 +7,13 @@ import { haUpdateState, haUpdateStateValueReport } from './ha/update_state'; import { VinculumPd7Device } from './fimp/vinculum_pd7_device'; import { haUpdateAvailability } from './ha/update_availability'; import { delay } from './utils'; -import { exposeSmarthubTools, handleExclusionReport, handleExclusionStatusReport, handleInclusionStatusReport } from './ha/admin'; +import { + exposeSmarthubTools, + handleExclusionReport, + handleExclusionStatusReport, + handleInclusionReport, + handleInclusionStatusReport, +} from './ha/admin'; import { pollVinculum } from './fimp/vinculum'; (async () => { @@ -264,12 +270,17 @@ import { pollVinculum } from './fimp/vinculum'; handleInclusionStatusReport(hubId, msg); break; } - + case 'evt.thing.exclusion_status_report': { handleExclusionStatusReport(hubId, msg); break; } - + + case 'evt.thing.inclusion_report': { + handleInclusionReport(); + break; + } + case 'evt.thing.exclusion_report': { handleExclusionReport(); break; diff --git a/futurehome/src/thingsplex/thingsplex.ts b/futurehome/src/thingsplex/thingsplex.ts index 942050a..ee129ec 100644 --- a/futurehome/src/thingsplex/thingsplex.ts +++ b/futurehome/src/thingsplex/thingsplex.ts @@ -7,6 +7,11 @@ import { v4 as uuidv4 } from 'uuid'; /** * Logs in to the Thingsplex and extracts the tplex token. + * + * The use of Thingsplex is required for including and excluding devices, + * as the regular Local API (SmartHub MQTT broker on port 1884) + * does not support inclusion or exclusion commands. + * * @param username - The login username * @param password - The login password * @returns The tplex token if login is successful @@ -54,6 +59,11 @@ export async function loginToThingsplex(parameters: { /** * Connects to the Thingsplex websocket with the tplex token and sends the messages. + * + * The use of Thingsplex is required for including and excluding devices, + * as the regular Local API (SmartHub MQTT broker on port 1884) + * does not support inclusion or exclusion commands. + * * @param token - The tplex token from login */ export function connectThingsplexWebSocketAndSend(