Files
mesh-catalog/modules/home-assistant/index.ts
jschoubben 7b06a7a408 Event names are local now, in the manifests and in the code
Every module named its events the way the old bus spelled a routing key —
`module.<module>.<verb>`. Design 29 says a module names an event locally and the
mesh works out where it lands, so all 37 were stale against a rule already
decided. On the new bus that derives into a namespace belonging to a module
called "module", so no cross-module subscription in the mesh matched anything:
nothing failed, nothing reacted (novox/hq 04-ISSUES/127).

36 manifests converted, and 43 files of module code with them. The code mattered
as much as the manifests: the runtime builds the subject from what `emit()` is
handed, so a converted manifest with unconverted code would have had the
permission and the subject disagree.

Three things the new check found on the way:

- `photos` emitted an event its manifest never declared, which the new bus refuses
  outright. Declared.
- `showcase` waited for an event nothing emits, so its demo could never be
  triggered — only `showcase` may publish under its own name. It emits both halves
  now.
- `distribution` declared an event named after a different module. It emits
  `image.pushed` under its own name. An event about a *role* belongs on the seat,
  where the name outlives whoever holds it, but the sdk has no way to publish on a
  seat yet, so that stays recorded rather than declared.

The audit logger's "everything" pattern is `**` rather than the old bus's `#`.
2026-09-27 14:42:28 +02:00

67 lines
2.7 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("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");