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.
77 lines
3.3 KiB
TypeScript
77 lines
3.3 KiB
TypeScript
// home-assistant's tools — its own code (novox/hq ADR 0044), importing its own client. They return
|
|
// structured data; the mesh serves them through the sdk's tool harness.
|
|
|
|
import { registerModuleTools, type ToolDefinition } from "@novox/mesh-sdk/tools";
|
|
import { HomeAssistantClient, type HAEntityState } from "../client.js";
|
|
|
|
/** Trim an entity to the fields worth returning — the full attribute bag is large and mostly noise. */
|
|
function summarize(s: HAEntityState): { entity_id: string; state: string; name?: string; last_changed?: string } {
|
|
return {
|
|
entity_id: s.entity_id,
|
|
state: s.state,
|
|
name: typeof s.attributes.friendly_name === "string" ? s.attributes.friendly_name : undefined,
|
|
last_changed: s.last_changed,
|
|
};
|
|
}
|
|
|
|
export function getHomeAssistantTools(ha: HomeAssistantClient): ToolDefinition[] {
|
|
return [
|
|
{
|
|
name: "homeassistant_states",
|
|
description:
|
|
"Entity states from Home Assistant. With no argument, lists every entity; with `entity` (e.g. light.kitchen), returns just that one with its full attributes.",
|
|
input: { entity: { type: "string", description: "an entity id to fetch one entity; omitted lists all" } },
|
|
run: async (args) => {
|
|
if (args.entity) {
|
|
const s = await ha.getState(String(args.entity));
|
|
return { entity_id: s.entity_id, state: s.state, attributes: s.attributes, last_changed: s.last_changed };
|
|
}
|
|
const states = await ha.getStates();
|
|
return { count: states.length, entities: states.map(summarize) };
|
|
},
|
|
},
|
|
{
|
|
name: "homeassistant_call_service",
|
|
description:
|
|
"Call a Home Assistant service — turn a switch/light on or off, lock a door, etc. Give the domain (e.g. switch), the service (e.g. turn_on), and optionally a target entity and extra data.",
|
|
input: {
|
|
domain: { type: "string", description: "the service domain, e.g. light, switch, lock" },
|
|
service: { type: "string", description: "the service, e.g. turn_on, turn_off, toggle" },
|
|
entity: { type: "string", description: "the entity id to target (optional)" },
|
|
data: { type: "object", description: "extra service data merged into the call (optional)" },
|
|
},
|
|
run: async (args) => {
|
|
const data: Record<string, unknown> = { ...(args.data as Record<string, unknown> | undefined) };
|
|
if (args.entity) data.entity_id = String(args.entity);
|
|
const changed = await ha.callService(String(args.domain), String(args.service), data);
|
|
return { changed: changed.map(summarize) };
|
|
},
|
|
},
|
|
{
|
|
name: "homeassistant_config",
|
|
description: "Home Assistant instance config: version, location, timezone, loaded components.",
|
|
input: {},
|
|
run: async () => {
|
|
const c = await ha.getConfig();
|
|
return {
|
|
location: c.location_name,
|
|
version: c.version,
|
|
time_zone: c.time_zone,
|
|
state: c.state,
|
|
components: c.components?.length ?? 0,
|
|
};
|
|
},
|
|
},
|
|
];
|
|
}
|
|
|
|
// The tools exist only when a token is configured; without one, home-assistant contributes none
|
|
// rather than failing the whole runtime.
|
|
registerModuleTools("home-assistant", (env) => {
|
|
try {
|
|
return getHomeAssistantTools(HomeAssistantClient.fromEnv(env));
|
|
} catch {
|
|
return [];
|
|
}
|
|
});
|