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,78 @@
|
||||
// The Home Assistant API client — home-assistant's own code, living in the module (novox/hq
|
||||
// ADR 0044). Both this module's tools and its events entrypoint import it, and nothing outside
|
||||
// home-assistant does. Talks to the HA REST API (/api) with a long-lived access token.
|
||||
|
||||
export interface HAEntityState {
|
||||
entity_id: string;
|
||||
state: string;
|
||||
attributes: Record<string, unknown>;
|
||||
last_changed?: string;
|
||||
last_updated?: string;
|
||||
}
|
||||
|
||||
export interface HAConfig {
|
||||
location_name?: string;
|
||||
version?: string;
|
||||
components?: string[];
|
||||
time_zone?: string;
|
||||
state?: string;
|
||||
}
|
||||
|
||||
export class HomeAssistantClient {
|
||||
readonly baseUrl: string;
|
||||
|
||||
constructor(
|
||||
url: string,
|
||||
private readonly token: string,
|
||||
) {
|
||||
this.baseUrl = url.replace(/\/$/, "");
|
||||
}
|
||||
|
||||
/**
|
||||
* Build from the module's resolved environment. The URL defaults to the local server (HA runs on
|
||||
* the node); the token is the long-lived access token minted in HA's profile — required, since
|
||||
* every API call is Bearer-authenticated and there is nowhere to discover it from.
|
||||
*/
|
||||
static fromEnv(env: NodeJS.ProcessEnv = process.env): HomeAssistantClient {
|
||||
const url = env.MESH_HOMEASSISTANT_URL ?? `http://127.0.0.1:${env.HOMEASSISTANT_PORT ?? "8123"}`;
|
||||
const token = env.MESH_HOMEASSISTANT_TOKEN;
|
||||
if (!token) throw new Error("no Home Assistant token — set MESH_HOMEASSISTANT_TOKEN");
|
||||
return new HomeAssistantClient(url, token);
|
||||
}
|
||||
|
||||
private async request(path: string, init?: RequestInit): Promise<unknown> {
|
||||
const res = await fetch(`${this.baseUrl}${path}`, {
|
||||
...init,
|
||||
headers: {
|
||||
Authorization: `Bearer ${this.token}`,
|
||||
"Content-Type": "application/json",
|
||||
Accept: "application/json",
|
||||
...(init?.headers ?? {}),
|
||||
},
|
||||
});
|
||||
if (!res.ok) throw new Error(`Home Assistant API ${path}: ${res.status} ${await res.text()}`);
|
||||
return res.json();
|
||||
}
|
||||
|
||||
async getConfig(): Promise<HAConfig> {
|
||||
return (await this.request("/api/config")) as HAConfig;
|
||||
}
|
||||
|
||||
/** All entity states, or one entity when an id is given. */
|
||||
async getStates(): Promise<HAEntityState[]> {
|
||||
return (await this.request("/api/states")) as HAEntityState[];
|
||||
}
|
||||
|
||||
async getState(entityId: string): Promise<HAEntityState> {
|
||||
return (await this.request(`/api/states/${encodeURIComponent(entityId)}`)) as HAEntityState;
|
||||
}
|
||||
|
||||
/** Call a service (e.g. switch.turn_on) — how "turn the light on" reaches HA. Returns the states
|
||||
* the call changed. */
|
||||
async callService(domain: string, service: string, data: Record<string, unknown> = {}): Promise<HAEntityState[]> {
|
||||
return (await this.request(`/api/services/${encodeURIComponent(domain)}/${encodeURIComponent(service)}`, {
|
||||
method: "POST",
|
||||
body: JSON.stringify(data),
|
||||
})) as HAEntityState[];
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user