@digital-alchemy/hass 24.9.4 → 24.9.5

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 (82) hide show
  1. package/README.md +1 -1
  2. package/dist/helpers/notify.helper.d.ts +2 -2
  3. package/package.json +16 -14
  4. package/scripts/mock-assistant.sh +5 -0
  5. package/scripts/run-e2e.sh +7 -0
  6. package/scripts/test.sh +2 -0
  7. package/src/dynamic.ts +4254 -0
  8. package/src/extensions/area.extension.ts +118 -0
  9. package/src/extensions/backup.extension.ts +63 -0
  10. package/src/extensions/call-proxy.extension.ts +113 -0
  11. package/src/extensions/config.extension.ts +119 -0
  12. package/src/extensions/conversation.extension.ts +46 -0
  13. package/src/extensions/device.extension.ts +56 -0
  14. package/src/extensions/entity.extension.ts +344 -0
  15. package/src/extensions/events.extension.ts +25 -0
  16. package/src/extensions/fetch-api.extension.ts +269 -0
  17. package/src/extensions/floor.extension.ts +76 -0
  18. package/src/extensions/id-by.extension.ts +157 -0
  19. package/src/extensions/index.ts +16 -0
  20. package/src/extensions/internal.extension.ts +145 -0
  21. package/src/extensions/label.extension.ts +83 -0
  22. package/src/extensions/reference.extension.ts +330 -0
  23. package/src/extensions/registry.extension.ts +44 -0
  24. package/src/extensions/websocket-api.extension.ts +554 -0
  25. package/src/extensions/zone.extension.ts +69 -0
  26. package/src/hass.module.ts +217 -0
  27. package/src/helpers/backup.helper.ts +11 -0
  28. package/src/helpers/constants.helper.ts +30 -0
  29. package/src/helpers/device.helper.ts +25 -0
  30. package/src/helpers/entity-state.helper.ts +171 -0
  31. package/src/helpers/features.helper.ts +580 -0
  32. package/src/helpers/fetch/calendar.ts +54 -0
  33. package/src/helpers/fetch/configuration.ts +75 -0
  34. package/src/helpers/fetch/index.ts +5 -0
  35. package/src/helpers/fetch/server-log.ts +28 -0
  36. package/src/helpers/fetch/service-list.ts +64 -0
  37. package/src/helpers/fetch/weather-forecasts.ts +86 -0
  38. package/src/helpers/fetch.helper.ts +328 -0
  39. package/src/helpers/id-by.helper.ts +53 -0
  40. package/src/helpers/index.ts +13 -0
  41. package/src/helpers/interfaces.helper.ts +340 -0
  42. package/src/helpers/manifest.helper.ts +0 -0
  43. package/src/helpers/notify.helper.ts +302 -0
  44. package/src/helpers/registry.ts +281 -0
  45. package/src/helpers/utility.helper.ts +147 -0
  46. package/src/helpers/websocket.helper.ts +117 -0
  47. package/src/index.ts +5 -0
  48. package/src/mock_assistant/extensions/area.extension.ts +62 -0
  49. package/src/mock_assistant/extensions/config.extension.ts +33 -0
  50. package/src/mock_assistant/extensions/device.extension.ts +44 -0
  51. package/src/mock_assistant/extensions/entity-registry.extension.ts +41 -0
  52. package/src/mock_assistant/extensions/entity.extension.ts +114 -0
  53. package/src/mock_assistant/extensions/events.extension.ts +37 -0
  54. package/src/mock_assistant/extensions/fetch.extension.ts +3 -0
  55. package/src/mock_assistant/extensions/fixtures.extension.ts +79 -0
  56. package/src/mock_assistant/extensions/floor.extension.ts +64 -0
  57. package/src/mock_assistant/extensions/index.ts +12 -0
  58. package/src/mock_assistant/extensions/label.extension.ts +64 -0
  59. package/src/mock_assistant/extensions/services.extension.ts +25 -0
  60. package/src/mock_assistant/extensions/websocket-api.extension.ts +84 -0
  61. package/src/mock_assistant/extensions/zone.extension.ts +65 -0
  62. package/src/mock_assistant/helpers/fixtures.ts +22 -0
  63. package/src/mock_assistant/helpers/index.ts +1 -0
  64. package/src/mock_assistant/index.ts +3 -0
  65. package/src/mock_assistant/main.ts +46 -0
  66. package/src/mock_assistant/mock-assistant.module.ts +90 -0
  67. package/src/quickboot.module.ts +23 -0
  68. package/src/testing/area.spec.ts +189 -0
  69. package/src/testing/backup.spec.ts +157 -0
  70. package/src/testing/config.spec.ts +188 -0
  71. package/src/testing/device.spec.ts +89 -0
  72. package/src/testing/entity.spec.ts +171 -0
  73. package/src/testing/events.spec.ts +78 -0
  74. package/src/testing/fetch-api.spec.ts +410 -0
  75. package/src/testing/fixtures.spec.ts +158 -0
  76. package/src/testing/floor.spec.ts +186 -0
  77. package/src/testing/id-by.spec.ts +140 -0
  78. package/src/testing/label.spec.ts +186 -0
  79. package/src/testing/ref-by.spec.ts +300 -0
  80. package/src/testing/websocket.spec.ts +63 -0
  81. package/src/testing/workflow.spec.ts +195 -0
  82. package/src/testing/zone.spec.ts +109 -0
@@ -0,0 +1,41 @@
1
+ import { TServiceParams } from "@digital-alchemy/core";
2
+
3
+ import { TRawEntityIds } from "../../dynamic";
4
+ import { EntityRegistryItem } from "../../helpers";
5
+
6
+ export function MockEntityRegistryExtension({ mock_assistant, hass }: TServiceParams) {
7
+ let entityRegistry = new Map<TRawEntityIds, EntityRegistryItem<TRawEntityIds>>();
8
+
9
+ hass.entity.registry.list = async () => [...entityRegistry.values()];
10
+
11
+ const sendUpdate = () =>
12
+ mock_assistant.socket.sendMessage({
13
+ event: { event_type: "entity_registry_updated" },
14
+ type: "event",
15
+ });
16
+
17
+ mock_assistant.socket.onMessage<{ entity_id: TRawEntityIds }>(
18
+ "config/entity_registry/get",
19
+ message => {
20
+ mock_assistant.socket.sendMessage({
21
+ id: message.id,
22
+ result: entityRegistry.get(message.entity_id),
23
+ type: "result",
24
+ });
25
+ },
26
+ );
27
+
28
+ return {
29
+ /**
30
+ * does not imply sendUpdate
31
+ */
32
+ loadFixtures(incoming: EntityRegistryItem<TRawEntityIds>[]) {
33
+ entityRegistry = new Map(incoming.map(i => [i.entity_id, i]));
34
+ },
35
+
36
+ /**
37
+ * emit entity_registry_updated
38
+ */
39
+ sendUpdate,
40
+ };
41
+ }
@@ -0,0 +1,114 @@
1
+ import { deepExtend, InternalError, is, sleep, TServiceParams } from "@digital-alchemy/core";
2
+
3
+ import { TRawEntityIds } from "../../dynamic";
4
+ import { ENTITY_STATE, PICK_ENTITY } from "../../helpers";
5
+
6
+ export function MockEntityExtension({
7
+ hass,
8
+ internal,
9
+ context,
10
+ logger,
11
+ config,
12
+ mock_assistant,
13
+ }: TServiceParams) {
14
+ let entities = new Map<TRawEntityIds, ENTITY_STATE<TRawEntityIds>>();
15
+
16
+ const origGetAll = hass.fetch.getAllEntities;
17
+
18
+ hass.fetch.getAllEntities = async () => [...entities.values()];
19
+
20
+ function setupState(incoming: SetupStateOptions) {
21
+ if (internal.boot.completedLifecycleEvents.has("PreInit")) {
22
+ logger.error(`run [setupState] as part of the .setup command of your test`);
23
+ throw new InternalError(context, "LATE_SETUP", "Must call setupState before preInit");
24
+ }
25
+ const list = Object.keys(incoming) as PICK_ENTITY[];
26
+ list.forEach((key: PICK_ENTITY) => {
27
+ const data = entities.get(key);
28
+ entities.set(key, {
29
+ ...data,
30
+ state: incoming[key].state,
31
+ });
32
+ });
33
+ }
34
+
35
+ async function emitChange<ENTITY extends PICK_ENTITY>(
36
+ entity: ENTITY,
37
+ update: PartialUpdate<ENTITY>,
38
+ ) {
39
+ const old_state = entities.get(entity);
40
+ if (hass.socket.connectionState !== "connected") {
41
+ throw new InternalError(context, "EARLY_CHANGE", "Websocket does not identify as connected");
42
+ }
43
+ if (!old_state) {
44
+ throw new InternalError(
45
+ context,
46
+ "MISSING_ENTITY",
47
+ "Cannot find existing entity for old_state",
48
+ );
49
+ }
50
+ const new_state = deepExtend({}, old_state);
51
+ if ("state" in update) {
52
+ new_state.state = update.state;
53
+ }
54
+ if (!is.empty(update.attributes)) {
55
+ new_state.attributes = deepExtend(new_state.attributes, update.attributes);
56
+ }
57
+
58
+ mock_assistant.socket.sendMessage({
59
+ event: {
60
+ data: { new_state, old_state },
61
+ event_type: "state_changed",
62
+ },
63
+ type: "event",
64
+ });
65
+
66
+ // allow changes to propagate properly
67
+ await sleep(config.mock_assistant.EMIT_SLEEP);
68
+ }
69
+
70
+ return {
71
+ /**
72
+ *
73
+ */
74
+ emitChange,
75
+
76
+ /**
77
+ * @internal
78
+ */
79
+ loadFixtures(incoming: ENTITY_STATE<TRawEntityIds>[]) {
80
+ if (!is.empty(entities)) {
81
+ // this should not be possible, the dependency resolution order of tests SHOULD prevent
82
+ // if you get this error, let me know how
83
+ throw new InternalError(
84
+ context,
85
+ "FIXTURES_ALREADY_LOADED",
86
+ "There is data in the entity fixtures already, order of operations wrong",
87
+ );
88
+ }
89
+ entities = new Map(incoming.map(i => [i.entity_id, i]));
90
+ },
91
+
92
+ /**
93
+ * @internal
94
+ *
95
+ * restores code references, only used for testing internals
96
+ */
97
+ monkeyReset() {
98
+ hass.fetch.getAllEntities = origGetAll;
99
+ },
100
+
101
+ /**
102
+ * Does not emit update event
103
+ *
104
+ * Intended for test setup
105
+ */
106
+ setupState,
107
+ };
108
+ }
109
+
110
+ type PartialUpdate<ENTITY extends PICK_ENTITY> = Partial<
111
+ Pick<ENTITY_STATE<ENTITY>, "state" | "attributes">
112
+ >;
113
+
114
+ type SetupStateOptions = Partial<{ [ENTITY in PICK_ENTITY]: PartialUpdate<ENTITY> }>;
@@ -0,0 +1,37 @@
1
+ import { sleep, TServiceParams } from "@digital-alchemy/core";
2
+
3
+ import { ANY_ENTITY, ENTITY_STATE, EntityUpdateEvent } from "../../helpers";
4
+
5
+ const SUPER_SHORT = 1;
6
+
7
+ export function MockEvents({ mock_assistant, hass }: TServiceParams) {
8
+ let id = 1000;
9
+
10
+ async function emitEvent(event: string, data: object) {
11
+ id++;
12
+ await hass.socket.onMessage({
13
+ event: {
14
+ data,
15
+ event_type: event,
16
+ } as EntityUpdateEvent,
17
+ id: id,
18
+ type: "event",
19
+ });
20
+ }
21
+
22
+ async function emitEntityUpdate<ENTITY extends ANY_ENTITY>(
23
+ entity: ENTITY,
24
+ new_state: Partial<ENTITY_STATE<ENTITY>>,
25
+ ) {
26
+ const old_state = mock_assistant.fixtures.byId(entity);
27
+ new_state = mock_assistant.fixtures.replace(entity, new_state);
28
+ await emitEvent("state_changed", { new_state, old_state });
29
+ // help ensure all the async flows settle
30
+ await sleep(SUPER_SHORT);
31
+ }
32
+
33
+ return {
34
+ emitEntityUpdate,
35
+ emitEvent,
36
+ };
37
+ }
@@ -0,0 +1,3 @@
1
+ export function MockFetchExtension() {
2
+ //
3
+ }
@@ -0,0 +1,79 @@
1
+ import { BootstrapException, is, TServiceParams } from "@digital-alchemy/core";
2
+ import { existsSync, readFileSync } from "fs";
3
+
4
+ import { ANY_ENTITY, ENTITY_STATE } from "../../helpers";
5
+ import { ScannerCacheData } from "../helpers";
6
+
7
+ type StateOptions = Partial<{
8
+ [entity in ANY_ENTITY]: Partial<ENTITY_STATE<entity>>;
9
+ }>;
10
+
11
+ // this naming pattern is confusing sometimes
12
+ // don't think about it too much
13
+ export function MockFixtures({
14
+ lifecycle,
15
+ config,
16
+ internal,
17
+ context,
18
+ mock_assistant,
19
+ }: TServiceParams) {
20
+ // This file DELIBERATELY breaks some rules
21
+ // Setup actions that depend on config are not NORMALLY expected to run inside constructor
22
+
23
+ const { FIXTURES_FILE } = config.mock_assistant;
24
+ if (!existsSync(FIXTURES_FILE)) {
25
+ throw new BootstrapException(
26
+ context,
27
+ "MISSING_FIXTURES_FILE",
28
+ `${FIXTURES_FILE} does not exist`,
29
+ );
30
+ }
31
+ if (is.empty(config.hass.TOKEN)) {
32
+ // prevents throwing errors
33
+ internal.boilerplate.configuration.set("hass", "TOKEN", "--");
34
+ }
35
+ const data = JSON.parse(readFileSync(FIXTURES_FILE, "utf8")) as ScannerCacheData;
36
+ mock_assistant.device.loadFixtures(data.devices);
37
+ mock_assistant.floor.loadFixtures(data.floors);
38
+ mock_assistant.area.loadFixtures(data.areas);
39
+ mock_assistant.label.loadFixtures(data.labels);
40
+ mock_assistant.config.loadFixtures(data.config);
41
+ mock_assistant.entity.loadFixtures(data.entities);
42
+ mock_assistant.entity_registry.loadFixtures(data.entity_registry);
43
+ mock_assistant.services.loadFixtures(data.services);
44
+ // TODO zones are not currently included in fixtures
45
+ // more of a completion thing than them having any particular use
46
+ //
47
+ // mock_assistant.zone.set(data.);
48
+
49
+ function setState(options: StateOptions) {
50
+ lifecycle.onPreInit(() => {
51
+ const entities = Object.keys(options) as ANY_ENTITY[];
52
+ entities.forEach(i => replace(i, options[i]));
53
+ });
54
+ }
55
+
56
+ function byId(entity: ANY_ENTITY) {
57
+ return mock_assistant.fixtures.data.entities.find(i => i.entity_id === entity);
58
+ }
59
+
60
+ function replace<ENTITY extends ANY_ENTITY>(
61
+ entity: ENTITY,
62
+ new_state: Partial<ENTITY_STATE<ENTITY>>,
63
+ ): ENTITY_STATE<ENTITY> {
64
+ const old_state = byId(entity);
65
+ const { data } = mock_assistant.fixtures;
66
+ data.entities = data.entities.filter(i => i.entity_id !== entity);
67
+
68
+ const updated = { ...old_state, ...new_state } as ENTITY_STATE<ENTITY>;
69
+ mock_assistant.fixtures.data.entities.push(updated);
70
+ return updated;
71
+ }
72
+
73
+ return {
74
+ byId,
75
+ data,
76
+ replace,
77
+ setState,
78
+ };
79
+ }
@@ -0,0 +1,64 @@
1
+ import { TServiceParams } from "@digital-alchemy/core";
2
+
3
+ import { TFloorId } from "../../dynamic";
4
+ import { FloorDetails } from "../../helpers";
5
+
6
+ export function MockFloorExtension({ mock_assistant }: TServiceParams) {
7
+ let floors = new Map<TFloorId, FloorDetails>();
8
+
9
+ mock_assistant.socket.onMessage("config/floor_registry/list", message => {
10
+ mock_assistant.socket.sendMessage({
11
+ id: message.id,
12
+ result: [...floors.values()],
13
+ type: "result",
14
+ });
15
+ });
16
+
17
+ mock_assistant.socket.onMessage<{ floor_id: TFloorId }>(
18
+ "config/floor_registry/delete",
19
+ message => {
20
+ floors.delete(message.floor_id);
21
+ sendUpdate();
22
+ mock_assistant.socket.sendMessage({
23
+ id: message.id,
24
+ result: null,
25
+ type: "result",
26
+ });
27
+ },
28
+ );
29
+
30
+ mock_assistant.socket.onMessage<FloorDetails>("config/floor_registry/create", message => {
31
+ message.floor_id = message.name as TFloorId;
32
+ floors.set(message.floor_id as TFloorId, message);
33
+ sendUpdate();
34
+ mock_assistant.socket.sendMessage({
35
+ id: message.id,
36
+ result: null,
37
+ type: "result",
38
+ });
39
+ });
40
+ mock_assistant.socket.onMessage<FloorDetails>("config/floor_registry/update", message => {
41
+ floors.set(message.floor_id as TFloorId, message);
42
+ sendUpdate();
43
+ mock_assistant.socket.sendMessage({
44
+ id: message.id,
45
+ result: null,
46
+ type: "result",
47
+ });
48
+ });
49
+
50
+ const sendUpdate = () =>
51
+ mock_assistant.socket.sendMessage({
52
+ event: { event_type: "floor_registry_updated" },
53
+ type: "event",
54
+ });
55
+
56
+ return {
57
+ /**
58
+ * @internal
59
+ */
60
+ loadFixtures(incoming: FloorDetails[]) {
61
+ floors = new Map(incoming.map(i => [i.floor_id, i]));
62
+ },
63
+ };
64
+ }
@@ -0,0 +1,12 @@
1
+ export * from "./area.extension";
2
+ export * from "./config.extension";
3
+ export * from "./device.extension";
4
+ export * from "./entity.extension";
5
+ export * from "./entity-registry.extension";
6
+ export * from "./events.extension";
7
+ export * from "./fixtures.extension";
8
+ export * from "./floor.extension";
9
+ export * from "./label.extension";
10
+ export * from "./services.extension";
11
+ export * from "./websocket-api.extension";
12
+ export * from "./zone.extension";
@@ -0,0 +1,64 @@
1
+ import { TServiceParams } from "@digital-alchemy/core";
2
+
3
+ import { TLabelId } from "../../dynamic";
4
+ import { LabelDefinition } from "../../helpers";
5
+
6
+ export function MockLabelExtension({ mock_assistant }: TServiceParams) {
7
+ let labels = new Map<TLabelId, LabelDefinition>();
8
+
9
+ mock_assistant.socket.onMessage("config/label_registry/list", message => {
10
+ mock_assistant.socket.sendMessage({
11
+ id: message.id,
12
+ result: [...labels.values()],
13
+ type: "result",
14
+ });
15
+ });
16
+
17
+ mock_assistant.socket.onMessage<{ label_id: TLabelId }>(
18
+ "config/label_registry/delete",
19
+ message => {
20
+ labels.delete(message.label_id);
21
+ sendUpdate();
22
+ mock_assistant.socket.sendMessage({
23
+ id: message.id,
24
+ result: null,
25
+ type: "result",
26
+ });
27
+ },
28
+ );
29
+
30
+ mock_assistant.socket.onMessage<LabelDefinition>("config/label_registry/create", message => {
31
+ message.label_id = message.name as TLabelId;
32
+ labels.set(message.label_id as TLabelId, message);
33
+ sendUpdate();
34
+ mock_assistant.socket.sendMessage({
35
+ id: message.id,
36
+ result: null,
37
+ type: "result",
38
+ });
39
+ });
40
+ mock_assistant.socket.onMessage<LabelDefinition>("config/label_registry/update", message => {
41
+ labels.set(message.label_id as TLabelId, message);
42
+ sendUpdate();
43
+ mock_assistant.socket.sendMessage({
44
+ id: message.id,
45
+ result: null,
46
+ type: "result",
47
+ });
48
+ });
49
+
50
+ const sendUpdate = () =>
51
+ mock_assistant.socket.sendMessage({
52
+ event: { event_type: "label_registry_updated" },
53
+ type: "event",
54
+ });
55
+
56
+ return {
57
+ /**
58
+ * @internal
59
+ */
60
+ loadFixtures(incoming: LabelDefinition[]) {
61
+ labels = new Map(incoming.map(i => [i.label_id, i]));
62
+ },
63
+ };
64
+ }
@@ -0,0 +1,25 @@
1
+ import { TServiceParams } from "@digital-alchemy/core";
2
+
3
+ import { HassServiceDTO } from "../../helpers";
4
+
5
+ export function MockServices({ hass }: TServiceParams) {
6
+ let services: HassServiceDTO[];
7
+
8
+ const origList = hass.fetch.listServices;
9
+ hass.fetch.listServices = async () => services;
10
+
11
+ return {
12
+ /**
13
+ * @internal
14
+ */
15
+ loadFixtures(incoming: HassServiceDTO[]) {
16
+ services = incoming;
17
+ },
18
+ /**
19
+ * @internal
20
+ */
21
+ monkeyReset() {
22
+ hass.fetch.listServices = origList;
23
+ },
24
+ };
25
+ }
@@ -0,0 +1,84 @@
1
+ import { START, TBlackHole, TServiceParams } from "@digital-alchemy/core";
2
+ import EventEmitter from "events";
3
+ import { PartialDeep, WritableDeep } from "type-fest";
4
+ import WS from "ws";
5
+
6
+ import { SocketMessageDTO } from "../../helpers";
7
+
8
+ const CONNECTION_CLOSED = 0;
9
+ // const CONNECTION_OPEN = 1;
10
+ // const CONNECTION_FAILED = 2;
11
+ const UNLIMITED = 0;
12
+
13
+ export const INTERNAL_MESSAGE = "INTERNAL_MESSAGE";
14
+
15
+ export function MockWebsocketAPI({ hass, config, lifecycle }: TServiceParams) {
16
+ const connection = new EventEmitter() as WritableDeep<WS>;
17
+ connection.setMaxListeners(UNLIMITED);
18
+ lifecycle.onShutdownStart(() => {
19
+ connection.removeAllListeners();
20
+ });
21
+
22
+ connection.readyState = CONNECTION_CLOSED;
23
+ let id = START;
24
+ connection.close = () => {
25
+ connection.readyState = CONNECTION_CLOSED;
26
+ };
27
+ // connection.send = (...data) =>
28
+
29
+ hass.socket.createConnection = () => {
30
+ setImmediate(() => {
31
+ if (!config.mock_assistant.PASS_AUTH) {
32
+ return;
33
+ }
34
+ sendMessage({ type: "auth_ok" });
35
+ });
36
+ return connection;
37
+ };
38
+
39
+ connection.send = (data: string) => {
40
+ const payload = JSON.parse(data) as { type: string; id: number };
41
+ connection.emit(INTERNAL_MESSAGE, payload);
42
+ switch (payload.type) {
43
+ case "ping": {
44
+ sendMessage({ id: id++, type: "pong" });
45
+ return;
46
+ }
47
+ case "auth": {
48
+ return;
49
+ }
50
+ default: {
51
+ setImmediate(() => {
52
+ sendMessage({
53
+ id: payload.id,
54
+ result: null,
55
+ type: "result",
56
+ });
57
+ });
58
+ }
59
+ }
60
+ };
61
+
62
+ function sendMessage(data: PartialDeep<SocketMessageDTO>) {
63
+ setImmediate(() => {
64
+ connection.emit("message", JSON.stringify(data));
65
+ });
66
+ }
67
+
68
+ return {
69
+ connection: connection as WS,
70
+ onMessage<DATA extends object>(
71
+ type: string,
72
+ callback: (data: DATA & MessageData) => TBlackHole,
73
+ ) {
74
+ connection.on(INTERNAL_MESSAGE, (data: DATA & MessageData) => {
75
+ if (data.type === type) {
76
+ callback(data as DATA & MessageData);
77
+ }
78
+ });
79
+ },
80
+ sendMessage,
81
+ };
82
+ }
83
+
84
+ type MessageData = { id: number; type: string };
@@ -0,0 +1,65 @@
1
+ import { TServiceParams } from "@digital-alchemy/core";
2
+
3
+ import { TZoneId } from "../../dynamic";
4
+ import { ZoneDetails } from "../../helpers";
5
+
6
+ export function MockZoneExtension({ mock_assistant }: TServiceParams) {
7
+ let zones = new Map<TZoneId, ZoneDetails>();
8
+
9
+ mock_assistant.socket.onMessage("config/zone_registry/list", message => {
10
+ mock_assistant.socket.sendMessage({
11
+ id: message.id,
12
+ result: [...zones.values()],
13
+ type: "result",
14
+ });
15
+ });
16
+
17
+ mock_assistant.socket.onMessage<{ zone_id: TZoneId }>("config/zone_registry/delete", message => {
18
+ zones.delete(message.zone_id);
19
+ sendUpdate();
20
+ mock_assistant.socket.sendMessage({
21
+ id: message.id,
22
+ result: null,
23
+ type: "result",
24
+ });
25
+ });
26
+
27
+ mock_assistant.socket.onMessage<ZoneDetails>(
28
+ "config/zone_registry/create",
29
+ (message: ZoneDetails) => {
30
+ message.id = message.name as TZoneId;
31
+ zones.set(message.id as TZoneId, message);
32
+ sendUpdate();
33
+ mock_assistant.socket.sendMessage({
34
+ id: message.id,
35
+ result: null,
36
+ type: "result",
37
+ });
38
+ },
39
+ );
40
+
41
+ mock_assistant.socket.onMessage<ZoneDetails>("config/zone_registry/update", message => {
42
+ zones.set(message.id as TZoneId, message);
43
+ sendUpdate();
44
+ mock_assistant.socket.sendMessage({
45
+ id: message.id,
46
+ result: null,
47
+ type: "result",
48
+ });
49
+ });
50
+
51
+ const sendUpdate = () =>
52
+ mock_assistant.socket.sendMessage({
53
+ event: { event_type: "zone_registry_updated" },
54
+ type: "event",
55
+ });
56
+
57
+ return {
58
+ /**
59
+ * @internal
60
+ */
61
+ loadFixtures(incoming: ZoneDetails[]) {
62
+ zones = new Map(incoming.map(i => [i.id, i]));
63
+ },
64
+ };
65
+ }
@@ -0,0 +1,22 @@
1
+ import {
2
+ ANY_ENTITY,
3
+ AreaDetails,
4
+ DeviceDetails,
5
+ ENTITY_STATE,
6
+ EntityRegistryItem,
7
+ FloorDetails,
8
+ HassConfig,
9
+ HassServiceDTO as HassServiceDefinition,
10
+ LabelDefinition,
11
+ } from "../../helpers";
12
+
13
+ export type ScannerCacheData = {
14
+ areas: AreaDetails[];
15
+ config: HassConfig;
16
+ devices: DeviceDetails[];
17
+ entities: ENTITY_STATE<ANY_ENTITY>[];
18
+ entity_registry: EntityRegistryItem<ANY_ENTITY>[];
19
+ floors: FloorDetails[];
20
+ labels: LabelDefinition[];
21
+ services: HassServiceDefinition[];
22
+ };
@@ -0,0 +1 @@
1
+ export * from "./fixtures";
@@ -0,0 +1,3 @@
1
+ export * from "./extensions";
2
+ export * from "./helpers";
3
+ export * from "./mock-assistant.module";
@@ -0,0 +1,46 @@
1
+ #!/usr/bin/env node
2
+ import { CreateApplication, TServiceParams } from "@digital-alchemy/core";
3
+ import { writeFileSync } from "fs";
4
+ import { join } from "path";
5
+ import { cwd } from "process";
6
+
7
+ import { LIB_HASS } from "..";
8
+ import { ScannerCacheData } from "./helpers";
9
+
10
+ const writeFixtures = CreateApplication({
11
+ configuration: {
12
+ FIXTURES_FILE: {
13
+ default: join(cwd(), "fixtures.json"),
14
+ description: [],
15
+ type: "string",
16
+ },
17
+ },
18
+ libraries: [LIB_HASS],
19
+ name: "mock_assistant",
20
+ services: {
21
+ Write({ hass, lifecycle, config }: TServiceParams) {
22
+ lifecycle.onReady(async () => {
23
+ writeFileSync(
24
+ config.mock_assistant.FIXTURES_FILE,
25
+ JSON.stringify(
26
+ {
27
+ areas: hass.area.current,
28
+ config: await hass.fetch.getConfig(),
29
+ devices: hass.device.current,
30
+ entities: hass.entity.listEntities().map(i => hass.entity.getCurrentState(i)),
31
+ entity_registry: hass.entity.registry.current,
32
+ floors: hass.floor.current,
33
+ labels: hass.label.current,
34
+ services: await hass.fetch.listServices(),
35
+ } as ScannerCacheData,
36
+ undefined,
37
+ " ",
38
+ ),
39
+ "utf8",
40
+ );
41
+ process.exit();
42
+ });
43
+ },
44
+ },
45
+ });
46
+ setImmediate(async () => writeFixtures.bootstrap());