@coffeerunhobby/mcp-ha-connect 2.1.0 → 2.2.0

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.
Files changed (74) hide show
  1. package/README.md +19 -3
  2. package/dist/config.d.ts +4 -0
  3. package/dist/config.js +6 -0
  4. package/dist/config.js.map +1 -1
  5. package/dist/haClient/automations.d.ts +18 -2
  6. package/dist/haClient/automations.js +25 -7
  7. package/dist/haClient/automations.js.map +1 -1
  8. package/dist/haClient/events.d.ts +60 -7
  9. package/dist/haClient/events.js +252 -47
  10. package/dist/haClient/events.js.map +1 -1
  11. package/dist/haClient/index.d.ts +2 -1
  12. package/dist/haClient/index.js +4 -3
  13. package/dist/haClient/index.js.map +1 -1
  14. package/dist/haClient/wsCommand.d.ts +16 -0
  15. package/dist/haClient/wsCommand.js +36 -0
  16. package/dist/haClient/wsCommand.js.map +1 -0
  17. package/dist/omadaClient/site.js +15 -2
  18. package/dist/omadaClient/site.js.map +1 -1
  19. package/dist/ownerQuestions/service.d.ts +167 -0
  20. package/dist/ownerQuestions/service.js +439 -0
  21. package/dist/ownerQuestions/service.js.map +1 -0
  22. package/dist/server/common.d.ts +3 -0
  23. package/dist/server/common.js +2 -1
  24. package/dist/server/common.js.map +1 -1
  25. package/dist/server/http.js +30 -6
  26. package/dist/server/http.js.map +1 -1
  27. package/dist/server/stream.d.ts +2 -0
  28. package/dist/server/stream.js +2 -1
  29. package/dist/server/stream.js.map +1 -1
  30. package/dist/tools/common.d.ts +1 -1
  31. package/dist/tools/common.js +4 -1
  32. package/dist/tools/common.js.map +1 -1
  33. package/dist/tools/homeassistant/askOwner.d.ts +29 -0
  34. package/dist/tools/homeassistant/askOwner.js +85 -0
  35. package/dist/tools/homeassistant/askOwner.js.map +1 -0
  36. package/dist/tools/homeassistant/getAutomationTrace.d.ts +5 -0
  37. package/dist/tools/homeassistant/getAutomationTrace.js +10 -5
  38. package/dist/tools/homeassistant/getAutomationTrace.js.map +1 -1
  39. package/dist/tools/index.d.ts +3 -0
  40. package/dist/tools/index.js +5 -1
  41. package/dist/tools/index.js.map +1 -1
  42. package/dist/utils/plainText.d.ts +22 -0
  43. package/dist/utils/plainText.js +39 -0
  44. package/dist/utils/plainText.js.map +1 -0
  45. package/dist/version.d.ts +1 -1
  46. package/dist/version.js +1 -1
  47. package/docs/CHANGELOG.md +19 -0
  48. package/openapi.json +1 -1
  49. package/package.json +1 -1
  50. package/src/config.ts +10 -0
  51. package/src/haClient/automations.ts +39 -7
  52. package/src/haClient/events.ts +289 -56
  53. package/src/haClient/index.ts +10 -4
  54. package/src/haClient/wsCommand.ts +44 -0
  55. package/src/omadaClient/site.ts +17 -2
  56. package/src/ownerQuestions/service.ts +536 -0
  57. package/src/server/common.ts +5 -1
  58. package/src/server/http.ts +30 -6
  59. package/src/server/stream.ts +4 -1
  60. package/src/tools/common.ts +4 -1
  61. package/src/tools/homeassistant/askOwner.ts +133 -0
  62. package/src/tools/homeassistant/getAutomationTrace.ts +13 -7
  63. package/src/tools/index.ts +8 -1
  64. package/src/utils/plainText.ts +48 -0
  65. package/src/version.ts +1 -1
  66. package/tests/config.test.ts +12 -0
  67. package/tests/haClient/automationTrace.test.ts +59 -0
  68. package/tests/haClient/automations.test.ts +16 -0
  69. package/tests/haClient/events.test.ts +455 -0
  70. package/tests/haClient/eventsTls.test.ts +52 -0
  71. package/tests/omadaClient/ntp.test.ts +15 -0
  72. package/tests/ownerQuestions/service.test.ts +499 -0
  73. package/tests/ownerQuestions/tools.test.ts +186 -0
  74. package/tests/tools/homeassistant/handlers.test.ts +10 -1
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/tools/index.ts"],"names":[],"mappings":"AAAA;;GAEG;AAMH,OAAO,EAAE,MAAM,EAAE,MAAM,oBAAoB,CAAC;AAE5C,wCAAwC;AACxC,OAAO,EAAE,0BAA0B,EAAE,MAAM,0BAA0B,CAAC;AACtE,OAAO,EAAE,kBAAkB,EAA8B,MAAM,kBAAkB,CAAC;AAClF,OAAO,EAAE,eAAe,EAAE,MAAM,eAAe,CAAC;AAChD,OAAO,EAAE,kBAAkB,EAAmB,MAAM,kBAAkB,CAAC;AAavE;;GAEG;AACH,MAAM,UAAU,gBAAgB,CAAC,OAA6B;IAC5D,MAAM,EAAE,MAAM,EAAE,QAAQ,EAAE,WAAW,EAAE,QAAQ,EAAE,WAAW,EAAE,oBAAoB,EAAE,GAAG,OAAO,CAAC;IAC/F,MAAM,CAAC,KAAK,CAAC,uBAAuB,CAAC,CAAC;IAEtC,IAAI,UAAU,GAAG,CAAC,CAAC;IAEnB,4CAA4C;IAC5C,IAAI,QAAQ,EAAE,CAAC;QACb,MAAM,WAAW,GAAG,0BAA0B,CAAC,MAAM,EAAE,QAAQ,CAAC,CAAC;QACjE,UAAU,IAAI,WAAW,CAAC;IAC5B,CAAC;IAED,mCAAmC;IACnC,IAAI,WAAW,EAAE,CAAC;QAChB,MAAM,cAAc,GAAG,kBAAkB,CAAC,MAAM,EAAE,WAAW,EAAE,oBAAoB,CAAC,CAAC;QACrF,UAAU,IAAI,cAAc,CAAC;IAC/B,CAAC;IAED,iEAAiE;IACjE,MAAM,WAAW,GAAG,eAAe,CAAC,MAAM,EAAE,QAAQ,CAAC,CAAC;IACtD,UAAU,IAAI,WAAW,CAAC;IAE1B,sDAAsD;IACtD,UAAU,IAAI,kBAAkB,CAAC,MAAM,EAAE,WAAW,IAAI,EAAE,CAAC,CAAC;IAE5D,MAAM,CAAC,IAAI,CAAC,mCAAmC,EAAE;QAC/C,UAAU;QACV,SAAS,EAAE,CAAC,CAAC,QAAQ;QACrB,YAAY,EAAE,CAAC,CAAC,WAAW;QAC3B,SAAS,EAAE,CAAC,CAAC,QAAQ;QACrB,YAAY,EAAE,MAAM,CAAC,IAAI,CAAC,WAAW,IAAI,EAAE,CAAC,CAAC,MAAM,GAAG,CAAC;KACxD,CAAC,CAAC;AACL,CAAC;AAED,6BAA6B;AAC7B,OAAO,EAAE,YAAY,EAAE,eAAe,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;AAExE,qEAAqE;AACrE,OAAO,EAAE,0BAA0B,EAAE,MAAM,0BAA0B,CAAC;AACtE,OAAO,EAAE,kBAAkB,EAAE,MAAM,kBAAkB,CAAC;AACtD,OAAO,EAAE,eAAe,EAAE,MAAM,eAAe,CAAC"}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/tools/index.ts"],"names":[],"mappings":"AAAA;;GAEG;AAMH,OAAO,EAAE,MAAM,EAAE,MAAM,oBAAoB,CAAC;AAE5C,wCAAwC;AACxC,OAAO,EAAE,0BAA0B,EAAE,MAAM,0BAA0B,CAAC;AACtE,OAAO,EAAE,kBAAkB,EAA8B,MAAM,kBAAkB,CAAC;AAClF,OAAO,EAAE,eAAe,EAAE,MAAM,eAAe,CAAC;AAChD,OAAO,EAAE,kBAAkB,EAAmB,MAAM,kBAAkB,CAAC;AACvE,OAAO,EAAE,0BAA0B,EAAE,MAAM,6BAA6B,CAAC;AAgBzE;;GAEG;AACH,MAAM,UAAU,gBAAgB,CAAC,OAA6B;IAC5D,MAAM,EAAE,MAAM,EAAE,QAAQ,EAAE,WAAW,EAAE,QAAQ,EAAE,WAAW,EAAE,oBAAoB,EAAE,cAAc,EAAE,GAAG,OAAO,CAAC;IAC/G,MAAM,CAAC,KAAK,CAAC,uBAAuB,CAAC,CAAC;IAEtC,IAAI,UAAU,GAAG,CAAC,CAAC;IAEnB,4CAA4C;IAC5C,IAAI,QAAQ,EAAE,CAAC;QACb,MAAM,WAAW,GAAG,0BAA0B,CAAC,MAAM,EAAE,QAAQ,CAAC,CAAC;QACjE,UAAU,IAAI,WAAW,CAAC;QAC1B,IAAI,cAAc,EAAE,CAAC;YACnB,UAAU,IAAI,0BAA0B,CAAC,MAAM,EAAE,cAAc,CAAC,CAAC;QACnE,CAAC;IACH,CAAC;IAED,mCAAmC;IACnC,IAAI,WAAW,EAAE,CAAC;QAChB,MAAM,cAAc,GAAG,kBAAkB,CAAC,MAAM,EAAE,WAAW,EAAE,oBAAoB,CAAC,CAAC;QACrF,UAAU,IAAI,cAAc,CAAC;IAC/B,CAAC;IAED,iEAAiE;IACjE,MAAM,WAAW,GAAG,eAAe,CAAC,MAAM,EAAE,QAAQ,CAAC,CAAC;IACtD,UAAU,IAAI,WAAW,CAAC;IAE1B,sDAAsD;IACtD,UAAU,IAAI,kBAAkB,CAAC,MAAM,EAAE,WAAW,IAAI,EAAE,CAAC,CAAC;IAE5D,MAAM,CAAC,IAAI,CAAC,mCAAmC,EAAE;QAC/C,UAAU;QACV,SAAS,EAAE,CAAC,CAAC,QAAQ;QACrB,YAAY,EAAE,CAAC,CAAC,WAAW;QAC3B,SAAS,EAAE,CAAC,CAAC,QAAQ;QACrB,YAAY,EAAE,MAAM,CAAC,IAAI,CAAC,WAAW,IAAI,EAAE,CAAC,CAAC,MAAM,GAAG,CAAC;KACxD,CAAC,CAAC;AACL,CAAC;AAED,6BAA6B;AAC7B,OAAO,EAAE,YAAY,EAAE,eAAe,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;AAExE,qEAAqE;AACrE,OAAO,EAAE,0BAA0B,EAAE,MAAM,0BAA0B,CAAC;AACtE,OAAO,EAAE,kBAAkB,EAAE,MAAM,kBAAkB,CAAC;AACtD,OAAO,EAAE,eAAe,EAAE,MAAM,eAAe,CAAC"}
@@ -0,0 +1,22 @@
1
+ /**
2
+ * Plain-text validation for user-visible strings we put on the owner's phone.
3
+ *
4
+ * Text is NFC-normalized first (so composed letters such as Romanian ș/ț/ă count
5
+ * as one character), then every character must be a letter, digit, punctuation,
6
+ * a math/currency/modifier symbol, the degree sign, or a plain space; line
7
+ * breaks only where allowed. Everything else is refused, never stripped:
8
+ * emoji and pictographs, control and invisible format characters (zero-width
9
+ * joiners, bidi overrides that make text display differently than it reads),
10
+ * variation selectors and leftover combining marks ("zalgo"), private-use and
11
+ * lone surrogates.
12
+ */
13
+ export interface PlainTextOptions {
14
+ field: string;
15
+ maxLength: number;
16
+ allowNewlines?: boolean;
17
+ }
18
+ /**
19
+ * @returns the NFC-normalized text
20
+ * @throws Error naming the field and the first offending character
21
+ */
22
+ export declare function validatePlainText(text: string, options: PlainTextOptions): string;
@@ -0,0 +1,39 @@
1
+ /**
2
+ * Plain-text validation for user-visible strings we put on the owner's phone.
3
+ *
4
+ * Text is NFC-normalized first (so composed letters such as Romanian ș/ț/ă count
5
+ * as one character), then every character must be a letter, digit, punctuation,
6
+ * a math/currency/modifier symbol, the degree sign, or a plain space; line
7
+ * breaks only where allowed. Everything else is refused, never stripped:
8
+ * emoji and pictographs, control and invisible format characters (zero-width
9
+ * joiners, bidi overrides that make text display differently than it reads),
10
+ * variation selectors and leftover combining marks ("zalgo"), private-use and
11
+ * lone surrogates.
12
+ */
13
+ const ALLOWED_CHAR = /^[\p{L}\p{N}\p{P}\p{Sm}\p{Sc}\p{Sk}° ]$/u;
14
+ /**
15
+ * @returns the NFC-normalized text
16
+ * @throws Error naming the field and the first offending character
17
+ */
18
+ export function validatePlainText(text, options) {
19
+ const normalized = text.normalize('NFC');
20
+ const chars = [...normalized];
21
+ if (chars.length === 0 || normalized.trim() === '') {
22
+ throw new Error(`${options.field} must not be empty`);
23
+ }
24
+ if (chars.length > options.maxLength) {
25
+ throw new Error(`${options.field} is ${chars.length} characters; the maximum is ${options.maxLength}`);
26
+ }
27
+ for (const [index, char] of chars.entries()) {
28
+ if (char === '\n' && options.allowNewlines) {
29
+ continue;
30
+ }
31
+ if (!ALLOWED_CHAR.test(char)) {
32
+ const codePoint = char.codePointAt(0).toString(16).toUpperCase().padStart(4, '0');
33
+ throw new Error(`${options.field} contains a disallowed character U+${codePoint} at position ${index + 1}; ` +
34
+ 'only plain text is accepted (no emoji, control or invisible characters)');
35
+ }
36
+ }
37
+ return normalized;
38
+ }
39
+ //# sourceMappingURL=plainText.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"plainText.js","sourceRoot":"","sources":["../../src/utils/plainText.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAEH,MAAM,YAAY,GAAG,0CAA0C,CAAC;AAQhE;;;GAGG;AACH,MAAM,UAAU,iBAAiB,CAAC,IAAY,EAAE,OAAyB;IACrE,MAAM,UAAU,GAAG,IAAI,CAAC,SAAS,CAAC,KAAK,CAAC,CAAC;IACzC,MAAM,KAAK,GAAG,CAAC,GAAG,UAAU,CAAC,CAAC;IAC9B,IAAI,KAAK,CAAC,MAAM,KAAK,CAAC,IAAI,UAAU,CAAC,IAAI,EAAE,KAAK,EAAE,EAAE,CAAC;QACjD,MAAM,IAAI,KAAK,CAAC,GAAG,OAAO,CAAC,KAAK,oBAAoB,CAAC,CAAC;IAC1D,CAAC;IACD,IAAI,KAAK,CAAC,MAAM,GAAG,OAAO,CAAC,SAAS,EAAE,CAAC;QACnC,MAAM,IAAI,KAAK,CAAC,GAAG,OAAO,CAAC,KAAK,OAAO,KAAK,CAAC,MAAM,+BAA+B,OAAO,CAAC,SAAS,EAAE,CAAC,CAAC;IAC3G,CAAC;IACD,KAAK,MAAM,CAAC,KAAK,EAAE,IAAI,CAAC,IAAI,KAAK,CAAC,OAAO,EAAE,EAAE,CAAC;QAC1C,IAAI,IAAI,KAAK,IAAI,IAAI,OAAO,CAAC,aAAa,EAAE,CAAC;YACzC,SAAS;QACb,CAAC;QACD,IAAI,CAAC,YAAY,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE,CAAC;YAC3B,MAAM,SAAS,GAAG,IAAI,CAAC,WAAW,CAAC,CAAC,CAAE,CAAC,QAAQ,CAAC,EAAE,CAAC,CAAC,WAAW,EAAE,CAAC,QAAQ,CAAC,CAAC,EAAE,GAAG,CAAC,CAAC;YACnF,MAAM,IAAI,KAAK,CACX,GAAG,OAAO,CAAC,KAAK,sCAAsC,SAAS,gBAAgB,KAAK,GAAG,CAAC,IAAI;gBAC5F,yEAAyE,CAC5E,CAAC;QACN,CAAC;IACL,CAAC;IACD,OAAO,UAAU,CAAC;AACtB,CAAC"}
package/dist/version.d.ts CHANGED
@@ -2,4 +2,4 @@
2
2
  * Package version - hardcoded at build time for reliability
3
3
  * Keep in sync with package.json version
4
4
  */
5
- export declare const VERSION = "2.1.0";
5
+ export declare const VERSION = "2.2.0";
package/dist/version.js CHANGED
@@ -2,5 +2,5 @@
2
2
  * Package version - hardcoded at build time for reliability
3
3
  * Keep in sync with package.json version
4
4
  */
5
- export const VERSION = '2.1.0';
5
+ export const VERSION = '2.2.0';
6
6
  //# sourceMappingURL=version.js.map
package/docs/CHANGELOG.md CHANGED
@@ -1,3 +1,22 @@
1
+ ### 2.2.0
2
+ - **New: owner questions — approve or deny from the phone.** `askOwner` sends a sticky, persistent notification with 1-3 buttons (default Approve / Deny) and returns a `requestId` at once; `getOwnerAnswer` waits up to 50 s per call and returns `answered` (button index and label, `answeredAt`, the HA user who tapped), `pending`, `timeout`, `cancelled` or `unknown`; `cancelOwnerQuestion` withdraws a question and marks the notification "Cancelled". Two calls rather than one long one because Cloudflare cuts requests at ~100 s. Questions stay open up to 7 days (default 10 min); the phone removes the notification at expiry by itself.
3
+ - **No database, restart-proof:** the requestId carries the question, buttons, target, expiry and asking client, signed with a key derived from `MCP_AUTH_SECRET`; each button's action id is signed separately and cannot be derived from the requestId. After a restart the server still recognises open questions and the taps on them. Only a tap during the restart itself is lost (the question then expires as NO).
4
+ - **Security:** a tap counts only with origin REMOTE and a user (the mobile app webhook), so an automation firing a fake `mobile_app_notification_action` cannot approve anything; tampered ids, extended expiries and swapped button indexes are rejected; only the asking token `sub` can read or cancel; at most 3 open questions and 30 per hour per client; first tap wins; late taps are ignored. Needs `NOTIFY`.
5
+ - **Plain text only:** question at most 256 characters (line breaks allowed), title 60, button labels 20. Text is NFC-normalized (Romanian diacritics are fine), then emoji, control and invisible characters (zero-width joiners, bidi overrides), variation selectors and stacked combining marks are refused with the offending character named — never silently stripped or cut.
6
+ - **`MCP_OWNER_QUESTIONS_HA_USERS`**: comma-separated HA user ids allowed to answer (the `answeredBy` of an answer); unset = any HA user, with a startup warning.
7
+ - Known limit, accepted: cancellations and answers are kept in memory only. After a restart, someone with a Home Assistant login could re-send an old button id through HA's API and answer a question that had been cancelled or denied (the phone no longer shows its buttons; that access already controls the house).
8
+ - Needs HTTP mode, an **admin** Home Assistant token (button taps are not on HA's non-admin event allowlist) and `MCP_AUTH_SECRET`. If Home Assistant refuses the tap subscription at startup, the three tools are not offered (logged); if it refuses later, `askOwner` returns an error saying so instead of sending questions nobody can answer. New `MCP_OWNER_QUESTIONS_ENABLED` (default `true`).
9
+ - **Fix — `getAutomationTrace` always answered "Not Found".** Home Assistant has no REST trace endpoint; traces exist only over the WebSocket (`trace/list`, admin-only) and are keyed by the automation's `id` attribute, not its entity id. The tool now uses a one-off WebSocket command, returns the newest runs first (new `limit`, default 5, max 20) with trigger, state and errors, and explains when an automation defined in YAML without an `id` has no traces.
10
+ - **Fix — Home Assistant events stopped after an HA restart.** The WebSocket client reconnected but never re-sent its subscriptions (HA forgets them with the connection), so `/subscribe_events` went silent after every HA restart. Subscriptions are now restored on reconnect; the client keeps retrying with the delay capped at 60 s instead of giving up after 10 attempts; commands are matched to their results by id (they were matched in arrival order); connecting has a deadline and fails at once if HA closes before authenticating; and a subscription made while Home Assistant is unreachable (also at server startup) is queued and sent once HA is back, instead of failing (an SSE client stays connected and receives events when HA returns; previously owner questions and SSE stayed unavailable until a server restart). Unsubscribing works while HA is offline (the subscription is forgotten for good, never restored on reconnect), and a subscription cancelled before HA confirmed it is cancelled on HA's side too. A subscription HA refuses (e.g. a non-admin token) is reported and dropped; one whose confirmation is lost is cancelled and retried (no duplicate subscriptions on HA); and an event HA sends right after confirming a subscription is no longer dropped. Errors from these WebSocket commands reach callers and logs as HA's error code only (e.g. `Home Assistant rejected 'trace/list' (unauthorized)`), never HA's free text or the internal address; the detail stays in the server log. The WebSocket connections now follow `HA_STRICT_SSL` like the REST client.
11
+ - **Fix — duplicate events.** With two subscriptions to the same event type, each received every event twice (HA sends one copy per subscription). Each copy now goes only to its own subscription.
12
+ - **Logging — tool arguments are logged at debug level only.** The info-level `Tool invoked` line now carries just the tool and session; the arguments (message and question text, entity names, MAC addresses) appear only with `LOG_LEVEL=debug`.
13
+ - README: `MCP_SSE_EVENTS_ENABLED` defaults to `true` (the README said `false`).
14
+ - +88 tests (owner question service and tools incl. a real MCP round trip and concurrent rate limits, plain-text validation, WebSocket client against a fake HA server incl. reconnects, deadlines and the TLS policy, trace lookup; the security checks are mutation-tested).
15
+
16
+ ### 2.1.1
17
+ - **Fix — `omada_setSiteNtpServers` was rejected by the controller on sites with daylight saving off.** Omada returns the DST start/end as empty objects when DST is off, but rejects them on write ("Daylight saving parameter [month] should not be null"). The site update now sends DST start/end only when complete; with DST on and an incomplete start/end it refuses to write. The rejected 2.1.0 call changed nothing (the controller validates before saving).
18
+ - +2 tests (the live DST-off shape; DST on with an incomplete start/end).
19
+
1
20
  ### 2.1.0
2
21
  - **New: site NTP tools for Omada.** `omada_getSiteNtpStatus` (QUERY; also `omada_read /network/ntp` in graph mode) reads the NTP configuration and status. `omada_setSiteNtpServers` (**CONFIGURE**, not CONTROL: it changes site-wide configuration) replaces the site NTP server list (hostnames or IPv4, at most 5) and can turn NTP on or off. Omada has no NTP-only write: NTP lives in the site settings, so the tool reads the site, changes only the NTP fields and writes the whole site back (`PUT /sites/{siteId}`), translating the read and write shapes (servers are strings on read, `{ address }` objects on write; read-only DST fields are dropped). It refuses to write if the site lacks a required field, validates every address before contacting the controller, supports `dryRun` to preview the exact request, and reports the before/after lists as the controller holds them. The controller has no conditional update, so writes are serialized per site and the site is re-read just before the PUT: if a non-NTP setting changed in between, nothing is written and the call fails with a conflict. Not exposed on the Open WebUI REST bridge.
3
22
  - README: the Omada tool list was stale (said 24, missed the block/unblock, PoE and SSID action tools); it now lists all 30.
package/openapi.json CHANGED
@@ -2,7 +2,7 @@
2
2
  "openapi": "3.1.0",
3
3
  "info": {
4
4
  "title": "Home Assistant MCP Wrapper",
5
- "version": "2.1.0",
5
+ "version": "2.2.0",
6
6
  "description": "REST endpoints that wrap MCP tools for Home Assistant integration."
7
7
  },
8
8
  "servers": [
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@coffeerunhobby/mcp-ha-connect",
3
- "version": "2.1.0",
3
+ "version": "2.2.0",
4
4
  "description": "Smart home MCP server for Home Assistant plus Local AI integration",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
package/src/config.ts CHANGED
@@ -80,6 +80,8 @@ const envSchema = z
80
80
 
81
81
  // SSE Event Subscription Configuration
82
82
  sseEventsEnabled: createBooleanStringSchema(true),
83
+ ownerQuestionsEnabled: createBooleanStringSchema(true),
84
+ ownerQuestionsHaUsers: z.string().optional(),
83
85
  sseEventsPath: z.string().optional(),
84
86
 
85
87
  // Rate Limiting Configuration
@@ -230,6 +232,10 @@ export interface EnvironmentConfig {
230
232
 
231
233
  // SSE Event Subscription Configuration
232
234
  sseEventsEnabled: boolean;
235
+ /** askOwner / getOwnerAnswer / cancelOwnerQuestion (HTTP mode with Home Assistant). */
236
+ ownerQuestionsEnabled: boolean;
237
+ /** HA user ids allowed to answer owner questions; empty = any HA user. */
238
+ ownerQuestionsHaUsers: string[];
233
239
  sseEventsPath: string;
234
240
 
235
241
  // Rate Limiting Configuration
@@ -301,6 +307,8 @@ export function loadConfig(env: NodeJS.ProcessEnv = process.env): EnvironmentCon
301
307
 
302
308
  // SSE Event Subscription Configuration
303
309
  sseEventsEnabled: env.MCP_SSE_EVENTS_ENABLED,
310
+ ownerQuestionsEnabled: env.MCP_OWNER_QUESTIONS_ENABLED,
311
+ ownerQuestionsHaUsers: env.MCP_OWNER_QUESTIONS_HA_USERS,
304
312
  sseEventsPath: env.MCP_SSE_EVENTS_PATH,
305
313
 
306
314
  // Rate Limiting Configuration
@@ -401,6 +409,8 @@ export function loadConfig(env: NodeJS.ProcessEnv = process.env): EnvironmentCon
401
409
 
402
410
  // SSE Event Subscription Configuration
403
411
  sseEventsEnabled: parsed.data.sseEventsEnabled,
412
+ ownerQuestionsEnabled: parsed.data.ownerQuestionsEnabled,
413
+ ownerQuestionsHaUsers: (parsed.data.ownerQuestionsHaUsers ?? '').split(',').map((u) => u.trim()).filter(Boolean),
404
414
  sseEventsPath: parsed.data.sseEventsPath ?? '/subscribe_events',
405
415
 
406
416
  // Rate Limiting Configuration
@@ -8,6 +8,19 @@ import { logger } from '../utils/logger.js';
8
8
  import type { StateOperations } from './states.js';
9
9
  import type { ServiceOperations } from './services.js';
10
10
  import type { RequestHandler } from './request.js';
11
+ import type { HaWebSocketCommand } from './wsCommand.js';
12
+
13
+ /** One run of an automation, as `trace/list` summarizes it. */
14
+ export interface AutomationTraceSummary {
15
+ run_id: string;
16
+ state?: string;
17
+ script_execution?: string | null;
18
+ last_step?: string | null;
19
+ error?: string;
20
+ trigger?: string;
21
+ timestamp?: { start: string; finish: string | null };
22
+ [key: string]: unknown;
23
+ }
11
24
 
12
25
  /**
13
26
  * Automation-related operations for the Home Assistant API.
@@ -16,7 +29,8 @@ export class AutomationOperations {
16
29
  constructor(
17
30
  private readonly stateOps: StateOperations,
18
31
  private readonly serviceOps?: ServiceOperations,
19
- private readonly request?: RequestHandler
32
+ private readonly request?: RequestHandler,
33
+ private readonly wsCommand?: HaWebSocketCommand
20
34
  ) {}
21
35
 
22
36
  /**
@@ -216,14 +230,32 @@ export class AutomationOperations {
216
230
  /**
217
231
  * Get automation trace (execution history)
218
232
  */
219
- async getAutomationTrace(entityId: string): Promise<unknown[]> {
220
- if (!this.request) {
221
- throw new Error('RequestHandler not available');
233
+ async getAutomationTrace(entityId: string, limit = 5): Promise<AutomationTraceSummary[]> {
234
+ if (!this.wsCommand) {
235
+ throw new Error('Automation traces need the Home Assistant WebSocket API, which is not available');
222
236
  }
223
237
 
224
- const automationId = entityId.replace('automation.', '');
225
- logger.debug('Fetching automation trace', { automationId });
238
+ // Traces are WebSocket-only (there is no REST endpoint) and are keyed by the
239
+ // automation's config id (its `id` attribute), not by the entity id.
240
+ const state = await this.stateOps.getState(entityId);
241
+ if (!state) {
242
+ throw new Error(`Automation ${entityId} not found`);
243
+ }
244
+ const itemId = state.attributes.id;
245
+ if (itemId === undefined || itemId === null || itemId === '') {
246
+ throw new Error(
247
+ `Automation ${entityId} has no 'id' (an automation defined in YAML without an id), so Home Assistant keeps no traces for it`
248
+ );
249
+ }
250
+ logger.debug('Fetching automation traces', { entityId, itemId });
226
251
 
227
- return this.request.get<unknown[]>(`/trace/automation/${encodeURIComponent(automationId)}`);
252
+ const traces = await this.wsCommand<AutomationTraceSummary[]>({
253
+ type: 'trace/list',
254
+ domain: 'automation',
255
+ item_id: String(itemId),
256
+ });
257
+ return [...(traces ?? [])]
258
+ .sort((a, b) => (b.timestamp?.start ?? '').localeCompare(a.timestamp?.start ?? ''))
259
+ .slice(0, limit);
228
260
  }
229
261
  }