@digital-alchemy/hass 25.5.1 → 25.7.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 (89) hide show
  1. package/dist/dev/mappings.d.mts +14 -0
  2. package/dist/dev/registry.d.mts +67 -0
  3. package/dist/hass.module.d.mts +13 -1
  4. package/dist/hass.module.mjs +16 -1
  5. package/dist/hass.module.mjs.map +1 -1
  6. package/dist/helpers/fetch.mjs +0 -1
  7. package/dist/helpers/fetch.mjs.map +1 -1
  8. package/dist/helpers/utility.d.mts +1 -0
  9. package/dist/helpers/utility.mjs +4 -0
  10. package/dist/helpers/utility.mjs.map +1 -1
  11. package/dist/mock_assistant/mock-assistant.module.d.mts +12 -0
  12. package/dist/services/area.service.mjs +9 -1
  13. package/dist/services/area.service.mjs.map +1 -1
  14. package/dist/services/call-proxy.service.mjs +10 -2
  15. package/dist/services/call-proxy.service.mjs.map +1 -1
  16. package/dist/services/config.service.mjs +15 -13
  17. package/dist/services/config.service.mjs.map +1 -1
  18. package/dist/services/device.service.mjs +3 -1
  19. package/dist/services/device.service.mjs.map +1 -1
  20. package/dist/services/diagnostics.service.d.mts +26 -0
  21. package/dist/services/diagnostics.service.mjs +41 -0
  22. package/dist/services/diagnostics.service.mjs.map +1 -0
  23. package/dist/services/entity.service.mjs +12 -1
  24. package/dist/services/entity.service.mjs.map +1 -1
  25. package/dist/services/fetch-api.service.mjs +8 -2
  26. package/dist/services/fetch-api.service.mjs.map +1 -1
  27. package/dist/services/floor.service.mjs +3 -1
  28. package/dist/services/floor.service.mjs.map +1 -1
  29. package/dist/services/id-by.service.mjs +2 -1
  30. package/dist/services/id-by.service.mjs.map +1 -1
  31. package/dist/services/index.d.mts +1 -0
  32. package/dist/services/index.mjs +1 -0
  33. package/dist/services/index.mjs.map +1 -1
  34. package/dist/services/label.service.mjs +3 -1
  35. package/dist/services/label.service.mjs.map +1 -1
  36. package/dist/services/reference.service.mjs +15 -4
  37. package/dist/services/reference.service.mjs.map +1 -1
  38. package/dist/services/registry.service.mjs +8 -8
  39. package/dist/services/registry.service.mjs.map +1 -1
  40. package/dist/services/websocket-api.service.mjs +10 -2
  41. package/dist/services/websocket-api.service.mjs.map +1 -1
  42. package/dist/services/zone.service.mjs +3 -1
  43. package/dist/services/zone.service.mjs.map +1 -1
  44. package/dist/testing/area.spec.mjs +141 -132
  45. package/dist/testing/area.spec.mjs.map +1 -1
  46. package/dist/testing/device.spec.mjs +17 -0
  47. package/dist/testing/device.spec.mjs.map +1 -1
  48. package/dist/testing/entity.spec.mjs +167 -0
  49. package/dist/testing/entity.spec.mjs.map +1 -1
  50. package/dist/testing/fetch.spec.d.mts +1 -0
  51. package/dist/testing/fetch.spec.mjs +45 -0
  52. package/dist/testing/fetch.spec.mjs.map +1 -0
  53. package/dist/testing/floor.spec.mjs +17 -0
  54. package/dist/testing/floor.spec.mjs.map +1 -1
  55. package/dist/testing/label.spec.mjs +17 -0
  56. package/dist/testing/label.spec.mjs.map +1 -1
  57. package/dist/testing/workflow.spec.mjs +1 -1
  58. package/dist/testing/workflow.spec.mjs.map +1 -1
  59. package/dist/testing/zone.spec.mjs +24 -5
  60. package/dist/testing/zone.spec.mjs.map +1 -1
  61. package/package.json +28 -28
  62. package/src/dev/mappings.mts +14 -0
  63. package/src/dev/registry.mts +67 -0
  64. package/src/hass.module.mts +18 -0
  65. package/src/helpers/fetch.mts +1 -1
  66. package/src/helpers/utility.mts +5 -0
  67. package/src/services/area.service.mts +9 -0
  68. package/src/services/call-proxy.service.mts +16 -9
  69. package/src/services/config.service.mts +21 -16
  70. package/src/services/device.service.mts +3 -0
  71. package/src/services/diagnostics.service.mts +45 -0
  72. package/src/services/entity.service.mts +12 -0
  73. package/src/services/fetch-api.service.mts +11 -2
  74. package/src/services/floor.service.mts +3 -0
  75. package/src/services/id-by.service.mts +2 -1
  76. package/src/services/index.mts +1 -0
  77. package/src/services/label.service.mts +3 -0
  78. package/src/services/reference.service.mts +15 -3
  79. package/src/services/registry.service.mts +8 -8
  80. package/src/services/websocket-api.service.mts +10 -2
  81. package/src/services/zone.service.mts +3 -0
  82. package/src/testing/area.spec.mts +153 -140
  83. package/src/testing/device.spec.mts +22 -0
  84. package/src/testing/entity.spec.mts +201 -0
  85. package/src/testing/fetch.spec.mts +54 -0
  86. package/src/testing/floor.spec.mts +22 -0
  87. package/src/testing/label.spec.mts +22 -0
  88. package/src/testing/workflow.spec.mts +1 -1
  89. package/src/testing/zone.spec.mts +29 -5
@@ -1,187 +1,200 @@
1
1
  import { sleep } from "@digital-alchemy/core";
2
+ import { subscribe } from "diagnostics_channel";
2
3
 
3
4
  import { AREA_REGISTRY_UPDATED, AreaDetails } from "../helpers/index.mts";
4
5
  import { hassTestRunner, INTERNAL_MESSAGE } from "../mock_assistant/index.mts";
5
6
  import { TAreaId } from "../user.mts";
6
7
 
7
- describe("Area", () => {
8
- const EXAMPLE_AREA = {
9
- area_id: "empty_area" as TAreaId,
10
- floor_id: null,
11
- icon: null,
12
- labels: [],
13
- name: "Empty Area",
14
- picture: null,
15
- } as AreaDetails;
16
-
17
- afterEach(async () => {
18
- await hassTestRunner.teardown();
19
- vi.restoreAllMocks();
8
+ const EXAMPLE_AREA = {
9
+ area_id: "empty_area" as TAreaId,
10
+ floor_id: null,
11
+ icon: null,
12
+ labels: [],
13
+ name: "Empty Area",
14
+ picture: null,
15
+ } as AreaDetails;
16
+
17
+ afterEach(async () => {
18
+ await hassTestRunner.teardown();
19
+ vi.restoreAllMocks();
20
+ });
21
+
22
+ describe("Lifecycle", () => {
23
+ it("should force values to be available before ready", async () => {
24
+ expect.assertions(1);
25
+
26
+ const app = await hassTestRunner.run(({ mock_assistant, lifecycle, hass }) => {
27
+ const spy = vi.fn();
28
+ mock_assistant.socket.connection.on(INTERNAL_MESSAGE, spy);
29
+ lifecycle.onReady(async () => {
30
+ await hass.area.list();
31
+ expect(spy).toHaveBeenCalledWith(
32
+ expect.objectContaining({ type: "config/area_registry/list" }),
33
+ );
34
+ }, -1);
35
+ });
36
+ await app.teardown();
20
37
  });
38
+ });
21
39
 
22
- describe("Lifecycle", () => {
23
- it("should force values to be available before ready", async () => {
40
+ describe("API", () => {
41
+ describe("Formatting", () => {
42
+ it("should call list properly", async () => {
24
43
  expect.assertions(1);
25
-
26
- const app = await hassTestRunner.run(({ mock_assistant, lifecycle, hass }) => {
27
- const spy = vi.fn();
28
- mock_assistant.socket.connection.on(INTERNAL_MESSAGE, spy);
44
+ await hassTestRunner.run(({ lifecycle, hass }) => {
45
+ const spy = vi.spyOn(hass.socket, "sendMessage").mockImplementation(async () => []);
29
46
  lifecycle.onReady(async () => {
30
47
  await hass.area.list();
31
48
  expect(spy).toHaveBeenCalledWith(
32
49
  expect.objectContaining({ type: "config/area_registry/list" }),
33
50
  );
34
- }, -1);
51
+ });
35
52
  });
36
- await app.teardown();
37
53
  });
38
- });
39
54
 
40
- describe("API", () => {
41
- describe("Formatting", () => {
42
- it("should call list properly", async () => {
43
- expect.assertions(1);
44
- await hassTestRunner.run(({ lifecycle, hass }) => {
45
- const spy = vi.spyOn(hass.socket, "sendMessage").mockImplementation(async () => []);
46
- lifecycle.onReady(async () => {
47
- await hass.area.list();
48
- expect(spy).toHaveBeenCalledWith(
49
- expect.objectContaining({ type: "config/area_registry/list" }),
50
- );
55
+ it("should call update properly", async () => {
56
+ expect.assertions(1);
57
+ await hassTestRunner.run(({ lifecycle, hass, event }) => {
58
+ const spy = vi.spyOn(hass.socket, "sendMessage").mockImplementation(async () => undefined);
59
+ lifecycle.onReady(async () => {
60
+ setImmediate(() => event.emit(AREA_REGISTRY_UPDATED));
61
+ await hass.area.update(EXAMPLE_AREA);
62
+
63
+ expect(spy).toHaveBeenCalledWith({
64
+ type: "config/area_registry/update",
65
+ ...EXAMPLE_AREA,
51
66
  });
52
67
  });
53
68
  });
69
+ });
54
70
 
55
- it("should call update properly", async () => {
56
- expect.assertions(1);
57
- await hassTestRunner.run(({ lifecycle, hass, event }) => {
58
- const spy = vi
59
- .spyOn(hass.socket, "sendMessage")
60
- .mockImplementation(async () => undefined);
61
- lifecycle.onReady(async () => {
62
- setImmediate(() => event.emit(AREA_REGISTRY_UPDATED));
63
- await hass.area.update(EXAMPLE_AREA);
64
-
65
- expect(spy).toHaveBeenCalledWith({
66
- type: "config/area_registry/update",
67
- ...EXAMPLE_AREA,
68
- });
71
+ it("should debounce updates properly", async () => {
72
+ expect.assertions(1);
73
+ await hassTestRunner.run(({ lifecycle, hass }) => {
74
+ vi.spyOn(hass.socket, "sendMessage").mockImplementation(async () => undefined);
75
+ let counter = 0;
76
+ hass.events.onAreaRegistryUpdate(() => counter++);
77
+ lifecycle.onReady(async () => {
78
+ setImmediate(async () => {
79
+ hass.socket.socketEvents.emit("area_registry_updated");
80
+ await sleep(5);
81
+ hass.socket.socketEvents.emit("area_registry_updated");
82
+ await sleep(5);
83
+ hass.socket.socketEvents.emit("area_registry_updated");
84
+ await sleep(75);
85
+ hass.socket.socketEvents.emit("area_registry_updated");
69
86
  });
87
+ await sleep(200);
88
+ expect(counter).toBe(2);
70
89
  });
71
90
  });
91
+ });
72
92
 
73
- it("should debounce updates properly", async () => {
74
- expect.assertions(1);
75
- await hassTestRunner.run(({ lifecycle, hass }) => {
76
- vi.spyOn(hass.socket, "sendMessage").mockImplementation(async () => undefined);
77
- let counter = 0;
78
- hass.events.onAreaRegistryUpdate(() => counter++);
79
- lifecycle.onReady(async () => {
80
- setImmediate(async () => {
81
- hass.socket.socketEvents.emit("area_registry_updated");
82
- await sleep(5);
83
- hass.socket.socketEvents.emit("area_registry_updated");
84
- await sleep(5);
85
- hass.socket.socketEvents.emit("area_registry_updated");
86
- await sleep(75);
87
- hass.socket.socketEvents.emit("area_registry_updated");
88
- });
89
- await sleep(200);
90
- expect(counter).toBe(2);
93
+ it("should publish diagnostics on area registry update", async () => {
94
+ expect.assertions(1);
95
+ hassTestRunner.configure({ hass: { EMIT_DIAGNOSTICS: true } });
96
+ await hassTestRunner.run(({ lifecycle, hass }) => {
97
+ vi.spyOn(hass.socket, "sendMessage").mockImplementation(async () => undefined);
98
+ const spy = vi.fn();
99
+ subscribe(hass.diagnostics.area.registry_update.name, spy);
100
+ lifecycle.onReady(async () => {
101
+ setImmediate(async () => {
102
+ hass.socket.socketEvents.emit("area_registry_updated");
91
103
  });
104
+ await sleep(100);
105
+ expect(spy).toHaveBeenCalledWith(
106
+ expect.objectContaining({ ms: expect.any(Number) }),
107
+ hass.diagnostics.area.registry_update.name,
108
+ );
92
109
  });
93
110
  });
111
+ });
94
112
 
95
- it("should call delete properly", async () => {
96
- expect.assertions(1);
97
-
98
- await hassTestRunner.run(({ lifecycle, hass, event }) => {
99
- const spy = vi
100
- .spyOn(hass.socket, "sendMessage")
101
- .mockImplementation(async () => undefined);
102
- lifecycle.onReady(async () => {
103
- setImmediate(() => event.emit(AREA_REGISTRY_UPDATED));
104
- await hass.area.delete(EXAMPLE_AREA.area_id);
105
-
106
- expect(spy).toHaveBeenCalledWith({
107
- area_id: "empty_area",
108
- type: "config/area_registry/delete",
109
- });
113
+ it("should call delete properly", async () => {
114
+ expect.assertions(1);
115
+
116
+ await hassTestRunner.run(({ lifecycle, hass, event }) => {
117
+ const spy = vi.spyOn(hass.socket, "sendMessage").mockImplementation(async () => undefined);
118
+ lifecycle.onReady(async () => {
119
+ setImmediate(() => event.emit(AREA_REGISTRY_UPDATED));
120
+ await hass.area.delete(EXAMPLE_AREA.area_id);
121
+
122
+ expect(spy).toHaveBeenCalledWith({
123
+ area_id: "empty_area",
124
+ type: "config/area_registry/delete",
110
125
  });
111
126
  });
112
127
  });
128
+ });
113
129
 
114
- it("should call create properly", async () => {
115
- expect.assertions(1);
116
- await hassTestRunner.run(({ lifecycle, hass, event }) => {
117
- const spy = vi
118
- .spyOn(hass.socket, "sendMessage")
119
- .mockImplementation(async () => undefined);
120
- lifecycle.onReady(async () => {
121
- setImmediate(() => event.emit(AREA_REGISTRY_UPDATED));
122
- await hass.area.create(EXAMPLE_AREA);
123
-
124
- expect(spy).toHaveBeenCalledWith({
125
- type: "config/area_registry/create",
126
- ...EXAMPLE_AREA,
127
- });
130
+ it("should call create properly", async () => {
131
+ expect.assertions(1);
132
+ await hassTestRunner.run(({ lifecycle, hass, event }) => {
133
+ const spy = vi.spyOn(hass.socket, "sendMessage").mockImplementation(async () => undefined);
134
+ lifecycle.onReady(async () => {
135
+ setImmediate(() => event.emit(AREA_REGISTRY_UPDATED));
136
+ await hass.area.create(EXAMPLE_AREA);
137
+
138
+ expect(spy).toHaveBeenCalledWith({
139
+ type: "config/area_registry/create",
140
+ ...EXAMPLE_AREA,
128
141
  });
129
142
  });
130
143
  });
131
144
  });
145
+ });
132
146
 
133
- describe("Order of operations", () => {
134
- it("should wait for an update before returning when updating", async () => {
135
- expect.assertions(1);
136
- await hassTestRunner.run(({ lifecycle, hass, event }) => {
137
- vi.spyOn(hass.socket, "sendMessage").mockImplementation(async () => undefined);
138
- lifecycle.onReady(async () => {
139
- const response = hass.area.update(EXAMPLE_AREA);
140
- let order = "";
141
- setTimeout(() => {
142
- order += "a";
143
- event.emit(AREA_REGISTRY_UPDATED);
144
- }, 5);
145
- await response;
146
- order += "b";
147
- expect(order).toEqual("ab");
148
- });
147
+ describe("Order of operations", () => {
148
+ it("should wait for an update before returning when updating", async () => {
149
+ expect.assertions(1);
150
+ await hassTestRunner.run(({ lifecycle, hass, event }) => {
151
+ vi.spyOn(hass.socket, "sendMessage").mockImplementation(async () => undefined);
152
+ lifecycle.onReady(async () => {
153
+ const response = hass.area.update(EXAMPLE_AREA);
154
+ let order = "";
155
+ setTimeout(() => {
156
+ order += "a";
157
+ event.emit(AREA_REGISTRY_UPDATED);
158
+ }, 5);
159
+ await response;
160
+ order += "b";
161
+ expect(order).toEqual("ab");
149
162
  });
150
163
  });
164
+ });
151
165
 
152
- it("should wait for an update before returning when deleting", async () => {
153
- expect.assertions(1);
154
- await hassTestRunner.run(({ lifecycle, hass, event }) => {
155
- vi.spyOn(hass.socket, "sendMessage").mockImplementation(async () => undefined);
156
- lifecycle.onReady(async () => {
157
- const response = hass.area.delete("example_area" as TAreaId);
158
- let order = "";
159
- setTimeout(() => {
160
- order += "a";
161
- event.emit(AREA_REGISTRY_UPDATED);
162
- }, 5);
163
- await response;
164
- order += "b";
165
- expect(order).toEqual("ab");
166
- });
166
+ it("should wait for an update before returning when deleting", async () => {
167
+ expect.assertions(1);
168
+ await hassTestRunner.run(({ lifecycle, hass, event }) => {
169
+ vi.spyOn(hass.socket, "sendMessage").mockImplementation(async () => undefined);
170
+ lifecycle.onReady(async () => {
171
+ const response = hass.area.delete("example_area" as TAreaId);
172
+ let order = "";
173
+ setTimeout(() => {
174
+ order += "a";
175
+ event.emit(AREA_REGISTRY_UPDATED);
176
+ }, 5);
177
+ await response;
178
+ order += "b";
179
+ expect(order).toEqual("ab");
167
180
  });
168
181
  });
182
+ });
169
183
 
170
- it("should wait for an update before returning when creating", async () => {
171
- expect.assertions(1);
172
- await hassTestRunner.run(({ lifecycle, hass, event }) => {
173
- vi.spyOn(hass.socket, "sendMessage").mockImplementation(async () => undefined);
174
- lifecycle.onReady(async () => {
175
- const response = hass.area.create(EXAMPLE_AREA);
176
- let order = "";
177
- setTimeout(() => {
178
- order += "a";
179
- event.emit(AREA_REGISTRY_UPDATED);
180
- }, 5);
181
- await response;
182
- order += "b";
183
- expect(order).toEqual("ab");
184
- });
184
+ it("should wait for an update before returning when creating", async () => {
185
+ expect.assertions(1);
186
+ await hassTestRunner.run(({ lifecycle, hass, event }) => {
187
+ vi.spyOn(hass.socket, "sendMessage").mockImplementation(async () => undefined);
188
+ lifecycle.onReady(async () => {
189
+ const response = hass.area.create(EXAMPLE_AREA);
190
+ let order = "";
191
+ setTimeout(() => {
192
+ order += "a";
193
+ event.emit(AREA_REGISTRY_UPDATED);
194
+ }, 5);
195
+ await response;
196
+ order += "b";
197
+ expect(order).toEqual("ab");
185
198
  });
186
199
  });
187
200
  });
@@ -1,3 +1,5 @@
1
+ import { subscribe } from "node:diagnostics_channel";
2
+
1
3
  import { sleep } from "@digital-alchemy/core";
2
4
 
3
5
  import { DeviceDetails } from "../helpers/index.mts";
@@ -86,4 +88,24 @@ describe("Device", () => {
86
88
  });
87
89
  });
88
90
  });
91
+
92
+ it("should publish diagnostics on device registry update", async () => {
93
+ expect.assertions(1);
94
+ hassTestRunner.configure({ hass: { EMIT_DIAGNOSTICS: true } });
95
+ await hassTestRunner.run(({ lifecycle, hass }) => {
96
+ vi.spyOn(hass.socket, "sendMessage").mockImplementation(async () => undefined);
97
+ const spy = vi.fn();
98
+ subscribe(hass.diagnostics.device.registry_update.name, spy);
99
+ lifecycle.onReady(async () => {
100
+ setImmediate(async () => {
101
+ hass.socket.socketEvents.emit("device_registry_updated");
102
+ });
103
+ await sleep(100);
104
+ expect(spy).toHaveBeenCalledWith(
105
+ expect.objectContaining({ ms: expect.any(Number) }),
106
+ hass.diagnostics.device.registry_update.name,
107
+ );
108
+ });
109
+ });
110
+ });
89
111
  });
@@ -1,4 +1,7 @@
1
+ import { subscribe } from "node:diagnostics_channel";
2
+
1
3
  import { sleep } from "@digital-alchemy/core";
4
+ import dayjs from "dayjs";
2
5
 
3
6
  import { ENTITY_STATE } from "../index.mts";
4
7
  import { hassTestRunner } from "../mock_assistant/index.mts";
@@ -169,4 +172,202 @@ describe("Entity", () => {
169
172
  });
170
173
  });
171
174
  });
175
+
176
+ describe("Diagnostics", () => {
177
+ it("should publish diagnostics on history lookup", async () => {
178
+ expect.assertions(2);
179
+ await hassTestRunner
180
+ .configure({
181
+ hass: { EMIT_DIAGNOSTICS: true },
182
+ })
183
+ .run(({ lifecycle, hass }) => {
184
+ const spy = vi.fn();
185
+ subscribe(hass.diagnostics.entity.history_lookup.name, spy);
186
+
187
+ // Mock the socket response
188
+ const sendMessageSpy = vi.spyOn(hass.socket, "sendMessage").mockResolvedValue({
189
+ "sensor.magic": [
190
+ {
191
+ entity_id: "sensor.magic",
192
+ last_changed: "2024-01-01T00:00:00Z",
193
+ last_updated: "2024-01-01T00:00:00Z",
194
+ state: "test",
195
+ },
196
+ ],
197
+ });
198
+
199
+ lifecycle.onReady(async () => {
200
+ const now = new Date();
201
+ await hass.entity.history({
202
+ end_time: now.toISOString(),
203
+ entity_ids: ["sensor.magic"],
204
+ start_time: new Date(now.getTime() - 3600000).toISOString(), // 1 hour ago
205
+ });
206
+
207
+ expect(sendMessageSpy).toHaveBeenCalledWith(
208
+ expect.objectContaining({
209
+ entity_ids: ["sensor.magic"],
210
+ type: "history/history_during_period",
211
+ }),
212
+ );
213
+
214
+ expect(spy).toHaveBeenCalledWith(
215
+ expect.objectContaining({
216
+ ms: expect.any(Number),
217
+ payload: expect.objectContaining({
218
+ entity_ids: ["sensor.magic"],
219
+ }),
220
+ result: expect.objectContaining({
221
+ "sensor.magic": expect.arrayContaining([
222
+ expect.objectContaining({
223
+ entity_id: "sensor.magic",
224
+ state: "test",
225
+ }),
226
+ ]),
227
+ }),
228
+ }),
229
+ hass.diagnostics.entity.history_lookup.name,
230
+ );
231
+ });
232
+ });
233
+ });
234
+
235
+ it("should publish diagnostics on refresh entities", async () => {
236
+ expect.assertions(1);
237
+ await hassTestRunner
238
+ .configure({
239
+ hass: { EMIT_DIAGNOSTICS: true },
240
+ })
241
+ .run(({ lifecycle, hass }) => {
242
+ const spy = vi.fn();
243
+ subscribe(hass.diagnostics.entity.refresh_entities.name, spy);
244
+
245
+ lifecycle.onReady(async () => {
246
+ await hass.entity.refresh();
247
+ expect(spy).toHaveBeenCalledWith(
248
+ expect.objectContaining({
249
+ emitUpdates: [],
250
+ }),
251
+ hass.diagnostics.entity.refresh_entities.name,
252
+ );
253
+ });
254
+ });
255
+ });
256
+
257
+ it("should publish diagnostics on entity update", async () => {
258
+ expect.assertions(1);
259
+ await hassTestRunner
260
+ .configure({
261
+ hass: { EMIT_DIAGNOSTICS: true },
262
+ })
263
+ .run(({ lifecycle, hass, mock_assistant }) => {
264
+ const spy = vi.fn();
265
+ subscribe(hass.diagnostics.entity.entity_updated.name, spy);
266
+
267
+ lifecycle.onReady(async () => {
268
+ await mock_assistant.events.emitEntityUpdate("sensor.magic", {
269
+ state: "test",
270
+ });
271
+ expect(spy).toHaveBeenCalledWith(
272
+ expect.objectContaining({
273
+ entity_id: "sensor.magic",
274
+ new_state: expect.objectContaining({
275
+ entity_id: "sensor.magic",
276
+ state: "test",
277
+ }),
278
+ old_state: expect.objectContaining({
279
+ entity_id: "sensor.magic",
280
+ state: "unavailable",
281
+ }),
282
+ }),
283
+ hass.diagnostics.entity.entity_updated.name,
284
+ );
285
+ });
286
+ });
287
+ });
288
+
289
+ it("should publish diagnostics on entity remove", async () => {
290
+ expect.assertions(1);
291
+ await hassTestRunner
292
+ .configure({
293
+ hass: { EMIT_DIAGNOSTICS: true },
294
+ })
295
+ .run(({ lifecycle, hass }) => {
296
+ const spy = vi.fn();
297
+ subscribe(hass.diagnostics.entity.entity_remove.name, spy);
298
+
299
+ lifecycle.onReady(async () => {
300
+ // Directly call _entityUpdateReceiver with null new_state to trigger entity remove
301
+ hass.entity._entityUpdateReceiver("sensor.magic", null, {
302
+ attributes: {
303
+ friendly_name: "magic",
304
+ icon: "mdi:satellite-uplink",
305
+ restored: true,
306
+ supported_features: 0,
307
+ },
308
+ context: {
309
+ id: "test",
310
+ parent_id: null,
311
+ user_id: null,
312
+ },
313
+ entity_id: "sensor.magic",
314
+ last_changed: dayjs("2024-01-01T00:00:00Z"),
315
+ last_reported: dayjs("2024-01-01T00:00:00Z"),
316
+ last_updated: dayjs("2024-01-01T00:00:00Z"),
317
+ state: "test",
318
+ });
319
+
320
+ expect(spy).toHaveBeenCalledWith(
321
+ expect.objectContaining({
322
+ entity_id: "sensor.magic",
323
+ }),
324
+ hass.diagnostics.entity.entity_remove.name,
325
+ );
326
+ });
327
+ });
328
+ });
329
+
330
+ it("should publish diagnostics on entity add", async () => {
331
+ expect.assertions(1);
332
+ await hassTestRunner
333
+ .configure({
334
+ hass: { EMIT_DIAGNOSTICS: true },
335
+ })
336
+ .run(({ lifecycle, hass }) => {
337
+ const spy = vi.fn();
338
+ subscribe(hass.diagnostics.entity.entity_add.name, spy);
339
+
340
+ lifecycle.onReady(async () => {
341
+ // Call _entityUpdateReceiver with null old_state to trigger entity add
342
+ const newState = {
343
+ attributes: {
344
+ friendly_name: "magic" as const,
345
+ icon: "mdi:satellite-uplink" as const,
346
+ restored: true as const,
347
+ supported_features: 0 as const,
348
+ },
349
+ context: {
350
+ id: "test",
351
+ parent_id: null as null,
352
+ user_id: null as null,
353
+ },
354
+ entity_id: "sensor.magic" as const,
355
+ last_changed: dayjs("2024-01-01T00:00:00Z"),
356
+ last_reported: dayjs("2024-01-01T00:00:00Z"),
357
+ last_updated: dayjs("2024-01-01T00:00:00Z"),
358
+ state: "test",
359
+ };
360
+
361
+ hass.entity._entityUpdateReceiver("sensor.magic", newState, null);
362
+
363
+ expect(spy).toHaveBeenCalledWith(
364
+ expect.objectContaining({
365
+ entity_id: "sensor.magic",
366
+ }),
367
+ hass.diagnostics.entity.entity_add.name,
368
+ );
369
+ });
370
+ });
371
+ });
372
+ });
172
373
  });
@@ -0,0 +1,54 @@
1
+ import { subscribe } from "node:diagnostics_channel";
2
+
3
+ import { hassTestRunner } from "../mock_assistant/index.mts";
4
+
5
+ describe("Fetch", () => {
6
+ const BASE_URL = "http://homeassistant.local:8123";
7
+
8
+ afterEach(async () => {
9
+ await hassTestRunner.teardown();
10
+ vi.restoreAllMocks();
11
+ });
12
+
13
+ describe("API", () => {
14
+ describe("Diagnostics", () => {
15
+ it("should publish diagnostics on fetch operation", async () => {
16
+ expect.assertions(1);
17
+ hassTestRunner.configure({
18
+ hass: {
19
+ BASE_URL,
20
+ EMIT_DIAGNOSTICS: true,
21
+ },
22
+ });
23
+ await hassTestRunner.run(({ lifecycle, hass }) => {
24
+ const mockResponse = { data: "test response" };
25
+ vi.spyOn(globalThis, "fetch").mockImplementation(async () => {
26
+ return {
27
+ text: async () => JSON.stringify(mockResponse),
28
+ } as unknown as Response;
29
+ });
30
+
31
+ const spy = vi.fn();
32
+ subscribe(hass.diagnostics.fetch.fetch.name, spy);
33
+
34
+ lifecycle.onPostConfig(() => {
35
+ hass.fetch._fetcher.base_url = BASE_URL;
36
+ hass.fetch._fetcher.base_headers = { Authorization: "Bearer test_token" };
37
+ });
38
+
39
+ lifecycle.onReady(async () => {
40
+ await hass.fetch.fetch({ url: "/api/test" });
41
+ expect(spy).toHaveBeenCalledWith(
42
+ expect.objectContaining({
43
+ ms: expect.any(Number),
44
+ options: expect.objectContaining({ url: "/api/test" }),
45
+ out: mockResponse,
46
+ }),
47
+ hass.diagnostics.fetch.fetch.name,
48
+ );
49
+ });
50
+ });
51
+ });
52
+ });
53
+ });
54
+ });
@@ -1,3 +1,5 @@
1
+ import { subscribe } from "node:diagnostics_channel";
2
+
1
3
  import { sleep } from "@digital-alchemy/core";
2
4
 
3
5
  import { FLOOR_REGISTRY_UPDATED, FloorDetails } from "../helpers/index.mts";
@@ -103,6 +105,26 @@ describe("Floor", () => {
103
105
  });
104
106
  });
105
107
  });
108
+
109
+ it("should publish diagnostics on floor registry update", async () => {
110
+ expect.assertions(1);
111
+ hassTestRunner.configure({ hass: { EMIT_DIAGNOSTICS: true } });
112
+ await hassTestRunner.run(({ lifecycle, hass }) => {
113
+ vi.spyOn(hass.socket, "sendMessage").mockImplementation(async () => undefined);
114
+ const spy = vi.fn();
115
+ subscribe(hass.diagnostics.floor.registry_update.name, spy);
116
+ lifecycle.onReady(async () => {
117
+ setImmediate(async () => {
118
+ hass.socket.socketEvents.emit("floor_registry_updated");
119
+ });
120
+ await sleep(100);
121
+ expect(spy).toHaveBeenCalledWith(
122
+ expect.objectContaining({ ms: expect.any(Number) }),
123
+ hass.diagnostics.floor.registry_update.name,
124
+ );
125
+ });
126
+ });
127
+ });
106
128
  });
107
129
 
108
130
  describe("Order of operations", () => {