2025-07-21 22:28:31 +02:00
|
|
|
import { connectHub, connectHA } from "./client";
|
2025-07-22 00:51:14 +02:00
|
|
|
import { exposeSmarthubTools } from "./admin";
|
2025-07-22 23:21:34 +02:00
|
|
|
import { log } from "./logger";
|
|
|
|
import { FimpResponse, sendFimpMsg, setFimp } from "./fimp/fimp";
|
|
|
|
import { getInclusionReport } from "./fimp/inclusion_report";
|
|
|
|
import { adapterAddressFromServiceAddress, adapterServiceFromServiceAddress } from "./fimp/helpers";
|
|
|
|
import { setHa } from "./ha/globals";
|
|
|
|
import { haPublishDevice } from "./ha/publish_device";
|
2025-07-23 14:01:59 +02:00
|
|
|
import { haUpdateState, haUpdateStateSensorReport } from "./ha/update_state";
|
2025-07-22 23:21:34 +02:00
|
|
|
import { VinculumPd7Device } from "./fimp/vinculum_pd7_device";
|
|
|
|
import { haUpdateAvailability } from "./ha/update_availability";
|
2025-07-21 22:28:31 +02:00
|
|
|
|
|
|
|
(async () => {
|
2025-07-22 23:21:34 +02:00
|
|
|
const hubIp = process.env.FH_HUB_IP || "";
|
2025-07-22 00:25:16 +02:00
|
|
|
const hubUsername = process.env.FH_USERNAME || "";
|
|
|
|
const hubPassword = process.env.FH_PASSWORD || "";
|
2025-07-21 22:28:31 +02:00
|
|
|
|
2025-07-22 23:21:34 +02:00
|
|
|
const mqttHost = process.env.MQTT_HOST || "";
|
|
|
|
const mqttPort = Number(process.env.MQTT_PORT || "1883");
|
2025-07-21 23:04:44 +02:00
|
|
|
const mqttUsername = process.env.MQTT_USER || "";
|
2025-07-22 23:21:34 +02:00
|
|
|
const mqttPassword = process.env.MQTT_PWD || "";
|
2025-07-21 23:04:44 +02:00
|
|
|
|
2025-07-21 22:28:31 +02:00
|
|
|
// 1) Connect to HA broker (for discovery + state)
|
2025-07-22 23:21:34 +02:00
|
|
|
log.info("Connecting to HA broker...");
|
|
|
|
const { ha, retainedMessages } = await connectHA({ mqttHost, mqttPort, mqttUsername, mqttPassword, });
|
|
|
|
setHa(ha);
|
|
|
|
log.info("Connected to HA broker");
|
2025-07-21 22:28:31 +02:00
|
|
|
|
|
|
|
// 2) Connect to Futurehome hub (FIMP traffic)
|
2025-07-22 23:21:34 +02:00
|
|
|
log.info("Connecting to Futurehome hub...");
|
2025-07-22 00:25:16 +02:00
|
|
|
const fimp = await connectHub({ hubIp, username: hubUsername, password: hubPassword });
|
2025-07-22 23:21:34 +02:00
|
|
|
fimp.subscribe("#");
|
|
|
|
setFimp(fimp);
|
|
|
|
log.info("Connected to Futurehome hub");
|
|
|
|
|
|
|
|
let 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_t: 'object',
|
|
|
|
});
|
|
|
|
let hubId = house.val.param.house.hubId;
|
2025-07-21 22:28:31 +02:00
|
|
|
|
2025-07-22 23:21:34 +02:00
|
|
|
let devices = await sendFimpMsg({
|
|
|
|
address: '/rt:app/rn:vinculum/ad:1',
|
|
|
|
service: 'vinculum',
|
|
|
|
cmd: 'cmd.pd7.request',
|
|
|
|
val: { cmd: "get", component: null, param: { components: ['device'] } },
|
|
|
|
val_t: 'object',
|
|
|
|
});
|
|
|
|
|
|
|
|
const haConfig = retainedMessages.filter(msg => msg.topic.endsWith("/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)
|
|
|
|
|
|
|
|
const match = haDevice.topic.match(regex);
|
|
|
|
|
|
|
|
if (match) {
|
|
|
|
const deviceId = match[1];
|
|
|
|
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;;
|
|
|
|
|
|
|
|
if (!basicDeviceData || !firstServiceAddr) {
|
|
|
|
log.debug('Device was removed, removing from HA.');
|
|
|
|
ha?.publish(haDevice.topic, '', { retain: true, qos: 2 });
|
|
|
|
}
|
|
|
|
} else if (deviceId.toLowerCase() === "hub") {
|
|
|
|
// Hub admin tools, ignore
|
|
|
|
} else {
|
|
|
|
log.debug('Invalid format, removing.');
|
|
|
|
ha?.publish(haDevice.topic, '', { retain: true, qos: 2 });
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
log.debug('Invalid format, removing.');
|
|
|
|
ha?.publish(haDevice.topic, '', { retain: true, qos: 2 });
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
for (const device of devices.val.param.device) {
|
|
|
|
const vinculumDeviceData: VinculumPd7Device = device
|
|
|
|
const deviceId = vinculumDeviceData.id.toString()
|
|
|
|
const services: { [key: string]: any } = vinculumDeviceData?.services
|
|
|
|
const firstServiceAddr = services ? Object.values(services)[0]?.addr : undefined;;
|
|
|
|
|
|
|
|
if (!firstServiceAddr) { continue; }
|
|
|
|
|
|
|
|
const adapterAddress = adapterAddressFromServiceAddress(firstServiceAddr)
|
|
|
|
const adapterService = adapterServiceFromServiceAddress(firstServiceAddr)
|
|
|
|
|
|
|
|
// Get additional metadata like manufacutrer or sw/hw version directly from the adapter
|
|
|
|
const deviceInclusionReport = await getInclusionReport({ adapterAddress, adapterService, deviceId });
|
|
|
|
|
|
|
|
if (!retainedMessages.some(msg => msg.topic === `homeassistant/device/futurehome_${hubId}_${deviceId}/availability`)) {
|
|
|
|
// Set initial availability
|
|
|
|
haUpdateAvailability({ hubId, deviceAvailability: { address: deviceId, status: 'UP' } });
|
|
|
|
}
|
2025-07-23 01:03:28 +02:00
|
|
|
haPublishDevice({ hubId, vinculumDeviceData, deviceInclusionReport })
|
2025-07-22 23:21:34 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
// todo
|
|
|
|
// exposeSmarthubTools();
|
2025-07-22 00:51:14 +02:00
|
|
|
|
2025-07-21 22:28:31 +02:00
|
|
|
fimp.on("message", (topic, buf) => {
|
|
|
|
try {
|
2025-07-22 23:21:34 +02:00
|
|
|
const msg: FimpResponse = JSON.parse(buf.toString());
|
2025-07-23 14:01:59 +02:00
|
|
|
log.debug(`Received FIMP message on topic "${topic}":\n${JSON.stringify(msg, null, 0)}`);
|
|
|
|
|
|
|
|
switch (msg.type) {
|
|
|
|
case 'evt.pd7.response': {
|
|
|
|
const devicesState = msg.val?.param?.state?.devices;
|
|
|
|
if (!devicesState) { return; }
|
|
|
|
for (const deviceState of devicesState) {
|
|
|
|
haUpdateState({ hubId, deviceState });
|
|
|
|
}
|
|
|
|
break;
|
2025-07-22 23:21:34 +02:00
|
|
|
}
|
2025-07-23 14:01:59 +02:00
|
|
|
case 'evt.sensor.report': {
|
|
|
|
haUpdateStateSensorReport({ topic, value: msg.val, attrName: 'sensor' })
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
case 'evt.presence.report': {
|
|
|
|
if (!(msg.serv === 'sensor_presence')) { return; }
|
|
|
|
haUpdateStateSensorReport({ topic, value: msg.val, attrName: 'presence' })
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
case 'evt.open.report': {
|
|
|
|
if (!(msg.serv === 'sensor_contact')) { return; }
|
|
|
|
haUpdateStateSensorReport({ topic, value: msg.val, attrName: 'open' })
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
case 'evt.lvl.report': {
|
|
|
|
if (!(msg.serv === 'battery')) { return; }
|
|
|
|
haUpdateStateSensorReport({ topic, value: msg.val, attrName: 'lvl' })
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
case 'evt.alarm.report': {
|
|
|
|
if (!(msg.serv === 'battery')) { return; }
|
|
|
|
haUpdateStateSensorReport({ topic, value: msg.val, attrName: 'alarm' })
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
case 'evt.network.all_nodes_report': {
|
|
|
|
const devicesAvailability = msg.val;
|
|
|
|
if (!devicesAvailability) { return; }
|
|
|
|
for (const deviceAvailability of devicesAvailability) {
|
|
|
|
haUpdateAvailability({ hubId, deviceAvailability });
|
|
|
|
}
|
|
|
|
break;
|
2025-07-22 23:21:34 +02:00
|
|
|
}
|
2025-07-21 22:28:31 +02:00
|
|
|
}
|
|
|
|
} catch (e) {
|
2025-07-22 23:21:34 +02:00
|
|
|
log.warn("Bad FIMP JSON", e, topic, buf);
|
2025-07-21 22:28:31 +02:00
|
|
|
}
|
|
|
|
});
|
|
|
|
|
2025-07-22 23:21:34 +02:00
|
|
|
// Request initial state
|
|
|
|
await sendFimpMsg({
|
|
|
|
address: '/rt:app/rn:vinculum/ad:1',
|
|
|
|
service: 'vinculum',
|
|
|
|
cmd: 'cmd.pd7.request',
|
|
|
|
val: { cmd: "get", component: null, param: { components: ['state'] } },
|
|
|
|
val_t: 'object',
|
|
|
|
});
|
2025-07-21 22:28:31 +02:00
|
|
|
})();
|