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 `#`.
67 lines
2.7 KiB
TypeScript
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");
|