@digital-alchemy/hass 25.10.25 → 25.10.26

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 (38) hide show
  1. package/dist/dev/mappings.d.mts +13 -7
  2. package/dist/dev/registry.d.mts +238 -0
  3. package/dist/hass.module.d.mts +5 -1
  4. package/dist/hass.module.mjs +5 -1
  5. package/dist/hass.module.mjs.map +1 -1
  6. package/dist/helpers/fetch/service-list.d.mts +15 -4
  7. package/dist/helpers/index.d.mts +1 -0
  8. package/dist/helpers/index.mjs +1 -0
  9. package/dist/helpers/index.mjs.map +1 -1
  10. package/dist/helpers/supported-features.d.mts +447 -0
  11. package/dist/helpers/supported-features.mjs +290 -0
  12. package/dist/helpers/supported-features.mjs.map +1 -0
  13. package/dist/mock_assistant/mock-assistant.module.d.mts +2 -0
  14. package/dist/services/feature.service.d.mts +10 -0
  15. package/dist/services/feature.service.mjs +95 -0
  16. package/dist/services/feature.service.mjs.map +1 -0
  17. package/dist/services/index.d.mts +1 -0
  18. package/dist/services/index.mjs +1 -0
  19. package/dist/services/index.mjs.map +1 -1
  20. package/dist/testing/feature.spec.d.mts +1 -0
  21. package/dist/testing/feature.spec.mjs +203 -0
  22. package/dist/testing/feature.spec.mjs.map +1 -0
  23. package/dist/testing/id-by.spec.mjs +3 -3
  24. package/dist/testing/id-by.spec.mjs.map +1 -1
  25. package/dist/testing/ref-by.spec.mjs +2 -2
  26. package/dist/testing/ref-by.spec.mjs.map +1 -1
  27. package/package.json +9 -9
  28. package/src/dev/mappings.mts +38 -7
  29. package/src/dev/registry.mts +232 -0
  30. package/src/hass.module.mts +6 -0
  31. package/src/helpers/fetch/service-list.mts +13 -5
  32. package/src/helpers/index.mts +1 -0
  33. package/src/helpers/supported-features.mts +328 -0
  34. package/src/services/feature.service.mts +127 -0
  35. package/src/services/index.mts +1 -0
  36. package/src/testing/feature.spec.mts +219 -0
  37. package/src/testing/id-by.spec.mts +3 -3
  38. package/src/testing/ref-by.spec.mts +2 -2
@@ -0,0 +1,328 @@
1
+ /* eslint-disable sort-keys-fix/sort-keys-fix */
2
+
3
+ import type { ALL_DOMAINS } from "../user.mts";
4
+
5
+ /**
6
+ * homeassistant/components/light/const.py
7
+ */
8
+ export const LIGHT = {
9
+ EFFECT: 4,
10
+ FLASH: 8,
11
+ TRANSITION: 32,
12
+ } as const;
13
+
14
+ /**
15
+ * homeassistant/components/vacuum/__init__.py
16
+ */
17
+ export const VACUUM = {
18
+ /**
19
+ * Deprecated
20
+ */
21
+ TURN_ON: 1,
22
+ /**
23
+ * Deprecated
24
+ */
25
+ TURN_OFF: 2,
26
+ PAUSE: 4,
27
+ STOP: 8,
28
+ RETURN_HOME: 16,
29
+ FAN_SPEED: 32,
30
+ BATTERY: 64,
31
+ /**
32
+ * Deprecated
33
+ */
34
+ STATUS: 128,
35
+ SEND_COMMAND: 256,
36
+ LOCATE: 512,
37
+ CLEAN_SPOT: 1024,
38
+ MAP: 2048,
39
+ STATE: 4096,
40
+ START: 8192,
41
+ } as const;
42
+
43
+ /**
44
+ * homeassistant/components/water_heater/__init__.py
45
+ */
46
+ export const WATER_HEATER = {
47
+ TARGET_TEMPERATURE: 1,
48
+ OPERATION_MODE: 2,
49
+ AWAY_MODE: 4,
50
+ ON_OFF: 8,
51
+ } as const;
52
+
53
+ /**
54
+ * homeassistant/components/valve/__init__.py
55
+ */
56
+ export const VALVE = {
57
+ OPEN: 1,
58
+ CLOSE: 2,
59
+ SET_POSITION: 4,
60
+ STOP: 8,
61
+ } as const;
62
+
63
+ /**
64
+ * homeassistant/components/weather/const.py
65
+ */
66
+ export const WEATHER = {
67
+ FORECAST_DAILY: 1,
68
+ FORECAST_HOURLY: 2,
69
+ FORECAST_TWICE_DAILY: 4,
70
+ } as const;
71
+
72
+ /**
73
+ * homeassistant/components/ai_task/const.py
74
+ */
75
+ export const AI_TASK = {
76
+ ATTACHMENTS: 2,
77
+ } as const;
78
+
79
+ /**
80
+ * homeassistant/components/switch/__init__.py
81
+ */
82
+ export const SWITCH = {
83
+ TURN_ON: 1,
84
+ TURN_OFF: 2,
85
+ } as const;
86
+
87
+ /**
88
+ * homeassistant/components/cover/__init__.py
89
+ */
90
+ export const COVER = {
91
+ OPEN: 1,
92
+ CLOSE: 2,
93
+ SET_POSITION: 4,
94
+ STOP: 8,
95
+ OPEN_TILT: 16,
96
+ CLOSE_TILT: 32,
97
+ STOP_TILT: 64,
98
+ SET_TILT_POSITION: 128,
99
+ } as const;
100
+
101
+ /**
102
+ * homeassistant/components/fan/__init__.py
103
+ */
104
+ export const FAN = {
105
+ SET_SPEED: 1,
106
+ OSCILLATE: 2,
107
+ DIRECTION: 4,
108
+ PRESET_MODE: 8,
109
+ TURN_OFF: 16,
110
+ TURN_ON: 32,
111
+ } as const;
112
+
113
+ /**
114
+ * homeassistant/components/climate/const.py
115
+ */
116
+ export const CLIMATE = {
117
+ TARGET_TEMPERATURE: 1,
118
+ TARGET_TEMPERATURE_RANGE: 2,
119
+ TARGET_HUMIDITY: 4,
120
+ FAN_MODE: 8,
121
+ PRESET_MODE: 16,
122
+ SWING_MODE: 32,
123
+ TURN_OFF: 128,
124
+ TURN_ON: 256,
125
+ SWING_HORIZONTAL_MODE: 512,
126
+ } as const;
127
+
128
+ /**
129
+ * homeassistant/components/media_player/const.py
130
+ */
131
+ export const MEDIA_PLAYER = {
132
+ PAUSE: 1,
133
+ SEEK: 2,
134
+ VOLUME_SET: 4,
135
+ VOLUME_MUTE: 8,
136
+ PREVIOUS_TRACK: 16,
137
+ NEXT_TRACK: 32,
138
+ TURN_ON: 128,
139
+ TURN_OFF: 256,
140
+ PLAY_MEDIA: 512,
141
+ VOLUME_STEP: 1024,
142
+ SELECT_SOURCE: 2048,
143
+ STOP: 4096,
144
+ CLEAR_PLAYLIST: 8192,
145
+ PLAY: 16384,
146
+ SHUFFLE_SET: 32768,
147
+ SELECT_SOUND_MODE: 65536,
148
+ BROWSE_MEDIA: 131072,
149
+ REPEAT_SET: 262144,
150
+ GROUPING: 524288,
151
+ MEDIA_ANNOUNCE: 1048576,
152
+ MEDIA_ENQUEUE: 2097152,
153
+ SEARCH_MEDIA: 4194304,
154
+ } as const;
155
+
156
+ /**
157
+ * homeassistant/components/camera/__init__.py
158
+ */
159
+ export const CAMERA = {
160
+ ON_OFF: 1,
161
+ STREAM: 2,
162
+ } as const;
163
+
164
+ /**
165
+ * homeassistant/components/lock/__init__.py
166
+ */
167
+ export const LOCK = {
168
+ OPEN: 1,
169
+ } as const;
170
+
171
+ /**
172
+ * homeassistant/components/alarm_control_panel/__init__.py
173
+ */
174
+ export const ALARM_CONTROL_PANEL = {
175
+ ARM_HOME: 1,
176
+ ARM_AWAY: 2,
177
+ ARM_NIGHT: 4,
178
+ ARM_VACATION: 8,
179
+ ARM_CUSTOM_BYPASS: 16,
180
+ } as const;
181
+
182
+ /**
183
+ * homeassistant/components/binary_sensor/__init__.py
184
+ */
185
+ export const BINARY_SENSOR = {
186
+ TURN_ON: 1,
187
+ TURN_OFF: 2,
188
+ } as const;
189
+
190
+ /**
191
+ * homeassistant/components/sensor/__init__.py
192
+ */
193
+ export const SENSOR = {
194
+ TURN_ON: 1,
195
+ TURN_OFF: 2,
196
+ } as const;
197
+
198
+ /**
199
+ * homeassistant/components/update/const.py
200
+ */
201
+ export const UPDATE = {
202
+ INSTALL: 1,
203
+ SPECIFIC_VERSION: 2,
204
+ PROGRESS: 4,
205
+ BACKUP: 8,
206
+ RELEASE_NOTES: 16,
207
+ } as const;
208
+
209
+ /**
210
+ * homeassistant/components/button/__init__.py
211
+ */
212
+ export const BUTTON = {
213
+ TURN_ON: 1,
214
+ TURN_OFF: 2,
215
+ } as const;
216
+
217
+ /**
218
+ * homeassistant/components/number/__init__.py
219
+ */
220
+ export const NUMBER = {
221
+ TURN_ON: 1,
222
+ TURN_OFF: 2,
223
+ } as const;
224
+
225
+ /**
226
+ * homeassistant/components/select/__init__.py
227
+ */
228
+ export const SELECT = {
229
+ TURN_ON: 1,
230
+ TURN_OFF: 2,
231
+ } as const;
232
+
233
+ /**
234
+ * homeassistant/components/siren/const.py
235
+ */
236
+ export const SIREN = {
237
+ TURN_ON: 1,
238
+ TURN_OFF: 2,
239
+ TONES: 4,
240
+ VOLUME_SET: 8,
241
+ DURATION: 16,
242
+ } as const;
243
+
244
+ /**
245
+ * homeassistant/components/text/__init__.py
246
+ */
247
+ export const TEXT = {
248
+ TURN_ON: 1,
249
+ TURN_OFF: 2,
250
+ } as const;
251
+
252
+ /**
253
+ * homeassistant/components/todo/const.py
254
+ */
255
+ export const TODO = {
256
+ CREATE_TODO_ITEM: 1,
257
+ DELETE_TODO_ITEM: 2,
258
+ UPDATE_TODO_ITEM: 4,
259
+ MOVE_TODO_ITEM: 8,
260
+ SET_DUE_DATE_ON_ITEM: 16,
261
+ SET_DUE_DATETIME_ON_ITEM: 32,
262
+ SET_DESCRIPTION_ON_ITEM: 64,
263
+ } as const;
264
+
265
+ /**
266
+ * homeassistant/components/humidifier/const.py
267
+ */
268
+ export const HUMIDIFIER = {
269
+ MODES: 1,
270
+ } as const;
271
+
272
+ /**
273
+ * homeassistant/components/remote/__init__.py
274
+ */
275
+ export const REMOTE = {
276
+ LEARN_COMMAND: 1,
277
+ DELETE_COMMAND: 2,
278
+ ACTIVITY: 4,
279
+ } as const;
280
+
281
+ /**
282
+ * homeassistant/components/notify/__init__.py
283
+ */
284
+ export const NOTIFY = {
285
+ TITLE: 1,
286
+ } as const;
287
+
288
+ /**
289
+ * Registry for other library logic to take advantage of
290
+ */
291
+ export const SUPPORTED_FEATURES = {
292
+ AI_TASK,
293
+ ALARM_CONTROL_PANEL,
294
+ BINARY_SENSOR,
295
+ BUTTON,
296
+ CAMERA,
297
+ CLIMATE,
298
+ COVER,
299
+ FAN,
300
+ HUMIDIFIER,
301
+ LIGHT,
302
+ LOCK,
303
+ MEDIA_PLAYER,
304
+ NOTIFY,
305
+ NUMBER,
306
+ REMOTE,
307
+ SELECT,
308
+ SENSOR,
309
+ SIREN,
310
+ SWITCH,
311
+ TEXT,
312
+ TODO,
313
+ UPDATE,
314
+ VACUUM,
315
+ VALVE,
316
+ WATER_HEATER,
317
+ WEATHER,
318
+ } as const;
319
+
320
+ export type SUPPORTED_FEATURES = typeof SUPPORTED_FEATURES;
321
+ export type SupportedFeatureDomains = Extract<keyof SUPPORTED_FEATURES, string>;
322
+ export type UsedSupportedFeatureDomains = Extract<Lowercase<SupportedFeatureDomains>, ALL_DOMAINS>;
323
+
324
+ export type SupportedFeatures<DOMAIN extends SupportedFeatureDomains = SupportedFeatureDomains> =
325
+ `${DOMAIN}.${Extract<keyof SUPPORTED_FEATURES[DOMAIN], string>}`;
326
+ export type SupportedEntityFeatures<
327
+ DOMAIN extends UsedSupportedFeatureDomains = UsedSupportedFeatureDomains,
328
+ > = `${Uppercase<DOMAIN>}.${Extract<keyof SUPPORTED_FEATURES[Uppercase<DOMAIN>], string>}`;
@@ -0,0 +1,127 @@
1
+ /* eslint-disable @typescript-eslint/no-magic-numbers */
2
+ import { LABEL, type TServiceParams } from "@digital-alchemy/core";
3
+
4
+ import type { ByIdProxy } from "../helpers/entity-state.mts";
5
+ import type {
6
+ SupportedEntityFeatures,
7
+ SupportedFeatureDomains,
8
+ UsedSupportedFeatureDomains,
9
+ } from "../index.mts";
10
+ import { domain, SUPPORTED_FEATURES } from "../index.mts";
11
+ import type { PICK_ENTITY } from "../user.mts";
12
+
13
+ export function HassFeatureService({
14
+ hass,
15
+ logger,
16
+ internal: {
17
+ utils: { is },
18
+ },
19
+ }: TServiceParams) {
20
+ /**
21
+ * Helper function to create supported features from an array of feature numbers
22
+ */
23
+ function createSupportedFeatures<T extends UsedSupportedFeatureDomains>(
24
+ features: (number | SupportedEntityFeatures<T>)[],
25
+ ): number {
26
+ return features.reduce((acc: number, feature) => {
27
+ if (is.string(feature)) {
28
+ const original = feature;
29
+ const [domain, featureName] = feature.split(".") as [SupportedFeatureDomains, string];
30
+ const featureDomain = SUPPORTED_FEATURES[domain] as Record<string, number>;
31
+ feature = featureDomain?.[featureName];
32
+ if (!featureDomain?.[featureName]) {
33
+ feature = 0;
34
+ logger.error({ feature: original }, `invalid feature lookup`);
35
+ }
36
+ }
37
+ return acc | feature;
38
+ }, 0);
39
+ }
40
+
41
+ function lookup(input: PICK_ENTITY | ByIdProxy<PICK_ENTITY>) {
42
+ const ref = is.string(input) ? hass.refBy.id(input) : input;
43
+ const attributes = ref.attributes as { supported_features: number };
44
+ return attributes?.supported_features ?? 0;
45
+ }
46
+
47
+ /**
48
+ * Helper function to check if an entity supports a specific feature
49
+ */
50
+ function hasFeature<T extends UsedSupportedFeatureDomains>(
51
+ input: number | PICK_ENTITY<T> | ByIdProxy<PICK_ENTITY<T>>,
52
+ feature: number | SupportedEntityFeatures<T>,
53
+ ): boolean {
54
+ const features = is.number(input) ? input : lookup(input);
55
+ if (is.string(feature)) {
56
+ const original = feature;
57
+ const [domain, featureName] = feature.split(".") as [SupportedFeatureDomains, string];
58
+ const featureDomain = SUPPORTED_FEATURES[domain] as Record<string, number>;
59
+ feature = featureDomain?.[featureName];
60
+ if (!feature) {
61
+ feature = 0;
62
+ logger.error({ feature: original }, `invalid feature lookup`);
63
+ }
64
+ }
65
+ return (features & feature) !== 0;
66
+ }
67
+
68
+ /**
69
+ * Helper function to get all supported features as an array
70
+ * Can accept a number (bitmask), entity ID, or entity proxy
71
+ */
72
+ function getSupportedFeatures(input: number | PICK_ENTITY | ByIdProxy<PICK_ENTITY>): number[] {
73
+ const features = is.number(input) ? input : lookup(input);
74
+
75
+ const supported: number[] = [];
76
+ let bit = 1;
77
+ while (bit <= features) {
78
+ if ((features & bit) !== 0) {
79
+ supported.push(bit);
80
+ }
81
+ bit <<= 1;
82
+ }
83
+ return supported;
84
+ }
85
+
86
+ function listEntityFeatures<
87
+ DOMAIN extends UsedSupportedFeatureDomains = UsedSupportedFeatureDomains,
88
+ >(
89
+ input: PICK_ENTITY<DOMAIN> | ByIdProxy<PICK_ENTITY<DOMAIN>>,
90
+ ): SupportedEntityFeatures<DOMAIN>[] {
91
+ const inputDomain = domain(is.string(input) ? input : input.entity_id);
92
+ const domainFeatures = SUPPORTED_FEATURES[inputDomain.toUpperCase() as SupportedFeatureDomains];
93
+ if (!domainFeatures) {
94
+ return [];
95
+ }
96
+ const features = lookup(input);
97
+
98
+ const supported: number[] = [];
99
+ let bit = 1;
100
+ while (bit <= features) {
101
+ if ((features & bit) !== 0) {
102
+ supported.push(bit);
103
+ }
104
+ bit <<= 1;
105
+ }
106
+
107
+ const options = Object.entries(domainFeatures);
108
+ return supported
109
+ .map(i => {
110
+ const found = options.find(([, value]) => value === i);
111
+ if (!found) return null;
112
+ return (inputDomain.toUpperCase() +
113
+ "." +
114
+ found[
115
+ LABEL
116
+ ]) as `${Uppercase<DOMAIN>}.${Extract<keyof SUPPORTED_FEATURES[Uppercase<DOMAIN>], string>}`;
117
+ })
118
+ .filter(Boolean);
119
+ }
120
+
121
+ return {
122
+ createSupportedFeatures,
123
+ getSupportedFeatures,
124
+ hasFeature,
125
+ listEntityFeatures,
126
+ };
127
+ }
@@ -6,6 +6,7 @@ export * from "./device.service.mts";
6
6
  export * from "./diagnostics.service.mts";
7
7
  export * from "./entity.service.mts";
8
8
  export * from "./events.service.mts";
9
+ export * from "./feature.service.mts";
9
10
  export * from "./fetch-api.service.mts";
10
11
  export * from "./floor.service.mts";
11
12
  export * from "./id-by.service.mts";
@@ -0,0 +1,219 @@
1
+ import { LIGHT } from "../helpers/supported-features.mts";
2
+ import { hassTestRunner } from "../mock_assistant/index.mts";
3
+
4
+ afterEach(async () => {
5
+ await hassTestRunner.teardown();
6
+ vi.restoreAllMocks();
7
+ });
8
+
9
+ describe("Feature Service", () => {
10
+ describe("createSupportedFeatures", () => {
11
+ it("should create bitmask from array of numbers", async () => {
12
+ expect.assertions(1);
13
+ await hassTestRunner.run(({ lifecycle, hass }) => {
14
+ lifecycle.onReady(async () => {
15
+ const result = hass.feature.createSupportedFeatures([LIGHT.EFFECT, LIGHT.FLASH]);
16
+ expect(result).toBe(LIGHT.EFFECT | LIGHT.FLASH);
17
+ });
18
+ });
19
+ });
20
+
21
+ it("should create bitmask from array of feature strings", async () => {
22
+ expect.assertions(1);
23
+ await hassTestRunner.run(({ lifecycle, hass }) => {
24
+ lifecycle.onReady(async () => {
25
+ const result = hass.feature.createSupportedFeatures<"light">([
26
+ "LIGHT.EFFECT",
27
+ "LIGHT.FLASH",
28
+ ]);
29
+ expect(result).toBe(LIGHT.EFFECT | LIGHT.FLASH);
30
+ });
31
+ });
32
+ });
33
+
34
+ it("should handle mixed array of numbers and strings", async () => {
35
+ expect.assertions(1);
36
+ await hassTestRunner.run(({ lifecycle, hass }) => {
37
+ lifecycle.onReady(async () => {
38
+ const result = hass.feature.createSupportedFeatures<"light">([
39
+ LIGHT.EFFECT,
40
+ "LIGHT.FLASH",
41
+ ]);
42
+ expect(result).toBe(LIGHT.EFFECT | LIGHT.FLASH);
43
+ });
44
+ });
45
+ });
46
+
47
+ it("should handle invalid feature strings gracefully", async () => {
48
+ expect.assertions(1);
49
+ await hassTestRunner.run(({ lifecycle, hass }) => {
50
+ lifecycle.onReady(async () => {
51
+ const result = hass.feature.createSupportedFeatures<"light">([
52
+ // @ts-expect-error: part of the test
53
+ "INVALID.DOMAIN",
54
+ "LIGHT.EFFECT",
55
+ ]);
56
+ // Should return only the valid feature, ignoring the invalid one
57
+ expect(result).toBe(LIGHT.EFFECT);
58
+ });
59
+ });
60
+ });
61
+ });
62
+
63
+ describe("hasFeature", () => {
64
+ it("should return true when entity has the feature", async () => {
65
+ expect.assertions(1);
66
+ await hassTestRunner.run(({ lifecycle, hass }) => {
67
+ lifecycle.onReady(async () => {
68
+ // Use numeric features input instead of entity lookup to avoid test environment issues
69
+ const features = LIGHT.EFFECT | LIGHT.FLASH;
70
+ const result = hass.feature.hasFeature(features, LIGHT.EFFECT);
71
+ expect(result).toBe(true);
72
+ });
73
+ });
74
+ });
75
+
76
+ it("should return false when entity does not have the feature", async () => {
77
+ expect.assertions(1);
78
+ await hassTestRunner.run(({ lifecycle, hass }) => {
79
+ lifecycle.onReady(async () => {
80
+ // Use numeric features input instead of entity lookup
81
+ const features = LIGHT.EFFECT; // Only EFFECT, no FLASH
82
+ const result = hass.feature.hasFeature(features, LIGHT.FLASH);
83
+ expect(result).toBe(false);
84
+ });
85
+ });
86
+ });
87
+
88
+ it("should work with feature string", async () => {
89
+ expect.assertions(1);
90
+ await hassTestRunner.run(({ lifecycle, hass }) => {
91
+ lifecycle.onReady(async () => {
92
+ const features = LIGHT.EFFECT | LIGHT.FLASH;
93
+ const result = hass.feature.hasFeature<"light">(features, "LIGHT.EFFECT");
94
+ expect(result).toBe(true);
95
+ });
96
+ });
97
+ });
98
+
99
+ it("should work with numeric features input", async () => {
100
+ expect.assertions(1);
101
+ await hassTestRunner.run(({ lifecycle, hass }) => {
102
+ lifecycle.onReady(async () => {
103
+ const features = LIGHT.EFFECT | LIGHT.FLASH;
104
+ const result = hass.feature.hasFeature(features, LIGHT.EFFECT);
105
+ expect(result).toBe(true);
106
+ });
107
+ });
108
+ });
109
+ });
110
+
111
+ describe("getSupportedFeatures", () => {
112
+ it("should return array of supported feature numbers", async () => {
113
+ expect.assertions(1);
114
+ await hassTestRunner.run(({ lifecycle, hass }) => {
115
+ lifecycle.onReady(async () => {
116
+ // Use numeric features input instead of entity lookup
117
+ const features = LIGHT.EFFECT | LIGHT.FLASH | LIGHT.TRANSITION;
118
+ const result = hass.feature.getSupportedFeatures(features);
119
+ expect(result).toEqual([LIGHT.EFFECT, LIGHT.FLASH, LIGHT.TRANSITION]);
120
+ });
121
+ });
122
+ });
123
+
124
+ it("should work with numeric features input", async () => {
125
+ expect.assertions(1);
126
+ await hassTestRunner.run(({ lifecycle, hass }) => {
127
+ lifecycle.onReady(async () => {
128
+ const features = LIGHT.EFFECT | LIGHT.FLASH;
129
+ const result = hass.feature.getSupportedFeatures(features);
130
+ expect(result).toEqual([LIGHT.EFFECT, LIGHT.FLASH]);
131
+ });
132
+ });
133
+ });
134
+
135
+ it("should return empty array when no features are supported", async () => {
136
+ expect.assertions(1);
137
+ await hassTestRunner.run(({ lifecycle, hass }) => {
138
+ lifecycle.onReady(async () => {
139
+ const features = 0;
140
+ const result = hass.feature.getSupportedFeatures(features);
141
+ expect(result).toEqual([]);
142
+ });
143
+ });
144
+ });
145
+ });
146
+
147
+ describe("listEntityFeatures", () => {
148
+ it("should return array of feature strings for light entity", async () => {
149
+ expect.assertions(1);
150
+ await hassTestRunner.run(({ lifecycle, hass }) => {
151
+ lifecycle.onReady(async () => {
152
+ // Use REAL entity reference from fixtures
153
+ const entity = hass.refBy.id("light.test_light");
154
+ const result = hass.feature.listEntityFeatures(entity);
155
+ // light.test_light has supported_features: 44 (LIGHT.EFFECT | LIGHT.FLASH | LIGHT.TRANSITION)
156
+ expect(result).toEqual(["LIGHT.EFFECT", "LIGHT.FLASH", "LIGHT.TRANSITION"]);
157
+ });
158
+ });
159
+ });
160
+
161
+ it("should return array of feature strings for todo entity", async () => {
162
+ expect.assertions(1);
163
+ await hassTestRunner.run(({ lifecycle, hass }) => {
164
+ lifecycle.onReady(async () => {
165
+ // Use REAL entity reference from fixtures
166
+ const entity = hass.refBy.id("todo.test_todo");
167
+ const result = hass.feature.listEntityFeatures(entity);
168
+ // todo.test_todo has supported_features: 15 (TODO.CREATE_TODO_ITEM | TODO.DELETE_TODO_ITEM | TODO.UPDATE_TODO_ITEM | TODO.MOVE_TODO_ITEM)
169
+ expect(result).toEqual([
170
+ "TODO.CREATE_TODO_ITEM",
171
+ "TODO.DELETE_TODO_ITEM",
172
+ "TODO.UPDATE_TODO_ITEM",
173
+ "TODO.MOVE_TODO_ITEM",
174
+ ]);
175
+ });
176
+ });
177
+ });
178
+
179
+ it("should return array of feature strings for climate entity", async () => {
180
+ expect.assertions(1);
181
+ await hassTestRunner.run(({ lifecycle, hass }) => {
182
+ lifecycle.onReady(async () => {
183
+ // Use REAL entity reference from fixtures
184
+ const entity = hass.refBy.id("climate.test_climate");
185
+ const result = hass.feature.listEntityFeatures(entity);
186
+ // climate.test_climate has supported_features: 1 (CLIMATE.TARGET_TEMPERATURE)
187
+ expect(result).toEqual(["CLIMATE.TARGET_TEMPERATURE"]);
188
+ });
189
+ });
190
+ });
191
+
192
+ it("should return empty array for unsupported domain", async () => {
193
+ expect.assertions(1);
194
+ await hassTestRunner.run(({ lifecycle, hass }) => {
195
+ lifecycle.onReady(async () => {
196
+ // Use REAL entity reference from fixtures
197
+ const entity = hass.refBy.id("unsupported.test");
198
+ // @ts-expect-error part of the test
199
+ const result = hass.feature.listEntityFeatures(entity);
200
+ // unsupported.test has supported_features: 1 but domain is unsupported
201
+ expect(result).toEqual([]);
202
+ });
203
+ });
204
+ });
205
+
206
+ it("should return empty array when no features are supported", async () => {
207
+ expect.assertions(1);
208
+ await hassTestRunner.run(({ lifecycle, hass }) => {
209
+ lifecycle.onReady(async () => {
210
+ // Use REAL entity reference from fixtures that has no features
211
+ const entity = hass.refBy.id("sensor.magic");
212
+ const result = hass.feature.listEntityFeatures(entity);
213
+ // sensor.magic has supported_features: 0
214
+ expect(result).toEqual([]);
215
+ });
216
+ });
217
+ });
218
+ });
219
+ });
@@ -100,7 +100,7 @@ describe("enabled entities", () => {
100
100
  await hassTestRunner.run(({ lifecycle, hass }) => {
101
101
  lifecycle.onReady(() => {
102
102
  const synapse = hass.idBy.platform("synapse");
103
- expect(synapse.length).toBe(7);
103
+ expect(synapse.length).toBe(11);
104
104
  });
105
105
  });
106
106
  });
@@ -110,7 +110,7 @@ describe("enabled entities", () => {
110
110
  await hassTestRunner.run(({ lifecycle, hass }) => {
111
111
  lifecycle.onReady(() => {
112
112
  const synapse = hass.idBy.platform("synapse", "light");
113
- expect(synapse.length).toBe(0);
113
+ expect(synapse.length).toBe(1);
114
114
  });
115
115
  });
116
116
  });
@@ -205,7 +205,7 @@ describe("disabled entities", () => {
205
205
  await hassTestRunner.run(({ lifecycle, hass }) => {
206
206
  lifecycle.onReady(() => {
207
207
  const synapse = hass.idBy.platform("synapse");
208
- expect(synapse.length).toBe(8);
208
+ expect(synapse.length).toBe(12);
209
209
  });
210
210
  });
211
211
  });