@digital-alchemy/hass 24.9.5 → 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 (45) hide show
  1. package/dist/dynamic.d.ts +3 -1
  2. package/dist/extensions/call-proxy.extension.d.ts +1 -1
  3. package/dist/extensions/call-proxy.extension.js +4 -1
  4. package/dist/extensions/call-proxy.extension.js.map +1 -1
  5. package/dist/extensions/entity.extension.js +3 -0
  6. package/dist/extensions/entity.extension.js.map +1 -1
  7. package/dist/extensions/events.extension.d.ts +1 -1
  8. package/dist/extensions/events.extension.js +26 -7
  9. package/dist/extensions/events.extension.js.map +1 -1
  10. package/dist/extensions/internal.extension.js +1 -1
  11. package/dist/extensions/internal.extension.js.map +1 -1
  12. package/dist/extensions/reference.extension.d.ts +51 -1
  13. package/dist/extensions/reference.extension.js +287 -181
  14. package/dist/extensions/reference.extension.js.map +1 -1
  15. package/dist/extensions/websocket-api.extension.js +9 -14
  16. package/dist/extensions/websocket-api.extension.js.map +1 -1
  17. package/dist/hass.module.d.ts +3 -3
  18. package/dist/hass.module.js +3 -3
  19. package/dist/hass.module.js.map +1 -1
  20. package/dist/helpers/entity-state.helper.d.ts +3 -5
  21. package/dist/helpers/interfaces.helper.d.ts +10 -10
  22. package/dist/helpers/interfaces.helper.js.map +1 -1
  23. package/dist/helpers/utility.helper.d.ts +5 -0
  24. package/dist/helpers/utility.helper.js +5 -0
  25. package/dist/helpers/utility.helper.js.map +1 -1
  26. package/dist/mock_assistant/mock-assistant.module.d.ts +4 -4
  27. package/dist/mock_assistant/mock-assistant.module.js +7 -0
  28. package/dist/mock_assistant/mock-assistant.module.js.map +1 -1
  29. package/dist/testing/fetch-api.spec.js +13 -13
  30. package/dist/testing/fetch-api.spec.js.map +1 -1
  31. package/package.json +27 -26
  32. package/scripts/test.sh +1 -1
  33. package/src/dynamic.ts +82 -77
  34. package/src/extensions/call-proxy.extension.ts +10 -1
  35. package/src/extensions/entity.extension.ts +3 -0
  36. package/src/extensions/events.extension.ts +26 -10
  37. package/src/extensions/internal.extension.ts +1 -1
  38. package/src/extensions/reference.extension.ts +314 -200
  39. package/src/extensions/websocket-api.extension.ts +12 -14
  40. package/src/hass.module.ts +4 -4
  41. package/src/helpers/entity-state.helper.ts +3 -4
  42. package/src/helpers/interfaces.helper.ts +10 -9
  43. package/src/helpers/utility.helper.ts +8 -0
  44. package/src/mock_assistant/mock-assistant.module.ts +7 -0
  45. 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
  }
@@ -175,17 +175,14 @@ export function WebsocketAPI({
175
175
  // #MARK: attachScheduledFunctions
176
176
  function attachScheduledFunctions() {
177
177
  logger.trace({ name: attachScheduledFunctions }, `attaching interval schedules`);
178
- scheduler.interval({
179
- exec: async () => await manageConnection(),
180
- interval: config.hass.RETRY_INTERVAL * SECOND,
181
- });
182
- scheduler.interval({
183
- exec: () => {
184
- const target = Date.now() - SECOND * config.hass.SOCKET_AVG_DURATION;
185
- MESSAGE_TIMESTAMPS = MESSAGE_TIMESTAMPS.filter(time => time > target);
186
- },
187
- interval: CLEANUP_INTERVAL * SECOND,
188
- });
178
+ scheduler.setInterval(
179
+ async () => await manageConnection(),
180
+ config.hass.RETRY_INTERVAL * SECOND,
181
+ );
182
+ scheduler.setInterval(() => {
183
+ const target = Date.now() - SECOND * config.hass.SOCKET_AVG_DURATION;
184
+ MESSAGE_TIMESTAMPS = MESSAGE_TIMESTAMPS.filter(time => time > target);
185
+ }, CLEANUP_INTERVAL * SECOND);
189
186
  }
190
187
 
191
188
  lifecycle.onShutdownStart(async () => {
@@ -496,10 +493,10 @@ export function WebsocketAPI({
496
493
  } else {
497
494
  socketEvents.on(event, callback);
498
495
  }
499
- return () => {
496
+ return is.removeFn(() => {
500
497
  logger.trace({ context, event, name: onEvent }, `removing socket event listener`);
501
498
  socketEvents.removeListener(event, callback);
502
- };
499
+ });
503
500
  }
504
501
 
505
502
  // #MARK: subscribe
@@ -509,7 +506,7 @@ export function WebsocketAPI({
509
506
  exec,
510
507
  }: SocketSubscribeOptions<EVENT>) {
511
508
  await hass.socket.sendMessage({ event_type, type: "subscribe_events" });
512
- hass.socket.onEvent({
509
+ return hass.socket.onEvent({
513
510
  context,
514
511
  event: event_type,
515
512
  exec,
@@ -530,6 +527,7 @@ export function WebsocketAPI({
530
527
  // attach anyways, for restarts or whatever
531
528
  }
532
529
  event.on(SOCKET_CONNECTED, wrapped);
530
+ return is.removeFn(() => event.removeListener(SOCKET_CONNECTED, wrapped));
533
531
  }
534
532
 
535
533
  // #MARK: return object