Skip to content

Commit b442c9b

Browse files
committed
feat(home-assistant): add WebSocket-only registry and automation actions
The REST API serves entity states but not the registries behind them, so an agent could read light.living_room without learning which device, area, or floor it belongs to. Those registries, related-item search, per-device automation capabilities, script execution, and config validation are only reachable over the WebSocket API. Add five actions covering that gap. Each is a one-shot command, so it maps onto the existing request/response executor model without change; subscription commands are deliberately left out because the action model has no streaming and every Home Assistant subscription that carries an initial snapshot already has a one-shot equivalent. get_registries sends all five registry commands over a single connection. The handshake costs two round trips before any command can be sent, so batching matters more here than it would over HTTP.
1 parent 99acfb2 commit b442c9b

4 files changed

Lines changed: 536 additions & 7 deletions

File tree

src/providers/home_assistant/actions.ts

Lines changed: 161 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import type { ActionDefinition } from "../../core/types.ts";
1+
import type { ActionDefinition, JsonSchema } from "../../core/types.ts";
22

33
import { s } from "../../core/json-schema.ts";
44
import { defineProviderAction } from "../../core/provider-definition.ts";
@@ -33,6 +33,42 @@ const entityInputSchema = s.actionInput(
3333
"Input parameters for selecting one Home Assistant entity.",
3434
);
3535

36+
/** Output field for one registry that only the Home Assistant WebSocket API serves. */
37+
export type HomeAssistantRegistryName = "entities" | "devices" | "areas" | "floors" | "labels";
38+
39+
/** Every registry `get_registries` can fetch, in output-field order. */
40+
export const homeAssistantRegistryNames: HomeAssistantRegistryName[] = [
41+
"entities",
42+
"devices",
43+
"areas",
44+
"floors",
45+
"labels",
46+
];
47+
48+
const registryNames: string[] = homeAssistantRegistryNames;
49+
50+
// Home Assistant's search ItemType enum; the values it accepts as a search origin.
51+
const searchItemTypes: string[] = [
52+
"area",
53+
"automation",
54+
"automation_blueprint",
55+
"config_entry",
56+
"device",
57+
"entity",
58+
"floor",
59+
"group",
60+
"integration",
61+
"label",
62+
"person",
63+
"scene",
64+
"script",
65+
"script_blueprint",
66+
];
67+
68+
function registryListSchema(description: string): JsonSchema {
69+
return s.nullable(s.array(description, s.looseObject("One Home Assistant registry entry.")));
70+
}
71+
3672
export const homeAssistantActions: ActionDefinition[] = [
3773
defineProviderAction(service, {
3874
name: "get_config",
@@ -141,9 +177,125 @@ export const homeAssistantActions: ActionDefinition[] = [
141177
"The rendered Home Assistant template response.",
142178
),
143179
}),
180+
defineProviderAction(service, {
181+
name: "get_registries",
182+
description:
183+
"List the Home Assistant entity, device, area, floor, and label registries in one call. These registries expose the device and room structure behind entity ids, which the REST API does not serve.",
184+
inputSchema: s.actionInput(
185+
{
186+
include: s.array(
187+
"The registries to fetch. Defaults to all five when omitted or empty.",
188+
s.stringEnum("One Home Assistant registry name.", registryNames),
189+
),
190+
},
191+
[],
192+
"Input parameters for selecting which Home Assistant registries to fetch.",
193+
),
194+
outputSchema: s.actionOutput(
195+
{
196+
entities: registryListSchema(
197+
"The entity registry entries, including the device and area each entity belongs to.",
198+
),
199+
devices: registryListSchema("The device registry entries, including manufacturer, model, and area."),
200+
areas: registryListSchema("The area registry entries."),
201+
floors: registryListSchema("The floor registry entries."),
202+
labels: registryListSchema("The label registry entries."),
203+
},
204+
"The requested Home Assistant registries. Registries excluded from the request are null.",
205+
),
206+
}),
207+
defineProviderAction(service, {
208+
name: "search_related",
209+
description:
210+
"Find the Home Assistant items related to one entity, device, area, automation, or config entry, such as the automations that reference a given light.",
211+
inputSchema: s.actionInput(
212+
{
213+
itemType: s.stringEnum("The Home Assistant item type to search from.", searchItemTypes),
214+
itemId: s.nonEmptyString(
215+
"The identifier of the item to search from, for example light.living_room for an entity.",
216+
),
217+
},
218+
["itemType", "itemId"],
219+
"Input parameters for one Home Assistant related-items search.",
220+
),
221+
outputSchema: s.actionOutput(
222+
{
223+
related: s.looseObject("The related Home Assistant item identifiers, keyed by item type."),
224+
},
225+
"The Home Assistant items related to the requested item.",
226+
),
227+
}),
228+
defineProviderAction(service, {
229+
name: "list_device_automations",
230+
description:
231+
"List the triggers, conditions, and actions one Home Assistant device supports, for building automations against that device.",
232+
inputSchema: s.actionInput(
233+
{
234+
deviceId: s.nonEmptyString("The Home Assistant device registry identifier."),
235+
},
236+
["deviceId"],
237+
"Input parameters for listing one Home Assistant device's automation capabilities.",
238+
),
239+
outputSchema: s.actionOutput(
240+
{
241+
triggers: s.array("The device triggers.", s.looseObject("One Home Assistant device trigger.")),
242+
conditions: s.array("The device conditions.", s.looseObject("One Home Assistant device condition.")),
243+
actions: s.array("The device actions.", s.looseObject("One Home Assistant device action.")),
244+
},
245+
"The automation capabilities for the requested Home Assistant device.",
246+
),
247+
}),
248+
defineProviderAction(service, {
249+
name: "execute_script",
250+
description:
251+
"Run a Home Assistant script sequence, which can chain several service calls, delays, and conditions in one request instead of one service call at a time.",
252+
inputSchema: s.actionInput(
253+
{
254+
sequence: s.array(
255+
"The Home Assistant script steps to run, in the same format as a script's sequence.",
256+
s.looseObject("One Home Assistant script step."),
257+
),
258+
variables: s.looseObject("Optional variables made available to the script sequence."),
259+
},
260+
["sequence"],
261+
"Input parameters for running one Home Assistant script sequence.",
262+
),
263+
outputSchema: s.actionOutput(
264+
{
265+
context: s.nullable(s.looseObject("The Home Assistant context for the script run.")),
266+
response: s.nullable(s.looseObject("The optional script response variable returned by Home Assistant.")),
267+
},
268+
"The Home Assistant script execution result.",
269+
),
270+
}),
271+
defineProviderAction(service, {
272+
name: "validate_config",
273+
description:
274+
"Validate Home Assistant trigger, condition, and action configurations before storing them in an automation.",
275+
inputSchema: s.actionInput(
276+
{
277+
triggers: s.array("The trigger configurations to validate.", s.looseObject("One Home Assistant trigger.")),
278+
conditions: s.array(
279+
"The condition configurations to validate.",
280+
s.looseObject("One Home Assistant condition."),
281+
),
282+
actions: s.array("The action configurations to validate.", s.looseObject("One Home Assistant action.")),
283+
},
284+
[],
285+
"Input parameters for validating Home Assistant automation configuration. At least one list is required.",
286+
),
287+
outputSchema: s.actionOutput(
288+
{
289+
validation: s.looseObject(
290+
"The validation result keyed by triggers, conditions, and actions, each with valid and error fields.",
291+
),
292+
},
293+
"The Home Assistant configuration validation result.",
294+
),
295+
}),
144296
];
145297

146-
export type HomeAssistantActionName =
298+
export type HomeAssistantRestActionName =
147299
| "get_config"
148300
| "list_states"
149301
| "get_state"
@@ -152,3 +304,10 @@ export type HomeAssistantActionName =
152304
| "list_events"
153305
| "fire_event"
154306
| "render_template";
307+
308+
export type HomeAssistantWebSocketActionName =
309+
| "get_registries"
310+
| "search_related"
311+
| "list_device_automations"
312+
| "execute_script"
313+
| "validate_config";

src/providers/home_assistant/executors.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import type { HomeAssistantActionContext } from "./runtime.ts";
33

44
import { isPrivateNetworkAccessAllowed } from "../../core/request.ts";
55
import { defineProviderExecutors, requireApiKeyCredential } from "../provider-runtime.ts";
6+
import { homeAssistantWebSocketActionHandlers } from "./runtime-ws.ts";
67
import {
78
homeAssistantActionHandlers,
89
resolveHomeAssistantBaseUrl,
@@ -13,7 +14,7 @@ const service = "home_assistant";
1314

1415
export const executors: ProviderExecutors = defineProviderExecutors<HomeAssistantActionContext>({
1516
service,
16-
handlers: homeAssistantActionHandlers,
17+
handlers: { ...homeAssistantActionHandlers, ...homeAssistantWebSocketActionHandlers },
1718
allowPrivateNetwork: isPrivateNetworkAccessAllowed,
1819
async createContext(context: ExecutionContext, fetcher: typeof fetch): Promise<HomeAssistantActionContext> {
1920
const credential = await requireApiKeyCredential(context, service);

0 commit comments

Comments
 (0)