mirror of
https://github.com/adrianjagielak/home-assistant-futurehome.git
synced 2025-11-18 17:19:02 +00:00
Add initial version of the add-on code
This commit is contained in:
19
futurehome/src/client.ts
Normal file
19
futurehome/src/client.ts
Normal file
@@ -0,0 +1,19 @@
|
||||
import mqtt, { MqttClient } from "mqtt";
|
||||
|
||||
export function connectHub(opts: { hubIp: string; username: string; password: string; }): Promise<MqttClient> {
|
||||
const url = `mqtt://${opts.hubIp || "futurehome-smarthub.local"}`;
|
||||
return makeClient(url, opts.username, opts.password);
|
||||
}
|
||||
|
||||
export function connectHA(): Promise<MqttClient> {
|
||||
const url = "mqtt://homeassistant";
|
||||
return makeClient(url);
|
||||
}
|
||||
|
||||
function makeClient(url: string, username = "", password = ""): Promise<MqttClient> {
|
||||
return new Promise((resolve, reject) => {
|
||||
const client = mqtt.connect(url, { username, password, protocolVersion: 4 });
|
||||
client.once("connect", () => resolve(client));
|
||||
client.once("error", reject);
|
||||
});
|
||||
}
|
||||
27
futurehome/src/discovery.ts
Normal file
27
futurehome/src/discovery.ts
Normal file
@@ -0,0 +1,27 @@
|
||||
import { MqttClient } from 'mqtt';
|
||||
import { handleBattery } from './parsers/battery';
|
||||
import { handleBinSwitch } from './parsers/out_bin_switch';
|
||||
import { handleLvlSwitch } from './parsers/out_lvl_switch';
|
||||
import { handleTempSensor } from './parsers/sensor_temp';
|
||||
|
||||
// map Futurehome → Home Assistant MQTT Discovery
|
||||
export async function publishDiscovery(client: MqttClient, device: any) {
|
||||
for (const svc of device.services) {
|
||||
switch (svc.name) {
|
||||
case 'battery':
|
||||
handleBattery(client, device, svc);
|
||||
break;
|
||||
case 'out_bin_switch':
|
||||
handleBinSwitch(client, device, svc);
|
||||
break;
|
||||
case 'out_lvl_switch':
|
||||
handleLvlSwitch(client, device, svc);
|
||||
break;
|
||||
case 'sensor_temp':
|
||||
handleTempSensor(client, device, svc);
|
||||
break;
|
||||
default:
|
||||
// not implemented yet
|
||||
}
|
||||
}
|
||||
}
|
||||
39
futurehome/src/index.ts
Normal file
39
futurehome/src/index.ts
Normal file
@@ -0,0 +1,39 @@
|
||||
import { connectHub, connectHA } from "./client";
|
||||
import { publishDiscovery } from "./discovery";
|
||||
|
||||
(async () => {
|
||||
const hubIp = process.env.FH_HUB_IP || "";
|
||||
const user = process.env.FH_USERNAME || "";
|
||||
const pass = process.env.FH_PASSWORD || "";
|
||||
|
||||
// 1) Connect to HA broker (for discovery + state)
|
||||
const ha = await connectHA();
|
||||
|
||||
// 2) Connect to Futurehome hub (FIMP traffic)
|
||||
const fimp = await connectHub({ hubIp, username: user, password: pass });
|
||||
|
||||
// -- subscribe to FIMP events -----------------------------------------
|
||||
fimp.subscribe("#");
|
||||
fimp.on("message", (topic, buf) => {
|
||||
try {
|
||||
const msg = JSON.parse(buf.toString());
|
||||
if (msg.type === "evt.pd7.response") {
|
||||
const devices = msg.val?.param?.devices ?? [];
|
||||
devices.forEach((d: any) => publishDiscovery(ha, d));
|
||||
}
|
||||
// …forward state events as needed…
|
||||
} catch (e) {
|
||||
console.warn("Bad FIMP JSON", e);
|
||||
}
|
||||
});
|
||||
|
||||
// -- ask hub for the device list --------------------------------------
|
||||
fimp.publish("pt:j1/mt:cmd/rt:app/rn:vinculum/ad:1", JSON.stringify({
|
||||
type: "cmd.pd7.request",
|
||||
service: "vinculum",
|
||||
uid: crypto.randomUUID(),
|
||||
val_t: "object",
|
||||
val: { cmd: "get", component: "state" },
|
||||
resp_to: "pt:j1/mt:rsp/rt:app/rn:ha-futurehome/ad:addon"
|
||||
}), { qos: 1 });
|
||||
})();
|
||||
22
futurehome/src/parsers/battery.ts
Normal file
22
futurehome/src/parsers/battery.ts
Normal file
@@ -0,0 +1,22 @@
|
||||
import { MqttClient } from 'mqtt';
|
||||
|
||||
export function handleBattery(client: MqttClient, dev: any, svc: any) {
|
||||
const uid = `fh_${dev.id}_${svc.name}`;
|
||||
const base = `homeassistant/sensor/${uid}`;
|
||||
|
||||
// config
|
||||
client.publish(`${base}/config`, JSON.stringify({
|
||||
name: `${dev.name} Battery`,
|
||||
uniq_id: uid,
|
||||
dev_cla: "battery",
|
||||
stat_t: `${base}/state`,
|
||||
unit_of_meas: "%",
|
||||
device: { identifiers: [dev.id.toString()], name: dev.name, model: dev.model }
|
||||
}), { retain: true });
|
||||
|
||||
// initial state if available
|
||||
const lvl = svc.attributes?.find((a: any) => a.name === 'lvl')?.values?.[0]?.val;
|
||||
if (lvl !== undefined) {
|
||||
client.publish(`${base}/state`, String(lvl), { retain: true });
|
||||
}
|
||||
}
|
||||
35
futurehome/src/parsers/out_bin_switch.ts
Normal file
35
futurehome/src/parsers/out_bin_switch.ts
Normal file
@@ -0,0 +1,35 @@
|
||||
import { MqttClient } from 'mqtt';
|
||||
import { v4 as uuid } from 'uuid';
|
||||
|
||||
export function handleBinSwitch(client: MqttClient, dev: any, svc: any) {
|
||||
const uid = `fh_${dev.id}_${svc.name}`;
|
||||
const base = `homeassistant/switch/${uid}`;
|
||||
|
||||
client.publish(`${base}/config`, JSON.stringify({
|
||||
name: dev.name,
|
||||
uniq_id: uid,
|
||||
cmd_t: `${base}/set`,
|
||||
stat_t: `${base}/state`,
|
||||
device: { identifiers: [dev.id.toString()], name: dev.name, model: dev.model }
|
||||
}), { retain: true });
|
||||
|
||||
// current value
|
||||
const bin = svc.attributes?.find((a: any) => a.name === 'binary')?.values?.[0]?.val;
|
||||
client.publish(`${base}/state`, bin ? 'ON' : 'OFF', { retain: true });
|
||||
|
||||
// HA → Smarthub
|
||||
client.subscribe(`${base}/set`, { qos: 0 });
|
||||
client.on('message', (topic, payload) => {
|
||||
if (topic !== `${base}/set`) return;
|
||||
const target = payload.toString() === 'ON';
|
||||
|
||||
client.publish(`pt:j1/mt:cmd/${svc.address}`, JSON.stringify({
|
||||
type: "cmd.binary.set",
|
||||
service: svc.name,
|
||||
uid: uuid(),
|
||||
val_t: "bool",
|
||||
val: target,
|
||||
src: "ha-futurehome"
|
||||
}), { qos: 1 });
|
||||
});
|
||||
}
|
||||
47
futurehome/src/parsers/out_lvl_switch.ts
Normal file
47
futurehome/src/parsers/out_lvl_switch.ts
Normal file
@@ -0,0 +1,47 @@
|
||||
import { MqttClient } from 'mqtt';
|
||||
import { v4 as uuid } from 'uuid';
|
||||
|
||||
export function handleLvlSwitch(client: MqttClient, dev: any, svc: any) {
|
||||
const uid = `fh_${dev.id}_${svc.name}`;
|
||||
const base = `homeassistant/light/${uid}`;
|
||||
|
||||
client.publish(`${base}/config`, JSON.stringify({
|
||||
name: dev.name,
|
||||
uniq_id: uid,
|
||||
cmd_t: `${base}/set`,
|
||||
stat_t: `${base}/state`,
|
||||
bri_cmd_t: `${base}/brightness/set`,
|
||||
bri_stat_t: `${base}/brightness/state`,
|
||||
schema: "template",
|
||||
device: { identifiers: [dev.id.toString()], name: dev.name, model: dev.model }
|
||||
}), { retain: true });
|
||||
|
||||
const bin = svc.attributes?.find((a: any) => a.name === 'binary')?.values?.[0]?.val;
|
||||
const lvl = svc.attributes?.find((a: any) => a.name === 'lvl')?.values?.[0]?.val;
|
||||
client.publish(`${base}/state`, bin ? "ON" : "OFF", { retain: true });
|
||||
if (lvl !== undefined) client.publish(`${base}/brightness/state`, String(lvl), { retain: true });
|
||||
|
||||
client.subscribe([`${base}/set`, `${base}/brightness/set`]);
|
||||
|
||||
client.on('message', (topic, payload) => {
|
||||
if (topic === `${base}/set`) {
|
||||
const on = payload.toString() === 'ON';
|
||||
client.publish(`pt:j1/mt:cmd/${svc.address}`, JSON.stringify({
|
||||
type: "cmd.binary.set",
|
||||
service: svc.name,
|
||||
uid: uuid(),
|
||||
val_t: "bool",
|
||||
val: on
|
||||
}), { qos: 1 });
|
||||
} else if (topic === `${base}/brightness/set`) {
|
||||
const value = parseInt(payload.toString(), 10);
|
||||
client.publish(`pt:j1/mt:cmd/${svc.address}`, JSON.stringify({
|
||||
type: "cmd.lvl.set",
|
||||
service: svc.name,
|
||||
uid: uuid(),
|
||||
val_t: "int",
|
||||
val: value
|
||||
}), { qos: 1 });
|
||||
}
|
||||
});
|
||||
}
|
||||
20
futurehome/src/parsers/sensor_presence.ts
Normal file
20
futurehome/src/parsers/sensor_presence.ts
Normal file
@@ -0,0 +1,20 @@
|
||||
import { MqttClient } from 'mqtt';
|
||||
|
||||
export function handlePresenceSensor(client: MqttClient, dev: any, svc: any) {
|
||||
const uid = `fh_${dev.id}_${svc.name}`;
|
||||
const base = `homeassistant/sensor/${uid}`;
|
||||
|
||||
client.publish(`${base}/config`, JSON.stringify({
|
||||
name: `${dev.name} Presence`,
|
||||
uniq_id: uid,
|
||||
dev_cla: "presence",
|
||||
stat_t: `${base}/state`,
|
||||
unit_of_meas: "℃",
|
||||
device: { identifiers: [dev.id.toString()], name: dev.name, model: dev.model }
|
||||
}), { retain: true });
|
||||
|
||||
const presence = svc.attributes?.find((a: any) => a.name === 'presence')?.values?.[0]?.val;
|
||||
if (presence !== undefined) {
|
||||
client.publish(`${base}/state`, String(presence), { retain: true });
|
||||
}
|
||||
}
|
||||
20
futurehome/src/parsers/sensor_temp.ts
Normal file
20
futurehome/src/parsers/sensor_temp.ts
Normal file
@@ -0,0 +1,20 @@
|
||||
import { MqttClient } from 'mqtt';
|
||||
|
||||
export function handleTempSensor(client: MqttClient, dev: any, svc: any) {
|
||||
const uid = `fh_${dev.id}_${svc.name}`;
|
||||
const base = `homeassistant/sensor/${uid}`;
|
||||
|
||||
client.publish(`${base}/config`, JSON.stringify({
|
||||
name: `${dev.name} Temperature`,
|
||||
uniq_id: uid,
|
||||
dev_cla: "temperature",
|
||||
stat_t: `${base}/state`,
|
||||
unit_of_meas: "℃",
|
||||
device: { identifiers: [dev.id.toString()], name: dev.name, model: dev.model }
|
||||
}), { retain: true });
|
||||
|
||||
const temp = svc.attributes?.find((a: any) => a.name === 'sensor')?.values?.[0]?.val;
|
||||
if (temp !== undefined) {
|
||||
client.publish(`${base}/state`, String(temp), { retain: true });
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user