@digital-alchemy/hass 24.10.1 → 24.10.2

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 +287 -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 +3 -5
  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 -26
  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 +314 -200
  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 +3 -4
  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,
@@ -28,13 +28,62 @@ import {
28
28
  PICK_FROM_PLATFORM,
29
29
  } from "../helpers";
30
30
 
31
- export function ReferenceExtension({
31
+ /**
32
+ * ## Overview
33
+ *
34
+ * This service is intended for the creation of type safe entity references based on entity id.
35
+ *
36
+ * ```typescript
37
+ * const ref = hass.refBy.id("switch.example");
38
+ * ```
39
+ *
40
+ * These contain specialized type definitions that gets looked up by entity id, making typescript report info about the desired entity.
41
+ *
42
+ * ## Capabilities
43
+ *
44
+ * ### Event Listeners
45
+ *
46
+ * Run a callback in response to
47
+ * - `.onUpdate((new_state, old_state) => ...)`
48
+ * - `.once((new_state, old_state) => ...)`
49
+ *
50
+ * ### State lookups
51
+ *
52
+ * - `.nextState(timeout?)`
53
+ * - `.waitForState(state, timeout?)`
54
+ * - `.previous.state` | `.previous.attributes`
55
+ *
56
+ * ### Service Calling
57
+ *
58
+ * For services that appear on the same domain as the provided entity, the service call can be made directly from the reference.
59
+ *
60
+ * > Ex: `switch.example` calling `switch.turn_on`
61
+ *
62
+ * ```typescript
63
+ * ref.turn_on();
64
+ * ```
65
+ *
66
+ * The `entity_id` property that would normally be required is automatically provided by the proxy.
67
+ *
68
+ * ### Property Lookups
69
+ *
70
+ * - `.state`
71
+ * - `.entity_id`
72
+ * - `.attributes`
73
+ *
74
+ * ## Garbage Collection
75
+ *
76
+ * - `.removeAllListeners()`
77
+ *
78
+ * The reference may call the remove all active event listeners and timers at any time.
79
+ * It will interrupt any timers (nextState/waitForState), as well as detach any event listeners
80
+ */
81
+ export function ReferenceService({
32
82
  hass,
33
83
  logger,
34
84
  internal,
35
85
  event,
36
86
  }: TServiceParams): HassReferenceService {
37
- const ENTITY_PROXIES = new Map<ANY_ENTITY, ByIdProxy<ANY_ENTITY>>();
38
87
  // #MARK:proxyGetLogic
39
88
  function proxyGetLogic<ENTITY extends ANY_ENTITY = ANY_ENTITY, PROPERTY extends string = string>(
40
89
  entity: ENTITY,
@@ -64,238 +113,300 @@ export function ReferenceExtension({
64
113
  }
65
114
 
66
115
  // #MARK: byId
116
+ // ! Calls to this function MUST ALWAYS go through `hass.refBy.id`
117
+ // never call this function directly 💧
67
118
  function byId<ENTITY_ID extends ANY_ENTITY>(entity_id: ENTITY_ID): ByIdProxy<ENTITY_ID> {
68
119
  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;
120
+ const { ...thing } = hass.entity.getCurrentState(entity_id) as ByIdProxy<ENTITY_ID>;
121
+ let loaded = false;
72
122
 
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
- ];
123
+ function keys() {
124
+ const entityDomain = domain(entity_id);
125
+ return [
126
+ "attributes",
127
+ "entity_id",
128
+ "history",
129
+ "last",
130
+ "nextState",
131
+ "once",
132
+ "onUpdate",
133
+ "previous",
134
+ "removeAllListeners",
135
+ "state",
136
+ "waitForState",
137
+ ...hass.configure
138
+ .getServices()
139
+ .filter(({ domain }) => domain === entityDomain)
140
+ .flatMap(i => Object.keys(i.services))
141
+ .sort((a, b) => (a > b ? UP : DOWN)),
142
+ ];
143
+ }
144
+
145
+ function appendKeys(force = false) {
146
+ if (loaded && !force) {
147
+ return;
93
148
  }
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
- }
149
+ // Not gonna build types for this, and ts-expect-error fails in jest
150
+ // This is a weird hack for an obscure feature, so sue me
151
+ //
152
+ // eslint-disable-next-line @typescript-eslint/ban-ts-comment
153
+ // @ts-ignore
154
+ keys().forEach(i => (thing[i] ??= () => {}));
155
+ if (!is.empty(hass.configure.getServices())) {
156
+ loaded = true;
107
157
  }
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
- }
158
+ }
127
159
 
128
- event.on(entity_id, removableCallback);
129
- return { remove };
130
- };
160
+ event.on(SERVICE_LIST_UPDATED, () => appendKeys(true));
161
+ const listeners = new Set<() => void>();
162
+
163
+ // just because you can't do generics properly....
164
+ return new Proxy(thing, {
165
+ // things that shouldn't be needed: this extract
166
+ // eslint-disable-next-line sonarjs/function-return-type
167
+ get: (_, property: Extract<keyof ByIdProxy<ENTITY_ID>, string>) => {
168
+ switch (property) {
169
+ // #MARK: onUpdate
170
+ case "onUpdate": {
171
+ return (callback: TAnyFunction) => {
172
+ const removableCallback = async (
173
+ new_state: ENTITY_STATE<ENTITY_ID>,
174
+ old_state: ENTITY_STATE<ENTITY_ID>,
175
+ ) => await internal.safeExec(async () => callback(new_state, old_state, remove));
176
+ function remove() {
177
+ event.removeListener(entity_id, removableCallback);
178
+ event.removeListener(SOCKET_CONNECTED, removableCallback);
179
+ listeners.delete(remove);
180
+ logger.trace({ entity_id }, "remove [onUpdate] listener");
131
181
  }
132
182
 
133
- // * removeAllListeners
134
- case "removeAllListeners": {
135
- return function () {
136
- event.removeAllListeners(entity_id);
183
+ event.on(entity_id, removableCallback);
184
+ event.on(SOCKET_CONNECTED, removableCallback);
185
+ listeners.add(remove);
186
+
187
+ return is.removeFn(remove);
188
+ };
189
+ }
190
+
191
+ // #MARK: removeAllListeners
192
+ case "removeAllListeners": {
193
+ return function () {
194
+ // remove will delete from set
195
+ listeners.forEach(remove => remove());
196
+ };
197
+ }
198
+
199
+ // #MARK: history
200
+ case "history": {
201
+ return async function (from: Dayjs | Date, to: Dayjs | Date) {
202
+ return await hass.fetch.fetchEntityHistory(entity_id, from, to);
203
+ };
204
+ }
205
+
206
+ // #MARK: once
207
+ case "once": {
208
+ return (callback: TAnyFunction) => {
209
+ const remove = () => {
210
+ event.removeListener(entity_id, wrapped);
211
+ listeners.delete(remove);
212
+ logger.trace({ entity_id }, "remove [once] listener");
213
+ };
214
+ const wrapped = async (a: ENTITY_STATE<ENTITY_ID>, b: ENTITY_STATE<ENTITY_ID>) => {
215
+ listeners.delete(remove);
216
+ callback(a, b);
217
+ };
218
+ listeners.add(remove);
219
+ event.once(entity_id, wrapped);
220
+ return is.removeFn(remove);
221
+ };
222
+ }
223
+
224
+ // #MARK: entity_id
225
+ case "entity_id": {
226
+ return entity_id;
227
+ }
228
+
229
+ // #MARK: previous
230
+ case "previous": {
231
+ return hass.entity.previousState(entity_id);
232
+ }
233
+
234
+ // #MARK: nextState
235
+ case "nextState": {
236
+ return async (timeout?: number) =>
237
+ await new Promise<ENTITY_STATE<ENTITY_ID>>(async done => {
238
+ // - set up cleanup function
239
+ const remove = () => {
240
+ listeners.delete(remove);
241
+ event.removeListener(entity_id, complete);
242
+ done = undefined;
243
+ logger.trace({ entity_id }, "remove [nextState] listener");
244
+ if (wait) {
245
+ logger.trace({ entity_id }, "stopping [nextState] race timer");
246
+ wait.kill("stop");
247
+ }
137
248
  };
138
- }
249
+ listeners.add(remove);
139
250
 
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);
251
+ // - add wrapper & make friendly with race
252
+ const complete = (entity: ENTITY_STATE<ENTITY_ID>) => {
253
+ if (done) {
254
+ done(entity satisfies ENTITY_STATE<ENTITY_ID>);
255
+ done = undefined;
256
+ }
144
257
  };
145
- }
146
258
 
147
- // * once
148
- case "once": {
149
- return (callback: TAnyFunction) =>
150
- event.once(entity_id, async (a, b) => callback(a, b));
151
- }
259
+ // - attach single run listener
260
+ event.once(entity_id, complete);
152
261
 
153
- // * entity_id
154
- case "entity_id": {
155
- return entity_id;
156
- }
262
+ // - race!
263
+ let wait: ReturnType<typeof sleep>;
264
+ if (is.number(timeout) && timeout > NONE) {
265
+ // keep track of sleep so it can be cleaned up also
266
+ wait = sleep(timeout);
267
+ await wait;
268
+ wait = undefined;
269
+ if (done) {
270
+ logger.debug({ entity_id, name: "nextState", timeout }, "timed out");
271
+ done(undefined);
272
+ remove();
273
+ }
274
+ }
275
+ });
276
+ }
157
277
 
158
- // * previous
159
- case "previous": {
160
- return hass.entity.previousState(entity_id);
161
- }
278
+ // #MARK: waitForState
279
+ case "waitForState": {
280
+ return async (state: string | number, timeout?: number) =>
281
+ await new Promise<ENTITY_STATE<ENTITY_ID>>(async done => {
282
+ const remove = () => {
283
+ done = undefined;
284
+ listeners.delete(remove);
285
+ done = undefined;
286
+ logger.trace({ entity_id }, "remove [waitForState] listener");
287
+ if (wait) {
288
+ logger.trace({ entity_id }, "stopping [waitForState] race timer");
162
289
 
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
- }
290
+ wait.kill("stop");
291
+ }
292
+ event.removeListener(entity_id, complete);
293
+ };
294
+ listeners.add(remove);
185
295
 
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
- });
228
- };
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
- });
248
- });
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
- });
296
+ const complete = (entity: ENTITY_STATE<ENTITY_ID>) => {
297
+ if (entity.state !== state) {
298
+ logger.trace(
299
+ {
300
+ expected: state,
301
+ incoming: entity.state,
302
+ name: "waitForState",
303
+ },
304
+ `state did not match`,
305
+ );
306
+ return;
307
+ }
308
+ if (done) {
309
+ done(entity satisfies ENTITY_STATE<ENTITY_ID>);
310
+ remove();
311
+ }
312
+ };
313
+
314
+ event.on(entity_id, complete);
315
+ let wait: ReturnType<typeof sleep>;
316
+ if (is.number(timeout) && timeout > NONE) {
317
+ wait = sleep(timeout);
318
+ await wait;
319
+ wait = undefined;
320
+ if (done) {
321
+ logger.debug({ entity_id, name: "waitForState", timeout }, "timed out");
322
+ done(undefined);
323
+ remove();
324
+ }
325
+ }
265
326
  });
266
- return true;
267
- }
268
- logger.error({ entity_id, property }, `cannot set property on entity`);
327
+ }
328
+ }
329
+ // #MARK: service calls
330
+ if (hass.configure.isService(entity_domain, property)) {
331
+ return async function (data = {}) {
332
+ // @ts-expect-error it's fine
333
+ return await hass.call[entity_domain][property]({
334
+ entity_id,
335
+ ...data,
336
+ });
337
+ };
338
+ }
339
+ return proxyGetLogic(entity_id, property);
340
+ },
341
+ // #MARK: has
342
+ has(_, property: string) {
343
+ appendKeys();
344
+ return property in thing;
345
+ },
346
+ // #MARK: ownKeys
347
+ ownKeys() {
348
+ appendKeys();
349
+ return Object.keys(thing);
350
+ },
351
+ // #MARK: set
352
+ set(_, property: Extract<keyof ByIdProxy<ENTITY_ID>, string>, value: unknown) {
353
+ // * state
354
+ if (property === "state") {
355
+ setImmediate(async () => {
356
+ logger.debug({ entity_id, state: value }, `emitting set state via rest`);
357
+ await hass.fetch.updateEntity(entity_id, {
358
+ state: value as string | number,
359
+ });
360
+ });
361
+ return true;
362
+ }
363
+ // * attributes
364
+ if (property === "attributes") {
365
+ if (!is.object(value)) {
366
+ logger.error(`can only provide objects as attributes`);
269
367
  return false;
270
- },
271
- }),
272
- );
273
- }
274
- return ENTITY_PROXIES.get(entity_id) as ByIdProxy<ENTITY_ID>;
368
+ }
369
+ setImmediate(async () => {
370
+ logger.debug(
371
+ { attributes: Object.keys(value), entity_id },
372
+ `updating attributes via rest`,
373
+ );
374
+ await hass.fetch.updateEntity(entity_id, {
375
+ attributes: value,
376
+ });
377
+ });
378
+ return true;
379
+ }
380
+ logger.error({ entity_id, property }, `cannot set property on entity`);
381
+ return false;
382
+ },
383
+ });
275
384
  }
276
385
 
386
+ // #MARK: <return>
277
387
  return {
278
388
  area: <AREA extends TAreaId, DOMAINS extends TRawDomains = TRawDomains>(
279
389
  area: AREA,
280
390
  ...domains: DOMAINS[]
281
391
  ): ByIdProxy<PICK_FROM_AREA<AREA, DOMAINS>>[] =>
282
- hass.idBy.area<AREA, DOMAINS>(area, ...domains).map(id => byId(id)),
392
+ hass.idBy.area<AREA, DOMAINS>(area, ...domains).map(id => hass.refBy.id(id)),
283
393
 
284
394
  device: <DEVICE extends TDeviceId, DOMAINS extends TRawDomains = TRawDomains>(
285
395
  device: DEVICE,
286
396
  ...domains: DOMAINS[]
287
397
  ): ByIdProxy<PICK_FROM_DEVICE<DEVICE, DOMAINS>>[] =>
288
- hass.idBy.device<DEVICE, DOMAINS>(device, ...domains).map(id => byId(id)),
398
+ hass.idBy.device<DEVICE, DOMAINS>(device, ...domains).map(id => hass.refBy.id(id)),
289
399
 
290
400
  domain: <DOMAIN extends TRawDomains = TRawDomains>(
291
401
  domain: DOMAIN,
292
- ): ByIdProxy<PICK_ENTITY<DOMAIN>>[] => hass.idBy.domain<DOMAIN>(domain).map(id => byId(id)),
402
+ ): ByIdProxy<PICK_ENTITY<DOMAIN>>[] =>
403
+ hass.idBy.domain<DOMAIN>(domain).map(id => hass.refBy.id(id)),
293
404
 
294
405
  floor: <FLOOR extends TFloorId, DOMAINS extends TRawDomains = TRawDomains>(
295
406
  floor: FLOOR,
296
407
  ...domains: DOMAINS[]
297
408
  ): ByIdProxy<PICK_FROM_FLOOR<FLOOR, DOMAINS>>[] =>
298
- hass.idBy.floor<FLOOR, DOMAINS>(floor, ...domains).map(id => byId(id)),
409
+ hass.idBy.floor<FLOOR, DOMAINS>(floor, ...domains).map(id => hass.refBy.id(id)),
299
410
 
300
411
  id: byId,
301
412
 
@@ -303,13 +414,13 @@ export function ReferenceExtension({
303
414
  label: LABEL,
304
415
  ...domains: DOMAINS[]
305
416
  ): ByIdProxy<PICK_FROM_LABEL<LABEL, DOMAINS>>[] =>
306
- hass.idBy.label<LABEL, DOMAINS>(label, ...domains).map(id => byId(id)),
417
+ hass.idBy.label<LABEL, DOMAINS>(label, ...domains).map(id => hass.refBy.id(id)),
307
418
 
308
419
  platform: <PLATFORM extends TPlatformId, DOMAINS extends TRawDomains = TRawDomains>(
309
420
  platform: PLATFORM,
310
421
  ...domains: DOMAINS[]
311
422
  ): ByIdProxy<PICK_FROM_PLATFORM<PLATFORM, DOMAINS>>[] =>
312
- hass.idBy.platform<PLATFORM, DOMAINS>(platform, ...domains).map(id => byId(id)),
423
+ hass.idBy.platform<PLATFORM, DOMAINS>(platform, ...domains).map(id => hass.refBy.id(id)),
313
424
 
314
425
  unique_id: <
315
426
  UNIQUE_ID extends TUniqueId,
@@ -322,9 +433,12 @@ export function ReferenceExtension({
322
433
  ): ByIdProxy<ENTITY_ID> => {
323
434
  const id = hass.idBy.unique_id<UNIQUE_ID, ENTITY_ID>(unique_id);
324
435
  if (!id) {
436
+ // mental note:
437
+ // this is technically fixable, but would require emitting internal events
438
+ // for both entity_id & unique_id
325
439
  return undefined;
326
440
  }
327
- return byId(id);
441
+ return hass.refBy.id(id);
328
442
  },
329
443
  };
330
444
  }
@@ -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