@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.
- package/README.md +1 -1
- package/dist/helpers/notify.helper.d.ts +2 -2
- package/package.json +16 -14
- package/scripts/mock-assistant.sh +5 -0
- package/scripts/run-e2e.sh +7 -0
- package/scripts/test.sh +2 -0
- package/src/dynamic.ts +4254 -0
- package/src/extensions/area.extension.ts +118 -0
- package/src/extensions/backup.extension.ts +63 -0
- package/src/extensions/call-proxy.extension.ts +113 -0
- package/src/extensions/config.extension.ts +119 -0
- package/src/extensions/conversation.extension.ts +46 -0
- package/src/extensions/device.extension.ts +56 -0
- package/src/extensions/entity.extension.ts +344 -0
- package/src/extensions/events.extension.ts +25 -0
- package/src/extensions/fetch-api.extension.ts +269 -0
- package/src/extensions/floor.extension.ts +76 -0
- package/src/extensions/id-by.extension.ts +157 -0
- package/src/extensions/index.ts +16 -0
- package/src/extensions/internal.extension.ts +145 -0
- package/src/extensions/label.extension.ts +83 -0
- package/src/extensions/reference.extension.ts +330 -0
- package/src/extensions/registry.extension.ts +44 -0
- package/src/extensions/websocket-api.extension.ts +554 -0
- package/src/extensions/zone.extension.ts +69 -0
- package/src/hass.module.ts +217 -0
- package/src/helpers/backup.helper.ts +11 -0
- package/src/helpers/constants.helper.ts +30 -0
- package/src/helpers/device.helper.ts +25 -0
- package/src/helpers/entity-state.helper.ts +171 -0
- package/src/helpers/features.helper.ts +580 -0
- package/src/helpers/fetch/calendar.ts +54 -0
- package/src/helpers/fetch/configuration.ts +75 -0
- package/src/helpers/fetch/index.ts +5 -0
- package/src/helpers/fetch/server-log.ts +28 -0
- package/src/helpers/fetch/service-list.ts +64 -0
- package/src/helpers/fetch/weather-forecasts.ts +86 -0
- package/src/helpers/fetch.helper.ts +328 -0
- package/src/helpers/id-by.helper.ts +53 -0
- package/src/helpers/index.ts +13 -0
- package/src/helpers/interfaces.helper.ts +340 -0
- package/src/helpers/manifest.helper.ts +0 -0
- package/src/helpers/notify.helper.ts +302 -0
- package/src/helpers/registry.ts +281 -0
- package/src/helpers/utility.helper.ts +147 -0
- package/src/helpers/websocket.helper.ts +117 -0
- package/src/index.ts +5 -0
- package/src/mock_assistant/extensions/area.extension.ts +62 -0
- package/src/mock_assistant/extensions/config.extension.ts +33 -0
- package/src/mock_assistant/extensions/device.extension.ts +44 -0
- package/src/mock_assistant/extensions/entity-registry.extension.ts +41 -0
- package/src/mock_assistant/extensions/entity.extension.ts +114 -0
- package/src/mock_assistant/extensions/events.extension.ts +37 -0
- package/src/mock_assistant/extensions/fetch.extension.ts +3 -0
- package/src/mock_assistant/extensions/fixtures.extension.ts +79 -0
- package/src/mock_assistant/extensions/floor.extension.ts +64 -0
- package/src/mock_assistant/extensions/index.ts +12 -0
- package/src/mock_assistant/extensions/label.extension.ts +64 -0
- package/src/mock_assistant/extensions/services.extension.ts +25 -0
- package/src/mock_assistant/extensions/websocket-api.extension.ts +84 -0
- package/src/mock_assistant/extensions/zone.extension.ts +65 -0
- package/src/mock_assistant/helpers/fixtures.ts +22 -0
- package/src/mock_assistant/helpers/index.ts +1 -0
- package/src/mock_assistant/index.ts +3 -0
- package/src/mock_assistant/main.ts +46 -0
- package/src/mock_assistant/mock-assistant.module.ts +90 -0
- package/src/quickboot.module.ts +23 -0
- package/src/testing/area.spec.ts +189 -0
- package/src/testing/backup.spec.ts +157 -0
- package/src/testing/config.spec.ts +188 -0
- package/src/testing/device.spec.ts +89 -0
- package/src/testing/entity.spec.ts +171 -0
- package/src/testing/events.spec.ts +78 -0
- package/src/testing/fetch-api.spec.ts +410 -0
- package/src/testing/fixtures.spec.ts +158 -0
- package/src/testing/floor.spec.ts +186 -0
- package/src/testing/id-by.spec.ts +140 -0
- package/src/testing/label.spec.ts +186 -0
- package/src/testing/ref-by.spec.ts +300 -0
- package/src/testing/websocket.spec.ts +63 -0
- package/src/testing/workflow.spec.ts +195 -0
- 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,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,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());
|