67 lines
2.8 KiB
TypeScript
67 lines
2.8 KiB
TypeScript
// home-assistant's events. The tool runtime imports this once the broker is bound. It watches the
|
|
// entities whose state changing is a real signal — a door opening, a lock turning, a switch
|
|
// flipping — and emits when one does.
|
|
//
|
|
// Emits (novox/hq ADR 0041/0042):
|
|
// module.home-assistant.state.changed — a watched entity's state value changed
|
|
//
|
|
// Bounded on purpose. Home Assistant has hundreds of entities and many (temperature, humidity,
|
|
// power draw) tick constantly; emitting every tick would be noise, not signal. So the watch is
|
|
// limited to actuator/contact domains where a change is an event a human would care about, and
|
|
// only the discrete `state` value is diffed — not the attribute bag. The set is overridable with
|
|
// MESH_HOMEASSISTANT_WATCH (comma-separated entity ids) for a node that wants a specific few.
|
|
|
|
import { emit } from "@novox/mesh-sdk/events";
|
|
import { HomeAssistantClient, type HAEntityState } from "./client.js";
|
|
|
|
const ha = HomeAssistantClient.fromEnv();
|
|
|
|
// Domains whose state changing is meaningful rather than a continuous reading.
|
|
const WATCH_DOMAINS = new Set(["binary_sensor", "lock", "cover", "switch", "input_boolean", "light", "alarm_control_panel"]);
|
|
|
|
// An explicit allowlist of entity ids, if the node set one; otherwise fall back to the domain filter.
|
|
const watchList = (process.env.MESH_HOMEASSISTANT_WATCH ?? "")
|
|
.split(",")
|
|
.map((s) => s.trim())
|
|
.filter(Boolean);
|
|
const watchSet = watchList.length ? new Set(watchList) : null;
|
|
|
|
function isWatched(s: HAEntityState): boolean {
|
|
if (watchSet) return watchSet.has(s.entity_id);
|
|
return WATCH_DOMAINS.has(s.entity_id.split(".")[0] ?? "");
|
|
}
|
|
|
|
const nameOf = (s: HAEntityState): string | undefined =>
|
|
typeof s.attributes.friendly_name === "string" ? s.attributes.friendly_name : undefined;
|
|
|
|
// Last seen state per watched entity. Primed silently on the first poll so a restart does not
|
|
// re-announce the current state of everything as a fresh change.
|
|
const lastState = new Map<string, string>();
|
|
let primed = false;
|
|
|
|
async function pollStates(): Promise<void> {
|
|
const states = (await ha.getStates()).filter(isWatched);
|
|
for (const s of states) {
|
|
const prev = lastState.get(s.entity_id);
|
|
if (primed && prev !== undefined && prev !== s.state) {
|
|
await emit("module.home-assistant.state.changed", {
|
|
entity: s.entity_id,
|
|
name: nameOf(s),
|
|
from: prev,
|
|
to: s.state,
|
|
});
|
|
}
|
|
lastState.set(s.entity_id, s.state);
|
|
}
|
|
primed = true;
|
|
}
|
|
|
|
const tick = (fn: () => Promise<void>, everyMs: number): void => {
|
|
const run = (): void => void fn().catch((err) => console.error(`[home-assistant] ${err}`));
|
|
setInterval(run, everyMs);
|
|
run();
|
|
};
|
|
tick(pollStates, 15_000);
|
|
|
|
console.log("[home-assistant] watching entity states, emitting on change");
|