Inclusion/exclusion improvements

This commit is contained in:
Adrian Jagielak 2025-07-28 16:57:57 +02:00
parent d131297f2a
commit 1aad80ed38
No known key found for this signature in database
GPG Key ID: 0818CF7AF6C62BFB
5 changed files with 121 additions and 108 deletions

View File

@ -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 = { export type VinculumPd7Device = {
client?: { client?: {
// User-defined device name // User-defined device name
@ -5,75 +63,29 @@ export type VinculumPd7Device = {
} | null; } | null;
// FIMP Device ID. // FIMP Device ID.
id: number; 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). // FIMP Thing Address (ID).
thing?: number | null; thing?: string | null;
// "Model" string, e.g. "zb - _TZ3040_bb6xaihh - TS0202" // "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. // The first one is the adapter, the second one is the manufacturer, the third one is the device model.
model?: string | null; model?: string | null;
// Device model, e.g. "TS0202" // Device model, e.g. "TS0202"
modelAlias?: string | null; modelAlias?: string | null;
functionality?: functionality?: DeviceFunctionality | null;
| 'appliance'
| 'climate'
| 'energy'
| 'ev_charger'
| 'lighting'
| 'media'
| 'other'
| 'power'
| 'safety'
| 'security'
| 'shading'
| string
| null;
services?: Record<string, VinculumPd7Service> | null; services?: Record<string, VinculumPd7Service> | null;
type?: { type?: {
// User-defined device type // User-defined device type
type?: type?: DeviceType | null;
| '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;
// User-defined device subtype // User-defined device subtype
subtype?: subtype?: DeviceSubType | null;
| 'car_charger' // Supported device types and subtypes for this device
| 'door' supported?: {
| 'door_lock' [key: DeviceType]: DeviceSubType[];
| 'garage' } | null;
| 'lock'
| 'main_elec'
| 'none'
| 'other'
| 'presence'
| 'scene'
| 'window'
| 'window_lock'
| string
| null;
} | null; } | null;
}; };

View File

@ -340,21 +340,9 @@ export function handleInclusionStatusReport(hubId: string, msg: FimpResponse) {
case 'ADD_NODE_GET_NODE_INFO': case 'ADD_NODE_GET_NODE_INFO':
case 'ADD_NODE_PROTOCOL_DONE': case 'ADD_NODE_PROTOCOL_DONE':
localizedStatus = 'Device added successfully!'; 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; break;
case 'ADD_NODE_DONE': case 'ADD_NODE_DONE':
localizedStatus = '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; break;
case 'NET_NODE_INCL_CTRL_OP_FAILED': case 'NET_NODE_INCL_CTRL_OP_FAILED':
localizedStatus = "Operation failed. The device can't be included."; localizedStatus = "Operation failed. The device can't be included.";
@ -389,12 +377,6 @@ export function handleExclusionStatusReport(hubId: string, msg: FimpResponse) {
break; break;
case 'REMOVE_NODE_DONE': case 'REMOVE_NODE_DONE':
localizedStatus = '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; break;
case 'NET_NODE_REMOVE_FAILED': case 'NET_NODE_REMOVE_FAILED':
localizedStatus = "Operation failed. The device can't be excluded."; 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() { export function handleExclusionReport() {
pollVinculum('device').catch((e) => log.warn('Failed to request devices', e)); pollVinculum('device').catch((e) => log.warn('Failed to request devices', e));
pollVinculum('state').catch((e) => log.warn('Failed to request state', e)); pollVinculum('state').catch((e) => log.warn('Failed to request state', e));

View File

@ -31,7 +31,10 @@ import { sound_switch__components } from '../services/sound_switch';
import { thermostat__components } from '../services/thermostat'; import { thermostat__components } from '../services/thermostat';
import { user_code__components } from '../services/user_code'; import { user_code__components } from '../services/user_code';
import { water_heater__components } from '../services/water_heater'; 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 { abbreviateHaMqttKeys } from './abbreviate_ha_mqtt_keys';
import { ha, haCommandHandlers } from './globals'; import { ha, haCommandHandlers } from './globals';
import { HaDeviceConfig } from './ha_device_config'; import { HaDeviceConfig } from './ha_device_config';
@ -258,35 +261,26 @@ export function haPublishDevice(parameters: {
Object.assign(handlers, result.commandHandlers); Object.assign(handlers, result.commandHandlers);
} }
const firstSvcAddr =
Object.entries(parameters.vinculumDeviceData.services ?? {})?.[0]?.[1]
.addr ?? '';
if ( if (
parameters.thingsplexUsername && parameters.thingsplexUsername &&
parameters.thingsplexPassword && parameters.thingsplexPassword &&
parameters.vinculumDeviceData.thing && parameters.vinculumDeviceData.fimp?.address &&
(firstSvcAddr.includes('/rn:zigbee/ad:1/') || (parameters.vinculumDeviceData.fimp?.adapter === 'zigbee' ||
firstSvcAddr.includes('/rn:zw/ad:1/')) parameters.vinculumDeviceData.fimp?.adapter === 'zwave-ad')
) { ) {
const deleteCommandTopic = `${topicPrefix}/delete/command`; const deleteCommandTopic = `${topicPrefix}/delete/command`;
const availabilityTopic = `${topicPrefix}/delete/availability`;
components[`${topicPrefix}_delete_button`] = { components[`${topicPrefix}_delete_button`] = {
unique_id: `${topicPrefix}_delete_button`, unique_id: `${topicPrefix}_delete_button`,
platform: 'button', platform: 'button',
entity_category: 'diagnostic', entity_category: 'diagnostic',
name: firstSvcAddr.includes('/rn:zigbee/ad:1/') name:
? 'ZigBee: Unpair Device' parameters.vinculumDeviceData.fimp?.adapter === 'zigbee'
: 'Z-Wave: Unpair Device', ? 'ZigBee: Unpair Device'
: 'Z-Wave: Unpair Device',
icon: 'mdi:delete-forever', icon: 'mdi:delete-forever',
command_topic: deleteCommandTopic, command_topic: deleteCommandTopic,
availability_topic: availabilityTopic,
availability_template: `{% if value == "online" or value == "" %}online{% else %}offline{% endif %}`,
} as any; } as any;
handlers[deleteCommandTopic] = async (_payload: string) => { handlers[deleteCommandTopic] = async (_payload: string) => {
ha?.publish(availabilityTopic, 'offline', {
retain: true,
qos: 2,
});
try { try {
const token = await loginToThingsplex({ const token = await loginToThingsplex({
host: parameters.hubIp, host: parameters.hubIp,
@ -300,25 +294,24 @@ export function haPublishDevice(parameters: {
}, },
[ [
{ {
address: firstSvcAddr.includes('/rn:zigbee/ad:1/') address:
? 'pt:j1/mt:cmd/rt:ad/rn:zigbee/ad:1' parameters.vinculumDeviceData.fimp?.adapter === 'zigbee'
: 'pt:j1/mt:evt/rt:ad/rn:zw/ad:1', ? 'pt:j1/mt:cmd/rt:ad/rn:zigbee/ad:1'
service: firstSvcAddr.includes('/rn:zigbee/ad:1/') : 'pt:j1/mt:evt/rt:ad/rn:zw/ad:1',
? 'zigbee' service:
: 'zwave-ad', parameters.vinculumDeviceData.fimp?.adapter === 'zigbee'
? 'zigbee'
: 'zwave-ad',
cmd: 'cmd.thing.delete', cmd: 'cmd.thing.delete',
val_t: 'str_map', val_t: 'str_map',
val: { val: {
address: parameters.vinculumDeviceData.thing, address: parameters.vinculumDeviceData.fimp?.address,
}, },
}, },
], ],
); );
} catch (e) { } catch (e) {
ha?.publish(availabilityTopic, 'online', { log.error('Failed to delete device:', e);
retain: true,
qos: 2,
});
} }
}; };
} }

View File

@ -7,7 +7,13 @@ import { haUpdateState, haUpdateStateValueReport } from './ha/update_state';
import { VinculumPd7Device } from './fimp/vinculum_pd7_device'; import { VinculumPd7Device } from './fimp/vinculum_pd7_device';
import { haUpdateAvailability } from './ha/update_availability'; import { haUpdateAvailability } from './ha/update_availability';
import { delay } from './utils'; 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'; import { pollVinculum } from './fimp/vinculum';
(async () => { (async () => {
@ -270,6 +276,11 @@ import { pollVinculum } from './fimp/vinculum';
break; break;
} }
case 'evt.thing.inclusion_report': {
handleInclusionReport();
break;
}
case 'evt.thing.exclusion_report': { case 'evt.thing.exclusion_report': {
handleExclusionReport(); handleExclusionReport();
break; break;

View File

@ -7,6 +7,11 @@ import { v4 as uuidv4 } from 'uuid';
/** /**
* Logs in to the Thingsplex and extracts the tplex token. * 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 username - The login username
* @param password - The login password * @param password - The login password
* @returns The tplex token if login is successful * @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. * 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 * @param token - The tplex token from login
*/ */
export function connectThingsplexWebSocketAndSend( export function connectThingsplexWebSocketAndSend(