home-assistant, influxdb: full nox modules (ADR 0044/0046)
home-assistant: states/call-service/config tools, emits state.changed bounded to actuator/contact domains (not attribute ticks), overridable via a watch allowlist. influxdb: health/buckets/flux-query tools — tools-only, since a time-series DB has no lifecycle event to emit here. Typecheck; manifests parse.
This commit is contained in:
@@ -0,0 +1,66 @@
|
||||
// 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 0046/0047):
|
||||
// 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");
|
||||
Reference in New Issue
Block a user