diff --git a/futurehome/src/ha/update_state.ts b/futurehome/src/ha/update_state.ts index 50ce8cc..3412789 100644 --- a/futurehome/src/ha/update_state.ts +++ b/futurehome/src/ha/update_state.ts @@ -75,15 +75,28 @@ topic: homeassistant/device/futurehome_123456_1/state } ``` */ -export function haUpdateState(parameters: { hubId: string, deviceState: DeviceState }) { - const stateTopic = `homeassistant/device/futurehome_${parameters.hubId}_${parameters.deviceState.id?.toString()}/state` - const haState: { [addr: string]: { [attrName: string]: any } } = {}; +const haStateCache: Record< + string, // state topic + Record> // payload (addr → { attr → value }) +> = {}; + +/** + * Publishes the full state of a Futurehome device to Home Assistant and + * stores a copy in the private cache above. + * + * Example MQTT topic produced for hub 123456 and device id 1: + * homeassistant/device/futurehome_123456_1/state + */ +export function haUpdateState(parameters: { hubId: string; deviceState: DeviceState }) { + const stateTopic = `homeassistant/device/futurehome_${parameters.hubId}_${parameters.deviceState.id?.toString()}/state`; + + const haState: Record> = {}; for (const service of parameters.deviceState.services || []) { - if (!service.addr) { continue; } + if (!service.addr) continue; - const serviceState: { [attrName: string]: any } = {}; + const serviceState: Record = {}; for (const attr of service.attributes || []) { const value = attr.values?.[0]?.val; @@ -93,6 +106,39 @@ export function haUpdateState(parameters: { hubId: string, deviceState: DeviceSt haState[service.addr] = serviceState; } - log.debug(`Publishing HA state "${stateTopic}"`) + log.debug(`Publishing HA state "${stateTopic}"`); ha?.publish(stateTopic, JSON.stringify(haState), { retain: true, qos: 2 }); + + // ---- cache state for later incremental updates ---- + haStateCache[stateTopic] = haState; } + +/** + * Incrementally updates a single sensor value inside cached state payload + * that references the given device‑service address and republishes + * the modified payload(s). + * + * @param topic Full FIMP event topic, e.g. + * "pt:j1/mt:evt/rt:dev/rn:zigbee/ad:1/sv:sensor_temp/ad:3_1" + * @param value The new sensor reading (number, boolean, string, …) + * @param attrName Attribute name to store the reading to + * + * The prefix "pt:j1/mt:evt" is removed before matching so that the remainder + * exactly matches the address keys stored in the cached HA payloads. + */ +export function haUpdateStateSensorReport(parameters: { topic: string; value: any, attrName: string }) { + // Strip the FIMP envelope so we end up with "/rt:dev/…/ad:x_y" + const sensorAddr = parameters.topic.replace(/^pt:j1\/mt:evt/, ""); + + for (const [stateTopic, payload] of Object.entries(haStateCache)) { + if (!payload[sensorAddr]) continue; + + // Update the reading in‑place + payload[sensorAddr][parameters.attrName] = parameters.value; + + log.debug(`Publishing updated sensor value for "${sensorAddr}" to "${stateTopic}"`); + ha?.publish(stateTopic, JSON.stringify(payload), { retain: true, qos: 2 }); + + haStateCache[stateTopic] = payload; + } +} \ No newline at end of file diff --git a/futurehome/src/index.ts b/futurehome/src/index.ts index ecc46ba..eb6d08c 100644 --- a/futurehome/src/index.ts +++ b/futurehome/src/index.ts @@ -6,7 +6,7 @@ import { getInclusionReport } from "./fimp/inclusion_report"; import { adapterAddressFromServiceAddress, adapterServiceFromServiceAddress } from "./fimp/helpers"; import { setHa } from "./ha/globals"; import { haPublishDevice } from "./ha/publish_device"; -import { haUpdateState } from "./ha/update_state"; +import { haUpdateState, haUpdateStateSensorReport } from "./ha/update_state"; import { VinculumPd7Device } from "./fimp/vinculum_pd7_device"; import { haUpdateAvailability } from "./ha/update_availability"; @@ -109,18 +109,48 @@ import { haUpdateAvailability } from "./ha/update_availability"; fimp.on("message", (topic, buf) => { try { const msg: FimpResponse = JSON.parse(buf.toString()); - log.debug(JSON.stringify(msg, null, 0)); - if (msg.type === "evt.pd7.response") { - const devicesState = msg.val?.param?.state?.devices; - if (!devicesState) { return; } - for (const deviceState of devicesState) { - haUpdateState({ hubId, deviceState }); + 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; } - } else if (msg.type === "evt.network.all_nodes_report") { - const devicesAvailability = msg.val; - if (!devicesAvailability) { return; } - for (const deviceAvailability of devicesAvailability) { - haUpdateAvailability({ hubId, deviceAvailability }); + 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; } } } catch (e) {