@digital-alchemy/hass 25.11.17-beta.0 → 25.11.27
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/dist/helpers/fetch/service-list.d.mts +86 -5
- package/dist/helpers/id-by.d.mts +1 -1
- package/dist/helpers/interfaces.d.mts +1 -1
- package/dist/helpers/interfaces.mjs.map +1 -1
- package/dist/mock_assistant/main.mjs +3 -3
- package/dist/mock_assistant/main.mjs.map +1 -1
- package/dist/mock_assistant/mock-assistant.module.mjs +2 -2
- package/dist/mock_assistant/mock-assistant.module.mjs.map +1 -1
- package/dist/mock_assistant/services/fixtures.service.mjs +1 -1
- package/dist/mock_assistant/services/fixtures.service.mjs.map +1 -1
- package/dist/mock_assistant/services/websocket-api.service.mjs +1 -1
- package/dist/mock_assistant/services/websocket-api.service.mjs.map +1 -1
- package/dist/services/config.service.mjs +2 -1
- package/dist/services/config.service.mjs.map +1 -1
- package/dist/services/diagnostics.service.mjs +1 -1
- package/dist/services/diagnostics.service.mjs.map +1 -1
- package/dist/services/id-by.service.mjs +4 -1
- package/dist/services/id-by.service.mjs.map +1 -1
- package/dist/services/internal.service.mjs +3 -3
- package/dist/services/internal.service.mjs.map +1 -1
- package/dist/services/reference.service.mjs +3 -2
- package/dist/services/reference.service.mjs.map +1 -1
- package/dist/services/websocket-api.service.mjs +1 -1
- package/dist/services/websocket-api.service.mjs.map +1 -1
- package/dist/testing/area.spec.mjs +143 -2
- package/dist/testing/area.spec.mjs.map +1 -1
- package/dist/testing/call-proxy.spec.d.mts +1 -0
- package/dist/testing/call-proxy.spec.mjs +204 -0
- package/dist/testing/call-proxy.spec.mjs.map +1 -0
- package/dist/testing/config.spec.mjs +1 -1
- package/dist/testing/config.spec.mjs.map +1 -1
- package/dist/testing/conversation.spec.mjs +1 -1
- package/dist/testing/conversation.spec.mjs.map +1 -1
- package/dist/testing/id-by.spec.mjs +38 -0
- package/dist/testing/id-by.spec.mjs.map +1 -1
- package/dist/testing/ref-by.spec.mjs +4 -3
- package/dist/testing/ref-by.spec.mjs.map +1 -1
- package/dist/testing/websocket.spec.mjs +25 -0
- package/dist/testing/websocket.spec.mjs.map +1 -1
- package/package.json +7 -6
- package/src/helpers/fetch/service-list.mts +89 -5
- package/src/helpers/id-by.mts +1 -0
- package/src/helpers/interfaces.mts +2 -1
- package/src/mock_assistant/main.mts +4 -3
- package/src/mock_assistant/mock-assistant.module.mts +3 -2
- package/src/mock_assistant/services/fixtures.service.mts +2 -1
- package/src/mock_assistant/services/websocket-api.service.mts +2 -1
- package/src/services/config.service.mts +2 -1
- package/src/services/diagnostics.service.mts +2 -1
- package/src/services/id-by.service.mts +4 -1
- package/src/services/internal.service.mts +4 -3
- package/src/services/reference.service.mts +3 -2
- package/src/services/websocket-api.service.mts +2 -1
- package/src/testing/area.spec.mts +168 -3
- package/src/testing/call-proxy.spec.mts +241 -0
- package/src/testing/config.spec.mts +2 -1
- package/src/testing/conversation.spec.mts +1 -1
- package/src/testing/id-by.spec.mts +50 -0
- package/src/testing/ref-by.spec.mts +4 -3
- package/src/testing/websocket.spec.mts +33 -0
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
import type { LiteralUnion } from "type-fest";
|
|
2
2
|
|
|
3
3
|
import type { ALL_DOMAINS, PICK_ENTITY, TPlatformId } from "../../user.mts";
|
|
4
|
+
import type { SupportedCountries } from "../countries.mts";
|
|
4
5
|
import type { ColorMode } from "../features.mts";
|
|
6
|
+
import type { SupportedLanguages } from "../languages.mts";
|
|
5
7
|
|
|
6
8
|
export type EntityFilterSelector = {
|
|
7
9
|
integration?: TPlatformId;
|
|
@@ -33,16 +35,23 @@ export type LegacyDeviceSelector = {
|
|
|
33
35
|
export interface ServiceListSelector {
|
|
34
36
|
action: null;
|
|
35
37
|
addon: {
|
|
38
|
+
/** The name of the addon */
|
|
36
39
|
name?: string;
|
|
40
|
+
/** The slug identifier of the addon */
|
|
37
41
|
slug?: string;
|
|
38
42
|
};
|
|
39
43
|
area: {
|
|
44
|
+
/** Device filter selector(s) to filter areas by device */
|
|
40
45
|
device?: DeviceFilterSelector | DeviceFilterSelector[];
|
|
46
|
+
/** Entity filter selector(s) to filter areas by entity */
|
|
41
47
|
entity?: EntityFilterSelector | EntityFilterSelector[];
|
|
48
|
+
/** Whether multiple areas can be selected */
|
|
42
49
|
multiple?: boolean;
|
|
43
50
|
};
|
|
44
51
|
attribute: {
|
|
52
|
+
/** The entity ID to get attributes from */
|
|
45
53
|
entity_id?: PICK_ENTITY;
|
|
54
|
+
/** List of attribute names to hide from the selector */
|
|
46
55
|
hide_attributes?: string[];
|
|
47
56
|
};
|
|
48
57
|
assist_pipeline: null;
|
|
@@ -50,82 +59,131 @@ export interface ServiceListSelector {
|
|
|
50
59
|
boolean: null;
|
|
51
60
|
color_rgb: null;
|
|
52
61
|
color_temp: {
|
|
62
|
+
/** The unit of measurement for color temperature */
|
|
53
63
|
unit?: "kelvin" | "mired";
|
|
64
|
+
/** Minimum color temperature value */
|
|
54
65
|
min?: number;
|
|
66
|
+
/** Maximum color temperature value */
|
|
55
67
|
max?: number;
|
|
68
|
+
/** Maximum color temperature in mireds */
|
|
56
69
|
max_mireds?: number;
|
|
70
|
+
/** Minimum color temperature in mireds */
|
|
57
71
|
min_mireds?: number;
|
|
58
72
|
};
|
|
59
73
|
condition: null;
|
|
60
74
|
config_entry: {
|
|
75
|
+
/** The integration platform ID */
|
|
61
76
|
integration: TPlatformId;
|
|
62
77
|
};
|
|
63
78
|
constant: {
|
|
79
|
+
/** Display label for the constant value */
|
|
64
80
|
label?: string;
|
|
81
|
+
/** The constant value */
|
|
65
82
|
value: string | number | boolean;
|
|
83
|
+
/** Translation key for the label */
|
|
66
84
|
translation_key?: string;
|
|
67
85
|
};
|
|
68
86
|
conversation_agent: {
|
|
87
|
+
/** Language code for the conversation agent */
|
|
69
88
|
language?: string;
|
|
70
89
|
};
|
|
71
90
|
country: {
|
|
72
|
-
|
|
91
|
+
/** Country code(s) to include in the selector */
|
|
92
|
+
countries?: SupportedCountries | SupportedCountries[];
|
|
93
|
+
/** Whether to disable sorting of countries */
|
|
73
94
|
no_sort?: boolean;
|
|
74
95
|
};
|
|
75
96
|
date: null;
|
|
76
97
|
datetime: null;
|
|
77
98
|
device: LegacyDeviceSelector & {
|
|
99
|
+
/** Whether multiple devices can be selected */
|
|
78
100
|
multiple?: boolean;
|
|
101
|
+
/** Device filter selector(s) to filter devices */
|
|
79
102
|
filter?: DeviceFilterSelector | DeviceFilterSelector[];
|
|
103
|
+
/** Entity filter selector(s) to filter devices by their entities */
|
|
80
104
|
entity?: EntityFilterSelector | EntityFilterSelector[];
|
|
81
105
|
};
|
|
82
106
|
duration: {
|
|
107
|
+
/** Whether to enable day selection in duration input */
|
|
83
108
|
enable_day?: boolean;
|
|
109
|
+
/** Whether to enable millisecond precision in duration input */
|
|
84
110
|
enable_millisecond?: boolean;
|
|
111
|
+
/** Whether to allow negative duration values */
|
|
85
112
|
allow_negative?: boolean;
|
|
86
113
|
};
|
|
87
114
|
entity: LegacyEntitySelector & {
|
|
88
|
-
|
|
89
|
-
include_entities?: PICK_ENTITY | PICK_ENTITY[];
|
|
115
|
+
/** Whether multiple entities can be selected */
|
|
90
116
|
multiple?: boolean;
|
|
117
|
+
/** Whether entities can be reordered */
|
|
91
118
|
reorder?: boolean;
|
|
119
|
+
/** Entity filter selector(s) to filter entities */
|
|
92
120
|
filter?: EntityFilterSelector | EntityFilterSelector[];
|
|
93
|
-
}
|
|
121
|
+
} & (
|
|
122
|
+
| {
|
|
123
|
+
/** Entity ID(s) to exclude from the selector */
|
|
124
|
+
exclude_entities?: PICK_ENTITY | PICK_ENTITY[];
|
|
125
|
+
include_entities?: never;
|
|
126
|
+
}
|
|
127
|
+
| {
|
|
128
|
+
/** Entity ID(s) to include in the selector */
|
|
129
|
+
include_entities?: PICK_ENTITY | PICK_ENTITY[];
|
|
130
|
+
exclude_entities?: never;
|
|
131
|
+
}
|
|
132
|
+
);
|
|
94
133
|
file: {
|
|
134
|
+
/** MIME type(s) or file extension(s) to accept */
|
|
95
135
|
accept: string;
|
|
96
136
|
};
|
|
97
137
|
floor: {
|
|
138
|
+
/** Entity filter selector(s) to filter floors by entity */
|
|
98
139
|
entity?: EntityFilterSelector | EntityFilterSelector[];
|
|
140
|
+
/** Device filter selector(s) to filter floors by device */
|
|
99
141
|
device?: DeviceFilterSelector | DeviceFilterSelector[];
|
|
142
|
+
/** Whether multiple floors can be selected */
|
|
100
143
|
multiple?: boolean;
|
|
101
144
|
};
|
|
102
145
|
icon: {
|
|
146
|
+
/** Placeholder text for the icon selector */
|
|
103
147
|
placeholder?: string;
|
|
104
148
|
};
|
|
105
149
|
label: {
|
|
150
|
+
/** Whether multiple labels can be selected */
|
|
106
151
|
multiple?: boolean;
|
|
107
152
|
};
|
|
108
153
|
language: {
|
|
109
|
-
|
|
154
|
+
/** Language code(s) to include in the selector */
|
|
155
|
+
languages?: SupportedLanguages | SupportedLanguages[];
|
|
156
|
+
/** Whether to display native language names */
|
|
110
157
|
native_name?: boolean;
|
|
158
|
+
/** Whether to disable sorting of languages */
|
|
111
159
|
no_sort?: boolean;
|
|
112
160
|
};
|
|
113
161
|
location: {
|
|
162
|
+
/** Whether to include radius selection */
|
|
114
163
|
radius?: boolean;
|
|
164
|
+
/** Icon to display for the location selector */
|
|
115
165
|
icon?: string;
|
|
116
166
|
};
|
|
117
167
|
media: {
|
|
168
|
+
/** MIME type(s) or file extension(s) to accept */
|
|
118
169
|
accept?: string | string[];
|
|
119
170
|
};
|
|
120
171
|
number: {
|
|
172
|
+
/** Minimum value allowed */
|
|
121
173
|
min?: number;
|
|
174
|
+
/** Maximum value allowed */
|
|
122
175
|
max?: number;
|
|
176
|
+
/** Input mode: "box" for text input or "slider" for slider control */
|
|
123
177
|
mode?: "box" | "slider";
|
|
178
|
+
/** Translation key for the number field */
|
|
124
179
|
translation_key?: string;
|
|
180
|
+
/** Step value for increment/decrement, or "any" for no step */
|
|
125
181
|
step?: number | "any";
|
|
182
|
+
/** Unit of measurement to display */
|
|
126
183
|
unit_of_measurement?: string;
|
|
127
184
|
};
|
|
128
185
|
object: {
|
|
186
|
+
/** Field definitions for the object structure */
|
|
129
187
|
fields?: Record<
|
|
130
188
|
string,
|
|
131
189
|
{
|
|
@@ -134,38 +192,58 @@ export interface ServiceListSelector {
|
|
|
134
192
|
label?: string;
|
|
135
193
|
}
|
|
136
194
|
>;
|
|
195
|
+
/** Whether multiple objects can be selected */
|
|
137
196
|
multiple?: boolean;
|
|
197
|
+
/** Field name to use as the label for each object */
|
|
138
198
|
label_field?: string;
|
|
199
|
+
/** Field name to use as the description for each object */
|
|
139
200
|
description_field?: string;
|
|
201
|
+
/** Translation key for the object selector */
|
|
140
202
|
translation_key?: string;
|
|
141
203
|
};
|
|
142
204
|
qr_code: {
|
|
205
|
+
/** Data to encode in the QR code */
|
|
143
206
|
data: string;
|
|
207
|
+
/** Scale factor for the QR code size */
|
|
144
208
|
scale?: number;
|
|
209
|
+
/** Error correction level: L (low), M (medium), Q (quartile), H (high) */
|
|
145
210
|
error_correction_level?: "L" | "M" | "Q" | "H";
|
|
146
211
|
};
|
|
147
212
|
select: {
|
|
213
|
+
/** Available options for selection */
|
|
148
214
|
options: (string | { label: string; value: string })[];
|
|
215
|
+
/** Whether multiple options can be selected */
|
|
149
216
|
multiple?: boolean;
|
|
217
|
+
/** Whether custom values can be entered */
|
|
150
218
|
custom_value?: boolean;
|
|
219
|
+
/** Display mode: "dropdown" or "list" */
|
|
151
220
|
mode?: "dropdown" | "list";
|
|
221
|
+
/** Translation key for the select field */
|
|
152
222
|
translation_key?: string;
|
|
223
|
+
/** Whether to sort the options */
|
|
153
224
|
sort?: boolean;
|
|
154
225
|
};
|
|
155
226
|
state: {
|
|
227
|
+
/** Entity ID to get states from */
|
|
156
228
|
entity_id?: PICK_ENTITY;
|
|
229
|
+
/** Whether multiple states can be selected */
|
|
157
230
|
multiple?: boolean;
|
|
231
|
+
/** List of state values to hide from the selector */
|
|
158
232
|
hide_states?: string[];
|
|
159
233
|
};
|
|
160
234
|
statistic: {
|
|
235
|
+
/** Whether multiple statistics can be selected */
|
|
161
236
|
multiple?: boolean;
|
|
162
237
|
};
|
|
163
238
|
target: {
|
|
239
|
+
/** Entity filter selector(s) to filter targets by entity */
|
|
164
240
|
entity?: EntityFilterSelector | EntityFilterSelector[];
|
|
241
|
+
/** Device filter selector(s) to filter targets by device */
|
|
165
242
|
device?: DeviceFilterSelector | DeviceFilterSelector[];
|
|
166
243
|
};
|
|
167
244
|
template: null;
|
|
168
245
|
text: {
|
|
246
|
+
/** HTML input type for the text field */
|
|
169
247
|
type?:
|
|
170
248
|
| "color"
|
|
171
249
|
| "date"
|
|
@@ -180,13 +258,19 @@ export interface ServiceListSelector {
|
|
|
180
258
|
| "time"
|
|
181
259
|
| "url"
|
|
182
260
|
| "week";
|
|
261
|
+
/** Autocomplete attribute value */
|
|
183
262
|
autocomplete?: string;
|
|
263
|
+
/** Whether the text input should be multiline */
|
|
184
264
|
multiline?: boolean;
|
|
265
|
+
/** Prefix text to display before the input */
|
|
185
266
|
prefix?: string;
|
|
267
|
+
/** Suffix text to display after the input */
|
|
186
268
|
suffix?: string;
|
|
269
|
+
/** Whether multiple text values can be entered */
|
|
187
270
|
multiple?: boolean;
|
|
188
271
|
};
|
|
189
272
|
theme: {
|
|
273
|
+
/** Whether to include default themes in the selector */
|
|
190
274
|
include_defaults?: boolean;
|
|
191
275
|
};
|
|
192
276
|
time: null;
|
package/src/helpers/id-by.mts
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
+
import { writeFileSync } from "node:fs";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
import { cwd } from "node:process";
|
|
5
|
+
|
|
2
6
|
import type { TServiceParams } from "@digital-alchemy/core";
|
|
3
7
|
import { CreateApplication } from "@digital-alchemy/core";
|
|
4
|
-
import { writeFileSync } from "fs";
|
|
5
|
-
import { join } from "path";
|
|
6
|
-
import { cwd } from "process";
|
|
7
8
|
|
|
8
9
|
import { LIB_HASS } from "../index.mts";
|
|
9
10
|
import type { ScannerCacheData } from "./helpers/index.mts";
|
|
@@ -1,6 +1,7 @@
|
|
|
1
|
+
import { join } from "node:path";
|
|
2
|
+
import { cwd } from "node:process";
|
|
3
|
+
|
|
1
4
|
import { CreateLibrary, createModule, SINGLE } from "@digital-alchemy/core";
|
|
2
|
-
import { join } from "path";
|
|
3
|
-
import { cwd } from "process";
|
|
4
5
|
|
|
5
6
|
import { LIB_HASS } from "../hass.module.mts";
|
|
6
7
|
import {
|
|
@@ -1,6 +1,7 @@
|
|
|
1
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
2
|
+
|
|
1
3
|
import type { TServiceParams } from "@digital-alchemy/core";
|
|
2
4
|
import { BootstrapException } from "@digital-alchemy/core";
|
|
3
|
-
import { existsSync, readFileSync } from "fs";
|
|
4
5
|
|
|
5
6
|
import type { ENTITY_STATE } from "../../index.mts";
|
|
6
7
|
import type { ANY_ENTITY } from "../../user.mts";
|
|
@@ -1,6 +1,7 @@
|
|
|
1
|
+
import EventEmitter from "node:events";
|
|
2
|
+
|
|
1
3
|
import type { TBlackHole, TServiceParams } from "@digital-alchemy/core";
|
|
2
4
|
import { START } from "@digital-alchemy/core";
|
|
3
|
-
import EventEmitter from "events";
|
|
4
5
|
import type { PartialDeep, WritableDeep } from "type-fest";
|
|
5
6
|
import type WS from "ws";
|
|
6
7
|
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
/* eslint-disable @typescript-eslint/no-magic-numbers */
|
|
2
|
+
import { env } from "node:process";
|
|
3
|
+
|
|
2
4
|
import type { TServiceParams } from "@digital-alchemy/core";
|
|
3
5
|
import { asyncNoop, debounce, INCREMENT, SECOND, sleep, START } from "@digital-alchemy/core";
|
|
4
|
-
import { env } from "process";
|
|
5
6
|
|
|
6
7
|
import type {
|
|
7
8
|
ALL_SERVICE_DOMAINS,
|
|
@@ -1,5 +1,6 @@
|
|
|
1
|
+
import { channel } from "node:diagnostics_channel";
|
|
2
|
+
|
|
1
3
|
import type { TServiceParams } from "@digital-alchemy/core";
|
|
2
|
-
import { channel } from "diagnostics_channel";
|
|
3
4
|
|
|
4
5
|
function createDiagnostics<CHANNEL extends string>(context: string, channels: CHANNEL[]) {
|
|
5
6
|
return Object.fromEntries(channels.map(i => [i, channel(`hass:${context}:${i}`)])) as Record<
|
|
@@ -64,11 +64,14 @@ export function IDByExtension({
|
|
|
64
64
|
HassUniqueIdMapping[UNIQUE_ID],
|
|
65
65
|
ANY_ENTITY
|
|
66
66
|
>,
|
|
67
|
-
>(unique_id: UNIQUE_ID): ENTITY_ID {
|
|
67
|
+
>(unique_id: UNIQUE_ID, platform?: TPlatformId): ENTITY_ID {
|
|
68
68
|
hass.entity.warnEarly("byUniqueId");
|
|
69
69
|
let list = hass.entity.registry.current.filter(
|
|
70
70
|
i => i.unique_id === unique_id,
|
|
71
71
|
) as EntityRegistryItem<ENTITY_ID>[];
|
|
72
|
+
if (!is.empty(platform)) {
|
|
73
|
+
list = list.filter(i => i.platform === platform);
|
|
74
|
+
}
|
|
72
75
|
if (is.empty(list)) {
|
|
73
76
|
logger.error({ name: unique_id, unique_id }, `could not find an entity`);
|
|
74
77
|
return undefined;
|
|
@@ -1,8 +1,9 @@
|
|
|
1
|
+
import { createWriteStream } from "node:fs";
|
|
2
|
+
import { pipeline } from "node:stream";
|
|
3
|
+
import { promisify } from "node:util";
|
|
4
|
+
|
|
1
5
|
import type { TServiceParams } from "@digital-alchemy/core";
|
|
2
6
|
import { FIRST, InternalError } from "@digital-alchemy/core";
|
|
3
|
-
import { createWriteStream } from "fs";
|
|
4
|
-
import { pipeline } from "stream";
|
|
5
|
-
import { promisify } from "util";
|
|
6
7
|
|
|
7
8
|
import type {
|
|
8
9
|
DownloadOptions,
|
|
@@ -136,7 +136,9 @@ export function ReferenceService({
|
|
|
136
136
|
"attributes",
|
|
137
137
|
"entity_id",
|
|
138
138
|
"history",
|
|
139
|
-
"
|
|
139
|
+
"last_changed",
|
|
140
|
+
"last_reported",
|
|
141
|
+
"last_updated",
|
|
140
142
|
"nextState",
|
|
141
143
|
"once",
|
|
142
144
|
"onStateFor",
|
|
@@ -357,7 +359,6 @@ export function ReferenceService({
|
|
|
357
359
|
return async (state: string | number, timeout?: TOffset) =>
|
|
358
360
|
await new Promise<ENTITY_STATE<ENTITY_ID>>(async done => {
|
|
359
361
|
const remove = () => {
|
|
360
|
-
done = undefined;
|
|
361
362
|
listeners.delete(remove);
|
|
362
363
|
done = undefined;
|
|
363
364
|
logger.trace({ entity_id }, "remove [waitForState] listener");
|
|
@@ -1,8 +1,9 @@
|
|
|
1
|
+
import EventEmitter from "node:events";
|
|
2
|
+
|
|
1
3
|
import type { TBlackHole, TServiceParams } from "@digital-alchemy/core";
|
|
2
4
|
import { INCREMENT, InternalError, NONE, SECOND, sleep, START } from "@digital-alchemy/core";
|
|
3
5
|
import type { Dayjs } from "dayjs";
|
|
4
6
|
import dayjs from "dayjs";
|
|
5
|
-
import EventEmitter from "events";
|
|
6
7
|
import type { EmptyObject } from "type-fest";
|
|
7
8
|
import WS from "ws";
|
|
8
9
|
|
|
@@ -1,10 +1,11 @@
|
|
|
1
|
+
import { subscribe } from "node:diagnostics_channel";
|
|
2
|
+
|
|
1
3
|
import { sleep } from "@digital-alchemy/core";
|
|
2
|
-
import { subscribe } from "diagnostics_channel";
|
|
3
4
|
|
|
4
5
|
import type { AreaDetails } from "../helpers/index.mts";
|
|
5
|
-
import { AREA_REGISTRY_UPDATED } from "../helpers/index.mts";
|
|
6
|
+
import { AREA_REGISTRY_UPDATED, ENTITY_REGISTRY_UPDATED } from "../helpers/index.mts";
|
|
6
7
|
import { hassTestRunner, INTERNAL_MESSAGE } from "../mock_assistant/index.mts";
|
|
7
|
-
import type { TAreaId } from "../user.mts";
|
|
8
|
+
import type { ANY_ENTITY, TAreaId } from "../user.mts";
|
|
8
9
|
|
|
9
10
|
const EXAMPLE_AREA = {
|
|
10
11
|
area_id: "empty_area" as TAreaId,
|
|
@@ -200,4 +201,168 @@ describe("API", () => {
|
|
|
200
201
|
});
|
|
201
202
|
});
|
|
202
203
|
});
|
|
204
|
+
|
|
205
|
+
describe("apply", () => {
|
|
206
|
+
it("should apply area to a single entity", async () => {
|
|
207
|
+
expect.assertions(2);
|
|
208
|
+
await hassTestRunner.run(({ lifecycle, hass, event }) => {
|
|
209
|
+
const spy = vi.spyOn(hass.socket, "sendMessage").mockImplementation(async () => undefined);
|
|
210
|
+
lifecycle.onReady(async () => {
|
|
211
|
+
const entity = "sensor.magic" as ANY_ENTITY;
|
|
212
|
+
const area = "living_room" as TAreaId;
|
|
213
|
+
const response = hass.area.apply(area, [entity]);
|
|
214
|
+
setImmediate(() => event.emit(ENTITY_REGISTRY_UPDATED));
|
|
215
|
+
const result = await response;
|
|
216
|
+
|
|
217
|
+
expect(spy).toHaveBeenCalledWith({
|
|
218
|
+
area_id: area,
|
|
219
|
+
entity_id: entity,
|
|
220
|
+
type: "config/entity_registry/update",
|
|
221
|
+
});
|
|
222
|
+
expect(result.updated).toEqual([entity]);
|
|
223
|
+
});
|
|
224
|
+
});
|
|
225
|
+
});
|
|
226
|
+
|
|
227
|
+
it("should apply area to multiple entities", async () => {
|
|
228
|
+
expect.assertions(3);
|
|
229
|
+
await hassTestRunner.run(({ lifecycle, hass, event }) => {
|
|
230
|
+
lifecycle.onReady(async () => {
|
|
231
|
+
const updateCalls: Array<{ area_id: TAreaId; entity_id: ANY_ENTITY; type: string }> = [];
|
|
232
|
+
vi.spyOn(hass.socket, "sendMessage").mockImplementation(async message => {
|
|
233
|
+
if (message?.type === "config/entity_registry/update") {
|
|
234
|
+
updateCalls.push(
|
|
235
|
+
message as { area_id: TAreaId; entity_id: ANY_ENTITY; type: string },
|
|
236
|
+
);
|
|
237
|
+
// Emit event asynchronously to ensure listener is registered
|
|
238
|
+
setImmediate(() => event.emit(ENTITY_REGISTRY_UPDATED));
|
|
239
|
+
}
|
|
240
|
+
return undefined;
|
|
241
|
+
});
|
|
242
|
+
|
|
243
|
+
const entities = ["sensor.magic", "light.kitchen_lamp"] as ANY_ENTITY[];
|
|
244
|
+
const area = "living_room" as TAreaId;
|
|
245
|
+
const result = await hass.area.apply(area, entities);
|
|
246
|
+
|
|
247
|
+
expect(updateCalls).toHaveLength(2);
|
|
248
|
+
expect(updateCalls).toEqual([
|
|
249
|
+
{
|
|
250
|
+
area_id: area,
|
|
251
|
+
entity_id: entities[0],
|
|
252
|
+
type: "config/entity_registry/update",
|
|
253
|
+
},
|
|
254
|
+
{
|
|
255
|
+
area_id: area,
|
|
256
|
+
entity_id: entities[1],
|
|
257
|
+
type: "config/entity_registry/update",
|
|
258
|
+
},
|
|
259
|
+
]);
|
|
260
|
+
expect(result.updated).toEqual(entities);
|
|
261
|
+
});
|
|
262
|
+
});
|
|
263
|
+
});
|
|
264
|
+
|
|
265
|
+
it("should skip entities that already have the correct area", async () => {
|
|
266
|
+
expect.assertions(2);
|
|
267
|
+
await hassTestRunner.run(({ lifecycle, hass }) => {
|
|
268
|
+
lifecycle.onReady(async () => {
|
|
269
|
+
const spy = vi
|
|
270
|
+
.spyOn(hass.socket, "sendMessage")
|
|
271
|
+
.mockImplementation(async () => undefined);
|
|
272
|
+
// Find an entity that already has an area assigned
|
|
273
|
+
const entityWithArea = hass.entity.registry.current.find(item => item.area_id !== null);
|
|
274
|
+
if (!entityWithArea) {
|
|
275
|
+
throw new Error("No entity with area found in fixtures");
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
const entity = entityWithArea.entity_id as ANY_ENTITY;
|
|
279
|
+
const area = entityWithArea.area_id as TAreaId;
|
|
280
|
+
const result = await hass.area.apply(area, [entity]);
|
|
281
|
+
|
|
282
|
+
const updateCalls = spy.mock.calls.filter(
|
|
283
|
+
call => call[0]?.type === "config/entity_registry/update",
|
|
284
|
+
);
|
|
285
|
+
expect(updateCalls).toHaveLength(0);
|
|
286
|
+
expect(result.updated).toEqual([]);
|
|
287
|
+
});
|
|
288
|
+
});
|
|
289
|
+
});
|
|
290
|
+
|
|
291
|
+
it("should throw error for unknown entity", async () => {
|
|
292
|
+
expect.assertions(1);
|
|
293
|
+
await hassTestRunner.run(({ lifecycle, hass }) => {
|
|
294
|
+
lifecycle.onReady(async () => {
|
|
295
|
+
const unknownEntity = "sensor.unknown_entity" as ANY_ENTITY;
|
|
296
|
+
const area = "living_room" as TAreaId;
|
|
297
|
+
|
|
298
|
+
try {
|
|
299
|
+
await hass.area.apply(area, [unknownEntity]);
|
|
300
|
+
} catch (error) {
|
|
301
|
+
// InternalError structure: check various possible properties
|
|
302
|
+
const err = error as Record<string, unknown>;
|
|
303
|
+
const errorString = String(error);
|
|
304
|
+
const hasCode = err.code === "UNKNOWN_ENTITY";
|
|
305
|
+
const hasName = err.name === "UNKNOWN_ENTITY";
|
|
306
|
+
const hasMessage = String(err.message || "").includes("UNKNOWN_ENTITY");
|
|
307
|
+
const hasString = errorString.includes("UNKNOWN_ENTITY");
|
|
308
|
+
expect(hasCode || hasName || hasMessage || hasString).toBe(true);
|
|
309
|
+
}
|
|
310
|
+
});
|
|
311
|
+
});
|
|
312
|
+
});
|
|
313
|
+
|
|
314
|
+
it("should wait for ENTITY_REGISTRY_UPDATED before continuing", async () => {
|
|
315
|
+
expect.assertions(1);
|
|
316
|
+
await hassTestRunner.run(({ lifecycle, hass, event }) => {
|
|
317
|
+
vi.spyOn(hass.socket, "sendMessage").mockImplementation(async () => undefined);
|
|
318
|
+
lifecycle.onReady(async () => {
|
|
319
|
+
const entity = "sensor.magic" as ANY_ENTITY;
|
|
320
|
+
const area = "living_room" as TAreaId;
|
|
321
|
+
const response = hass.area.apply(area, [entity]);
|
|
322
|
+
let order = "";
|
|
323
|
+
setTimeout(() => {
|
|
324
|
+
order += "a";
|
|
325
|
+
event.emit(ENTITY_REGISTRY_UPDATED);
|
|
326
|
+
}, 5);
|
|
327
|
+
await response;
|
|
328
|
+
order += "b";
|
|
329
|
+
expect(order).toEqual("ab");
|
|
330
|
+
});
|
|
331
|
+
});
|
|
332
|
+
});
|
|
333
|
+
|
|
334
|
+
it("should return only updated entities when some are skipped", async () => {
|
|
335
|
+
expect.assertions(2);
|
|
336
|
+
await hassTestRunner.run(({ lifecycle, hass, event }) => {
|
|
337
|
+
lifecycle.onReady(async () => {
|
|
338
|
+
const spy = vi.spyOn(hass.socket, "sendMessage").mockImplementation(async message => {
|
|
339
|
+
if (message?.type === "config/entity_registry/update") {
|
|
340
|
+
setImmediate(() => event.emit(ENTITY_REGISTRY_UPDATED));
|
|
341
|
+
}
|
|
342
|
+
return undefined;
|
|
343
|
+
});
|
|
344
|
+
|
|
345
|
+
// Find an entity that already has "kitchen" area so it will be skipped
|
|
346
|
+
const entityWithArea = hass.entity.registry.current.find(
|
|
347
|
+
item => item.area_id === "kitchen",
|
|
348
|
+
);
|
|
349
|
+
if (!entityWithArea) {
|
|
350
|
+
throw new Error("No entity with kitchen area found in fixtures");
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
const existingEntity = entityWithArea.entity_id as ANY_ENTITY;
|
|
354
|
+
const newEntity = "sensor.magic" as ANY_ENTITY;
|
|
355
|
+
const newArea = "kitchen" as TAreaId;
|
|
356
|
+
|
|
357
|
+
const result = await hass.area.apply(newArea, [existingEntity, newEntity]);
|
|
358
|
+
|
|
359
|
+
const updateCalls = spy.mock.calls.filter(
|
|
360
|
+
call => call[0]?.type === "config/entity_registry/update",
|
|
361
|
+
);
|
|
362
|
+
expect(updateCalls).toHaveLength(1);
|
|
363
|
+
expect(result.updated).toEqual([newEntity]);
|
|
364
|
+
});
|
|
365
|
+
});
|
|
366
|
+
});
|
|
367
|
+
});
|
|
203
368
|
});
|