@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
|
@@ -290,19 +290,20 @@ export function EntityManager({
|
|
|
290
290
|
}
|
|
291
291
|
|
|
292
292
|
// #MARK: onConnect
|
|
293
|
+
void hass.socket.subscribe({
|
|
294
|
+
context,
|
|
295
|
+
event_type: "entity_registry_updated",
|
|
296
|
+
async exec() {
|
|
297
|
+
const ms = perf();
|
|
298
|
+
await debounce(ENTITY_REGISTRY_UPDATED, config.hass.EVENT_DEBOUNCE_MS);
|
|
299
|
+
logger.debug("entity registry updated");
|
|
300
|
+
hass.entity.registry.current = await hass.entity.registry.list();
|
|
301
|
+
event.emit(ENTITY_REGISTRY_UPDATED);
|
|
302
|
+
hass.diagnostics.entity?.registry_updated.publish({ ms: ms() });
|
|
303
|
+
},
|
|
304
|
+
});
|
|
305
|
+
|
|
293
306
|
hass.socket.onConnect(async () => {
|
|
294
|
-
hass.socket.subscribe({
|
|
295
|
-
context,
|
|
296
|
-
event_type: "entity_registry_updated",
|
|
297
|
-
async exec() {
|
|
298
|
-
const ms = perf();
|
|
299
|
-
await debounce(ENTITY_REGISTRY_UPDATED, config.hass.EVENT_DEBOUNCE_MS);
|
|
300
|
-
logger.debug("entity registry updated");
|
|
301
|
-
hass.entity.registry.current = await hass.entity.registry.list();
|
|
302
|
-
event.emit(ENTITY_REGISTRY_UPDATED);
|
|
303
|
-
hass.diagnostics.entity?.registry_updated.publish({ ms: ms() });
|
|
304
|
-
},
|
|
305
|
-
});
|
|
306
307
|
hass.entity.registry.current = await hass.entity.registry.list();
|
|
307
308
|
});
|
|
308
309
|
|
|
@@ -13,6 +13,19 @@ export function Floor({
|
|
|
13
13
|
logger,
|
|
14
14
|
lifecycle,
|
|
15
15
|
}: TServiceParams): HassFloorService {
|
|
16
|
+
void hass.socket.subscribe({
|
|
17
|
+
context,
|
|
18
|
+
event_type: "floor_registry_updated",
|
|
19
|
+
async exec() {
|
|
20
|
+
const ms = perf();
|
|
21
|
+
await debounce(FLOOR_REGISTRY_UPDATED, config.hass.EVENT_DEBOUNCE_MS);
|
|
22
|
+
hass.floor.current = await hass.floor.list();
|
|
23
|
+
logger.debug(`floor registry updated`);
|
|
24
|
+
event.emit(FLOOR_REGISTRY_UPDATED);
|
|
25
|
+
hass.diagnostics.floor?.registry_update.publish({ ms: ms() });
|
|
26
|
+
},
|
|
27
|
+
});
|
|
28
|
+
|
|
16
29
|
hass.socket.onConnect(async () => {
|
|
17
30
|
let loading = new Promise<void>(async done => {
|
|
18
31
|
hass.floor.current = await hass.floor.list();
|
|
@@ -20,19 +33,6 @@ export function Floor({
|
|
|
20
33
|
done();
|
|
21
34
|
});
|
|
22
35
|
lifecycle.onReady(async () => loading && (await loading), EARLY_ON_READY);
|
|
23
|
-
|
|
24
|
-
hass.socket.subscribe({
|
|
25
|
-
context,
|
|
26
|
-
event_type: "floor_registry_updated",
|
|
27
|
-
async exec() {
|
|
28
|
-
const ms = perf();
|
|
29
|
-
await debounce(FLOOR_REGISTRY_UPDATED, config.hass.EVENT_DEBOUNCE_MS);
|
|
30
|
-
hass.floor.current = await hass.floor.list();
|
|
31
|
-
logger.debug(`floor registry updated`);
|
|
32
|
-
event.emit(FLOOR_REGISTRY_UPDATED);
|
|
33
|
-
hass.diagnostics.floor?.registry_update.publish({ ms: ms() });
|
|
34
|
-
},
|
|
35
|
-
});
|
|
36
36
|
});
|
|
37
37
|
|
|
38
38
|
return {
|
package/src/services/index.mts
CHANGED
|
@@ -3,6 +3,7 @@ export * from "./area.service.mts";
|
|
|
3
3
|
export * from "./backup.service.mts";
|
|
4
4
|
export * from "./call-proxy.service.mts";
|
|
5
5
|
export * from "./config.service.mts";
|
|
6
|
+
export * from "./conversation.service.mts";
|
|
6
7
|
export * from "./device.service.mts";
|
|
7
8
|
export * from "./diagnostics.service.mts";
|
|
8
9
|
export * from "./entity.service.mts";
|
|
@@ -13,6 +13,19 @@ export function Label({
|
|
|
13
13
|
event,
|
|
14
14
|
context,
|
|
15
15
|
}: TServiceParams): HassLabelService {
|
|
16
|
+
void hass.socket.subscribe({
|
|
17
|
+
context,
|
|
18
|
+
event_type: "label_registry_updated",
|
|
19
|
+
async exec() {
|
|
20
|
+
const ms = perf();
|
|
21
|
+
await debounce(LABEL_REGISTRY_UPDATED, config.hass.EVENT_DEBOUNCE_MS);
|
|
22
|
+
hass.label.current = await hass.label.list();
|
|
23
|
+
logger.debug(`label registry updated`);
|
|
24
|
+
event.emit(LABEL_REGISTRY_UPDATED);
|
|
25
|
+
hass.diagnostics.label?.registry_update.publish({ ms: ms() });
|
|
26
|
+
},
|
|
27
|
+
});
|
|
28
|
+
|
|
16
29
|
hass.socket.onConnect(async () => {
|
|
17
30
|
let loading = new Promise<void>(async done => {
|
|
18
31
|
hass.label.current = await hass.label.list();
|
|
@@ -20,19 +33,6 @@ export function Label({
|
|
|
20
33
|
done();
|
|
21
34
|
});
|
|
22
35
|
lifecycle.onReady(async () => loading && (await loading), EARLY_ON_READY);
|
|
23
|
-
|
|
24
|
-
hass.socket.subscribe({
|
|
25
|
-
context,
|
|
26
|
-
event_type: "label_registry_updated",
|
|
27
|
-
async exec() {
|
|
28
|
-
const ms = perf();
|
|
29
|
-
await debounce(LABEL_REGISTRY_UPDATED, config.hass.EVENT_DEBOUNCE_MS);
|
|
30
|
-
hass.label.current = await hass.label.list();
|
|
31
|
-
logger.debug(`label registry updated`);
|
|
32
|
-
event.emit(LABEL_REGISTRY_UPDATED);
|
|
33
|
-
hass.diagnostics.label?.registry_update.publish({ ms: ms() });
|
|
34
|
-
},
|
|
35
|
-
});
|
|
36
36
|
});
|
|
37
37
|
|
|
38
38
|
async function create(details: LabelOptions) {
|
|
@@ -1,7 +1,7 @@
|
|
|
1
|
+
/* eslint-disable sonarjs/deprecation */
|
|
1
2
|
import type { TServiceParams } from "@digital-alchemy/core";
|
|
2
3
|
|
|
3
4
|
import type {
|
|
4
|
-
ConfigEntry,
|
|
5
5
|
HassConfig,
|
|
6
6
|
HassRegistryService,
|
|
7
7
|
ManifestItem,
|
|
@@ -29,10 +29,11 @@ export function Registry({ hass }: TServiceParams): HassRegistryService {
|
|
|
29
29
|
});
|
|
30
30
|
}
|
|
31
31
|
|
|
32
|
+
/**
|
|
33
|
+
* @deprecated Use hass.configure.get() instead. This method will be removed in a future version.
|
|
34
|
+
*/
|
|
32
35
|
async function getConfigEntries() {
|
|
33
|
-
return await hass.
|
|
34
|
-
type: "config_entries/get",
|
|
35
|
-
});
|
|
36
|
+
return await hass.configure.get();
|
|
36
37
|
}
|
|
37
38
|
|
|
38
39
|
return {
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { TBlackHole, TServiceParams } from "@digital-alchemy/core";
|
|
2
|
-
import { InternalError, SECOND, sleep, START } from "@digital-alchemy/core";
|
|
2
|
+
import { INCREMENT, InternalError, NONE, SECOND, sleep, START } from "@digital-alchemy/core";
|
|
3
3
|
import type { Dayjs } from "dayjs";
|
|
4
4
|
import dayjs from "dayjs";
|
|
5
5
|
import EventEmitter from "events";
|
|
@@ -41,6 +41,7 @@ export function WebsocketAPI({
|
|
|
41
41
|
lifecycle,
|
|
42
42
|
logger,
|
|
43
43
|
scheduler,
|
|
44
|
+
als,
|
|
44
45
|
}: TServiceParams): HassWebsocketAPI {
|
|
45
46
|
const { is } = internal.utils;
|
|
46
47
|
|
|
@@ -57,6 +58,10 @@ export function WebsocketAPI({
|
|
|
57
58
|
const isOld = (date: Dayjs) =>
|
|
58
59
|
is.undefined(date) || date.diff(dayjs(), "s") >= config.hass.RETRY_INTERVAL;
|
|
59
60
|
|
|
61
|
+
// Track all subscriptions for re-establishment on reconnect
|
|
62
|
+
// Map of event_type to count of active subscriptions
|
|
63
|
+
const subscriptionRegistry = new Map<string, number>();
|
|
64
|
+
|
|
60
65
|
// Start the socket
|
|
61
66
|
lifecycle.onBootstrap(async () => {
|
|
62
67
|
logger.debug({ name: "onBootstrap" }, `auto starting connection`);
|
|
@@ -298,7 +303,7 @@ export function WebsocketAPI({
|
|
|
298
303
|
callback: (message: T) => TBlackHole,
|
|
299
304
|
) {
|
|
300
305
|
const handlers = messageHandlers.get(type) ?? [];
|
|
301
|
-
logger.trace(
|
|
306
|
+
logger.trace("register socket message handler [%s]", type);
|
|
302
307
|
if (!messageHandlers.has(type)) {
|
|
303
308
|
messageHandlers.set(type, []);
|
|
304
309
|
}
|
|
@@ -504,18 +509,73 @@ export function WebsocketAPI({
|
|
|
504
509
|
EVENT extends string,
|
|
505
510
|
PAYLOAD extends Record<string, unknown> = EmptyObject,
|
|
506
511
|
>({ event_type, context, exec }: SocketSubscribeOptions<EVENT, PAYLOAD>) {
|
|
507
|
-
|
|
508
|
-
|
|
512
|
+
// Memory leak detection: warn if subscribe is called during onConnect using ALS
|
|
513
|
+
const store = als.getStore();
|
|
514
|
+
if (store?.logs?.hassOnConnect) {
|
|
515
|
+
logger.warn(
|
|
516
|
+
{ context, event_type, name: subscribe },
|
|
517
|
+
`⚠️ MEMORY LEAK DETECTED: subscribe() called during onConnect callback. ` +
|
|
518
|
+
`This will create duplicate subscriptions on each reconnect. ` +
|
|
519
|
+
`Move subscription to root level using lifecycle hooks instead.`,
|
|
520
|
+
);
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
const current = subscriptionRegistry.get(event_type) ?? NONE;
|
|
524
|
+
if (current == NONE && hass.socket.connectionState === "connected") {
|
|
525
|
+
await hass.socket.sendMessage({ event_type, type: "subscribe_events" });
|
|
526
|
+
}
|
|
527
|
+
subscriptionRegistry.set(event_type, current + INCREMENT);
|
|
528
|
+
|
|
529
|
+
// Register event handler
|
|
530
|
+
const { remove } = hass.socket.onEvent({
|
|
509
531
|
context,
|
|
510
532
|
event: event_type,
|
|
511
533
|
exec,
|
|
512
534
|
});
|
|
535
|
+
|
|
536
|
+
return internal.removeFn(() => {
|
|
537
|
+
remove();
|
|
538
|
+
const current = subscriptionRegistry.get(event_type) - INCREMENT;
|
|
539
|
+
if (current === NONE) {
|
|
540
|
+
subscriptionRegistry.delete(event_type);
|
|
541
|
+
return;
|
|
542
|
+
}
|
|
543
|
+
subscriptionRegistry.set(event_type, current);
|
|
544
|
+
});
|
|
513
545
|
}
|
|
514
546
|
|
|
515
547
|
// #MARK: onConnect
|
|
516
548
|
function onConnect(callback: () => TBlackHole) {
|
|
517
549
|
const wrapped = async () => {
|
|
518
|
-
|
|
550
|
+
// Get current store or create new one
|
|
551
|
+
const currentStore = als.getStore();
|
|
552
|
+
const baseData = currentStore || { logs: {} };
|
|
553
|
+
|
|
554
|
+
// Set ALS flag to indicate we're in a connect callback
|
|
555
|
+
als.enterWith({
|
|
556
|
+
...baseData,
|
|
557
|
+
logs: {
|
|
558
|
+
...baseData.logs,
|
|
559
|
+
hassOnConnect: true,
|
|
560
|
+
},
|
|
561
|
+
});
|
|
562
|
+
|
|
563
|
+
try {
|
|
564
|
+
await internal.safeExec(async () => await callback());
|
|
565
|
+
} finally {
|
|
566
|
+
// Restore previous ALS context (or clear if none)
|
|
567
|
+
if (currentStore) {
|
|
568
|
+
als.enterWith({
|
|
569
|
+
...currentStore,
|
|
570
|
+
logs: {
|
|
571
|
+
...currentStore.logs,
|
|
572
|
+
hassOnConnect: false,
|
|
573
|
+
},
|
|
574
|
+
});
|
|
575
|
+
} else {
|
|
576
|
+
als.enterWith({ logs: {} });
|
|
577
|
+
}
|
|
578
|
+
}
|
|
519
579
|
};
|
|
520
580
|
if (hass.socket.connectionState === "connected") {
|
|
521
581
|
logger.debug(
|
|
@@ -544,6 +604,27 @@ export function WebsocketAPI({
|
|
|
544
604
|
event.emit(SOCKET_CONNECTED);
|
|
545
605
|
});
|
|
546
606
|
|
|
607
|
+
// Re-establish all registered subscriptions once per connection
|
|
608
|
+
hass.socket.onConnect(async () => {
|
|
609
|
+
if (is.empty(subscriptionRegistry)) {
|
|
610
|
+
return;
|
|
611
|
+
}
|
|
612
|
+
const subscriptions = [...subscriptionRegistry.keys()];
|
|
613
|
+
logger.trace(
|
|
614
|
+
{ name: onConnect, subscriptions },
|
|
615
|
+
`re-establishing [%s] subscriptions`,
|
|
616
|
+
subscriptionRegistry.size,
|
|
617
|
+
);
|
|
618
|
+
await Promise.all(
|
|
619
|
+
subscriptions.map(event_type =>
|
|
620
|
+
hass.socket.sendMessage({
|
|
621
|
+
event_type,
|
|
622
|
+
type: "subscribe_events",
|
|
623
|
+
}),
|
|
624
|
+
),
|
|
625
|
+
);
|
|
626
|
+
});
|
|
627
|
+
|
|
547
628
|
hass.socket.registerMessageHandler("event", async (message: SocketMessageDTO) => {
|
|
548
629
|
const id = Number(message.id);
|
|
549
630
|
return await onMessageEvent(id, message);
|
|
@@ -12,6 +12,19 @@ export function Zone({
|
|
|
12
12
|
context,
|
|
13
13
|
lifecycle,
|
|
14
14
|
}: TServiceParams): HassZoneService {
|
|
15
|
+
void hass.socket.subscribe({
|
|
16
|
+
context,
|
|
17
|
+
event_type: "zone_registry_updated",
|
|
18
|
+
async exec() {
|
|
19
|
+
const ms = perf();
|
|
20
|
+
await debounce(ZONE_REGISTRY_UPDATED, config.hass.EVENT_DEBOUNCE_MS);
|
|
21
|
+
hass.zone.current = await hass.zone.list();
|
|
22
|
+
logger.debug(`zone registry updated`);
|
|
23
|
+
event.emit(ZONE_REGISTRY_UPDATED);
|
|
24
|
+
hass.diagnostics.zone?.registry_update.publish({ ms: ms() });
|
|
25
|
+
},
|
|
26
|
+
});
|
|
27
|
+
|
|
15
28
|
hass.socket.onConnect(async () => {
|
|
16
29
|
let loading = new Promise<void>(async done => {
|
|
17
30
|
hass.zone.current = await hass.zone.list();
|
|
@@ -19,19 +32,6 @@ export function Zone({
|
|
|
19
32
|
done();
|
|
20
33
|
});
|
|
21
34
|
lifecycle.onReady(async () => loading && (await loading), EARLY_ON_READY);
|
|
22
|
-
|
|
23
|
-
hass.socket.subscribe({
|
|
24
|
-
context,
|
|
25
|
-
event_type: "zone_registry_updated",
|
|
26
|
-
async exec() {
|
|
27
|
-
const ms = perf();
|
|
28
|
-
await debounce(ZONE_REGISTRY_UPDATED, config.hass.EVENT_DEBOUNCE_MS);
|
|
29
|
-
hass.zone.current = await hass.zone.list();
|
|
30
|
-
logger.debug(`zone registry updated`);
|
|
31
|
-
event.emit(ZONE_REGISTRY_UPDATED);
|
|
32
|
-
hass.diagnostics.zone?.registry_update.publish({ ms: ms() });
|
|
33
|
-
},
|
|
34
|
-
});
|
|
35
35
|
});
|
|
36
36
|
|
|
37
37
|
async function ZoneCreate(options: ZoneOptions) {
|