@orellbuehler/homeassistant-mcp 0.4.0 → 0.5.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -163,6 +163,26 @@ prefs, then save), so editing the Energy dashboard in the HA UI at the same mome
163
163
  Pass an `automation.*` or `script.*` entity id; the trace key is resolved automatically. Use these
164
164
  to debug **whether and how** an automation you authored actually ran.
165
165
 
166
+ **ZHA / Zigbee groups** (WebSocket)
167
+
168
+ | Tool | Description |
169
+ | -------------------------- | ----------------------------------------------------------------------------------- |
170
+ | `list_zha_groups` | Existing ZHA groups (group_id, name, members). |
171
+ | `list_zha_groupable` | Endpoints that can join a group (ieee + endpoint_id + their entities). |
172
+ | `create_zha_group` | Create a group on the coordinator; members by `entity_id` or `{ieee, endpoint_id}`. |
173
+ | `add_zha_group_members` | Add members to a group. |
174
+ | `remove_zha_group_members` | Remove members from a group. |
175
+ | `remove_zha_group` | Delete group(s) by id (member devices untouched). |
176
+
177
+ A ZHA group lives on the Zigbee coordinator and is exposed to HA as one group entity (e.g. a single
178
+ `light.*` that drives all members together via Zigbee multicast). This is **configuration
179
+ authoring**, not device control — like creating a helper, it changes what entities exist, not their
180
+ on/off state. The write tools call `zha/group/*` and **require an admin token**. Members must be
181
+ groupable ZHA endpoints (devices that support the Zigbee Groups cluster); `create_zha_group` /
182
+ `add_zha_group_members` accept `entity_id`s and resolve them to `{ieee, endpoint_id}` via
183
+ `list_zha_groupable`, or you can pass the `{ieee, endpoint_id}` pairs directly. The HA group entity
184
+ is created asynchronously, so read its final `entity_id` from the entity registry afterwards.
185
+
166
186
  ## Safety boundary
167
187
 
168
188
  - **No device control.** There is no generic `call_service`, no `set_state`, and no `fire_event`.
@@ -172,6 +192,9 @@ to debug **whether and how** an automation you authored actually ran.
172
192
  - **Energy dashboard config is writable** via `energy/save_prefs` (the `*_energy_*` tools). This is
173
193
  configuration authoring — the dashboard's sources and Individual-devices list — not device control,
174
194
  and it needs an admin token. No `call_service`, `set_state`, or `fire_event` is added.
195
+ - **ZHA groups are writable** via `zha/group/*` (the `*_zha_group*` tools). Creating/editing a Zigbee
196
+ group changes which group entities exist (config authoring), not any device's on/off state, and it
197
+ needs an admin token. Still no `call_service`, `set_state`, or `fire_event`.
175
198
  - **Read tools expose your home's data** (entity names, states, areas) to the agent/LLM. Use a
176
199
  scoped HA user if that matters to you.
177
200
  - Prefer `https` and a trusted network path to your instance.
package/dist/server.js CHANGED
@@ -8,8 +8,9 @@ import { registerReloadTools } from "./tools/reload.js";
8
8
  import { registerRegistryTools } from "./tools/registry.js";
9
9
  import { registerEnergyTools } from "./tools/energy.js";
10
10
  import { registerTraceTools } from "./tools/trace.js";
11
+ import { registerZhaTools } from "./tools/zha.js";
11
12
  export function createServer(client, wsClient) {
12
- const server = new McpServer({ name: "homeassistant-mcp", version: "0.4.0" });
13
+ const server = new McpServer({ name: "homeassistant-mcp", version: "0.5.1" });
13
14
  registerEntityTools(server, client);
14
15
  registerServiceTools(server, client);
15
16
  registerSystemTools(server, client);
@@ -19,5 +20,6 @@ export function createServer(client, wsClient) {
19
20
  registerRegistryTools(server, wsClient);
20
21
  registerEnergyTools(server, wsClient, client);
21
22
  registerTraceTools(server, wsClient, client);
23
+ registerZhaTools(server, wsClient);
22
24
  return server;
23
25
  }
@@ -0,0 +1,121 @@
1
+ import { z } from "zod";
2
+ import { ok, err } from "../hass/format.js";
3
+ // An entity_id can be listed either on the groupable endpoint itself or, on some
4
+ // HA versions where the endpoint list comes back empty/null, only under the
5
+ // device. Check both.
6
+ function endpointHasEntity(ep, entityId) {
7
+ const onEndpoint = ep.entities?.some((e) => e?.entity_id === entityId);
8
+ const onDevice = ep.device?.entities?.some((e) => e?.entity_id === entityId);
9
+ return Boolean(onEndpoint || onDevice);
10
+ }
11
+ function endpointEntityIds(ep) {
12
+ const refs = [...(ep.entities ?? []), ...(ep.device?.entities ?? [])];
13
+ return [
14
+ ...new Set(refs.filter((e) => !!e?.entity_id).map((e) => e.entity_id)),
15
+ ];
16
+ }
17
+ const memberSchema = z.union([
18
+ z.string(),
19
+ z.object({
20
+ ieee: z.string(),
21
+ endpoint_id: z.number().int().nonnegative(),
22
+ }),
23
+ ]);
24
+ async function resolveMembers(ws, members) {
25
+ const needLookup = members.some((m) => typeof m === "string");
26
+ let groupable = [];
27
+ if (needLookup) {
28
+ groupable = (await ws.command("zha/devices/groupable"));
29
+ }
30
+ return members.map((m) => {
31
+ if (typeof m !== "string")
32
+ return { ieee: m.ieee, endpoint_id: m.endpoint_id };
33
+ const hit = groupable.find((ep) => endpointHasEntity(ep, m));
34
+ if (!hit) {
35
+ throw new Error(`'${m}' is not a groupable ZHA entity. A ZHA group member must be a ZHA entity on an endpoint that supports the Zigbee Groups cluster (see list_zha_groupable). Non-ZHA entities (Wi-Fi/Shelly/Hue-bridge/etc.) cannot join a ZHA group.`);
36
+ }
37
+ return { ieee: hit.device.ieee, endpoint_id: hit.endpoint_id };
38
+ });
39
+ }
40
+ export function registerZhaTools(server, ws) {
41
+ server.tool("list_zha_groups", "List existing ZHA (Zigbee) groups via the WebSocket API (zha/groups). Each group has a group_id, name and its members. A ZHA group lives on the Zigbee coordinator and is exposed to HA as a single group entity (e.g. one light.* that drives all members together via Zigbee multicast).", {}, async () => {
42
+ try {
43
+ return ok(await ws.command("zha/groups"));
44
+ }
45
+ catch (e) {
46
+ return err(e);
47
+ }
48
+ });
49
+ server.tool("list_zha_groupable", "List ZHA device endpoints that can be added to a ZHA group (zha/devices/groupable) — endpoints whose device supports the Zigbee Groups cluster. Returns, per endpoint, its ieee + endpoint_id and the entities on it, so you can choose group members by entity_id. Use this to see what create_zha_group / add_zha_group_members accept.", {}, async () => {
50
+ try {
51
+ const eps = (await ws.command("zha/devices/groupable"));
52
+ const endpoints = eps.map((ep) => ({
53
+ ieee: ep.device?.ieee ?? null,
54
+ endpoint_id: ep.endpoint_id,
55
+ device_name: ep.device?.user_given_name ?? ep.device?.name ?? null,
56
+ entities: endpointEntityIds(ep),
57
+ }));
58
+ return ok({ count: endpoints.length, endpoints });
59
+ }
60
+ catch (e) {
61
+ return err(e);
62
+ }
63
+ });
64
+ server.tool("create_zha_group", "Create a ZHA (Zigbee) group on the coordinator (zha/group/add) and return the new group (group_id, name, members). This is configuration authoring — it does not switch anything; it tells the Zigbee network to treat the members as one group, and HA then exposes a single group entity (e.g. light.<name>) that controls them together via Zigbee multicast. 'members' is optional and accepts entity_ids (resolved to ieee+endpoint via list_zha_groupable) and/or explicit {ieee, endpoint_id} objects; only groupable ZHA entities are valid. The HA group entity is created asynchronously and may not be in this response — read its entity_id from the entity registry afterwards.", {
65
+ name: z.string().describe("Friendly name for the new group, e.g. 'Iris TV'"),
66
+ members: z
67
+ .array(memberSchema)
68
+ .optional()
69
+ .describe("Initial members: entity_ids (e.g. 'light.philips_iris_tv_left') and/or {ieee, endpoint_id} objects. Omit to create an empty group and add members later."),
70
+ }, async ({ name, members }) => {
71
+ try {
72
+ const payload = { group_name: name };
73
+ if (members && members.length)
74
+ payload.members = await resolveMembers(ws, members);
75
+ return ok(await ws.command("zha/group/add", payload));
76
+ }
77
+ catch (e) {
78
+ return err(e);
79
+ }
80
+ });
81
+ server.tool("add_zha_group_members", "Add members to an existing ZHA group (zha/group/members/add). 'members' accepts entity_ids (resolved via list_zha_groupable) and/or {ieee, endpoint_id} objects. Returns the updated group.", {
82
+ group_id: z.number().int().describe("Target group_id (from list_zha_groups)"),
83
+ members: z
84
+ .array(memberSchema)
85
+ .min(1)
86
+ .describe("Members to add: entity_ids and/or {ieee, endpoint_id} objects"),
87
+ }, async ({ group_id, members }) => {
88
+ try {
89
+ const resolved = await resolveMembers(ws, members);
90
+ return ok(await ws.command("zha/group/members/add", { group_id, members: resolved }));
91
+ }
92
+ catch (e) {
93
+ return err(e);
94
+ }
95
+ });
96
+ server.tool("remove_zha_group_members", "Remove members from an existing ZHA group (zha/group/members/remove). 'members' accepts entity_ids (resolved via list_zha_groupable) and/or {ieee, endpoint_id} objects. Returns the updated group.", {
97
+ group_id: z.number().int().describe("Target group_id (from list_zha_groups)"),
98
+ members: z
99
+ .array(memberSchema)
100
+ .min(1)
101
+ .describe("Members to remove: entity_ids and/or {ieee, endpoint_id} objects"),
102
+ }, async ({ group_id, members }) => {
103
+ try {
104
+ const resolved = await resolveMembers(ws, members);
105
+ return ok(await ws.command("zha/group/members/remove", { group_id, members: resolved }));
106
+ }
107
+ catch (e) {
108
+ return err(e);
109
+ }
110
+ });
111
+ server.tool("remove_zha_group", "Delete one or more ZHA groups by id (zha/group/remove). This removes the group from the Zigbee coordinator and its HA group entity; the member devices themselves are untouched. Returns the remaining groups.", {
112
+ group_ids: z.array(z.number().int()).min(1).describe("group_id(s) to delete"),
113
+ }, async ({ group_ids }) => {
114
+ try {
115
+ return ok(await ws.command("zha/group/remove", { group_ids }));
116
+ }
117
+ catch (e) {
118
+ return err(e);
119
+ }
120
+ });
121
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@orellbuehler/homeassistant-mcp",
3
- "version": "0.4.0",
3
+ "version": "0.5.1",
4
4
  "description": "Model Context Protocol server for Home Assistant: introspect entities, services, events, registries, render templates and validate configuration to help AI agents author HA configs and automations.",
5
5
  "type": "module",
6
6
  "bin": {