@digital-alchemy/hass 25.10.27-beta.1 → 25.11.16
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.
- package/dist/dev/mappings.d.mts +26 -0
- package/dist/dev/services.mjs +0 -1
- package/dist/dev/services.mjs.map +1 -1
- package/dist/hass.module.d.mts +8 -1
- package/dist/hass.module.mjs +5 -1
- package/dist/hass.module.mjs.map +1 -1
- package/dist/helpers/fetch/configuration.d.mts +5 -5
- package/dist/helpers/fetch/configuration.mjs.map +1 -1
- package/dist/helpers/interfaces.d.mts +11 -0
- package/dist/helpers/interfaces.mjs.map +1 -1
- package/dist/mock_assistant/mock-assistant.module.d.mts +2 -0
- package/dist/services/area.service.mjs +12 -12
- package/dist/services/area.service.mjs.map +1 -1
- package/dist/services/config.service.d.mts +2 -1
- package/dist/services/config.service.mjs +35 -5
- package/dist/services/config.service.mjs.map +1 -1
- package/dist/services/conversation.service.d.mts +1 -1
- package/dist/services/conversation.service.mjs +9 -1
- package/dist/services/conversation.service.mjs.map +1 -1
- package/dist/services/device.service.mjs +12 -19
- package/dist/services/device.service.mjs.map +1 -1
- package/dist/services/entity.service.mjs +12 -12
- package/dist/services/entity.service.mjs.map +1 -1
- package/dist/services/floor.service.mjs +12 -12
- package/dist/services/floor.service.mjs.map +1 -1
- package/dist/services/index.d.mts +1 -0
- package/dist/services/index.mjs +1 -0
- package/dist/services/index.mjs.map +1 -1
- package/dist/services/label.service.mjs +12 -12
- package/dist/services/label.service.mjs.map +1 -1
- package/dist/services/registry.service.mjs +4 -3
- package/dist/services/registry.service.mjs.map +1 -1
- package/dist/services/websocket-api.service.d.mts +1 -1
- package/dist/services/websocket-api.service.mjs +70 -6
- package/dist/services/websocket-api.service.mjs.map +1 -1
- package/dist/services/zone.service.mjs +12 -12
- package/dist/services/zone.service.mjs.map +1 -1
- package/dist/testing/config.spec.mjs +367 -0
- package/dist/testing/config.spec.mjs.map +1 -1
- package/dist/testing/conversation.spec.d.mts +1 -0
- package/dist/testing/conversation.spec.mjs +42 -0
- package/dist/testing/conversation.spec.mjs.map +1 -0
- package/dist/testing/fetch-api.spec.mjs +2 -5
- package/dist/testing/fetch-api.spec.mjs.map +1 -1
- package/dist/testing/websocket.spec.mjs +177 -0
- package/dist/testing/websocket.spec.mjs.map +1 -1
- package/dist/user.d.mts +7 -0
- package/package.json +1 -1
- package/src/dev/mappings.mts +15 -0
- package/src/dev/registry.mts +0 -1
- package/src/dev/services.mts +0 -1
- package/src/hass.module.mts +10 -0
- package/src/helpers/fetch/configuration.mts +11 -5
- package/src/helpers/interfaces.mts +14 -0
- package/src/services/area.service.mts +13 -13
- package/src/services/config.service.mts +53 -6
- package/src/services/conversation.service.mts +16 -2
- package/src/services/device.service.mts +13 -21
- package/src/services/entity.service.mts +13 -12
- package/src/services/floor.service.mts +13 -13
- package/src/services/index.mts +1 -0
- package/src/services/label.service.mts +13 -13
- package/src/services/registry.service.mts +5 -4
- package/src/services/websocket-api.service.mts +86 -5
- package/src/services/zone.service.mts +13 -13
- package/src/testing/config.spec.mts +410 -0
- package/src/testing/conversation.spec.mts +48 -0
- package/src/testing/fetch-api.spec.mts +2 -5
- package/src/testing/websocket.spec.mts +225 -0
- package/src/user.mts +14 -0
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { sleep } from "@digital-alchemy/core";
|
|
2
|
+
|
|
1
3
|
import { hassTestRunner } from "../mock_assistant/index.mts";
|
|
2
4
|
|
|
3
5
|
describe("Websocket", () => {
|
|
@@ -5,6 +7,9 @@ describe("Websocket", () => {
|
|
|
5
7
|
await hassTestRunner.teardown();
|
|
6
8
|
vi.restoreAllMocks();
|
|
7
9
|
});
|
|
10
|
+
beforeEach(() => {
|
|
11
|
+
hassTestRunner.emitLogs("silent");
|
|
12
|
+
});
|
|
8
13
|
|
|
9
14
|
describe("API Interactions", () => {
|
|
10
15
|
it("should emit events onConnect", async () => {
|
|
@@ -212,4 +217,224 @@ describe("Websocket", () => {
|
|
|
212
217
|
});
|
|
213
218
|
});
|
|
214
219
|
});
|
|
220
|
+
|
|
221
|
+
describe("Memory Leak Detection", () => {
|
|
222
|
+
it("should NOT warn when subscribe is called outside onConnect", async () => {
|
|
223
|
+
expect.assertions(1);
|
|
224
|
+
await hassTestRunner.run(({ lifecycle, hass, logger, context }) => {
|
|
225
|
+
const warnSpy = vi.spyOn(logger, "warn").mockImplementation(() => {});
|
|
226
|
+
|
|
227
|
+
lifecycle.onReady(async () => {
|
|
228
|
+
await hass.socket.subscribe({
|
|
229
|
+
context,
|
|
230
|
+
event_type: "test_event",
|
|
231
|
+
exec: () => {},
|
|
232
|
+
});
|
|
233
|
+
|
|
234
|
+
// Should not have been called with memory leak warning
|
|
235
|
+
const memoryLeakWarnings = warnSpy.mock.calls.filter(call =>
|
|
236
|
+
call[1]?.toString().includes("MEMORY LEAK DETECTED"),
|
|
237
|
+
);
|
|
238
|
+
expect(memoryLeakWarnings.length).toBe(0);
|
|
239
|
+
});
|
|
240
|
+
});
|
|
241
|
+
});
|
|
242
|
+
|
|
243
|
+
it("should warn when subscribe is called inside onConnect callback", async () => {
|
|
244
|
+
expect.assertions(2);
|
|
245
|
+
const warnSpy = vi.spyOn(console, "warn").mockImplementation(() => {});
|
|
246
|
+
await hassTestRunner.emitLogs("warn").run(({ lifecycle, hass, internal, context, event }) => {
|
|
247
|
+
internal.boilerplate.configuration.set("boilerplate", "LOG_LEVEL", "warn");
|
|
248
|
+
lifecycle.onReady(async () => {
|
|
249
|
+
// Wait for initial connection to complete
|
|
250
|
+
await new Promise(resolve => setTimeout(resolve, 100));
|
|
251
|
+
|
|
252
|
+
// Ensure socket is connected
|
|
253
|
+
expect(hass.socket.connectionState).toBe("connected");
|
|
254
|
+
|
|
255
|
+
// Register onConnect callback that subscribes (this should trigger warning)
|
|
256
|
+
hass.socket.onConnect(async () => {
|
|
257
|
+
await hass.socket.subscribe({
|
|
258
|
+
context,
|
|
259
|
+
event_type: "test_event_warn",
|
|
260
|
+
exec: () => {},
|
|
261
|
+
});
|
|
262
|
+
});
|
|
263
|
+
|
|
264
|
+
// Trigger onConnect by emitting SOCKET_CONNECTED event again
|
|
265
|
+
event.emit("SOCKET_CONNECTED");
|
|
266
|
+
await sleep(0);
|
|
267
|
+
|
|
268
|
+
expect(warnSpy).toHaveBeenCalledWith(expect.stringContaining("MEMORY LEAK DETECTED"));
|
|
269
|
+
});
|
|
270
|
+
});
|
|
271
|
+
});
|
|
272
|
+
});
|
|
273
|
+
|
|
274
|
+
describe("Subscription Registry", () => {
|
|
275
|
+
it("should prevent duplicate subscriptions", async () => {
|
|
276
|
+
expect.assertions(1);
|
|
277
|
+
await hassTestRunner.run(({ lifecycle, hass, context }) => {
|
|
278
|
+
const sendMessageSpy = vi
|
|
279
|
+
.spyOn(hass.socket, "sendMessage")
|
|
280
|
+
.mockImplementation(async () => undefined);
|
|
281
|
+
|
|
282
|
+
lifecycle.onReady(async () => {
|
|
283
|
+
// Clear spy after initial bootstrap subscriptions
|
|
284
|
+
sendMessageSpy.mockClear();
|
|
285
|
+
|
|
286
|
+
// Subscribe to the same event twice (even with same context)
|
|
287
|
+
// Since registry tracks by event_type only, second subscription won't send message
|
|
288
|
+
await hass.socket.subscribe({
|
|
289
|
+
context,
|
|
290
|
+
event_type: "duplicate_test",
|
|
291
|
+
exec: () => {},
|
|
292
|
+
});
|
|
293
|
+
|
|
294
|
+
await hass.socket.subscribe({
|
|
295
|
+
context,
|
|
296
|
+
event_type: "duplicate_test",
|
|
297
|
+
exec: () => {},
|
|
298
|
+
});
|
|
299
|
+
|
|
300
|
+
// Should only send subscribe message once (duplicate event_type is prevented)
|
|
301
|
+
const subscribeCalls = sendMessageSpy.mock.calls.filter(
|
|
302
|
+
call =>
|
|
303
|
+
call[0]?.type === "subscribe_events" && call[0]?.event_type === "duplicate_test",
|
|
304
|
+
);
|
|
305
|
+
expect(subscribeCalls.length).toBe(1);
|
|
306
|
+
});
|
|
307
|
+
});
|
|
308
|
+
});
|
|
309
|
+
|
|
310
|
+
it("should allow different event types with same context", async () => {
|
|
311
|
+
expect.assertions(1);
|
|
312
|
+
await hassTestRunner.run(({ lifecycle, hass, context }) => {
|
|
313
|
+
const sendMessageSpy = vi
|
|
314
|
+
.spyOn(hass.socket, "sendMessage")
|
|
315
|
+
.mockImplementation(async () => undefined);
|
|
316
|
+
|
|
317
|
+
lifecycle.onReady(async () => {
|
|
318
|
+
await hass.socket.subscribe({
|
|
319
|
+
context,
|
|
320
|
+
event_type: "event_type_1",
|
|
321
|
+
exec: () => {},
|
|
322
|
+
});
|
|
323
|
+
|
|
324
|
+
await hass.socket.subscribe({
|
|
325
|
+
context,
|
|
326
|
+
event_type: "event_type_2",
|
|
327
|
+
exec: () => {},
|
|
328
|
+
});
|
|
329
|
+
|
|
330
|
+
// Should send subscribe message for both
|
|
331
|
+
const subscribeCalls = sendMessageSpy.mock.calls.filter(
|
|
332
|
+
call => call[0]?.type === "subscribe_events",
|
|
333
|
+
);
|
|
334
|
+
expect(subscribeCalls.length).toBeGreaterThanOrEqual(2);
|
|
335
|
+
});
|
|
336
|
+
});
|
|
337
|
+
});
|
|
338
|
+
|
|
339
|
+
it("should re-establish subscriptions on reconnect", async () => {
|
|
340
|
+
expect.assertions(2);
|
|
341
|
+
await hassTestRunner.run(({ lifecycle, hass, context, event }) => {
|
|
342
|
+
const sendMessageSpy = vi
|
|
343
|
+
.spyOn(hass.socket, "sendMessage")
|
|
344
|
+
.mockImplementation(async () => undefined);
|
|
345
|
+
|
|
346
|
+
lifecycle.onReady(async () => {
|
|
347
|
+
// Register a subscription
|
|
348
|
+
await hass.socket.subscribe({
|
|
349
|
+
context,
|
|
350
|
+
event_type: "reconnect_test",
|
|
351
|
+
exec: () => {},
|
|
352
|
+
});
|
|
353
|
+
|
|
354
|
+
// Clear the spy to only track re-establishment calls
|
|
355
|
+
sendMessageSpy.mockClear();
|
|
356
|
+
|
|
357
|
+
// Register an onConnect callback that will trigger re-establishment
|
|
358
|
+
hass.socket.onConnect(async () => {
|
|
359
|
+
// This callback will trigger re-establishment
|
|
360
|
+
});
|
|
361
|
+
|
|
362
|
+
// Trigger reconnect by emitting SOCKET_CONNECTED event
|
|
363
|
+
event.emit("SOCKET_CONNECTED");
|
|
364
|
+
|
|
365
|
+
// Wait for async operations
|
|
366
|
+
await new Promise(resolve => setTimeout(resolve, 50));
|
|
367
|
+
|
|
368
|
+
// Should have re-established the subscription
|
|
369
|
+
const reestablishCalls = sendMessageSpy.mock.calls.filter(
|
|
370
|
+
call =>
|
|
371
|
+
call[0]?.type === "subscribe_events" && call[0]?.event_type === "reconnect_test",
|
|
372
|
+
);
|
|
373
|
+
expect(reestablishCalls.length).toBeGreaterThan(0);
|
|
374
|
+
expect(sendMessageSpy).toHaveBeenCalled();
|
|
375
|
+
});
|
|
376
|
+
});
|
|
377
|
+
});
|
|
378
|
+
|
|
379
|
+
it("should re-establish multiple subscriptions on reconnect", async () => {
|
|
380
|
+
expect.assertions(4);
|
|
381
|
+
await hassTestRunner.run(({ lifecycle, hass, context, event }) => {
|
|
382
|
+
const sendMessageSpy = vi
|
|
383
|
+
.spyOn(hass.socket, "sendMessage")
|
|
384
|
+
.mockImplementation(async () => undefined);
|
|
385
|
+
|
|
386
|
+
lifecycle.onReady(async () => {
|
|
387
|
+
// Wait for initial connection to complete
|
|
388
|
+
await new Promise(resolve => setTimeout(resolve, 50));
|
|
389
|
+
|
|
390
|
+
// Register multiple subscriptions
|
|
391
|
+
await hass.socket.subscribe({
|
|
392
|
+
context,
|
|
393
|
+
event_type: "multi_test_1",
|
|
394
|
+
exec: () => {},
|
|
395
|
+
});
|
|
396
|
+
|
|
397
|
+
await hass.socket.subscribe({
|
|
398
|
+
context,
|
|
399
|
+
event_type: "multi_test_2",
|
|
400
|
+
exec: () => {},
|
|
401
|
+
});
|
|
402
|
+
|
|
403
|
+
await hass.socket.subscribe({
|
|
404
|
+
context,
|
|
405
|
+
event_type: "multi_test_3",
|
|
406
|
+
exec: () => {},
|
|
407
|
+
});
|
|
408
|
+
|
|
409
|
+
// Clear the spy to only track re-establishment calls
|
|
410
|
+
sendMessageSpy.mockClear();
|
|
411
|
+
|
|
412
|
+
// Register an onConnect callback that will trigger re-establishment
|
|
413
|
+
hass.socket.onConnect(async () => {
|
|
414
|
+
// This callback will trigger re-establishment
|
|
415
|
+
});
|
|
416
|
+
|
|
417
|
+
// Trigger reconnect by emitting SOCKET_CONNECTED event
|
|
418
|
+
event.emit("SOCKET_CONNECTED");
|
|
419
|
+
|
|
420
|
+
// Wait for async operations
|
|
421
|
+
await new Promise(resolve => setTimeout(resolve, 150));
|
|
422
|
+
|
|
423
|
+
// Should have re-established our specific subscriptions (along with others)
|
|
424
|
+
const reestablishCalls = sendMessageSpy.mock.calls.filter(
|
|
425
|
+
call =>
|
|
426
|
+
call[0]?.type === "subscribe_events" &&
|
|
427
|
+
(call[0]?.event_type === "multi_test_1" ||
|
|
428
|
+
call[0]?.event_type === "multi_test_2" ||
|
|
429
|
+
call[0]?.event_type === "multi_test_3"),
|
|
430
|
+
);
|
|
431
|
+
// Verify each specific subscription was re-established
|
|
432
|
+
expect(reestablishCalls.length).toBe(3);
|
|
433
|
+
expect(reestablishCalls.some(call => call[0]?.event_type === "multi_test_1")).toBe(true);
|
|
434
|
+
expect(reestablishCalls.some(call => call[0]?.event_type === "multi_test_2")).toBe(true);
|
|
435
|
+
expect(reestablishCalls.some(call => call[0]?.event_type === "multi_test_3")).toBe(true);
|
|
436
|
+
});
|
|
437
|
+
});
|
|
438
|
+
});
|
|
439
|
+
});
|
|
215
440
|
});
|
package/src/user.mts
CHANGED
|
@@ -50,12 +50,26 @@ export interface HassThemeMapping {
|
|
|
50
50
|
// "{theme_name}": true | "light" | "dark" | "light" | "dark"
|
|
51
51
|
}
|
|
52
52
|
|
|
53
|
+
export interface HassConfigEntryMapping {
|
|
54
|
+
// "{entry_id}": { title: string; domain: string }
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// Reverse lookup: find entry_id by title
|
|
58
|
+
export type FindEntryIdByTitle<
|
|
59
|
+
TITLE extends HassConfigEntryMapping[keyof HassConfigEntryMapping]["title"],
|
|
60
|
+
> = {
|
|
61
|
+
[K in keyof HassConfigEntryMapping]: HassConfigEntryMapping[K]["title"] extends TITLE ? K : never;
|
|
62
|
+
}[keyof HassConfigEntryMapping];
|
|
63
|
+
|
|
64
|
+
export type TConfigEntryTitle = HassConfigEntryMapping[keyof HassConfigEntryMapping]["title"];
|
|
65
|
+
|
|
53
66
|
// #MARK: extract
|
|
54
67
|
export type TAreaId = UnPrefix<keyof HassAreaMapping>;
|
|
55
68
|
export type TDeviceId = UnPrefix<keyof HassDeviceMapping>;
|
|
56
69
|
export type TFloorId = UnPrefix<keyof HassFloorMapping>;
|
|
57
70
|
export type TLabelId = UnPrefix<keyof HassLabelMapping>;
|
|
58
71
|
export type TPlatformId = UnPrefix<keyof HassPlatformMapping>;
|
|
72
|
+
export type TConfigEntryId = keyof HassConfigEntryMapping;
|
|
59
73
|
|
|
60
74
|
export type TRawDomains = keyof HassDomainMapping;
|
|
61
75
|
export type TZoneId = keyof HassZoneMapping;
|