@digital-alchemy/hass 25.8.21 → 25.10.19-beta.0

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 (150) hide show
  1. package/dist/dev/services.d.mts +1 -1
  2. package/dist/helpers/device.d.mts +1 -1
  3. package/dist/helpers/entity-state.d.mts +6 -6
  4. package/dist/helpers/fetch/calendar.d.mts +2 -2
  5. package/dist/helpers/fetch/configuration.d.mts +1 -1
  6. package/dist/helpers/fetch/service-list.d.mts +3 -3
  7. package/dist/helpers/fetch.d.mts +2 -2
  8. package/dist/helpers/fetch.mjs.map +1 -1
  9. package/dist/helpers/id-by.d.mts +1 -1
  10. package/dist/helpers/interfaces.d.mts +22 -13
  11. package/dist/helpers/interfaces.mjs.map +1 -1
  12. package/dist/helpers/registry.d.mts +1 -1
  13. package/dist/helpers/utility.d.mts +5 -5
  14. package/dist/helpers/utility.mjs.map +1 -1
  15. package/dist/helpers/websocket.d.mts +9 -8
  16. package/dist/merge.d.mts +1 -1
  17. package/dist/mock_assistant/helpers/fixtures.d.mts +2 -2
  18. package/dist/mock_assistant/main.mjs.map +1 -1
  19. package/dist/mock_assistant/services/area.service.d.mts +2 -2
  20. package/dist/mock_assistant/services/config.service.d.mts +2 -2
  21. package/dist/mock_assistant/services/config.service.mjs.map +1 -1
  22. package/dist/mock_assistant/services/device.service.d.mts +2 -2
  23. package/dist/mock_assistant/services/device.service.mjs.map +1 -1
  24. package/dist/mock_assistant/services/entity-registry.service.d.mts +3 -3
  25. package/dist/mock_assistant/services/entity.service.d.mts +3 -3
  26. package/dist/mock_assistant/services/entity.service.mjs.map +1 -1
  27. package/dist/mock_assistant/services/events.service.d.mts +3 -3
  28. package/dist/mock_assistant/services/events.service.mjs.map +1 -1
  29. package/dist/mock_assistant/services/fixtures.service.d.mts +4 -4
  30. package/dist/mock_assistant/services/fixtures.service.mjs.map +1 -1
  31. package/dist/mock_assistant/services/floor.service.d.mts +2 -2
  32. package/dist/mock_assistant/services/label.service.d.mts +2 -2
  33. package/dist/mock_assistant/services/services.service.d.mts +2 -2
  34. package/dist/mock_assistant/services/websocket-api.service.d.mts +4 -4
  35. package/dist/mock_assistant/services/websocket-api.service.mjs.map +1 -1
  36. package/dist/mock_assistant/services/zone.service.d.mts +2 -2
  37. package/dist/quickboot.module.d.mts +1 -1
  38. package/dist/quickboot.module.mjs.map +1 -1
  39. package/dist/services/area.service.d.mts +2 -2
  40. package/dist/services/area.service.mjs +1 -1
  41. package/dist/services/area.service.mjs.map +1 -1
  42. package/dist/services/backup.service.d.mts +2 -2
  43. package/dist/services/backup.service.mjs.map +1 -1
  44. package/dist/services/call-proxy.service.d.mts +2 -2
  45. package/dist/services/call-proxy.service.mjs +1 -1
  46. package/dist/services/call-proxy.service.mjs.map +1 -1
  47. package/dist/services/config.service.d.mts +2 -2
  48. package/dist/services/config.service.mjs +1 -2
  49. package/dist/services/config.service.mjs.map +1 -1
  50. package/dist/services/conversation.service.d.mts +2 -2
  51. package/dist/services/conversation.service.mjs +1 -1
  52. package/dist/services/conversation.service.mjs.map +1 -1
  53. package/dist/services/device.service.d.mts +2 -2
  54. package/dist/services/device.service.mjs +1 -1
  55. package/dist/services/device.service.mjs.map +1 -1
  56. package/dist/services/diagnostics.service.d.mts +1 -1
  57. package/dist/services/entity.service.d.mts +2 -2
  58. package/dist/services/entity.service.mjs +2 -2
  59. package/dist/services/entity.service.mjs.map +1 -1
  60. package/dist/services/events.service.d.mts +2 -2
  61. package/dist/services/events.service.mjs.map +1 -1
  62. package/dist/services/fetch-api.service.d.mts +5 -5
  63. package/dist/services/fetch-api.service.mjs +1 -1
  64. package/dist/services/fetch-api.service.mjs.map +1 -1
  65. package/dist/services/floor.service.d.mts +2 -2
  66. package/dist/services/floor.service.mjs +1 -1
  67. package/dist/services/floor.service.mjs.map +1 -1
  68. package/dist/services/id-by.service.d.mts +2 -2
  69. package/dist/services/id-by.service.mjs.map +1 -1
  70. package/dist/services/internal.service.d.mts +2 -2
  71. package/dist/services/internal.service.mjs +1 -1
  72. package/dist/services/internal.service.mjs.map +1 -1
  73. package/dist/services/label.service.d.mts +2 -2
  74. package/dist/services/label.service.mjs +1 -1
  75. package/dist/services/label.service.mjs.map +1 -1
  76. package/dist/services/reference.service.d.mts +2 -2
  77. package/dist/services/reference.service.mjs +1 -1
  78. package/dist/services/reference.service.mjs.map +1 -1
  79. package/dist/services/registry.service.d.mts +2 -2
  80. package/dist/services/websocket-api.service.d.mts +2 -2
  81. package/dist/services/websocket-api.service.mjs +53 -39
  82. package/dist/services/websocket-api.service.mjs.map +1 -1
  83. package/dist/services/zone.service.d.mts +2 -2
  84. package/dist/services/zone.service.mjs +1 -1
  85. package/dist/services/zone.service.mjs.map +1 -1
  86. package/dist/testing/area.spec.mjs.map +1 -1
  87. package/dist/testing/floor.spec.mjs.map +1 -1
  88. package/dist/testing/label.spec.mjs.map +1 -1
  89. package/dist/testing/websocket.spec.mjs +125 -0
  90. package/dist/testing/websocket.spec.mjs.map +1 -1
  91. package/dist/testing/zone.spec.mjs.map +1 -1
  92. package/package.json +28 -28
  93. package/src/dev/services.mts +1 -1
  94. package/src/helpers/device.mts +1 -1
  95. package/src/helpers/entity-state.mts +6 -6
  96. package/src/helpers/fetch/calendar.mts +2 -2
  97. package/src/helpers/fetch/configuration.mts +1 -1
  98. package/src/helpers/fetch/service-list.mts +3 -3
  99. package/src/helpers/fetch.mts +3 -2
  100. package/src/helpers/id-by.mts +1 -1
  101. package/src/helpers/interfaces.mts +22 -15
  102. package/src/helpers/registry.mts +1 -1
  103. package/src/helpers/utility.mts +6 -5
  104. package/src/helpers/websocket.mts +12 -8
  105. package/src/merge.mts +1 -1
  106. package/src/mock_assistant/helpers/fixtures.mts +2 -2
  107. package/src/mock_assistant/main.mts +3 -2
  108. package/src/mock_assistant/services/area.service.mts +3 -3
  109. package/src/mock_assistant/services/config.service.mts +3 -2
  110. package/src/mock_assistant/services/device.service.mts +4 -3
  111. package/src/mock_assistant/services/entity-registry.service.mts +3 -3
  112. package/src/mock_assistant/services/entity.service.mts +4 -3
  113. package/src/mock_assistant/services/events.service.mts +4 -3
  114. package/src/mock_assistant/services/fixtures.service.mts +5 -4
  115. package/src/mock_assistant/services/floor.service.mts +3 -3
  116. package/src/mock_assistant/services/label.service.mts +3 -3
  117. package/src/mock_assistant/services/services.service.mts +2 -2
  118. package/src/mock_assistant/services/websocket-api.service.mts +5 -4
  119. package/src/mock_assistant/services/zone.service.mts +3 -3
  120. package/src/quickboot.module.mts +2 -1
  121. package/src/services/area.service.mts +5 -11
  122. package/src/services/backup.service.mts +3 -2
  123. package/src/services/call-proxy.service.mts +4 -4
  124. package/src/services/config.service.mts +5 -8
  125. package/src/services/conversation.service.mts +3 -7
  126. package/src/services/device.service.mts +4 -8
  127. package/src/services/diagnostics.service.mts +1 -1
  128. package/src/services/entity.service.mts +7 -15
  129. package/src/services/events.service.mts +2 -3
  130. package/src/services/fetch-api.service.mts +8 -7
  131. package/src/services/floor.service.mts +5 -10
  132. package/src/services/id-by.service.mts +5 -3
  133. package/src/services/internal.service.mts +4 -4
  134. package/src/services/label.service.mts +5 -10
  135. package/src/services/reference.service.mts +8 -7
  136. package/src/services/registry.service.mts +2 -2
  137. package/src/services/websocket-api.service.mts +74 -60
  138. package/src/services/zone.service.mts +4 -10
  139. package/src/testing/area.spec.mts +3 -2
  140. package/src/testing/backup.spec.mts +1 -1
  141. package/src/testing/config.spec.mts +1 -1
  142. package/src/testing/device.spec.mts +1 -1
  143. package/src/testing/entity.spec.mts +2 -2
  144. package/src/testing/fetch-api.spec.mts +2 -2
  145. package/src/testing/floor.spec.mts +3 -2
  146. package/src/testing/id-by.spec.mts +1 -1
  147. package/src/testing/label.spec.mts +3 -2
  148. package/src/testing/ref-by.spec.mts +2 -2
  149. package/src/testing/websocket.spec.mts +156 -0
  150. package/src/testing/zone.spec.mts +2 -1
@@ -1,17 +1,18 @@
1
- import { DOWN, NONE, sleep, TAnyFunction, TServiceParams, UP } from "@digital-alchemy/core";
2
- import dayjs, { Dayjs } from "dayjs";
3
- import { Get } from "type-fest";
1
+ import type { TAnyFunction, TServiceParams } from "@digital-alchemy/core";
2
+ import { DOWN, NONE, sleep, UP } from "@digital-alchemy/core";
3
+ import type { Dayjs } from "dayjs";
4
+ import dayjs from "dayjs";
5
+ import type { Get } from "type-fest";
4
6
 
5
- import {
7
+ import type {
6
8
  ALL_SERVICE_DOMAINS,
7
9
  ByIdProxy,
8
- domain,
9
10
  ENTITY_STATE,
10
11
  HassReferenceService,
11
- perf,
12
12
  RemoveCallback,
13
13
  } from "../helpers/index.mts";
14
- import {
14
+ import { domain, perf } from "../helpers/index.mts";
15
+ import type {
15
16
  ANY_ENTITY,
16
17
  HassUniqueIdMapping,
17
18
  PICK_ENTITY,
@@ -1,6 +1,6 @@
1
- import { TServiceParams } from "@digital-alchemy/core";
1
+ import type { TServiceParams } from "@digital-alchemy/core";
2
2
 
3
- import {
3
+ import type {
4
4
  ConfigEntry,
5
5
  HassConfig,
6
6
  HassRegistryService,
@@ -1,16 +1,12 @@
1
- import {
2
- InternalError,
3
- SECOND,
4
- sleep,
5
- START,
6
- TBlackHole,
7
- TServiceParams,
8
- } from "@digital-alchemy/core";
9
- import dayjs, { Dayjs } from "dayjs";
1
+ import type { TBlackHole, TServiceParams } from "@digital-alchemy/core";
2
+ import { InternalError, SECOND, sleep, START } from "@digital-alchemy/core";
3
+ import type { Dayjs } from "dayjs";
4
+ import dayjs from "dayjs";
10
5
  import EventEmitter from "events";
6
+ import type { EmptyObject } from "type-fest";
11
7
  import WS from "ws";
12
8
 
13
- import {
9
+ import type {
14
10
  ConnectionState,
15
11
  EntityUpdateEvent,
16
12
  HassWebsocketAPI,
@@ -34,6 +30,8 @@ type WaitingMap = Map<
34
30
  }
35
31
  >;
36
32
 
33
+ type MessageHandlerMap = Map<string, Array<(message: { type: string }) => TBlackHole>>;
34
+
37
35
  export function WebsocketAPI({
38
36
  context,
39
37
  event,
@@ -55,6 +53,7 @@ export function WebsocketAPI({
55
53
  let MESSAGE_TIMESTAMPS: number[] = [];
56
54
  let onSocketReady: () => void;
57
55
  const waitingCallback: WaitingMap = new Map();
56
+ const messageHandlers: MessageHandlerMap = new Map();
58
57
  const isOld = (date: Dayjs) =>
59
58
  is.undefined(date) || date.diff(dayjs(), "s") >= config.hass.RETRY_INTERVAL;
60
59
 
@@ -293,6 +292,20 @@ export function WebsocketAPI({
293
292
  );
294
293
  }
295
294
 
295
+ // #MARK: registerMessageHandler
296
+ function registerMessageHandler<T extends { type: string }>(
297
+ type: string,
298
+ callback: (message: T) => TBlackHole,
299
+ ) {
300
+ const handlers = messageHandlers.get(type) ?? [];
301
+ logger.trace({ type }, "register socket message handler");
302
+ if (!messageHandlers.has(type)) {
303
+ messageHandlers.set(type, []);
304
+ }
305
+ handlers.push(callback as (message: { type: string }) => TBlackHole);
306
+ messageHandlers.set(type, handlers);
307
+ }
308
+
296
309
  // #MARK: countMessage
297
310
  function countMessage(): void | never {
298
311
  messageCount++;
@@ -403,54 +416,18 @@ export function WebsocketAPI({
403
416
  * ## result
404
417
  * Response to an outgoing emit
405
418
  */
406
- async function onMessage(message: SocketMessageDTO) {
407
- const id = Number(message.id);
419
+ async function onMessage<T extends { type: string }>(message: T) {
408
420
  setImmediate(() => hass.diagnostics.websocket?.message_received.publish({ message }));
409
- switch (message.type) {
410
- case "auth_required": {
411
- logger.trace({ name: onMessage }, `sending authentication`);
412
- void hass.socket.sendMessage({ access_token: config.hass.TOKEN, type: "auth" }, false);
413
- return;
414
- }
415
-
416
- case "auth_ok": {
417
- // * Flag as valid connection
418
- logger.trace({ name: onMessage }, `event subscriptions starting`);
419
- await hass.socket.sendMessage({ type: "subscribe_events" }, false);
420
- onSocketReady?.();
421
- event.emit(SOCKET_CONNECTED);
422
- return;
423
- }
424
421
 
425
- case "event": {
426
- return await onMessageEvent(id, message);
427
- }
428
-
429
- // 👾
430
- case "pong": {
431
- // nothing in particular needs to be done, just don't log an error (default)
432
- return;
433
- }
434
-
435
- case "result": {
436
- return await onMessageResult(id, message);
437
- }
438
-
439
- case "auth_invalid": {
440
- hass.socket.setConnectionState("invalid");
441
- logger.fatal(
442
- { message, name: onMessage },
443
- "received auth invalid {connecting} => {invalid}",
444
- );
445
- // ? If you have a use case for making this exit configurable, open a ticket
446
- process.exit();
447
- return;
448
- }
422
+ const handlers = messageHandlers.get(message.type);
423
+ if (is.empty(handlers)) {
424
+ logger.error(`unknown websocket message type: ${message.type}`);
425
+ return;
426
+ }
449
427
 
450
- default: {
451
- // Code error probably?
452
- logger.error({ name: onMessage }, `unknown websocket message type: ${message.type}`);
453
- }
428
+ // Execute all registered handlers for this message type
429
+ for (const handler of handlers) {
430
+ await handler(message as { type: string });
454
431
  }
455
432
  }
456
433
 
@@ -523,11 +500,10 @@ export function WebsocketAPI({
523
500
  }
524
501
 
525
502
  // #MARK: subscribe
526
- async function subscribe<EVENT extends string>({
527
- event_type,
528
- context,
529
- exec,
530
- }: SocketSubscribeOptions<EVENT>) {
503
+ async function subscribe<
504
+ EVENT extends string,
505
+ PAYLOAD extends Record<string, unknown> = EmptyObject,
506
+ >({ event_type, context, exec }: SocketSubscribeOptions<EVENT, PAYLOAD>) {
531
507
  await hass.socket.sendMessage({ event_type, type: "subscribe_events" });
532
508
  return hass.socket.onEvent({
533
509
  context,
@@ -553,6 +529,43 @@ export function WebsocketAPI({
553
529
  return internal.removeFn(() => event.removeListener(SOCKET_CONNECTED, wrapped));
554
530
  }
555
531
 
532
+ lifecycle.onPreInit(() => {
533
+ // Register all current message handlers
534
+ hass.socket.registerMessageHandler("auth_required", async () => {
535
+ logger.trace({ name: onMessage }, `sending authentication`);
536
+ void hass.socket.sendMessage({ access_token: config.hass.TOKEN, type: "auth" }, false);
537
+ });
538
+
539
+ hass.socket.registerMessageHandler("auth_ok", async () => {
540
+ // * Flag as valid connection
541
+ logger.trace({ name: onMessage }, `event subscriptions starting`);
542
+ await hass.socket.sendMessage({ type: "subscribe_events" }, false);
543
+ onSocketReady?.();
544
+ event.emit(SOCKET_CONNECTED);
545
+ });
546
+
547
+ hass.socket.registerMessageHandler("event", async (message: SocketMessageDTO) => {
548
+ const id = Number(message.id);
549
+ return await onMessageEvent(id, message);
550
+ });
551
+
552
+ hass.socket.registerMessageHandler("pong", async () => {
553
+ // nothing in particular needs to be done, just don't log an error (default)
554
+ });
555
+
556
+ hass.socket.registerMessageHandler("result", async (message: SocketMessageDTO) => {
557
+ const id = Number(message.id);
558
+ return await onMessageResult(id, message);
559
+ });
560
+
561
+ hass.socket.registerMessageHandler("auth_invalid", async (message: SocketMessageDTO) => {
562
+ hass.socket.setConnectionState("invalid");
563
+ logger.fatal({ message, name: onMessage }, "received auth invalid {connecting} => {invalid}");
564
+ // ? If you have a use case for making this exit configurable, open a ticket
565
+ process.exit();
566
+ });
567
+ });
568
+
556
569
  // #MARK: return object
557
570
  return {
558
571
  attachScheduledFunctions,
@@ -565,6 +578,7 @@ export function WebsocketAPI({
565
578
  onEvent,
566
579
  onMessage,
567
580
  pauseMessages: false,
581
+ registerMessageHandler,
568
582
  sendMessage,
569
583
  setConnectionState,
570
584
  socketEvents,
@@ -1,14 +1,8 @@
1
- import { debounce, TServiceParams } from "@digital-alchemy/core";
1
+ import type { TServiceParams } from "@digital-alchemy/core";
2
+ import { debounce } from "@digital-alchemy/core";
2
3
 
3
- import {
4
- EARLY_ON_READY,
5
- HassZoneService,
6
- ManifestItem,
7
- perf,
8
- ZONE_REGISTRY_UPDATED,
9
- ZoneDetails,
10
- ZoneOptions,
11
- } from "../helpers/index.mts";
4
+ import type { HassZoneService, ManifestItem, ZoneDetails, ZoneOptions } from "../helpers/index.mts";
5
+ import { EARLY_ON_READY, perf, ZONE_REGISTRY_UPDATED } from "../helpers/index.mts";
12
6
 
13
7
  export function Zone({
14
8
  config,
@@ -1,9 +1,10 @@
1
1
  import { sleep } from "@digital-alchemy/core";
2
2
  import { subscribe } from "diagnostics_channel";
3
3
 
4
- import { AREA_REGISTRY_UPDATED, AreaDetails } from "../helpers/index.mts";
4
+ import type { AreaDetails } from "../helpers/index.mts";
5
+ import { AREA_REGISTRY_UPDATED } from "../helpers/index.mts";
5
6
  import { hassTestRunner, INTERNAL_MESSAGE } from "../mock_assistant/index.mts";
6
- import { TAreaId } from "../user.mts";
7
+ import type { TAreaId } from "../user.mts";
7
8
 
8
9
  const EXAMPLE_AREA = {
9
10
  area_id: "empty_area" as TAreaId,
@@ -1,6 +1,6 @@
1
1
  import { SINGLE } from "@digital-alchemy/core";
2
2
 
3
- import { BackupResponse } from "../helpers/index.mts";
3
+ import type { BackupResponse } from "../helpers/index.mts";
4
4
  import { hassTestRunner } from "../mock_assistant/index.mts";
5
5
 
6
6
  describe("Backup", () => {
@@ -1,5 +1,5 @@
1
1
  import { env } from "process";
2
- import { MockInstance } from "vitest";
2
+ import type { MockInstance } from "vitest";
3
3
 
4
4
  import { hassTestRunner } from "../mock_assistant/index.mts";
5
5
 
@@ -2,7 +2,7 @@ import { subscribe } from "node:diagnostics_channel";
2
2
 
3
3
  import { sleep } from "@digital-alchemy/core";
4
4
 
5
- import { DeviceDetails } from "../helpers/index.mts";
5
+ import type { DeviceDetails } from "../helpers/index.mts";
6
6
  import { hassTestRunner } from "../mock_assistant/index.mts";
7
7
 
8
8
  describe("Device", () => {
@@ -3,9 +3,9 @@ import { subscribe } from "node:diagnostics_channel";
3
3
  import { sleep } from "@digital-alchemy/core";
4
4
  import dayjs from "dayjs";
5
5
 
6
- import { ENTITY_STATE } from "../index.mts";
6
+ import type { ENTITY_STATE } from "../index.mts";
7
7
  import { hassTestRunner } from "../mock_assistant/index.mts";
8
- import { ANY_ENTITY } from "../user.mts";
8
+ import type { ANY_ENTITY } from "../user.mts";
9
9
 
10
10
  describe("Entity", () => {
11
11
  afterEach(async () => {
@@ -1,7 +1,7 @@
1
1
  import dayjs from "dayjs";
2
- import { MockInstance } from "vitest";
2
+ import type { MockInstance } from "vitest";
3
3
 
4
- import { HassConfig } from "../helpers/index.mts";
4
+ import type { HassConfig } from "../helpers/index.mts";
5
5
  import { hassTestRunner } from "../mock_assistant/index.mts";
6
6
 
7
7
  describe("FetchAPI", () => {
@@ -2,9 +2,10 @@ import { subscribe } from "node:diagnostics_channel";
2
2
 
3
3
  import { sleep } from "@digital-alchemy/core";
4
4
 
5
- import { FLOOR_REGISTRY_UPDATED, FloorDetails } from "../helpers/index.mts";
5
+ import type { FloorDetails } from "../helpers/index.mts";
6
+ import { FLOOR_REGISTRY_UPDATED } from "../helpers/index.mts";
6
7
  import { hassTestRunner } from "../mock_assistant/index.mts";
7
- import { TFloorId } from "../user.mts";
8
+ import type { TFloorId } from "../user.mts";
8
9
 
9
10
  describe("Floor", () => {
10
11
  const EXAMPLE_FLOOR = {
@@ -1,5 +1,5 @@
1
1
  import { hassTestRunner } from "../mock_assistant/index.mts";
2
- import { PICK_ENTITY } from "../user.mts";
2
+ import type { PICK_ENTITY } from "../user.mts";
3
3
 
4
4
  afterEach(async () => {
5
5
  await hassTestRunner.teardown();
@@ -2,9 +2,10 @@ import { subscribe } from "node:diagnostics_channel";
2
2
 
3
3
  import { sleep } from "@digital-alchemy/core";
4
4
 
5
- import { LABEL_REGISTRY_UPDATED, LabelDefinition } from "../helpers/index.mts";
5
+ import type { LabelDefinition } from "../helpers/index.mts";
6
+ import { LABEL_REGISTRY_UPDATED } from "../helpers/index.mts";
6
7
  import { hassTestRunner } from "../mock_assistant/index.mts";
7
- import { TLabelId } from "../user.mts";
8
+ import type { TLabelId } from "../user.mts";
8
9
 
9
10
  describe("Label", () => {
10
11
  const EXAMPLE_LABEL = {
@@ -1,8 +1,8 @@
1
1
  import dayjs from "dayjs";
2
2
 
3
- import { ENTITY_STATE } from "../helpers/index.mts";
3
+ import type { ENTITY_STATE } from "../helpers/index.mts";
4
4
  import { hassTestRunner } from "../mock_assistant/index.mts";
5
- import { ANY_ENTITY } from "../user.mts";
5
+ import type { ANY_ENTITY } from "../user.mts";
6
6
 
7
7
  describe("References", () => {
8
8
  afterEach(async () => {
@@ -56,4 +56,160 @@ describe("Websocket", () => {
56
56
  });
57
57
  });
58
58
  });
59
+
60
+ describe("Message Handler Registration", () => {
61
+ it("should register and execute message handlers", async () => {
62
+ expect.assertions(4);
63
+ await hassTestRunner.run(({ lifecycle, hass }) => {
64
+ let handler1Called = false;
65
+ let handler2Called = false;
66
+
67
+ hass.socket.registerMessageHandler("test_message", async message => {
68
+ handler1Called = true;
69
+ expect(message.type).toBe("test_message");
70
+ });
71
+
72
+ hass.socket.registerMessageHandler("test_message", async message => {
73
+ handler2Called = true;
74
+ expect(message.type).toBe("test_message");
75
+ });
76
+
77
+ lifecycle.onReady(async () => {
78
+ await hass.socket.onMessage({
79
+ data: { test: "data" },
80
+ id: 1,
81
+ type: "test_message",
82
+ });
83
+
84
+ expect(handler1Called).toBe(true);
85
+ expect(handler2Called).toBe(true);
86
+ });
87
+ });
88
+ });
89
+
90
+ it("should handle generic message types", async () => {
91
+ expect.assertions(2);
92
+ await hassTestRunner.run(({ lifecycle, hass }) => {
93
+ interface CustomMessage {
94
+ type: string;
95
+ customProperty: string;
96
+ id: number;
97
+ }
98
+
99
+ let handlerCalled = false;
100
+
101
+ hass.socket.registerMessageHandler<CustomMessage>("custom_type", async message => {
102
+ handlerCalled = true;
103
+ expect(message.customProperty).toBe("test_value");
104
+ });
105
+
106
+ lifecycle.onReady(async () => {
107
+ await hass.socket.onMessage({
108
+ customProperty: "test_value",
109
+ id: 1,
110
+ type: "custom_type",
111
+ });
112
+
113
+ expect(handlerCalled).toBe(true);
114
+ });
115
+ });
116
+ });
117
+
118
+ it("should handle unknown message types gracefully", async () => {
119
+ expect.assertions(1);
120
+ await hassTestRunner.run(({ lifecycle, hass }) => {
121
+ let errorOccurred = false;
122
+
123
+ lifecycle.onReady(async () => {
124
+ try {
125
+ // Use a message type that definitely won't have handlers and won't trigger auth flow
126
+ await hass.socket.onMessage({
127
+ id: 1,
128
+ type: "completely_unknown_type_12345",
129
+ });
130
+ } catch {
131
+ errorOccurred = true;
132
+ }
133
+
134
+ // The function should complete without throwing an error
135
+ expect(errorOccurred).toBe(false);
136
+ });
137
+ });
138
+ });
139
+
140
+ it("should execute multiple handlers for the same message type", async () => {
141
+ expect.assertions(4);
142
+ await hassTestRunner.run(({ lifecycle, hass }) => {
143
+ const executionOrder: string[] = [];
144
+
145
+ hass.socket.registerMessageHandler("multi_handler", async message => {
146
+ executionOrder.push("first");
147
+ expect(message.type).toBe("multi_handler");
148
+ });
149
+
150
+ hass.socket.registerMessageHandler("multi_handler", async message => {
151
+ executionOrder.push("second");
152
+ expect(message.type).toBe("multi_handler");
153
+ });
154
+
155
+ hass.socket.registerMessageHandler("multi_handler", async message => {
156
+ executionOrder.push("third");
157
+ expect(message.type).toBe("multi_handler");
158
+ });
159
+
160
+ lifecycle.onReady(async () => {
161
+ await hass.socket.onMessage({
162
+ id: 1,
163
+ type: "multi_handler",
164
+ });
165
+
166
+ expect(executionOrder).toEqual(["first", "second", "third"]);
167
+ });
168
+ });
169
+ });
170
+
171
+ it("should handle async handlers correctly", async () => {
172
+ expect.assertions(2);
173
+ await hassTestRunner.run(({ lifecycle, hass }) => {
174
+ let asyncHandlerCompleted = false;
175
+
176
+ hass.socket.registerMessageHandler("async_test", async message => {
177
+ await new Promise(resolve => setTimeout(resolve, 10));
178
+ asyncHandlerCompleted = true;
179
+ expect(message.type).toBe("async_test");
180
+ });
181
+
182
+ lifecycle.onReady(async () => {
183
+ await hass.socket.onMessage({
184
+ id: 1,
185
+ type: "async_test",
186
+ });
187
+
188
+ expect(asyncHandlerCompleted).toBe(true);
189
+ });
190
+ });
191
+ });
192
+
193
+ it("should work with existing message types", async () => {
194
+ expect.assertions(2);
195
+ await hassTestRunner.run(({ lifecycle, hass }) => {
196
+ let customAuthHandlerCalled = false;
197
+
198
+ // Register an additional handler for auth_required
199
+ hass.socket.registerMessageHandler("auth_required", async message => {
200
+ customAuthHandlerCalled = true;
201
+ expect(message.type).toBe("auth_required");
202
+ });
203
+
204
+ lifecycle.onReady(async () => {
205
+ await hass.socket.onMessage({
206
+ id: 1,
207
+ type: "auth_required",
208
+ });
209
+
210
+ expect(customAuthHandlerCalled).toBe(true);
211
+ });
212
+ });
213
+ });
214
+ });
59
215
  });
@@ -2,7 +2,8 @@ import { subscribe } from "node:diagnostics_channel";
2
2
 
3
3
  import { sleep } from "@digital-alchemy/core";
4
4
 
5
- import { ZONE_REGISTRY_UPDATED, ZoneDetails } from "../helpers/index.mts";
5
+ import type { ZoneDetails } from "../helpers/index.mts";
6
+ import { ZONE_REGISTRY_UPDATED } from "../helpers/index.mts";
6
7
  import { hassTestRunner } from "../mock_assistant/index.mts";
7
8
 
8
9
  describe("Zone", () => {