@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.
- package/.env.example +1 -1
- package/README.md +11 -2
- package/dist/omadaClient/client.d.ts +6 -1
- package/dist/omadaClient/client.js +12 -1
- package/dist/omadaClient/client.js.map +1 -1
- package/dist/omadaClient/diagnose.d.ts +68 -0
- package/dist/omadaClient/diagnose.js +164 -0
- package/dist/omadaClient/diagnose.js.map +1 -0
- package/dist/omadaClient/index.d.ts +6 -0
- package/dist/omadaClient/index.js +15 -0
- package/dist/omadaClient/index.js.map +1 -1
- package/dist/omadaClient/macGroups.d.ts +58 -0
- package/dist/omadaClient/macGroups.js +124 -0
- package/dist/omadaClient/macGroups.js.map +1 -0
- package/dist/tools/infra/actions.d.ts +1 -1
- package/dist/tools/infra/actions.js +1 -1
- package/dist/tools/omada/index.js +6 -0
- package/dist/tools/omada/index.js.map +1 -1
- package/dist/tools/omada/macGroups.d.ts +19 -0
- package/dist/tools/omada/macGroups.js +77 -0
- package/dist/tools/omada/macGroups.js.map +1 -0
- package/dist/tools/omada/namespace.js +8 -0
- package/dist/tools/omada/namespace.js.map +1 -1
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +5 -1
- package/.codex/review-rules.md +0 -16
- package/.dockerignore +0 -8
- package/.gitattributes +0 -30
- package/.github/workflows/ci.yml +0 -193
- package/AGENTS.md +0 -389
- package/Dockerfile +0 -50
- package/docker-compose.yml +0 -23
- package/docs/CHANGELOG.md +0 -341
- package/docs/COVERAGE.md +0 -219
- package/docs/QUICK_START.md +0 -240
- package/eslint.config.js +0 -33
- package/n8n/ai-agent-safety-monitor.json +0 -355
- package/openapi.json +0 -1259
- package/scripts/check-version-sync.mjs +0 -69
- package/scripts/generate-jwt.ts +0 -121
- package/src/config.ts +0 -440
- package/src/haClient/automations.ts +0 -261
- package/src/haClient/calendars.ts +0 -94
- package/src/haClient/config.ts +0 -52
- package/src/haClient/devices.ts +0 -653
- package/src/haClient/entities.ts +0 -128
- package/src/haClient/events.ts +0 -689
- package/src/haClient/history.ts +0 -73
- package/src/haClient/index.ts +0 -336
- package/src/haClient/request.ts +0 -142
- package/src/haClient/services.ts +0 -45
- package/src/haClient/states.ts +0 -60
- package/src/haClient/updates.ts +0 -58
- package/src/haClient/wsCommand.ts +0 -44
- package/src/index.ts +0 -164
- package/src/localAI/index.ts +0 -88
- package/src/localAI/prompts.ts +0 -50
- package/src/localAI/providers/base.ts +0 -120
- package/src/localAI/providers/ollama.ts +0 -78
- package/src/localAI/providers/openai.ts +0 -86
- package/src/localAI/types.ts +0 -67
- package/src/omadaClient/auth.ts +0 -178
- package/src/omadaClient/client.ts +0 -337
- package/src/omadaClient/device.ts +0 -118
- package/src/omadaClient/index.ts +0 -310
- package/src/omadaClient/network.ts +0 -284
- package/src/omadaClient/request.ts +0 -365
- package/src/omadaClient/security.ts +0 -51
- package/src/omadaClient/site.ts +0 -344
- package/src/ownerQuestions/service.ts +0 -536
- package/src/permissions/index.ts +0 -143
- package/src/resources/index.ts +0 -213
- package/src/server/auth.ts +0 -175
- package/src/server/chatFace.ts +0 -789
- package/src/server/chatSlice.ts +0 -57
- package/src/server/common.ts +0 -170
- package/src/server/eventSubscription.ts +0 -343
- package/src/server/http.ts +0 -626
- package/src/server/instructions.ts +0 -97
- package/src/server/rateLimiter.ts +0 -227
- package/src/server/stdio.ts +0 -41
- package/src/server/stream.ts +0 -205
- package/src/tools/ai/analyzeSensors.ts +0 -33
- package/src/tools/ai/index.ts +0 -20
- package/src/tools/common.ts +0 -442
- package/src/tools/homeassistant/askOwner.ts +0 -133
- package/src/tools/homeassistant/automationControls.ts +0 -66
- package/src/tools/homeassistant/calendar.ts +0 -97
- package/src/tools/homeassistant/callService.ts +0 -25
- package/src/tools/homeassistant/controlClimate.ts +0 -68
- package/src/tools/homeassistant/controlCover.ts +0 -48
- package/src/tools/homeassistant/controlFan.ts +0 -44
- package/src/tools/homeassistant/controlLight.ts +0 -38
- package/src/tools/homeassistant/controlMediaPlayer.ts +0 -50
- package/src/tools/homeassistant/createAutomation.ts +0 -35
- package/src/tools/homeassistant/deleteAutomation.ts +0 -24
- package/src/tools/homeassistant/entityAction.ts +0 -35
- package/src/tools/homeassistant/entityGuard.ts +0 -28
- package/src/tools/homeassistant/getAllSensors.ts +0 -46
- package/src/tools/homeassistant/getAutomationTrace.ts +0 -30
- package/src/tools/homeassistant/getDomainSummary.ts +0 -29
- package/src/tools/homeassistant/getEntitiesByDomain.ts +0 -56
- package/src/tools/homeassistant/getHistory.ts +0 -24
- package/src/tools/homeassistant/getState.ts +0 -24
- package/src/tools/homeassistant/getStates.ts +0 -46
- package/src/tools/homeassistant/getVersion.ts +0 -21
- package/src/tools/homeassistant/index.ts +0 -121
- package/src/tools/homeassistant/listAutomations.ts +0 -35
- package/src/tools/homeassistant/listEntities.ts +0 -44
- package/src/tools/homeassistant/listPersons.ts +0 -46
- package/src/tools/homeassistant/sceneAndScript.ts +0 -53
- package/src/tools/homeassistant/searchEntities.ts +0 -58
- package/src/tools/homeassistant/sendNotification.ts +0 -179
- package/src/tools/homeassistant/systemTools.ts +0 -52
- package/src/tools/homeassistant/triggerAutomation.ts +0 -24
- package/src/tools/index.ts +0 -78
- package/src/tools/infra/actions.ts +0 -95
- package/src/tools/infra/index.ts +0 -138
- package/src/tools/omada/blockClient.ts +0 -28
- package/src/tools/omada/cyclePoePort.ts +0 -32
- package/src/tools/omada/deleteClient.ts +0 -27
- package/src/tools/omada/disableClientRateLimit.ts +0 -23
- package/src/tools/omada/getClient.ts +0 -14
- package/src/tools/omada/getDevice.ts +0 -14
- package/src/tools/omada/getFirewallSetting.ts +0 -14
- package/src/tools/omada/getInternetInfo.ts +0 -14
- package/src/tools/omada/getLanNetworkList.ts +0 -14
- package/src/tools/omada/getLanProfileList.ts +0 -14
- package/src/tools/omada/getPortForwardingStatus.ts +0 -26
- package/src/tools/omada/getRateLimitProfiles.ts +0 -14
- package/src/tools/omada/getSiteNtpStatus.ts +0 -14
- package/src/tools/omada/getSsidDetail.ts +0 -22
- package/src/tools/omada/getSsidList.ts +0 -21
- package/src/tools/omada/getSwitchStackDetail.ts +0 -14
- package/src/tools/omada/getThreatList.ts +0 -42
- package/src/tools/omada/getWlanGroupList.ts +0 -14
- package/src/tools/omada/graph.ts +0 -187
- package/src/tools/omada/index.ts +0 -153
- package/src/tools/omada/listBlockedClients.ts +0 -17
- package/src/tools/omada/listClients.ts +0 -14
- package/src/tools/omada/listClientsActivity.ts +0 -25
- package/src/tools/omada/listClientsPastConnections.ts +0 -44
- package/src/tools/omada/listDevices.ts +0 -14
- package/src/tools/omada/listDevicesStats.ts +0 -41
- package/src/tools/omada/listMostActiveClients.ts +0 -14
- package/src/tools/omada/listSites.ts +0 -18
- package/src/tools/omada/namespace.ts +0 -1164
- package/src/tools/omada/searchDevices.ts +0 -20
- package/src/tools/omada/setClientRateLimit.ts +0 -26
- package/src/tools/omada/setClientRateLimitProfile.ts +0 -25
- package/src/tools/omada/setSiteNtpServers.ts +0 -36
- package/src/tools/omada/setSsidEnabled.ts +0 -38
- package/src/tools/omada/unblockClient.ts +0 -26
- package/src/tools/types.ts +0 -11
- package/src/tools/utils/pagination-schema.ts +0 -27
- package/src/types/ai/index.ts +0 -11
- package/src/types/ai/types.ts +0 -67
- package/src/types/homeassistant/automations.ts +0 -43
- package/src/types/homeassistant/calendars.ts +0 -26
- package/src/types/homeassistant/config.ts +0 -14
- package/src/types/homeassistant/entities.ts +0 -14
- package/src/types/homeassistant/errors.ts +0 -33
- package/src/types/homeassistant/haConfig.ts +0 -7
- package/src/types/homeassistant/history.ts +0 -13
- package/src/types/homeassistant/index.ts +0 -46
- package/src/types/homeassistant/request.ts +0 -6
- package/src/types/homeassistant/serviceCall.ts +0 -18
- package/src/types/homeassistant/states.ts +0 -58
- package/src/types/homeassistant/updates.ts +0 -28
- package/src/types/index.ts +0 -12
- package/src/types/omada/activeClientInfo.ts +0 -17
- package/src/types/omada/clientActivity.ts +0 -32
- package/src/types/omada/clientBlockStatus.ts +0 -11
- package/src/types/omada/clientPastConnection.ts +0 -82
- package/src/types/omada/index.ts +0 -14
- package/src/types/omada/omadaApiResponse.ts +0 -5
- package/src/types/omada/omadaClientInfo.ts +0 -11
- package/src/types/omada/omadaDeviceInfo.ts +0 -11
- package/src/types/omada/omadaDeviceStats.ts +0 -31
- package/src/types/omada/omadaSiteSummary.ts +0 -5
- package/src/types/omada/oswStackDetail.ts +0 -11
- package/src/types/omada/paginatedResult.ts +0 -6
- package/src/types/omada/rateLimitProfile.ts +0 -58
- package/src/types/omada/threatInfo.ts +0 -38
- package/src/types/omada/tokenResult.ts +0 -6
- package/src/types.ts +0 -2
- package/src/utils/config-validations.ts +0 -68
- package/src/utils/jwt.ts +0 -112
- package/src/utils/logger.ts +0 -121
- package/src/utils/plainText.ts +0 -48
- package/src/utils/redact.ts +0 -59
- package/src/utils/sanitizeError.ts +0 -18
- package/src/utils/tlsDispatcher.ts +0 -69
- package/src/version.ts +0 -5
- package/tests/bdd/automation-creation.spec.ts +0 -214
- package/tests/bdd/automation-management.spec.ts +0 -267
- package/tests/bdd/calendar.spec.ts +0 -190
- package/tests/bdd/configuration.spec.ts +0 -283
- package/tests/bdd/device-control.spec.ts +0 -344
- package/tests/bdd/entity-listing.spec.ts +0 -208
- package/tests/bdd/entity-search.spec.ts +0 -183
- package/tests/bdd/history-retrieval.spec.ts +0 -195
- package/tests/bdd/notifications.spec.ts +0 -238
- package/tests/bdd/service-calls.spec.ts +0 -144
- package/tests/bdd/state-retrieval.spec.ts +0 -204
- package/tests/bench/hotpaths.bench.ts +0 -68
- package/tests/config.test.ts +0 -355
- package/tests/features/automation-creation.feature +0 -36
- package/tests/features/automation-management.feature +0 -54
- package/tests/features/calendar.feature +0 -37
- package/tests/features/configuration.feature +0 -70
- package/tests/features/device-control.feature +0 -100
- package/tests/features/entity-listing.feature +0 -51
- package/tests/features/entity-search.feature +0 -44
- package/tests/features/history-retrieval.feature +0 -38
- package/tests/features/notifications.feature +0 -37
- package/tests/features/service-calls.feature +0 -35
- package/tests/features/state-retrieval.feature +0 -37
- package/tests/fixtures/openapi-default-slice.json +0 -290
- package/tests/haClient/automationTrace.test.ts +0 -59
- package/tests/haClient/automations.test.ts +0 -149
- package/tests/haClient/config.test.ts +0 -117
- package/tests/haClient/entities.test.ts +0 -188
- package/tests/haClient/events.test.ts +0 -455
- package/tests/haClient/eventsTls.test.ts +0 -52
- package/tests/haClient/history.test.ts +0 -228
- package/tests/haClient/index.test.ts +0 -243
- package/tests/haClient/request.test.ts +0 -217
- package/tests/haClient/services.test.ts +0 -120
- package/tests/haClient/states.test.ts +0 -119
- package/tests/haClient/updates.test.ts +0 -193
- package/tests/index.test.ts +0 -160
- package/tests/integration/homeassistant.test.ts +0 -144
- package/tests/integration/omada.test.ts +0 -167
- package/tests/integration/server.test.ts +0 -151
- package/tests/localAI/index.test.ts +0 -177
- package/tests/localAI/ollama.test.ts +0 -187
- package/tests/localAI/openai.test.ts +0 -257
- package/tests/omadaClient/actions.test.ts +0 -161
- package/tests/omadaClient/clientBlocks.test.ts +0 -170
- package/tests/omadaClient/ntp.test.ts +0 -381
- package/tests/omadaClient/site.test.ts +0 -37
- package/tests/ownerQuestions/service.test.ts +0 -499
- package/tests/ownerQuestions/tools.test.ts +0 -186
- package/tests/permissions/index.test.ts +0 -217
- package/tests/resources/index.test.ts +0 -288
- package/tests/scripts/generateJwt.test.ts +0 -44
- package/tests/security/accessControl.test.ts +0 -223
- package/tests/security/authentication.test.ts +0 -117
- package/tests/security/configuration.test.ts +0 -170
- package/tests/security/cryptoTransport.test.ts +0 -172
- package/tests/security/dependencies.test.ts +0 -152
- package/tests/security/injection.test.ts +0 -117
- package/tests/security/insecureDesign.test.ts +0 -121
- package/tests/security/logging.test.ts +0 -131
- package/tests/security/resourceConsumption.test.ts +0 -142
- package/tests/server/auth.test.ts +0 -249
- package/tests/server/chatFace.test.ts +0 -320
- package/tests/server/common.test.ts +0 -84
- package/tests/server/http.test.ts +0 -561
- package/tests/server/instructions.test.ts +0 -152
- package/tests/server/memoizeJsonSchema.test.ts +0 -71
- package/tests/server/stdio.test.ts +0 -99
- package/tests/server/stream.test.ts +0 -285
- package/tests/server/toolVisibility.http.test.ts +0 -111
- package/tests/server/toolVisibility.test.ts +0 -129
- package/tests/tools/ai/analyzeSensors.test.ts +0 -227
- package/tests/tools/common.test.ts +0 -123
- package/tests/tools/homeassistant/handlers.test.ts +0 -872
- package/tests/tools/infra/invokeAction.test.ts +0 -280
- package/tests/tools/omada/graph.test.ts +0 -385
- package/tests/tools/omada/handlers.test.ts +0 -897
- package/tests/tools/omada/namespace.test.ts +0 -483
- package/tests/tools/registry.test.ts +0 -168
- package/tests/utils/config-validations.test.ts +0 -352
- package/tests/utils/jwt.test.ts +0 -115
- package/tests/utils/logger.test.ts +0 -327
- package/tests/utils/redact.test.ts +0 -105
- package/tsconfig.json +0 -24
- package/vitest.config.ts +0 -38
package/src/utils/plainText.ts
DELETED
|
@@ -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
|
-
}
|
package/src/utils/redact.ts
DELETED
|
@@ -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,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
|
-
});
|