@coffeerunhobby/mcp-ha-connect 2.2.1 → 2.3.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.
Files changed (281) hide show
  1. package/.env.example +1 -1
  2. package/README.md +11 -2
  3. package/dist/omadaClient/client.d.ts +6 -1
  4. package/dist/omadaClient/client.js +12 -1
  5. package/dist/omadaClient/client.js.map +1 -1
  6. package/dist/omadaClient/diagnose.d.ts +68 -0
  7. package/dist/omadaClient/diagnose.js +164 -0
  8. package/dist/omadaClient/diagnose.js.map +1 -0
  9. package/dist/omadaClient/index.d.ts +6 -0
  10. package/dist/omadaClient/index.js +15 -0
  11. package/dist/omadaClient/index.js.map +1 -1
  12. package/dist/omadaClient/macGroups.d.ts +58 -0
  13. package/dist/omadaClient/macGroups.js +124 -0
  14. package/dist/omadaClient/macGroups.js.map +1 -0
  15. package/dist/tools/infra/actions.d.ts +1 -1
  16. package/dist/tools/infra/actions.js +1 -1
  17. package/dist/tools/omada/index.js +6 -0
  18. package/dist/tools/omada/index.js.map +1 -1
  19. package/dist/tools/omada/macGroups.d.ts +19 -0
  20. package/dist/tools/omada/macGroups.js +77 -0
  21. package/dist/tools/omada/macGroups.js.map +1 -0
  22. package/dist/tools/omada/namespace.js +8 -0
  23. package/dist/tools/omada/namespace.js.map +1 -1
  24. package/dist/version.d.ts +1 -1
  25. package/dist/version.js +1 -1
  26. package/package.json +5 -1
  27. package/.codex/review-rules.md +0 -16
  28. package/.dockerignore +0 -8
  29. package/.gitattributes +0 -30
  30. package/.github/workflows/ci.yml +0 -193
  31. package/AGENTS.md +0 -389
  32. package/Dockerfile +0 -50
  33. package/docker-compose.yml +0 -23
  34. package/docs/CHANGELOG.md +0 -341
  35. package/docs/COVERAGE.md +0 -219
  36. package/docs/QUICK_START.md +0 -240
  37. package/eslint.config.js +0 -33
  38. package/n8n/ai-agent-safety-monitor.json +0 -355
  39. package/openapi.json +0 -1259
  40. package/scripts/check-version-sync.mjs +0 -69
  41. package/scripts/generate-jwt.ts +0 -121
  42. package/src/config.ts +0 -440
  43. package/src/haClient/automations.ts +0 -261
  44. package/src/haClient/calendars.ts +0 -94
  45. package/src/haClient/config.ts +0 -52
  46. package/src/haClient/devices.ts +0 -653
  47. package/src/haClient/entities.ts +0 -128
  48. package/src/haClient/events.ts +0 -689
  49. package/src/haClient/history.ts +0 -73
  50. package/src/haClient/index.ts +0 -336
  51. package/src/haClient/request.ts +0 -142
  52. package/src/haClient/services.ts +0 -45
  53. package/src/haClient/states.ts +0 -60
  54. package/src/haClient/updates.ts +0 -58
  55. package/src/haClient/wsCommand.ts +0 -44
  56. package/src/index.ts +0 -164
  57. package/src/localAI/index.ts +0 -88
  58. package/src/localAI/prompts.ts +0 -50
  59. package/src/localAI/providers/base.ts +0 -120
  60. package/src/localAI/providers/ollama.ts +0 -78
  61. package/src/localAI/providers/openai.ts +0 -86
  62. package/src/localAI/types.ts +0 -67
  63. package/src/omadaClient/auth.ts +0 -178
  64. package/src/omadaClient/client.ts +0 -337
  65. package/src/omadaClient/device.ts +0 -118
  66. package/src/omadaClient/index.ts +0 -310
  67. package/src/omadaClient/network.ts +0 -284
  68. package/src/omadaClient/request.ts +0 -365
  69. package/src/omadaClient/security.ts +0 -51
  70. package/src/omadaClient/site.ts +0 -344
  71. package/src/ownerQuestions/service.ts +0 -536
  72. package/src/permissions/index.ts +0 -143
  73. package/src/resources/index.ts +0 -213
  74. package/src/server/auth.ts +0 -175
  75. package/src/server/chatFace.ts +0 -789
  76. package/src/server/chatSlice.ts +0 -57
  77. package/src/server/common.ts +0 -170
  78. package/src/server/eventSubscription.ts +0 -343
  79. package/src/server/http.ts +0 -626
  80. package/src/server/instructions.ts +0 -97
  81. package/src/server/rateLimiter.ts +0 -227
  82. package/src/server/stdio.ts +0 -41
  83. package/src/server/stream.ts +0 -205
  84. package/src/tools/ai/analyzeSensors.ts +0 -33
  85. package/src/tools/ai/index.ts +0 -20
  86. package/src/tools/common.ts +0 -442
  87. package/src/tools/homeassistant/askOwner.ts +0 -133
  88. package/src/tools/homeassistant/automationControls.ts +0 -66
  89. package/src/tools/homeassistant/calendar.ts +0 -97
  90. package/src/tools/homeassistant/callService.ts +0 -25
  91. package/src/tools/homeassistant/controlClimate.ts +0 -68
  92. package/src/tools/homeassistant/controlCover.ts +0 -48
  93. package/src/tools/homeassistant/controlFan.ts +0 -44
  94. package/src/tools/homeassistant/controlLight.ts +0 -38
  95. package/src/tools/homeassistant/controlMediaPlayer.ts +0 -50
  96. package/src/tools/homeassistant/createAutomation.ts +0 -35
  97. package/src/tools/homeassistant/deleteAutomation.ts +0 -24
  98. package/src/tools/homeassistant/entityAction.ts +0 -35
  99. package/src/tools/homeassistant/entityGuard.ts +0 -28
  100. package/src/tools/homeassistant/getAllSensors.ts +0 -46
  101. package/src/tools/homeassistant/getAutomationTrace.ts +0 -30
  102. package/src/tools/homeassistant/getDomainSummary.ts +0 -29
  103. package/src/tools/homeassistant/getEntitiesByDomain.ts +0 -56
  104. package/src/tools/homeassistant/getHistory.ts +0 -24
  105. package/src/tools/homeassistant/getState.ts +0 -24
  106. package/src/tools/homeassistant/getStates.ts +0 -46
  107. package/src/tools/homeassistant/getVersion.ts +0 -21
  108. package/src/tools/homeassistant/index.ts +0 -121
  109. package/src/tools/homeassistant/listAutomations.ts +0 -35
  110. package/src/tools/homeassistant/listEntities.ts +0 -44
  111. package/src/tools/homeassistant/listPersons.ts +0 -46
  112. package/src/tools/homeassistant/sceneAndScript.ts +0 -53
  113. package/src/tools/homeassistant/searchEntities.ts +0 -58
  114. package/src/tools/homeassistant/sendNotification.ts +0 -179
  115. package/src/tools/homeassistant/systemTools.ts +0 -52
  116. package/src/tools/homeassistant/triggerAutomation.ts +0 -24
  117. package/src/tools/index.ts +0 -78
  118. package/src/tools/infra/actions.ts +0 -95
  119. package/src/tools/infra/index.ts +0 -138
  120. package/src/tools/omada/blockClient.ts +0 -28
  121. package/src/tools/omada/cyclePoePort.ts +0 -32
  122. package/src/tools/omada/deleteClient.ts +0 -27
  123. package/src/tools/omada/disableClientRateLimit.ts +0 -23
  124. package/src/tools/omada/getClient.ts +0 -14
  125. package/src/tools/omada/getDevice.ts +0 -14
  126. package/src/tools/omada/getFirewallSetting.ts +0 -14
  127. package/src/tools/omada/getInternetInfo.ts +0 -14
  128. package/src/tools/omada/getLanNetworkList.ts +0 -14
  129. package/src/tools/omada/getLanProfileList.ts +0 -14
  130. package/src/tools/omada/getPortForwardingStatus.ts +0 -26
  131. package/src/tools/omada/getRateLimitProfiles.ts +0 -14
  132. package/src/tools/omada/getSiteNtpStatus.ts +0 -14
  133. package/src/tools/omada/getSsidDetail.ts +0 -22
  134. package/src/tools/omada/getSsidList.ts +0 -21
  135. package/src/tools/omada/getSwitchStackDetail.ts +0 -14
  136. package/src/tools/omada/getThreatList.ts +0 -42
  137. package/src/tools/omada/getWlanGroupList.ts +0 -14
  138. package/src/tools/omada/graph.ts +0 -187
  139. package/src/tools/omada/index.ts +0 -153
  140. package/src/tools/omada/listBlockedClients.ts +0 -17
  141. package/src/tools/omada/listClients.ts +0 -14
  142. package/src/tools/omada/listClientsActivity.ts +0 -25
  143. package/src/tools/omada/listClientsPastConnections.ts +0 -44
  144. package/src/tools/omada/listDevices.ts +0 -14
  145. package/src/tools/omada/listDevicesStats.ts +0 -41
  146. package/src/tools/omada/listMostActiveClients.ts +0 -14
  147. package/src/tools/omada/listSites.ts +0 -18
  148. package/src/tools/omada/namespace.ts +0 -1164
  149. package/src/tools/omada/searchDevices.ts +0 -20
  150. package/src/tools/omada/setClientRateLimit.ts +0 -26
  151. package/src/tools/omada/setClientRateLimitProfile.ts +0 -25
  152. package/src/tools/omada/setSiteNtpServers.ts +0 -36
  153. package/src/tools/omada/setSsidEnabled.ts +0 -38
  154. package/src/tools/omada/unblockClient.ts +0 -26
  155. package/src/tools/types.ts +0 -11
  156. package/src/tools/utils/pagination-schema.ts +0 -27
  157. package/src/types/ai/index.ts +0 -11
  158. package/src/types/ai/types.ts +0 -67
  159. package/src/types/homeassistant/automations.ts +0 -43
  160. package/src/types/homeassistant/calendars.ts +0 -26
  161. package/src/types/homeassistant/config.ts +0 -14
  162. package/src/types/homeassistant/entities.ts +0 -14
  163. package/src/types/homeassistant/errors.ts +0 -33
  164. package/src/types/homeassistant/haConfig.ts +0 -7
  165. package/src/types/homeassistant/history.ts +0 -13
  166. package/src/types/homeassistant/index.ts +0 -46
  167. package/src/types/homeassistant/request.ts +0 -6
  168. package/src/types/homeassistant/serviceCall.ts +0 -18
  169. package/src/types/homeassistant/states.ts +0 -58
  170. package/src/types/homeassistant/updates.ts +0 -28
  171. package/src/types/index.ts +0 -12
  172. package/src/types/omada/activeClientInfo.ts +0 -17
  173. package/src/types/omada/clientActivity.ts +0 -32
  174. package/src/types/omada/clientBlockStatus.ts +0 -11
  175. package/src/types/omada/clientPastConnection.ts +0 -82
  176. package/src/types/omada/index.ts +0 -14
  177. package/src/types/omada/omadaApiResponse.ts +0 -5
  178. package/src/types/omada/omadaClientInfo.ts +0 -11
  179. package/src/types/omada/omadaDeviceInfo.ts +0 -11
  180. package/src/types/omada/omadaDeviceStats.ts +0 -31
  181. package/src/types/omada/omadaSiteSummary.ts +0 -5
  182. package/src/types/omada/oswStackDetail.ts +0 -11
  183. package/src/types/omada/paginatedResult.ts +0 -6
  184. package/src/types/omada/rateLimitProfile.ts +0 -58
  185. package/src/types/omada/threatInfo.ts +0 -38
  186. package/src/types/omada/tokenResult.ts +0 -6
  187. package/src/types.ts +0 -2
  188. package/src/utils/config-validations.ts +0 -68
  189. package/src/utils/jwt.ts +0 -112
  190. package/src/utils/logger.ts +0 -121
  191. package/src/utils/plainText.ts +0 -48
  192. package/src/utils/redact.ts +0 -59
  193. package/src/utils/sanitizeError.ts +0 -18
  194. package/src/utils/tlsDispatcher.ts +0 -69
  195. package/src/version.ts +0 -5
  196. package/tests/bdd/automation-creation.spec.ts +0 -214
  197. package/tests/bdd/automation-management.spec.ts +0 -267
  198. package/tests/bdd/calendar.spec.ts +0 -190
  199. package/tests/bdd/configuration.spec.ts +0 -283
  200. package/tests/bdd/device-control.spec.ts +0 -344
  201. package/tests/bdd/entity-listing.spec.ts +0 -208
  202. package/tests/bdd/entity-search.spec.ts +0 -183
  203. package/tests/bdd/history-retrieval.spec.ts +0 -195
  204. package/tests/bdd/notifications.spec.ts +0 -238
  205. package/tests/bdd/service-calls.spec.ts +0 -144
  206. package/tests/bdd/state-retrieval.spec.ts +0 -204
  207. package/tests/bench/hotpaths.bench.ts +0 -68
  208. package/tests/config.test.ts +0 -355
  209. package/tests/features/automation-creation.feature +0 -36
  210. package/tests/features/automation-management.feature +0 -54
  211. package/tests/features/calendar.feature +0 -37
  212. package/tests/features/configuration.feature +0 -70
  213. package/tests/features/device-control.feature +0 -100
  214. package/tests/features/entity-listing.feature +0 -51
  215. package/tests/features/entity-search.feature +0 -44
  216. package/tests/features/history-retrieval.feature +0 -38
  217. package/tests/features/notifications.feature +0 -37
  218. package/tests/features/service-calls.feature +0 -35
  219. package/tests/features/state-retrieval.feature +0 -37
  220. package/tests/fixtures/openapi-default-slice.json +0 -290
  221. package/tests/haClient/automationTrace.test.ts +0 -59
  222. package/tests/haClient/automations.test.ts +0 -149
  223. package/tests/haClient/config.test.ts +0 -117
  224. package/tests/haClient/entities.test.ts +0 -188
  225. package/tests/haClient/events.test.ts +0 -455
  226. package/tests/haClient/eventsTls.test.ts +0 -52
  227. package/tests/haClient/history.test.ts +0 -228
  228. package/tests/haClient/index.test.ts +0 -243
  229. package/tests/haClient/request.test.ts +0 -217
  230. package/tests/haClient/services.test.ts +0 -120
  231. package/tests/haClient/states.test.ts +0 -119
  232. package/tests/haClient/updates.test.ts +0 -193
  233. package/tests/index.test.ts +0 -160
  234. package/tests/integration/homeassistant.test.ts +0 -144
  235. package/tests/integration/omada.test.ts +0 -167
  236. package/tests/integration/server.test.ts +0 -151
  237. package/tests/localAI/index.test.ts +0 -177
  238. package/tests/localAI/ollama.test.ts +0 -187
  239. package/tests/localAI/openai.test.ts +0 -257
  240. package/tests/omadaClient/actions.test.ts +0 -161
  241. package/tests/omadaClient/clientBlocks.test.ts +0 -170
  242. package/tests/omadaClient/ntp.test.ts +0 -381
  243. package/tests/omadaClient/site.test.ts +0 -37
  244. package/tests/ownerQuestions/service.test.ts +0 -499
  245. package/tests/ownerQuestions/tools.test.ts +0 -186
  246. package/tests/permissions/index.test.ts +0 -217
  247. package/tests/resources/index.test.ts +0 -288
  248. package/tests/scripts/generateJwt.test.ts +0 -44
  249. package/tests/security/accessControl.test.ts +0 -223
  250. package/tests/security/authentication.test.ts +0 -117
  251. package/tests/security/configuration.test.ts +0 -170
  252. package/tests/security/cryptoTransport.test.ts +0 -172
  253. package/tests/security/dependencies.test.ts +0 -152
  254. package/tests/security/injection.test.ts +0 -117
  255. package/tests/security/insecureDesign.test.ts +0 -121
  256. package/tests/security/logging.test.ts +0 -131
  257. package/tests/security/resourceConsumption.test.ts +0 -142
  258. package/tests/server/auth.test.ts +0 -249
  259. package/tests/server/chatFace.test.ts +0 -320
  260. package/tests/server/common.test.ts +0 -84
  261. package/tests/server/http.test.ts +0 -561
  262. package/tests/server/instructions.test.ts +0 -152
  263. package/tests/server/memoizeJsonSchema.test.ts +0 -71
  264. package/tests/server/stdio.test.ts +0 -99
  265. package/tests/server/stream.test.ts +0 -285
  266. package/tests/server/toolVisibility.http.test.ts +0 -111
  267. package/tests/server/toolVisibility.test.ts +0 -129
  268. package/tests/tools/ai/analyzeSensors.test.ts +0 -227
  269. package/tests/tools/common.test.ts +0 -123
  270. package/tests/tools/homeassistant/handlers.test.ts +0 -872
  271. package/tests/tools/infra/invokeAction.test.ts +0 -280
  272. package/tests/tools/omada/graph.test.ts +0 -385
  273. package/tests/tools/omada/handlers.test.ts +0 -897
  274. package/tests/tools/omada/namespace.test.ts +0 -483
  275. package/tests/tools/registry.test.ts +0 -168
  276. package/tests/utils/config-validations.test.ts +0 -352
  277. package/tests/utils/jwt.test.ts +0 -115
  278. package/tests/utils/logger.test.ts +0 -327
  279. package/tests/utils/redact.test.ts +0 -105
  280. package/tsconfig.json +0 -24
  281. package/vitest.config.ts +0 -38
@@ -1,48 +0,0 @@
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
-
14
- const ALLOWED_CHAR = /^[\p{L}\p{N}\p{P}\p{Sm}\p{Sc}\p{Sk}° ]$/u;
15
-
16
- export interface PlainTextOptions {
17
- field: string;
18
- maxLength: number;
19
- allowNewlines?: boolean;
20
- }
21
-
22
- /**
23
- * @returns the NFC-normalized text
24
- * @throws Error naming the field and the first offending character
25
- */
26
- export function validatePlainText(text: string, options: PlainTextOptions): string {
27
- const normalized = text.normalize('NFC');
28
- const chars = [...normalized];
29
- if (chars.length === 0 || normalized.trim() === '') {
30
- throw new Error(`${options.field} must not be empty`);
31
- }
32
- if (chars.length > options.maxLength) {
33
- throw new Error(`${options.field} is ${chars.length} characters; the maximum is ${options.maxLength}`);
34
- }
35
- for (const [index, char] of chars.entries()) {
36
- if (char === '\n' && options.allowNewlines) {
37
- continue;
38
- }
39
- if (!ALLOWED_CHAR.test(char)) {
40
- const codePoint = char.codePointAt(0)!.toString(16).toUpperCase().padStart(4, '0');
41
- throw new Error(
42
- `${options.field} contains a disallowed character U+${codePoint} at position ${index + 1}; ` +
43
- 'only plain text is accepted (no emoji, control or invisible characters)'
44
- );
45
- }
46
- }
47
- return normalized;
48
- }
@@ -1,59 +0,0 @@
1
- /**
2
- * Redaction of secrets in tool responses.
3
- *
4
- * Home Assistant and Omada return secrets inside ordinary read results: the
5
- * Wi-Fi password (Omada `pskSetting.securityKey`), RADIUS / CoA / SNMP secrets,
6
- * PPSK and VPN keys, portal passwords, camera access tokens in image URLs. A tool
7
- * response goes to an AI client and from there into transcripts, so every tool
8
- * response is passed through `redactSecrets` (see toToolResult).
9
- *
10
- * Only non-empty string values under secret-looking names are replaced, with
11
- * "<redacted>": an empty value stays empty (so "is a password set?" is still
12
- * answerable), and booleans or numbers under similar names (`hidePwd`,
13
- * `encryptionPsk`) are left alone. `token=` / `access_token=` query values inside
14
- * URLs are masked as well. The input is never modified.
15
- *
16
- * Applied at the response boundary only, never inside the API clients: internal
17
- * read-modify-write paths (SSID schedule, site NTP) must see real values.
18
- */
19
-
20
- export const REDACTED = '<redacted>';
21
-
22
- /**
23
- * Secret-looking field names, matched on the whole name or its ending
24
- * (case-insensitive), from the Omada Open API and Home Assistant responses.
25
- */
26
- const SECRET_NAME =
27
- /(^key$|password|passwd|pwd$|secret|token$|passphrase|presharedkey|privatekey|securitykey|communitystring|^psk$|md5key|simplekey|authcode|devicekey)/i;
28
-
29
- /** Credentials inside URLs, e.g. /api/camera_proxy/camera.door?token=abc123 */
30
- const URL_SECRET = /([?&](?:access_token|token|key|password|secret)=)[^&#\s"]+/gi;
31
-
32
- export function isSecretName(name: string): boolean {
33
- return SECRET_NAME.test(name);
34
- }
35
-
36
- export function redactSecrets<T>(value: T): T {
37
- return redact(value, new WeakSet()) as T;
38
- }
39
-
40
- function redact(value: unknown, seen: WeakSet<object>): unknown {
41
- if (typeof value === 'string') {
42
- return value.includes('=') ? value.replace(URL_SECRET, `$1${REDACTED}`) : value;
43
- }
44
- if (typeof value !== 'object' || value === null) {
45
- return value;
46
- }
47
- if (seen.has(value)) {
48
- return '[circular]';
49
- }
50
- seen.add(value);
51
- if (Array.isArray(value)) {
52
- return value.map((item) => redact(item, seen));
53
- }
54
- const out: Record<string, unknown> = {};
55
- for (const [key, field] of Object.entries(value as Record<string, unknown>)) {
56
- out[key] = typeof field === 'string' && field !== '' && isSecretName(key) ? REDACTED : redact(field, seen);
57
- }
58
- return out;
59
- }
@@ -1,18 +0,0 @@
1
- /**
2
- * Error sanitization for client-facing responses (M6 / OWASP A09:2021).
3
- *
4
- * Raw `error.message` values routinely embed infrastructure detail — internal
5
- * hostnames, ports, filesystem paths, upstream stack fragments, occasionally
6
- * tokens. None of that belongs in an HTTP response body. Callers should log the
7
- * full error server-side (via `logger.error`) and send the return of this helper
8
- * to the client instead.
9
- *
10
- * The function intentionally ignores the error's own text: returning a constant
11
- * generic string is the whole point — there is no "safe subset" of an arbitrary
12
- * upstream message we can trust to forward.
13
- */
14
- const GENERIC_MESSAGE = 'Internal server error';
15
-
16
- export function sanitizeError(_error: unknown, fallback: string = GENERIC_MESSAGE): string {
17
- return fallback;
18
- }
@@ -1,69 +0,0 @@
1
- /**
2
- * Per-client TLS dispatcher helper (SEC-CRYPTO / H2).
3
- *
4
- * When a client must talk to a host with a self-signed certificate, the ONLY
5
- * acceptable way to relax certificate validation is to scope it to that single
6
- * client. The previous implementation toggled the process-global
7
- * `NODE_TLS_REJECT_UNAUTHORIZED` env var around each `fetch` call, which
8
- * disables TLS verification for EVERY concurrent outbound request in the
9
- * process for the duration of the call — a TOCTOU race that silently strips
10
- * certificate validation from unrelated Home Assistant / AI traffic.
11
- *
12
- * Node's global `fetch` (undici) ignores the legacy `agent` option; the
13
- * supported mechanism is a per-call `dispatcher`. We build one undici Agent per
14
- * client instance and pass it on every request, leaving the process-global TLS
15
- * policy untouched.
16
- *
17
- * THE PAIRING RULE (v1.7.0, root cause of the 2026-07-10 v1.5.5 incident):
18
- * a dispatcher built from the npm `undici` package must ONLY be passed to
19
- * `undici`'s own `fetch` — never to Node's BUILT-IN fetch. The built-in fetch
20
- * is powered by whatever undici Node bundles, and its dispatch-handler
21
- * interface drifts across majors: on Node 26 an npm-undici-6 Agent fails with
22
- * "invalid onError method", surfacing as `fetch failed` on every request (the
23
- * NAS crash-loop). `tlsAwareFetch` enforces the rule at one choke point.
24
- */
25
-
26
- import { Agent, fetch as undiciFetch, type Dispatcher } from 'undici';
27
-
28
- /**
29
- * `fetch` init augmented with undici's per-request dispatcher option, which is
30
- * not present in the lib.dom / @types/node `RequestInit` typings.
31
- */
32
- export type FetchInitWithDispatcher = RequestInit & { dispatcher?: Dispatcher };
33
-
34
- /**
35
- * Build an undici dispatcher that disables TLS certificate validation, scoped
36
- * to a single client instance. Returns `undefined` when `strictSsl` is not
37
- * explicitly disabled, so strict clients carry no dispatcher and use Node's
38
- * default (validating) global agent.
39
- *
40
- * @param strictSsl - When `false`, certificate validation is disabled for this
41
- * client only. Any other value (including `undefined`) keeps validation on.
42
- * @param baseUrl - The client's base URL. For plain-`http:` targets no
43
- * dispatcher is created even when `strictSsl` is false: TLS relaxation is
44
- * meaningless without TLS, and carrying a dispatcher there only risks the
45
- * version-mixing failure described above for zero benefit (this was exactly
46
- * the NAS production config that crashed v1.5.5 — http HA + strictSsl=false).
47
- */
48
- export function createTlsDispatcher(strictSsl: boolean | undefined, baseUrl?: string): Dispatcher | undefined {
49
- if (strictSsl === false) {
50
- if (baseUrl !== undefined && baseUrl.trim().toLowerCase().startsWith('http:')) {
51
- return undefined;
52
- }
53
- return new Agent({ connect: { rejectUnauthorized: false } });
54
- }
55
- return undefined;
56
- }
57
-
58
- /**
59
- * Fetch enforcing the pairing rule: requests carrying a dispatcher go through
60
- * undici's own `fetch` (Agent and fetch from the SAME library — compatible on
61
- * every Node version); requests without one use Node's built-in fetch
62
- * (unchanged behavior, and unit tests stubbing `global.fetch` keep working).
63
- */
64
- export function tlsAwareFetch(url: string, init: FetchInitWithDispatcher): Promise<Response> {
65
- if (init.dispatcher) {
66
- return undiciFetch(url, init as Parameters<typeof undiciFetch>[1]) as unknown as Promise<Response>;
67
- }
68
- return fetch(url, init);
69
- }
package/src/version.ts DELETED
@@ -1,5 +0,0 @@
1
- /**
2
- * Package version - hardcoded at build time for reliability
3
- * Keep in sync with package.json version
4
- */
5
- export const VERSION = '2.2.1';
@@ -1,214 +0,0 @@
1
- /**
2
- * BDD tests for Automation Creation
3
- * Uses vitest-cucumber for Gherkin syntax
4
- */
5
-
6
- import { loadFeature, describeFeature } from '@amiceli/vitest-cucumber';
7
- import { expect, vi } from 'vitest';
8
- import { AutomationOperations } from '../../src/haClient/automations.js';
9
- import type { StateOperations } from '../../src/haClient/states.js';
10
- import type { ServiceOperations } from '../../src/haClient/services.js';
11
- import type { RequestHandler } from '../../src/haClient/request.js';
12
- import type { AutomationConfig, AutomationTrigger, AutomationAction } from '../../src/types/index.js';
13
-
14
- const feature = await loadFeature('tests/features/automation-creation.feature');
15
-
16
- describeFeature(feature, ({ Background, Scenario }) => {
17
- // Shared test state
18
- let mockStateOps: StateOperations;
19
- let mockServiceOps: ServiceOperations;
20
- let mockRequest: RequestHandler;
21
- let automationOps: AutomationOperations;
22
- let automationConfig: Partial<AutomationConfig>;
23
- let triggers: AutomationTrigger[];
24
- let actions: AutomationAction[];
25
- let result: { id: string } | null;
26
- let error: Error | null;
27
-
28
- Background(({ Given }) => {
29
- Given('a connected Home Assistant client', () => {
30
- // Reset state for each scenario
31
- automationConfig = {};
32
- triggers = [];
33
- actions = [];
34
- result = null;
35
- error = null;
36
-
37
- // Setup mocks
38
- mockStateOps = {
39
- getStates: vi.fn().mockResolvedValue([]),
40
- getState: vi.fn().mockResolvedValue(null),
41
- } as unknown as StateOperations;
42
-
43
- mockServiceOps = {
44
- callService: vi.fn().mockResolvedValue({ context: { id: 'test' } }),
45
- } as unknown as ServiceOperations;
46
-
47
- mockRequest = {
48
- post: vi.fn().mockResolvedValue({ result: 'ok' }),
49
- get: vi.fn().mockResolvedValue({}),
50
- request: vi.fn().mockResolvedValue({}),
51
- } as unknown as RequestHandler;
52
-
53
- automationOps = new AutomationOperations(mockStateOps, mockServiceOps, mockRequest);
54
- });
55
- });
56
-
57
- Scenario('Create a simple time-based automation', ({ Given, When, Then, And }) => {
58
- Given('an automation config with alias "Morning Lights"', () => {
59
- automationConfig.alias = 'Morning Lights';
60
- });
61
-
62
- And('a time trigger at "06:00:00"', () => {
63
- triggers.push({
64
- platform: 'time',
65
- at: '06:00:00',
66
- });
67
- });
68
-
69
- And('an action to turn on "light.bedroom"', () => {
70
- actions.push({
71
- service: 'light.turn_on',
72
- target: { entity_id: 'light.bedroom' },
73
- });
74
- });
75
-
76
- When('I create the automation', async () => {
77
- try {
78
- result = await automationOps.createAutomation({
79
- alias: automationConfig.alias!,
80
- trigger: triggers,
81
- action: actions,
82
- });
83
- } catch (e) {
84
- error = e as Error;
85
- }
86
- });
87
-
88
- Then('the automation should be created successfully', () => {
89
- expect(error).toBeNull();
90
- expect(result).not.toBeNull();
91
- expect(mockRequest.post).toHaveBeenCalled();
92
- });
93
-
94
- And('the automation ID should be returned', () => {
95
- expect(result?.id).toBeDefined();
96
- expect(typeof result?.id).toBe('string');
97
- });
98
- });
99
-
100
- Scenario('Create an automation with state trigger', ({ Given, When, Then, And }) => {
101
- Given('an automation config with alias "Motion Alert"', () => {
102
- automationConfig.alias = 'Motion Alert';
103
- });
104
-
105
- And('a state trigger for "binary_sensor.motion" changing to "on"', () => {
106
- triggers.push({
107
- platform: 'state',
108
- entity_id: 'binary_sensor.motion',
109
- to: 'on',
110
- });
111
- });
112
-
113
- And('an action to send notification "Motion detected!"', () => {
114
- actions.push({
115
- service: 'notify.persistent_notification',
116
- data: { message: 'Motion detected!' },
117
- });
118
- });
119
-
120
- When('I create the automation', async () => {
121
- try {
122
- result = await automationOps.createAutomation({
123
- alias: automationConfig.alias!,
124
- trigger: triggers,
125
- action: actions,
126
- });
127
- } catch (e) {
128
- error = e as Error;
129
- }
130
- });
131
-
132
- Then('the automation should be created successfully', () => {
133
- expect(error).toBeNull();
134
- expect(result).not.toBeNull();
135
- });
136
- });
137
-
138
- Scenario('Reject automation without required fields', ({ Given, When, Then }) => {
139
- Given('an automation config without alias', () => {
140
- // Leave alias undefined
141
- automationConfig = {};
142
- });
143
-
144
- When('I attempt to create the automation', async () => {
145
- // Simulate the tool validation that happens in createAutomation.ts
146
- // The actual validation is in the tool layer, not AutomationOperations
147
- if (!automationConfig.alias) {
148
- error = new Error('alias, trigger, and action are required');
149
- }
150
- });
151
-
152
- Then('it should fail with error "alias, trigger, and action are required"', () => {
153
- expect(error).not.toBeNull();
154
- expect(error?.message).toBe('alias, trigger, and action are required');
155
- });
156
- });
157
-
158
- Scenario('Create automation with multiple actions', ({ Given, When, Then, And }) => {
159
- Given('an automation config with alias "Goodnight Routine"', () => {
160
- automationConfig.alias = 'Goodnight Routine';
161
- });
162
-
163
- And('a time trigger at "23:00:00"', () => {
164
- triggers.push({
165
- platform: 'time',
166
- at: '23:00:00',
167
- });
168
- });
169
-
170
- And('an action to turn off "light.living_room"', () => {
171
- actions.push({
172
- service: 'light.turn_off',
173
- target: { entity_id: 'light.living_room' },
174
- });
175
- });
176
-
177
- And('an action to turn off "light.kitchen"', () => {
178
- actions.push({
179
- service: 'light.turn_off',
180
- target: { entity_id: 'light.kitchen' },
181
- });
182
- });
183
-
184
- And('an action to set "climate.thermostat" to 18 degrees', () => {
185
- actions.push({
186
- service: 'climate.set_temperature',
187
- target: { entity_id: 'climate.thermostat' },
188
- data: { temperature: 18 },
189
- });
190
- });
191
-
192
- When('I create the automation', async () => {
193
- try {
194
- result = await automationOps.createAutomation({
195
- alias: automationConfig.alias!,
196
- trigger: triggers,
197
- action: actions,
198
- });
199
- } catch (e) {
200
- error = e as Error;
201
- }
202
- });
203
-
204
- Then('the automation should be created successfully', () => {
205
- expect(error).toBeNull();
206
- expect(result).not.toBeNull();
207
-
208
- // Verify the POST was called with all actions
209
- const postCall = (mockRequest.post as ReturnType<typeof vi.fn>).mock.calls[0];
210
- const payload = postCall[1];
211
- expect(payload.action).toHaveLength(3);
212
- });
213
- });
214
- });
@@ -1,267 +0,0 @@
1
- /**
2
- * BDD tests for Automation Management
3
- * Uses vitest-cucumber for Gherkin syntax
4
- */
5
-
6
- import { loadFeature, describeFeature } from '@amiceli/vitest-cucumber';
7
- import { expect, vi } from 'vitest';
8
- import { AutomationOperations } from '../../src/haClient/automations.js';
9
- import type { StateOperations } from '../../src/haClient/states.js';
10
- import type { ServiceOperations } from '../../src/haClient/services.js';
11
- import type { RequestHandler } from '../../src/haClient/request.js';
12
- import type { Entity, Automation } from '../../src/types/index.js';
13
-
14
- const feature = await loadFeature('tests/features/automation-management.feature');
15
-
16
- describeFeature(feature, ({ Scenario }) => {
17
- // Shared test state
18
- let mockStateOps: StateOperations;
19
- let mockServiceOps: ServiceOperations;
20
- let mockRequest: RequestHandler;
21
- let automationOps: AutomationOperations;
22
- let automations: Automation[];
23
- let automation: Automation | null;
24
- let entityId: string;
25
- let lastServiceCall: { domain: string; service: string; target?: { entity_id: string }; service_data?: Record<string, unknown> } | null;
26
- let lastRequest: { method: string; path: string } | null;
27
-
28
- const mockAutomationEntities: Entity[] = [
29
- {
30
- entity_id: 'automation.morning_lights',
31
- state: 'on',
32
- attributes: {
33
- friendly_name: 'Morning Lights',
34
- last_triggered: '2026-01-08T06:00:00.000Z',
35
- description: 'Turn on lights at sunrise',
36
- mode: 'single',
37
- },
38
- last_changed: '2026-01-08T06:00:00.000Z',
39
- last_updated: '2026-01-08T06:00:00.000Z',
40
- context: { id: 'ctx-1', parent_id: null, user_id: null },
41
- },
42
- {
43
- entity_id: 'automation.night_mode',
44
- state: 'off',
45
- attributes: {
46
- friendly_name: 'Night Mode',
47
- last_triggered: null,
48
- mode: 'restart',
49
- },
50
- last_changed: '2026-01-07T22:00:00.000Z',
51
- last_updated: '2026-01-08T10:00:00.000Z',
52
- context: { id: 'ctx-2', parent_id: null, user_id: null },
53
- },
54
- {
55
- entity_id: 'automation.dynamic_scene',
56
- state: 'on',
57
- attributes: {
58
- friendly_name: 'Dynamic Scene',
59
- mode: 'queued',
60
- },
61
- last_changed: '2026-01-08T09:30:00.000Z',
62
- last_updated: '2026-01-08T09:30:00.000Z',
63
- context: { id: 'ctx-3', parent_id: null, user_id: null },
64
- },
65
- ];
66
-
67
- function setupMocks() {
68
- lastServiceCall = null;
69
- lastRequest = null;
70
-
71
- mockStateOps = {
72
- getStates: vi.fn().mockResolvedValue(mockAutomationEntities),
73
- getState: vi.fn().mockImplementation(async (id: string) => {
74
- return mockAutomationEntities.find(e => e.entity_id === id) || null;
75
- }),
76
- } as unknown as StateOperations;
77
-
78
- mockServiceOps = {
79
- callService: vi.fn().mockImplementation(async (data) => {
80
- lastServiceCall = {
81
- domain: data.domain,
82
- service: data.service,
83
- target: data.target,
84
- service_data: data.service_data,
85
- };
86
- return { context: { id: 'test-ctx' } };
87
- }),
88
- } as unknown as ServiceOperations;
89
-
90
- mockRequest = {
91
- post: vi.fn().mockResolvedValue({ result: 'ok' }),
92
- get: vi.fn().mockResolvedValue([]),
93
- request: vi.fn().mockImplementation(async (path: string, options: { method: string }) => {
94
- lastRequest = { method: options.method, path };
95
- return {};
96
- }),
97
- } as unknown as RequestHandler;
98
-
99
- automationOps = new AutomationOperations(mockStateOps, mockServiceOps, mockRequest);
100
- }
101
-
102
- Scenario('List all automations', ({ Given, When, Then, And }) => {
103
- Given('a Home Assistant with automations', () => {
104
- setupMocks();
105
- });
106
-
107
- When('I list all automations', async () => {
108
- automations = await automationOps.getAutomations();
109
- });
110
-
111
- Then('I should get all automation entities', () => {
112
- expect(automations).toHaveLength(3);
113
- });
114
-
115
- And('each automation should have id, alias, and state', () => {
116
- automations.forEach(a => {
117
- expect(a.id).toBeDefined();
118
- expect(a.alias).toBeDefined();
119
- expect(a.state).toMatch(/^(on|off)$/);
120
- });
121
- });
122
- });
123
-
124
- Scenario('Get automation details', ({ Given, When, Then, And }) => {
125
- Given('an automation "automation.morning_lights" exists', () => {
126
- setupMocks();
127
- entityId = 'automation.morning_lights';
128
- });
129
-
130
- When('I get the automation details', async () => {
131
- automation = await automationOps.getAutomation(entityId);
132
- });
133
-
134
- Then('I should see the automation alias "Morning Lights"', () => {
135
- expect(automation?.alias).toBe('Morning Lights');
136
- });
137
-
138
- And('the state should be "on"', () => {
139
- expect(automation?.state).toBe('on');
140
- });
141
-
142
- And('the mode should be "single"', () => {
143
- expect(automation?.mode).toBe('single');
144
- });
145
- });
146
-
147
- Scenario('Trigger an automation manually', ({ Given, When, Then, And }) => {
148
- Given('an automation "automation.morning_lights" exists', () => {
149
- setupMocks();
150
- entityId = 'automation.morning_lights';
151
- });
152
-
153
- When('I trigger the automation', async () => {
154
- await automationOps.triggerAutomation(entityId);
155
- });
156
-
157
- Then('the automation.trigger service should be called', () => {
158
- expect(lastServiceCall?.domain).toBe('automation');
159
- expect(lastServiceCall?.service).toBe('trigger');
160
- });
161
-
162
- And('the target should be "automation.morning_lights"', () => {
163
- expect(lastServiceCall?.target?.entity_id).toBe('automation.morning_lights');
164
- });
165
- });
166
-
167
- Scenario('Trigger automation with variables', ({ Given, When, Then, And }) => {
168
- Given('an automation "automation.dynamic_scene" exists', () => {
169
- setupMocks();
170
- entityId = 'automation.dynamic_scene';
171
- });
172
-
173
- When('I trigger the automation with variables {"brightness": 100}', async () => {
174
- await automationOps.triggerAutomation(entityId, { brightness: 100 });
175
- });
176
-
177
- Then('the automation.trigger service should be called', () => {
178
- expect(lastServiceCall?.domain).toBe('automation');
179
- expect(lastServiceCall?.service).toBe('trigger');
180
- });
181
-
182
- And('the variables should be passed', () => {
183
- expect(lastServiceCall?.service_data?.variables).toEqual({ brightness: 100 });
184
- });
185
- });
186
-
187
- Scenario('Enable a disabled automation', ({ Given, When, Then }) => {
188
- Given('a disabled automation "automation.night_mode" exists', () => {
189
- setupMocks();
190
- entityId = 'automation.night_mode';
191
- });
192
-
193
- When('I enable the automation', async () => {
194
- await automationOps.enableAutomation(entityId);
195
- });
196
-
197
- Then('the automation.turn_on service should be called', () => {
198
- expect(lastServiceCall?.domain).toBe('automation');
199
- expect(lastServiceCall?.service).toBe('turn_on');
200
- expect(lastServiceCall?.target?.entity_id).toBe(entityId);
201
- });
202
- });
203
-
204
- Scenario('Disable an enabled automation', ({ Given, When, Then }) => {
205
- Given('an enabled automation "automation.morning_lights" exists', () => {
206
- setupMocks();
207
- entityId = 'automation.morning_lights';
208
- });
209
-
210
- When('I disable the automation', async () => {
211
- await automationOps.disableAutomation(entityId);
212
- });
213
-
214
- Then('the automation.turn_off service should be called', () => {
215
- expect(lastServiceCall?.domain).toBe('automation');
216
- expect(lastServiceCall?.service).toBe('turn_off');
217
- expect(lastServiceCall?.target?.entity_id).toBe(entityId);
218
- });
219
- });
220
-
221
- Scenario('Toggle automation state', ({ Given, When, Then }) => {
222
- Given('an automation "automation.morning_lights" exists', () => {
223
- setupMocks();
224
- entityId = 'automation.morning_lights';
225
- });
226
-
227
- When('I toggle the automation', async () => {
228
- await automationOps.toggleAutomation(entityId);
229
- });
230
-
231
- Then('the automation.toggle service should be called', () => {
232
- expect(lastServiceCall?.domain).toBe('automation');
233
- expect(lastServiceCall?.service).toBe('toggle');
234
- expect(lastServiceCall?.target?.entity_id).toBe(entityId);
235
- });
236
- });
237
-
238
- Scenario('Reload automations from configuration', ({ Given, When, Then }) => {
239
- Given('a Home Assistant with automations', () => {
240
- setupMocks();
241
- });
242
-
243
- When('I reload automations', async () => {
244
- await automationOps.reloadAutomations();
245
- });
246
-
247
- Then('the automation.reload service should be called', () => {
248
- expect(lastServiceCall?.domain).toBe('automation');
249
- expect(lastServiceCall?.service).toBe('reload');
250
- });
251
- });
252
-
253
- Scenario('Delete an automation', ({ Given, When, Then }) => {
254
- Given('an automation with ID "1234567890" exists', () => {
255
- setupMocks();
256
- });
257
-
258
- When('I delete the automation', async () => {
259
- await automationOps.deleteAutomation('1234567890');
260
- });
261
-
262
- Then('a DELETE request should be made to the config API', () => {
263
- expect(lastRequest?.method).toBe('DELETE');
264
- expect(lastRequest?.path).toBe('/config/automation/config/1234567890');
265
- });
266
- });
267
- });