422 lines
12 KiB
TypeScript
Raw Normal View History

2025-07-28 14:18:42 +02:00
import { CommandHandlers } from './publish_device';
import { HaDeviceConfig } from './ha_device_config';
import { ha } from './globals';
import { log } from '../logger';
import { abbreviateHaMqttKeys } from './abbreviate_ha_mqtt_keys';
import { FimpResponse } from '../fimp/fimp';
import {
connectThingsplexWebSocketAndSend,
loginToThingsplex,
} from '../thingsplex/thingsplex';
import { pollVinculum } from '../fimp/vinculum';
2025-07-28 16:12:12 +02:00
const inclusionExclusionNotRunningValues = [
'Ready',
'Done',
'Ready to start inclusion',
'Failed trying to start inclusion.',
"Operation failed. The device can't be included.",
'Device added successfully!',
'Ready to start exclusion',
'Failed trying to start exclusion.',
"Operation failed. The device can't be excluded.",
'',
];
const inclusionExclusionStartingStoppingValues = [
'Demo mode, inclusion not supported',
'Demo mode, exclusion not supported',
'Starting ZigBee inclusion',
'Starting ZigBee exclusion',
'Starting Z-Wave inclusion',
'Starting Z-Wave exclusion',
'Stopping',
];
2025-07-28 14:18:42 +02:00
let initializedState = false;
export function exposeSmarthubTools(parameters: {
hubId: string;
demoMode: boolean;
hubIp: string;
thingsplexUsername: string;
thingsplexPassword: string;
}): {
commandHandlers: CommandHandlers;
} {
// e.g. "homeassistant/device/futurehome_123456_hub"
const topicPrefix = `homeassistant/device/futurehome_${parameters.hubId}_hub`;
if (!initializedState) {
2025-07-28 16:12:12 +02:00
ha?.publish(`${topicPrefix}/inclusion_exclusion_status/state`, 'Ready', {
retain: true,
qos: 2,
});
2025-07-28 14:18:42 +02:00
initializedState = true;
}
const configTopic = `${topicPrefix}/config`;
const deviceId = `futurehome_${parameters.hubId}_hub`;
const config: HaDeviceConfig = {
device: {
identifiers: deviceId,
name: 'Futurehome Smarthub',
manufacturer: 'Futurehome',
model: 'Smarthub',
serial_number: parameters.hubId,
},
origin: {
name: 'futurehome',
support_url:
'https://github.com/adrianjagielak/home-assistant-futurehome',
},
components: {
2025-07-28 16:12:12 +02:00
[`${deviceId}_inclusion_exclusion_status`]: {
unique_id: `${deviceId}_inclusion_exclusion_status`,
platform: 'sensor',
entity_category: 'diagnostic',
device_class: 'enum',
name: 'Inclusion/exclusion status',
state_topic: `${topicPrefix}/inclusion_exclusion_status/state`,
},
[`${deviceId}_zwave_startExclusion`]: {
unique_id: `${deviceId}_zwave_startExclusion`,
platform: 'button',
entity_category: 'diagnostic',
name: 'Start Z-Wave exclusion',
icon: 'mdi:z-wave',
command_topic: `${topicPrefix}/start_exclusion/command`,
payload_press: 'zwave',
availability_topic: `${topicPrefix}/inclusion_exclusion_status/state`,
availability_template: `{% if ${inclusionExclusionNotRunningValues.map((v) => `value == "${v}"`).join(' or ')} %}online{% else %}offline{% endif %}`,
} as any,
2025-07-28 14:18:42 +02:00
[`${deviceId}_zwave_startInclusion`]: {
unique_id: `${deviceId}_zwave_startInclusion`,
platform: 'button',
entity_category: 'diagnostic',
name: 'Start Z-Wave inclusion',
icon: 'mdi:z-wave',
command_topic: `${topicPrefix}/start_inclusion/command`,
payload_press: 'zwave',
2025-07-28 16:12:12 +02:00
availability_topic: `${topicPrefix}/inclusion_exclusion_status/state`,
availability_template: `{% if ${inclusionExclusionNotRunningValues.map((v) => `value == "${v}"`).join(' or ')} %}online{% else %}offline{% endif %}`,
2025-07-28 14:18:42 +02:00
} as any,
[`${deviceId}_zigbee_startInclusion`]: {
unique_id: `${deviceId}_zigbee_startInclusion`,
platform: 'button',
entity_category: 'diagnostic',
name: 'Start ZigBee inclusion',
icon: 'mdi:zigbee',
command_topic: `${topicPrefix}/start_inclusion/command`,
payload_press: 'zigbee',
2025-07-28 16:12:12 +02:00
availability_topic: `${topicPrefix}/inclusion_exclusion_status/state`,
availability_template: `{% if ${inclusionExclusionNotRunningValues.map((v) => `value == "${v}"`).join(' or ')} %}online{% else %}offline{% endif %}`,
2025-07-28 14:18:42 +02:00
} as any,
2025-07-28 16:12:12 +02:00
[`${deviceId}_stopInclusionExclusion`]: {
unique_id: `${deviceId}_stopInclusionExclusion`,
2025-07-28 14:18:42 +02:00
platform: 'button',
entity_category: 'diagnostic',
2025-07-28 16:12:12 +02:00
name: 'Stop inclusion/exclusion',
2025-07-28 14:18:42 +02:00
icon: 'mdi:cancel',
2025-07-28 16:12:12 +02:00
command_topic: `${topicPrefix}/stop_inclusion_exclusion/command`,
availability_topic: `${topicPrefix}/inclusion_exclusion_status/state`,
availability_template: `{% if ${[...inclusionExclusionNotRunningValues, ...inclusionExclusionStartingStoppingValues].map((v) => `value == "${v}"`).join(' or ')} %}offline{% else %}online{% endif %}`,
2025-07-28 14:18:42 +02:00
} as any,
},
qos: 2,
};
log.debug('Publishing Smarthub tools');
ha?.publish(configTopic, JSON.stringify(abbreviateHaMqttKeys(config)), {
retain: true,
qos: 2,
});
const handlers: CommandHandlers = {
[`${topicPrefix}/start_inclusion/command`]: async (payload) => {
if (parameters.demoMode) {
ha?.publish(
2025-07-28 16:12:12 +02:00
`${topicPrefix}/inclusion_exclusion_status/state`,
2025-07-28 14:18:42 +02:00
'Demo mode, inclusion not supported',
{
retain: true,
qos: 2,
},
);
return;
}
2025-07-28 16:12:12 +02:00
ha?.publish(
`${topicPrefix}/inclusion_exclusion_status/state`,
payload == 'zwave'
? 'Starting Z-Wave inclusion'
: 'Starting ZigBee inclusion',
{
retain: true,
qos: 2,
},
);
2025-07-28 14:18:42 +02:00
try {
const token = await loginToThingsplex({
host: parameters.hubIp,
username: parameters.thingsplexUsername,
password: parameters.thingsplexPassword,
});
await connectThingsplexWebSocketAndSend(
{
host: parameters.hubIp,
token: token,
},
[
{
address:
payload == 'zwave'
? 'pt:j1/mt:cmd/rt:ad/rn:zw/ad:1'
: 'pt:j1/mt:cmd/rt:ad/rn:zigbee/ad:1',
service: payload == 'zwave' ? 'zwave-ad' : 'zigbee',
cmd: 'cmd.thing.inclusion',
val: true,
val_t: 'bool',
},
],
);
} catch (e) {
ha?.publish(
2025-07-28 16:12:12 +02:00
`${topicPrefix}/inclusion_exclusion_status/state`,
2025-07-28 14:18:42 +02:00
'Failed trying to start inclusion.',
{
retain: true,
qos: 2,
},
);
}
},
2025-07-28 16:12:12 +02:00
[`${topicPrefix}/stop_inclusion_exclusion/command`]: async (_payload) => {
2025-07-28 14:18:42 +02:00
if (parameters.demoMode) {
return;
}
2025-07-28 16:12:12 +02:00
ha?.publish(
`${topicPrefix}/inclusion_exclusion_status/state`,
'Stopping',
{
retain: true,
qos: 2,
},
);
2025-07-28 14:18:42 +02:00
try {
const token = await loginToThingsplex({
host: parameters.hubIp,
username: parameters.thingsplexUsername,
password: parameters.thingsplexPassword,
});
await connectThingsplexWebSocketAndSend(
{
host: parameters.hubIp,
token: token,
},
[
{
address: 'pt:j1/mt:cmd/rt:ad/rn:zigbee/ad:1',
service: 'zigbee',
cmd: 'cmd.thing.inclusion',
val: false,
val_t: 'bool',
},
{
address: 'pt:j1/mt:cmd/rt:ad/rn:zw/ad:1',
service: 'zwave-ad',
cmd: 'cmd.thing.inclusion',
val: false,
val_t: 'bool',
},
2025-07-28 16:12:12 +02:00
{
address: 'pt:j1/mt:cmd/rt:ad/rn:zigbee/ad:1',
service: 'zigbee',
cmd: 'cmd.thing.exclusion',
val: false,
val_t: 'bool',
},
{
address: 'pt:j1/mt:cmd/rt:ad/rn:zw/ad:1',
service: 'zwave-ad',
cmd: 'cmd.thing.exclusion',
val: false,
val_t: 'bool',
},
2025-07-28 14:18:42 +02:00
],
);
2025-07-28 16:12:12 +02:00
ha?.publish(`${topicPrefix}/inclusion_exclusion_status/state`, 'Done', {
2025-07-28 14:18:42 +02:00
retain: true,
qos: 2,
});
} catch (e) {
ha?.publish(
2025-07-28 16:12:12 +02:00
`${topicPrefix}/inclusion_exclusion_status/state`,
2025-07-28 14:18:42 +02:00
'Failed trying to stop inclusion.',
{
retain: true,
qos: 2,
},
);
}
},
2025-07-28 16:12:12 +02:00
[`${topicPrefix}/start_exclusion/command`]: async (payload) => {
if (parameters.demoMode) {
ha?.publish(
`${topicPrefix}/inclusion_exclusion_status/state`,
'Demo mode, exclusion not supported',
{
retain: true,
qos: 2,
},
);
return;
}
ha?.publish(
`${topicPrefix}/inclusion_exclusion_status/state`,
payload == 'zwave'
? 'Starting Z-Wave exclusion'
: 'Starting ZigBee exclusion',
{
retain: true,
qos: 2,
},
);
try {
const token = await loginToThingsplex({
host: parameters.hubIp,
username: parameters.thingsplexUsername,
password: parameters.thingsplexPassword,
});
await connectThingsplexWebSocketAndSend(
{
host: parameters.hubIp,
token: token,
},
[
{
address:
payload == 'zwave'
? 'pt:j1/mt:cmd/rt:ad/rn:zw/ad:1'
: 'pt:j1/mt:cmd/rt:ad/rn:zigbee/ad:1',
service: payload == 'zwave' ? 'zwave-ad' : 'zigbee',
cmd: 'cmd.thing.exclusion',
val: true,
val_t: 'bool',
},
],
);
} catch (e) {
ha?.publish(
`${topicPrefix}/inclusion_exclusion_status/state`,
'Failed trying to start exclusion.',
{
retain: true,
qos: 2,
},
);
}
},
2025-07-28 14:18:42 +02:00
};
return { commandHandlers: handlers };
}
export function handleInclusionStatusReport(hubId: string, msg: FimpResponse) {
const topicPrefix = `homeassistant/device/futurehome_${hubId}_hub`;
let localizedStatus: string;
switch (msg.val) {
case 'ADD_NODE_STARTING':
case 'ADD_NODE_STARTED':
localizedStatus = 'Looking for device';
break;
case 'ADD_NODE_ADDED':
case 'ADD_NODE_GET_NODE_INFO':
case 'ADD_NODE_PROTOCOL_DONE':
localizedStatus = 'Device added successfully!';
2025-07-28 16:12:12 +02:00
pollVinculum('device').catch((e) =>
log.warn('Failed to request devices', e),
);
pollVinculum('state').catch((e) =>
log.warn('Failed to request state', e),
);
2025-07-28 14:18:42 +02:00
break;
case 'ADD_NODE_DONE':
localizedStatus = 'Done';
2025-07-28 16:12:12 +02:00
pollVinculum('device').catch((e) =>
log.warn('Failed to request devices', e),
);
pollVinculum('state').catch((e) =>
log.warn('Failed to request state', e),
);
2025-07-28 14:18:42 +02:00
break;
case 'NET_NODE_INCL_CTRL_OP_FAILED':
localizedStatus = "Operation failed. The device can't be included.";
break;
default:
localizedStatus = msg.val;
log.warn(`Unknown inclusion status: ${msg.val}`);
break;
}
2025-07-28 16:12:12 +02:00
ha?.publish(
`${topicPrefix}/inclusion_exclusion_status/state`,
localizedStatus,
{
retain: true,
qos: 2,
},
);
2025-07-28 14:18:42 +02:00
}
2025-07-28 16:12:12 +02:00
export function handleExclusionStatusReport(hubId: string, msg: FimpResponse) {
const topicPrefix = `homeassistant/device/futurehome_${hubId}_hub`;
let localizedStatus: string;
switch (msg.val) {
case 'REMOVE_NODE_STARTING':
case 'REMOVE_NODE_STARTED':
localizedStatus = 'Looking for device in unpairing mode';
break;
case 'REMOVE_NODE_FOUND':
localizedStatus = 'Device found';
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.";
break;
default:
localizedStatus = msg.val;
log.warn(`Unknown exclusion status: ${msg.val}`);
break;
}
ha?.publish(
`${topicPrefix}/inclusion_exclusion_status/state`,
localizedStatus,
{
retain: true,
qos: 2,
},
);
}
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));
}