@digital-alchemy/hass 24.10.1 → 24.11.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (38) hide show
  1. package/dist/dynamic.d.ts +3 -1
  2. package/dist/extensions/events.extension.d.ts +1 -1
  3. package/dist/extensions/events.extension.js +26 -7
  4. package/dist/extensions/events.extension.js.map +1 -1
  5. package/dist/extensions/internal.extension.js +1 -1
  6. package/dist/extensions/internal.extension.js.map +1 -1
  7. package/dist/extensions/reference.extension.d.ts +51 -1
  8. package/dist/extensions/reference.extension.js +293 -181
  9. package/dist/extensions/reference.extension.js.map +1 -1
  10. package/dist/extensions/websocket-api.extension.js +4 -3
  11. package/dist/extensions/websocket-api.extension.js.map +1 -1
  12. package/dist/hass.module.d.ts +3 -3
  13. package/dist/hass.module.js +3 -3
  14. package/dist/hass.module.js.map +1 -1
  15. package/dist/helpers/entity-state.helper.d.ts +11 -7
  16. package/dist/helpers/interfaces.helper.d.ts +10 -10
  17. package/dist/helpers/interfaces.helper.js.map +1 -1
  18. package/dist/helpers/utility.helper.d.ts +5 -0
  19. package/dist/helpers/utility.helper.js +5 -0
  20. package/dist/helpers/utility.helper.js.map +1 -1
  21. package/dist/mock_assistant/mock-assistant.module.d.ts +4 -4
  22. package/dist/mock_assistant/mock-assistant.module.js +7 -0
  23. package/dist/mock_assistant/mock-assistant.module.js.map +1 -1
  24. package/dist/testing/fetch-api.spec.js +13 -13
  25. package/dist/testing/fetch-api.spec.js.map +1 -1
  26. package/package.json +27 -28
  27. package/scripts/test.sh +1 -1
  28. package/src/dynamic.ts +82 -77
  29. package/src/extensions/events.extension.ts +26 -10
  30. package/src/extensions/internal.extension.ts +1 -1
  31. package/src/extensions/reference.extension.ts +323 -201
  32. package/src/extensions/websocket-api.extension.ts +4 -3
  33. package/src/hass.module.ts +4 -4
  34. package/src/helpers/entity-state.helper.ts +11 -6
  35. package/src/helpers/interfaces.helper.ts +10 -9
  36. package/src/helpers/utility.helper.ts +8 -0
  37. package/src/mock_assistant/mock-assistant.module.ts +7 -0
  38. package/src/testing/fetch-api.spec.ts +13 -13
@@ -2,7 +2,7 @@ import { DOWN, is, NONE, sleep, TAnyFunction, TServiceParams, UP } from "@digita
2
2
  import dayjs, { Dayjs } from "dayjs";
3
3
  import { Get } from "type-fest";
4
4
 
5
- import { SERVICE_LIST_UPDATED } from "..";
5
+ import { SERVICE_LIST_UPDATED, SOCKET_CONNECTED } from "..";
6
6
  import {
7
7
  TAreaId,
8
8
  TDeviceId,
@@ -26,15 +26,65 @@ import {
26
26
  PICK_FROM_FLOOR,
27
27
  PICK_FROM_LABEL,
28
28
  PICK_FROM_PLATFORM,
29
+ RemoveCallback,
29
30
  } from "../helpers";
30
31
 
31
- export function ReferenceExtension({
32
+ /**
33
+ * ## Overview
34
+ *
35
+ * This service is intended for the creation of type safe entity references based on entity id.
36
+ *
37
+ * ```typescript
38
+ * const ref = hass.refBy.id("switch.example");
39
+ * ```
40
+ *
41
+ * These contain specialized type definitions that gets looked up by entity id, making typescript report info about the desired entity.
42
+ *
43
+ * ## Capabilities
44
+ *
45
+ * ### Event Listeners
46
+ *
47
+ * Run a callback in response to
48
+ * - `.onUpdate((new_state, old_state) => ...)`
49
+ * - `.once((new_state, old_state) => ...)`
50
+ *
51
+ * ### State lookups
52
+ *
53
+ * - `.nextState(timeout?)`
54
+ * - `.waitForState(state, timeout?)`
55
+ * - `.previous.state` | `.previous.attributes`
56
+ *
57
+ * ### Service Calling
58
+ *
59
+ * For services that appear on the same domain as the provided entity, the service call can be made directly from the reference.
60
+ *
61
+ * > Ex: `switch.example` calling `switch.turn_on`
62
+ *
63
+ * ```typescript
64
+ * ref.turn_on();
65
+ * ```
66
+ *
67
+ * The `entity_id` property that would normally be required is automatically provided by the proxy.
68
+ *
69
+ * ### Property Lookups
70
+ *
71
+ * - `.state`
72
+ * - `.entity_id`
73
+ * - `.attributes`
74
+ *
75
+ * ## Garbage Collection
76
+ *
77
+ * - `.removeAllListeners()`
78
+ *
79
+ * The reference may call the remove all active event listeners and timers at any time.
80
+ * It will interrupt any timers (nextState/waitForState), as well as detach any event listeners
81
+ */
82
+ export function ReferenceService({
32
83
  hass,
33
84
  logger,
34
85
  internal,
35
86
  event,
36
87
  }: TServiceParams): HassReferenceService {
37
- const ENTITY_PROXIES = new Map<ANY_ENTITY, ByIdProxy<ANY_ENTITY>>();
38
88
  // #MARK:proxyGetLogic
39
89
  function proxyGetLogic<ENTITY extends ANY_ENTITY = ANY_ENTITY, PROPERTY extends string = string>(
40
90
  entity: ENTITY,
@@ -64,238 +114,307 @@ export function ReferenceExtension({
64
114
  }
65
115
 
66
116
  // #MARK: byId
117
+ // ! Calls to this function MUST ALWAYS go through `hass.refBy.id`
118
+ // never call this function directly 💧
67
119
  function byId<ENTITY_ID extends ANY_ENTITY>(entity_id: ENTITY_ID): ByIdProxy<ENTITY_ID> {
68
120
  const entity_domain = domain(entity_id) as ALL_SERVICE_DOMAINS;
69
- if (!ENTITY_PROXIES.has(entity_id)) {
70
- const { ...thing } = hass.entity.getCurrentState(entity_id) as ByIdProxy<ENTITY_ID>;
71
- let loaded = false;
72
-
73
- function keys() {
74
- const entityDomain = domain(entity_id);
75
- return [
76
- "attributes",
77
- "entity_id",
78
- "history",
79
- "last",
80
- "nextState",
81
- "once",
82
- "onUpdate",
83
- "previous",
84
- "removeAllListeners",
85
- "state",
86
- "waitForState",
87
- ...hass.configure
88
- .getServices()
89
- .filter(({ domain }) => domain === entityDomain)
90
- .flatMap(i => Object.keys(i.services))
91
- .sort((a, b) => (a > b ? UP : DOWN)),
92
- ];
121
+ const { ...thing } = hass.entity.getCurrentState(entity_id) as ByIdProxy<ENTITY_ID>;
122
+ let loaded = false;
123
+
124
+ function keys() {
125
+ const entityDomain = domain(entity_id);
126
+ return [
127
+ "attributes",
128
+ "entity_id",
129
+ "history",
130
+ "last",
131
+ "nextState",
132
+ "once",
133
+ "onUpdate",
134
+ "previous",
135
+ "removeAllListeners",
136
+ "state",
137
+ "waitForState",
138
+ ...hass.configure
139
+ .getServices()
140
+ .filter(({ domain }) => domain === entityDomain)
141
+ .flatMap(i => Object.keys(i.services))
142
+ .sort((a, b) => (a > b ? UP : DOWN)),
143
+ ];
144
+ }
145
+
146
+ function appendKeys(force = false) {
147
+ if (loaded && !force) {
148
+ return;
93
149
  }
94
- function appendKeys(force = false) {
95
- if (loaded && !force) {
96
- return;
97
- }
98
- // Not gonna build types for this, and ts-expect-error fails in jest
99
- // This is a weird hack for an obscure feature, so sue me
100
- //
101
- // eslint-disable-next-line @typescript-eslint/ban-ts-comment
102
- // @ts-ignore
103
- keys().forEach(i => (thing[i] ??= () => {}));
104
- if (!is.empty(hass.configure.getServices())) {
105
- loaded = true;
106
- }
150
+ // Not gonna build types for this, and ts-expect-error fails in jest
151
+ // This is a weird hack for an obscure feature, so sue me
152
+ //
153
+ // eslint-disable-next-line @typescript-eslint/ban-ts-comment
154
+ // @ts-ignore
155
+ keys().forEach(i => (thing[i] ??= () => {}));
156
+ if (!is.empty(hass.configure.getServices())) {
157
+ loaded = true;
107
158
  }
108
- event.on(SERVICE_LIST_UPDATED, () => appendKeys(true));
109
- ENTITY_PROXIES.set(
110
- entity_id,
111
- // just because you can't do generics properly....
112
- new Proxy(thing, {
113
- // things that shouldn't be needed: this extract
114
- // eslint-disable-next-line sonarjs/function-return-type
115
- get: (_, property: Extract<keyof ByIdProxy<ENTITY_ID>, string>) => {
116
- switch (property) {
117
- // * onUpdate
118
- case "onUpdate": {
119
- return (callback: TAnyFunction) => {
120
- const removableCallback = async (
121
- a: ENTITY_STATE<ENTITY_ID>,
122
- b: ENTITY_STATE<ENTITY_ID>,
123
- ) => await internal.safeExec(async () => callback(a, b, remove));
124
- function remove() {
125
- event.removeListener(entity_id, removableCallback);
126
- }
159
+ }
127
160
 
128
- event.on(entity_id, removableCallback);
129
- return { remove };
130
- };
131
- }
161
+ event.on(SERVICE_LIST_UPDATED, () => appendKeys(true));
162
+ const listeners = new Set<() => void>();
132
163
 
133
- // * removeAllListeners
134
- case "removeAllListeners": {
135
- return function () {
136
- event.removeAllListeners(entity_id);
137
- };
164
+ // just because you can't do generics properly....
165
+ return new Proxy(thing, {
166
+ // things that shouldn't be needed: this extract
167
+ // eslint-disable-next-line sonarjs/function-return-type
168
+ get: (_, property: Extract<keyof ByIdProxy<ENTITY_ID>, string>) => {
169
+ switch (property) {
170
+ // #MARK: onUpdate
171
+ case "onUpdate": {
172
+ return (callback: TAnyFunction) => {
173
+ const removableCallback = async (
174
+ new_state: ENTITY_STATE<ENTITY_ID>,
175
+ old_state: ENTITY_STATE<ENTITY_ID>,
176
+ ) => await internal.safeExec(async () => callback(new_state, old_state, remove));
177
+ function remove() {
178
+ event.removeListener(entity_id, removableCallback);
179
+ event.removeListener(SOCKET_CONNECTED, removableCallback);
180
+ listeners.delete(remove);
181
+ logger.trace({ entity_id }, "remove [onUpdate] listener");
138
182
  }
139
183
 
140
- // * history
141
- case "history": {
142
- return async function (from: Dayjs | Date, to: Dayjs | Date) {
143
- return await hass.fetch.fetchEntityHistory(entity_id, from, to);
144
- };
145
- }
184
+ event.on(entity_id, removableCallback);
185
+ event.on(SOCKET_CONNECTED, removableCallback);
186
+ listeners.add(remove);
146
187
 
147
- // * once
148
- case "once": {
149
- return (callback: TAnyFunction) =>
150
- event.once(entity_id, async (a, b) => callback(a, b));
151
- }
188
+ return is.removeFn(remove);
189
+ };
190
+ }
152
191
 
153
- // * entity_id
154
- case "entity_id": {
155
- return entity_id;
156
- }
192
+ // #MARK: addListener
193
+ case "addListener": {
194
+ return function (remove: RemoveCallback) {
195
+ listeners.add(remove);
196
+ };
197
+ }
157
198
 
158
- // * previous
159
- case "previous": {
160
- return hass.entity.previousState(entity_id);
161
- }
199
+ // #MARK: removeAllListeners
200
+ case "removeAllListeners": {
201
+ return function () {
202
+ // remove will delete from set
203
+ listeners.forEach(remove => remove());
204
+ };
205
+ }
162
206
 
163
- // * nextState
164
- case "nextState": {
165
- return async (timeout?: number) =>
166
- await new Promise<ENTITY_STATE<ENTITY_ID>>(async done => {
167
- const complete = (entity: ENTITY_STATE<ENTITY_ID>) => {
168
- if (done) {
169
- done(entity satisfies ENTITY_STATE<ENTITY_ID>);
170
- done = undefined;
171
- }
172
- };
173
- event.once(entity_id, complete);
174
- if (is.number(timeout) && timeout > NONE) {
175
- await sleep(timeout);
176
- if (done) {
177
- logger.debug({ entity_id, name: "nextState", timeout }, "timed out");
178
- done(undefined);
179
- done = undefined;
180
- event.removeListener(entity_id, complete);
181
- }
182
- }
183
- });
184
- }
207
+ // #MARK: history
208
+ case "history": {
209
+ return async function (from: Dayjs | Date, to: Dayjs | Date) {
210
+ return await hass.fetch.fetchEntityHistory(entity_id, from, to);
211
+ };
212
+ }
185
213
 
186
- // * waitForState
187
- case "waitForState": {
188
- return async (state: string | number, timeout?: number) =>
189
- await new Promise<ENTITY_STATE<ENTITY_ID>>(async done => {
190
- const complete = (entity: ENTITY_STATE<ENTITY_ID>) => {
191
- if (entity.state !== state) {
192
- logger.trace(
193
- {
194
- expected: state,
195
- incoming: entity.state,
196
- name: "waitForState",
197
- },
198
- `state did not match`,
199
- );
200
- return;
201
- }
202
- if (done) {
203
- done(entity satisfies ENTITY_STATE<ENTITY_ID>);
204
- done = undefined;
205
- event.removeListener(entity_id, complete);
206
- }
207
- };
208
- event.on(entity_id, complete);
209
- if (is.number(timeout) && timeout > NONE) {
210
- await sleep(timeout);
211
- if (done) {
212
- logger.debug({ entity_id, name: "waitForState", timeout }, "timed out");
213
- done(undefined);
214
- done = undefined;
215
- event.removeListener(entity_id, complete);
216
- }
217
- }
218
- });
219
- }
220
- }
221
- if (hass.configure.isService(entity_domain, property)) {
222
- return async function (data = {}) {
223
- // @ts-expect-error it's fine
224
- return await hass.call[entity_domain][property]({
225
- entity_id,
226
- ...data,
227
- });
214
+ // #MARK: once
215
+ case "once": {
216
+ return (callback: TAnyFunction) => {
217
+ const remove = () => {
218
+ event.removeListener(entity_id, wrapped);
219
+ listeners.delete(remove);
220
+ logger.trace({ entity_id }, "remove [once] listener");
221
+ };
222
+ const wrapped = async (a: ENTITY_STATE<ENTITY_ID>, b: ENTITY_STATE<ENTITY_ID>) => {
223
+ listeners.delete(remove);
224
+ callback(a, b);
228
225
  };
229
- }
230
- return proxyGetLogic(entity_id, property);
231
- },
232
- has(_, property: string) {
233
- appendKeys();
234
- return property in thing;
235
- },
236
- ownKeys() {
237
- appendKeys();
238
- return Object.keys(thing);
239
- },
240
- set(_, property: Extract<keyof ByIdProxy<ENTITY_ID>, string>, value: unknown) {
241
- // * state
242
- if (property === "state") {
243
- setImmediate(async () => {
244
- logger.debug({ entity_id, state: value }, `emitting set state via rest`);
245
- await hass.fetch.updateEntity(entity_id, {
246
- state: value as string | number,
247
- });
226
+ listeners.add(remove);
227
+ event.once(entity_id, wrapped);
228
+ return is.removeFn(remove);
229
+ };
230
+ }
231
+
232
+ // #MARK: entity_id
233
+ case "entity_id": {
234
+ return entity_id;
235
+ }
236
+
237
+ // #MARK: previous
238
+ case "previous": {
239
+ return hass.entity.previousState(entity_id);
240
+ }
241
+
242
+ // #MARK: nextState
243
+ case "nextState": {
244
+ return async (timeout?: number) =>
245
+ await new Promise<ENTITY_STATE<ENTITY_ID>>(async done => {
246
+ // - set up cleanup function
247
+ const remove = () => {
248
+ listeners.delete(remove);
249
+ event.removeListener(entity_id, complete);
250
+ done = undefined;
251
+ logger.trace({ entity_id }, "remove [nextState] listener");
252
+ if (wait) {
253
+ logger.trace({ entity_id }, "stopping [nextState] race timer");
254
+ wait.kill("stop");
255
+ }
256
+ };
257
+ listeners.add(remove);
258
+
259
+ // - add wrapper & make friendly with race
260
+ const complete = (entity: ENTITY_STATE<ENTITY_ID>) => {
261
+ if (done) {
262
+ done(entity satisfies ENTITY_STATE<ENTITY_ID>);
263
+ done = undefined;
264
+ }
265
+ };
266
+
267
+ // - attach single run listener
268
+ event.once(entity_id, complete);
269
+
270
+ // - race!
271
+ let wait: ReturnType<typeof sleep>;
272
+ if (is.number(timeout) && timeout > NONE) {
273
+ // keep track of sleep so it can be cleaned up also
274
+ wait = sleep(timeout);
275
+ await wait;
276
+ wait = undefined;
277
+ if (done) {
278
+ logger.debug({ entity_id, name: "nextState", timeout }, "timed out");
279
+ done(undefined);
280
+ remove();
281
+ }
282
+ }
248
283
  });
249
- return true;
250
- }
251
- // * attributes
252
- if (property === "attributes") {
253
- if (!is.object(value)) {
254
- logger.error(`can only provide objects as attributes`);
255
- return false;
256
- }
257
- setImmediate(async () => {
258
- logger.debug(
259
- { attributes: Object.keys(value), entity_id },
260
- `updating attributes via rest`,
261
- );
262
- await hass.fetch.updateEntity(entity_id, {
263
- attributes: value,
264
- });
284
+ }
285
+
286
+ // #MARK: waitForState
287
+ case "waitForState": {
288
+ return async (state: string | number, timeout?: number) =>
289
+ await new Promise<ENTITY_STATE<ENTITY_ID>>(async done => {
290
+ const remove = () => {
291
+ done = undefined;
292
+ listeners.delete(remove);
293
+ done = undefined;
294
+ logger.trace({ entity_id }, "remove [waitForState] listener");
295
+ if (wait) {
296
+ logger.trace({ entity_id }, "stopping [waitForState] race timer");
297
+
298
+ wait.kill("stop");
299
+ }
300
+ event.removeListener(entity_id, complete);
301
+ };
302
+ listeners.add(remove);
303
+
304
+ const complete = (entity: ENTITY_STATE<ENTITY_ID>) => {
305
+ if (entity.state !== state) {
306
+ logger.trace(
307
+ {
308
+ expected: state,
309
+ incoming: entity.state,
310
+ name: "waitForState",
311
+ },
312
+ `state did not match`,
313
+ );
314
+ return;
315
+ }
316
+ if (done) {
317
+ done(entity satisfies ENTITY_STATE<ENTITY_ID>);
318
+ remove();
319
+ }
320
+ };
321
+
322
+ event.on(entity_id, complete);
323
+ let wait: ReturnType<typeof sleep>;
324
+ if (is.number(timeout) && timeout > NONE) {
325
+ wait = sleep(timeout);
326
+ await wait;
327
+ wait = undefined;
328
+ if (done) {
329
+ logger.debug({ entity_id, name: "waitForState", timeout }, "timed out");
330
+ done(undefined);
331
+ remove();
332
+ }
333
+ }
265
334
  });
266
- return true;
267
- }
268
- logger.error({ entity_id, property }, `cannot set property on entity`);
335
+ }
336
+ }
337
+ // #MARK: service calls
338
+ if (hass.configure.isService(entity_domain, property)) {
339
+ return async function (data = {}) {
340
+ // @ts-expect-error it's fine
341
+ return await hass.call[entity_domain][property]({
342
+ entity_id,
343
+ ...data,
344
+ });
345
+ };
346
+ }
347
+ return proxyGetLogic(entity_id, property);
348
+ },
349
+ // #MARK: has
350
+ has(_, property: string) {
351
+ appendKeys();
352
+ return property in thing;
353
+ },
354
+ // #MARK: ownKeys
355
+ ownKeys() {
356
+ appendKeys();
357
+ return Object.keys(thing);
358
+ },
359
+ // #MARK: set
360
+ set(_, property: Extract<keyof ByIdProxy<ENTITY_ID>, string>, value: unknown) {
361
+ // * state
362
+ if (property === "state") {
363
+ setImmediate(async () => {
364
+ logger.debug({ entity_id, state: value }, `emitting set state via rest`);
365
+ await hass.fetch.updateEntity(entity_id, {
366
+ state: value as string | number,
367
+ });
368
+ });
369
+ return true;
370
+ }
371
+ // * attributes
372
+ if (property === "attributes") {
373
+ if (!is.object(value)) {
374
+ logger.error(`can only provide objects as attributes`);
269
375
  return false;
270
- },
271
- }),
272
- );
273
- }
274
- return ENTITY_PROXIES.get(entity_id) as ByIdProxy<ENTITY_ID>;
376
+ }
377
+ setImmediate(async () => {
378
+ logger.debug(
379
+ { attributes: Object.keys(value), entity_id },
380
+ `updating attributes via rest`,
381
+ );
382
+ await hass.fetch.updateEntity(entity_id, {
383
+ attributes: value,
384
+ });
385
+ });
386
+ return true;
387
+ }
388
+ logger.error({ entity_id, property }, `cannot set property on entity`);
389
+ return false;
390
+ },
391
+ });
275
392
  }
276
393
 
394
+ // #MARK: <return>
277
395
  return {
278
396
  area: <AREA extends TAreaId, DOMAINS extends TRawDomains = TRawDomains>(
279
397
  area: AREA,
280
398
  ...domains: DOMAINS[]
281
399
  ): ByIdProxy<PICK_FROM_AREA<AREA, DOMAINS>>[] =>
282
- hass.idBy.area<AREA, DOMAINS>(area, ...domains).map(id => byId(id)),
400
+ hass.idBy.area<AREA, DOMAINS>(area, ...domains).map(id => hass.refBy.id(id)),
283
401
 
284
402
  device: <DEVICE extends TDeviceId, DOMAINS extends TRawDomains = TRawDomains>(
285
403
  device: DEVICE,
286
404
  ...domains: DOMAINS[]
287
405
  ): ByIdProxy<PICK_FROM_DEVICE<DEVICE, DOMAINS>>[] =>
288
- hass.idBy.device<DEVICE, DOMAINS>(device, ...domains).map(id => byId(id)),
406
+ hass.idBy.device<DEVICE, DOMAINS>(device, ...domains).map(id => hass.refBy.id(id)),
289
407
 
290
408
  domain: <DOMAIN extends TRawDomains = TRawDomains>(
291
409
  domain: DOMAIN,
292
- ): ByIdProxy<PICK_ENTITY<DOMAIN>>[] => hass.idBy.domain<DOMAIN>(domain).map(id => byId(id)),
410
+ ): ByIdProxy<PICK_ENTITY<DOMAIN>>[] =>
411
+ hass.idBy.domain<DOMAIN>(domain).map(id => hass.refBy.id(id)),
293
412
 
294
413
  floor: <FLOOR extends TFloorId, DOMAINS extends TRawDomains = TRawDomains>(
295
414
  floor: FLOOR,
296
415
  ...domains: DOMAINS[]
297
416
  ): ByIdProxy<PICK_FROM_FLOOR<FLOOR, DOMAINS>>[] =>
298
- hass.idBy.floor<FLOOR, DOMAINS>(floor, ...domains).map(id => byId(id)),
417
+ hass.idBy.floor<FLOOR, DOMAINS>(floor, ...domains).map(id => hass.refBy.id(id)),
299
418
 
300
419
  id: byId,
301
420
 
@@ -303,13 +422,13 @@ export function ReferenceExtension({
303
422
  label: LABEL,
304
423
  ...domains: DOMAINS[]
305
424
  ): ByIdProxy<PICK_FROM_LABEL<LABEL, DOMAINS>>[] =>
306
- hass.idBy.label<LABEL, DOMAINS>(label, ...domains).map(id => byId(id)),
425
+ hass.idBy.label<LABEL, DOMAINS>(label, ...domains).map(id => hass.refBy.id(id)),
307
426
 
308
427
  platform: <PLATFORM extends TPlatformId, DOMAINS extends TRawDomains = TRawDomains>(
309
428
  platform: PLATFORM,
310
429
  ...domains: DOMAINS[]
311
430
  ): ByIdProxy<PICK_FROM_PLATFORM<PLATFORM, DOMAINS>>[] =>
312
- hass.idBy.platform<PLATFORM, DOMAINS>(platform, ...domains).map(id => byId(id)),
431
+ hass.idBy.platform<PLATFORM, DOMAINS>(platform, ...domains).map(id => hass.refBy.id(id)),
313
432
 
314
433
  unique_id: <
315
434
  UNIQUE_ID extends TUniqueId,
@@ -322,9 +441,12 @@ export function ReferenceExtension({
322
441
  ): ByIdProxy<ENTITY_ID> => {
323
442
  const id = hass.idBy.unique_id<UNIQUE_ID, ENTITY_ID>(unique_id);
324
443
  if (!id) {
444
+ // mental note:
445
+ // this is technically fixable, but would require emitting internal events
446
+ // for both entity_id & unique_id
325
447
  return undefined;
326
448
  }
327
- return byId(id);
449
+ return hass.refBy.id(id);
328
450
  },
329
451
  };
330
452
  }
@@ -493,10 +493,10 @@ export function WebsocketAPI({
493
493
  } else {
494
494
  socketEvents.on(event, callback);
495
495
  }
496
- return () => {
496
+ return is.removeFn(() => {
497
497
  logger.trace({ context, event, name: onEvent }, `removing socket event listener`);
498
498
  socketEvents.removeListener(event, callback);
499
- };
499
+ });
500
500
  }
501
501
 
502
502
  // #MARK: subscribe
@@ -506,7 +506,7 @@ export function WebsocketAPI({
506
506
  exec,
507
507
  }: SocketSubscribeOptions<EVENT>) {
508
508
  await hass.socket.sendMessage({ event_type, type: "subscribe_events" });
509
- hass.socket.onEvent({
509
+ return hass.socket.onEvent({
510
510
  context,
511
511
  event: event_type,
512
512
  exec,
@@ -527,6 +527,7 @@ export function WebsocketAPI({
527
527
  // attach anyways, for restarts or whatever
528
528
  }
529
529
  event.on(SOCKET_CONNECTED, wrapped);
530
+ return is.removeFn(() => event.removeListener(SOCKET_CONNECTED, wrapped));
530
531
  }
531
532
 
532
533
  // #MARK: return object
@@ -7,13 +7,13 @@ import {
7
7
  Configure,
8
8
  Device,
9
9
  EntityManager,
10
- Events,
10
+ EventsService,
11
11
  FetchAPI,
12
12
  FetchInternals,
13
13
  Floor,
14
14
  IDByExtension,
15
15
  Label,
16
- ReferenceExtension,
16
+ ReferenceService,
17
17
  Registry,
18
18
  WebsocketAPI,
19
19
  Zone,
@@ -158,7 +158,7 @@ export const LIB_HASS = CreateLibrary({
158
158
  /**
159
159
  * named event attachments
160
160
  */
161
- events: Events,
161
+ events: EventsService,
162
162
 
163
163
  /**
164
164
  * rest api commands
@@ -188,7 +188,7 @@ export const LIB_HASS = CreateLibrary({
188
188
  /**
189
189
  * obtain references to entities
190
190
  */
191
- refBy: ReferenceExtension,
191
+ refBy: ReferenceService,
192
192
 
193
193
  /**
194
194
  * interact with the home assistant registry