@digital-alchemy/hass 24.9.4 → 24.10.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.
- package/README.md +1 -1
- package/dist/extensions/call-proxy.extension.d.ts +1 -1
- package/dist/extensions/call-proxy.extension.js +4 -1
- package/dist/extensions/call-proxy.extension.js.map +1 -1
- package/dist/extensions/entity.extension.js +3 -0
- package/dist/extensions/entity.extension.js.map +1 -1
- package/dist/extensions/websocket-api.extension.js +5 -11
- package/dist/extensions/websocket-api.extension.js.map +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 +122 -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 +347 -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 +551 -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,89 @@
|
|
|
1
|
+
import { sleep } from "@digital-alchemy/core";
|
|
2
|
+
|
|
3
|
+
import { DeviceDetails } from "../helpers";
|
|
4
|
+
import { hassTestRunner } from "../mock_assistant";
|
|
5
|
+
|
|
6
|
+
describe("Device", () => {
|
|
7
|
+
const EXAMPLE_DEVICE = {
|
|
8
|
+
area_id: null,
|
|
9
|
+
config_entries: ["42816b768aa8697c18c1b6d241112cef"],
|
|
10
|
+
configuration_url: null,
|
|
11
|
+
connections: [],
|
|
12
|
+
disabled_by: null,
|
|
13
|
+
entry_type: "service",
|
|
14
|
+
hw_version: null,
|
|
15
|
+
id: "e328cb3f7ec4e37b3b102374b05c37a9",
|
|
16
|
+
identifiers: [["hassio", "core"]],
|
|
17
|
+
labels: [],
|
|
18
|
+
manufacturer: "Home Assistant",
|
|
19
|
+
model: "Home Assistant Core",
|
|
20
|
+
name: "Home Assistant Core",
|
|
21
|
+
name_by_user: null,
|
|
22
|
+
serial_number: null,
|
|
23
|
+
sw_version: "2024.4.3",
|
|
24
|
+
via_device_id: null,
|
|
25
|
+
} as unknown as DeviceDetails;
|
|
26
|
+
|
|
27
|
+
afterEach(async () => {
|
|
28
|
+
await hassTestRunner.teardown();
|
|
29
|
+
jest.restoreAllMocks();
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
describe("Lifecycle", () => {
|
|
33
|
+
it("should force values to be available before ready", async () => {
|
|
34
|
+
expect.assertions(1);
|
|
35
|
+
await hassTestRunner.run(({ lifecycle, hass }) => {
|
|
36
|
+
const spy = jest
|
|
37
|
+
.spyOn(hass.socket, "sendMessage")
|
|
38
|
+
.mockImplementation(async () => [EXAMPLE_DEVICE]);
|
|
39
|
+
lifecycle.onReady(async () => {
|
|
40
|
+
await hass.device.list();
|
|
41
|
+
expect(spy).toHaveBeenCalledWith(
|
|
42
|
+
expect.objectContaining({ type: "config/device_registry/list" }),
|
|
43
|
+
);
|
|
44
|
+
});
|
|
45
|
+
});
|
|
46
|
+
});
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
it("should debounce updates properly", async () => {
|
|
50
|
+
expect.assertions(1);
|
|
51
|
+
await hassTestRunner.run(({ lifecycle, hass }) => {
|
|
52
|
+
jest.spyOn(hass.socket, "sendMessage").mockImplementation(async () => undefined);
|
|
53
|
+
let counter = 0;
|
|
54
|
+
hass.events.onDeviceRegistryUpdate(() => counter++);
|
|
55
|
+
lifecycle.onReady(async () => {
|
|
56
|
+
setImmediate(async () => {
|
|
57
|
+
hass.socket.socketEvents.emit("device_registry_updated");
|
|
58
|
+
await sleep(5);
|
|
59
|
+
hass.socket.socketEvents.emit("device_registry_updated");
|
|
60
|
+
await sleep(5);
|
|
61
|
+
hass.socket.socketEvents.emit("device_registry_updated");
|
|
62
|
+
await sleep(75);
|
|
63
|
+
hass.socket.socketEvents.emit("device_registry_updated");
|
|
64
|
+
});
|
|
65
|
+
await sleep(200);
|
|
66
|
+
expect(counter).toBe(2);
|
|
67
|
+
});
|
|
68
|
+
});
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
describe("API", () => {
|
|
72
|
+
describe("Formatting", () => {
|
|
73
|
+
it("should call list properly", async () => {
|
|
74
|
+
expect.assertions(1);
|
|
75
|
+
await hassTestRunner.run(({ lifecycle, hass }) => {
|
|
76
|
+
const spy = jest.spyOn(hass.socket, "sendMessage").mockImplementation(async () => []);
|
|
77
|
+
lifecycle.onReady(async () => {
|
|
78
|
+
await hass.device.list();
|
|
79
|
+
expect(spy).toHaveBeenCalledWith(
|
|
80
|
+
expect.objectContaining({
|
|
81
|
+
type: "config/device_registry/list",
|
|
82
|
+
}),
|
|
83
|
+
);
|
|
84
|
+
});
|
|
85
|
+
});
|
|
86
|
+
});
|
|
87
|
+
});
|
|
88
|
+
});
|
|
89
|
+
});
|
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
import { sleep } from "@digital-alchemy/core";
|
|
2
|
+
|
|
3
|
+
import { ANY_ENTITY, ENTITY_STATE } from "../helpers";
|
|
4
|
+
import { hassTestRunner } from "../mock_assistant";
|
|
5
|
+
|
|
6
|
+
describe("Entity", () => {
|
|
7
|
+
afterEach(async () => {
|
|
8
|
+
await hassTestRunner.teardown();
|
|
9
|
+
jest.restoreAllMocks();
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
describe("API", () => {
|
|
13
|
+
describe("Updates", () => {
|
|
14
|
+
xit("should debounce updates properly", async () => {
|
|
15
|
+
expect.assertions(1);
|
|
16
|
+
await new Promise<void>(async done => {
|
|
17
|
+
await hassTestRunner.run(({ hass }) => {
|
|
18
|
+
const spy = jest.fn();
|
|
19
|
+
hass.events.onEntityRegistryUpdate(spy);
|
|
20
|
+
hass.socket.onConnect(async () => {
|
|
21
|
+
setImmediate(async () => {
|
|
22
|
+
hass.socket.socketEvents.emit("entity_registry_updated");
|
|
23
|
+
await sleep(5);
|
|
24
|
+
hass.socket.socketEvents.emit("entity_registry_updated");
|
|
25
|
+
await sleep(5);
|
|
26
|
+
hass.socket.socketEvents.emit("entity_registry_updated");
|
|
27
|
+
await sleep(20);
|
|
28
|
+
hass.socket.socketEvents.emit("entity_registry_updated");
|
|
29
|
+
});
|
|
30
|
+
await sleep(50);
|
|
31
|
+
expect(spy).toHaveReturnedTimes(2);
|
|
32
|
+
done();
|
|
33
|
+
});
|
|
34
|
+
});
|
|
35
|
+
});
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
it("should emit updates on change", async () => {
|
|
39
|
+
expect.assertions(3);
|
|
40
|
+
await hassTestRunner.run(({ lifecycle, hass, event }) => {
|
|
41
|
+
lifecycle.onReady(() => {
|
|
42
|
+
const old_state = hass.entity.getCurrentState("sensor.magic");
|
|
43
|
+
const new_state = { ...old_state, state: "test" };
|
|
44
|
+
const spy = jest.spyOn(event, "emit");
|
|
45
|
+
hass.entity._entityUpdateReceiver("sensor.magic", new_state, old_state);
|
|
46
|
+
expect(spy).toHaveReturnedTimes(2);
|
|
47
|
+
expect(spy).toHaveBeenCalledWith("sensor.magic", new_state, old_state);
|
|
48
|
+
expect(spy).toHaveBeenCalledWith(
|
|
49
|
+
"e1806fdc93296bbd5ab42967003cd38729ff9ba6cfeefc3e15a03ad01ac894fe",
|
|
50
|
+
new_state,
|
|
51
|
+
old_state,
|
|
52
|
+
);
|
|
53
|
+
});
|
|
54
|
+
});
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
it("returns undefined from nextState when timeout is exceeded", async () => {
|
|
58
|
+
expect.assertions(1);
|
|
59
|
+
await hassTestRunner.run(({ lifecycle, hass }) => {
|
|
60
|
+
lifecycle.onReady(async () => {
|
|
61
|
+
const entity = hass.refBy.id("sensor.magic");
|
|
62
|
+
const old_state = hass.entity.getCurrentState("sensor.magic");
|
|
63
|
+
|
|
64
|
+
// Set a timeout of 100ms
|
|
65
|
+
const wait = new Promise<ENTITY_STATE<ANY_ENTITY> | undefined>(async done => {
|
|
66
|
+
done(await entity.nextState(25));
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
// Simulate delay longer than the timeout to ensure timeout is exceeded
|
|
70
|
+
setTimeout(() => {
|
|
71
|
+
const new_state = { ...old_state, state: "test" };
|
|
72
|
+
hass.entity._entityUpdateReceiver("sensor.magic", new_state, old_state);
|
|
73
|
+
}, 50); // 200ms delay
|
|
74
|
+
|
|
75
|
+
const result = await wait;
|
|
76
|
+
expect(result).toBeUndefined();
|
|
77
|
+
});
|
|
78
|
+
});
|
|
79
|
+
});
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
it("should find entities by unique_id", async () => {
|
|
83
|
+
expect.assertions(2);
|
|
84
|
+
await hassTestRunner.run(({ lifecycle, hass }) => {
|
|
85
|
+
lifecycle.onReady(() => {
|
|
86
|
+
const entity = hass.refBy.unique_id("5622d76001a335e3ea893c4d60d31b3d-next_dawn");
|
|
87
|
+
expect(entity).toBeDefined();
|
|
88
|
+
expect(entity.entity_id).toBe("sensor.sun_next_dawn");
|
|
89
|
+
});
|
|
90
|
+
});
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
it("should return unmodified entity state with .raw", async () => {
|
|
94
|
+
expect.assertions(1);
|
|
95
|
+
await hassTestRunner.run(({ lifecycle, hass }) => {
|
|
96
|
+
lifecycle.onReady(() => {
|
|
97
|
+
const allData = hass.entity._masterState();
|
|
98
|
+
const single = hass.entity.getCurrentState("sun.sun");
|
|
99
|
+
expect(single).toBe(allData.sun.sun);
|
|
100
|
+
});
|
|
101
|
+
});
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
it("should return previous entity state with .previous", async () => {
|
|
105
|
+
expect.assertions(3);
|
|
106
|
+
await hassTestRunner.run(({ lifecycle, hass, mock_assistant }) => {
|
|
107
|
+
const entity_id = "sensor.magic";
|
|
108
|
+
const value = "bar";
|
|
109
|
+
lifecycle.onReady(async () => {
|
|
110
|
+
const start = hass.entity.getCurrentState(entity_id);
|
|
111
|
+
await mock_assistant.events.emitEntityUpdate(entity_id, {
|
|
112
|
+
state: value,
|
|
113
|
+
});
|
|
114
|
+
const updated = hass.entity.getCurrentState(entity_id);
|
|
115
|
+
const previous = hass.entity.previousState(entity_id);
|
|
116
|
+
expect(updated.state).toBe(value);
|
|
117
|
+
expect(start.state).not.toBe(value);
|
|
118
|
+
expect(start).toEqual(previous);
|
|
119
|
+
});
|
|
120
|
+
});
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
it("should return undefined for no matches", async () => {
|
|
124
|
+
expect.assertions(1);
|
|
125
|
+
await hassTestRunner.run(({ lifecycle, hass }) => {
|
|
126
|
+
lifecycle.onReady(() => {
|
|
127
|
+
const entity = hass.refBy.unique_id(
|
|
128
|
+
// @ts-expect-error test
|
|
129
|
+
"5622d76001a335e3ea893c4d60d31b3d-previous_dawn",
|
|
130
|
+
);
|
|
131
|
+
expect(entity).not.toBeDefined();
|
|
132
|
+
});
|
|
133
|
+
});
|
|
134
|
+
});
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
describe("Refresh", () => {
|
|
138
|
+
it("should attempt to load entities onBootstrap", async () => {
|
|
139
|
+
expect.assertions(2);
|
|
140
|
+
await hassTestRunner.run(({ lifecycle, hass }) => {
|
|
141
|
+
const spy = jest.spyOn(hass.entity, "refresh").mockImplementation(async () => undefined);
|
|
142
|
+
|
|
143
|
+
lifecycle.onPostConfig(function latePostConfig() {
|
|
144
|
+
expect(spy).toHaveBeenCalled();
|
|
145
|
+
}, -1);
|
|
146
|
+
lifecycle.onPostConfig(function earlyPostConfig() {
|
|
147
|
+
expect(spy).not.toHaveBeenCalled();
|
|
148
|
+
}, 0);
|
|
149
|
+
});
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
it("should retry on failure", async () => {
|
|
153
|
+
expect.assertions(1);
|
|
154
|
+
await hassTestRunner.configure({ hass: { RETRY_INTERVAL: 0 } }).run(({ lifecycle, hass }) => {
|
|
155
|
+
const responses = [
|
|
156
|
+
{ text: "502 Bad Gateway" },
|
|
157
|
+
{ text: "502 Bad Gateway" },
|
|
158
|
+
{ text: "502 Bad Gateway" },
|
|
159
|
+
[],
|
|
160
|
+
[{ entity_id: "sensor.magic" } as ENTITY_STATE<ANY_ENTITY>],
|
|
161
|
+
];
|
|
162
|
+
const spy = jest
|
|
163
|
+
.spyOn(hass.fetch, "getAllEntities")
|
|
164
|
+
// @ts-expect-error it happens
|
|
165
|
+
.mockImplementation(async () => responses.shift());
|
|
166
|
+
|
|
167
|
+
lifecycle.onBootstrap(() => expect(spy).toHaveBeenCalledTimes(5));
|
|
168
|
+
});
|
|
169
|
+
});
|
|
170
|
+
});
|
|
171
|
+
});
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import {
|
|
2
|
+
AREA_REGISTRY_UPDATED,
|
|
3
|
+
DEVICE_REGISTRY_UPDATED,
|
|
4
|
+
ENTITY_REGISTRY_UPDATED,
|
|
5
|
+
FLOOR_REGISTRY_UPDATED,
|
|
6
|
+
LABEL_REGISTRY_UPDATED,
|
|
7
|
+
ZONE_REGISTRY_UPDATED,
|
|
8
|
+
} from "../helpers";
|
|
9
|
+
import { hassTestRunner } from "../mock_assistant";
|
|
10
|
+
|
|
11
|
+
describe("Events", () => {
|
|
12
|
+
afterEach(async () => {
|
|
13
|
+
await hassTestRunner.teardown();
|
|
14
|
+
jest.restoreAllMocks();
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
describe("Event Callbacks", () => {
|
|
18
|
+
it("should register callback for AREA_REGISTRY_UPDATED", async () => {
|
|
19
|
+
expect.assertions(1);
|
|
20
|
+
await hassTestRunner.run(({ event, hass }) => {
|
|
21
|
+
const spy = jest.spyOn(event, "on");
|
|
22
|
+
const callback = jest.fn();
|
|
23
|
+
hass.events.onAreaRegistryUpdate(callback);
|
|
24
|
+
expect(spy).toHaveBeenCalledWith(AREA_REGISTRY_UPDATED, callback);
|
|
25
|
+
});
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
it("should register callback for DEVICE_REGISTRY_UPDATED", async () => {
|
|
29
|
+
expect.assertions(1);
|
|
30
|
+
await hassTestRunner.run(({ event, hass }) => {
|
|
31
|
+
const spy = jest.spyOn(event, "on");
|
|
32
|
+
const callback = jest.fn();
|
|
33
|
+
hass.events.onDeviceRegistryUpdate(callback);
|
|
34
|
+
expect(spy).toHaveBeenCalledWith(DEVICE_REGISTRY_UPDATED, callback);
|
|
35
|
+
});
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
it("should register callback for ENTITY_REGISTRY_UPDATED", async () => {
|
|
39
|
+
expect.assertions(1);
|
|
40
|
+
await hassTestRunner.run(({ event, hass }) => {
|
|
41
|
+
const spy = jest.spyOn(event, "on");
|
|
42
|
+
const callback = jest.fn();
|
|
43
|
+
hass.events.onEntityRegistryUpdate(callback);
|
|
44
|
+
expect(spy).toHaveBeenCalledWith(ENTITY_REGISTRY_UPDATED, callback);
|
|
45
|
+
});
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
it("should register callback for FLOOR_REGISTRY_UPDATED", async () => {
|
|
49
|
+
expect.assertions(1);
|
|
50
|
+
await hassTestRunner.run(({ event, hass }) => {
|
|
51
|
+
const spy = jest.spyOn(event, "on");
|
|
52
|
+
const callback = jest.fn();
|
|
53
|
+
hass.events.onFloorRegistryUpdate(callback);
|
|
54
|
+
expect(spy).toHaveBeenCalledWith(FLOOR_REGISTRY_UPDATED, callback);
|
|
55
|
+
});
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
it("should register callback for LABEL_REGISTRY_UPDATED", async () => {
|
|
59
|
+
expect.assertions(1);
|
|
60
|
+
await hassTestRunner.run(({ event, hass }) => {
|
|
61
|
+
const spy = jest.spyOn(event, "on");
|
|
62
|
+
const callback = jest.fn();
|
|
63
|
+
hass.events.onLabelRegistryUpdate(callback);
|
|
64
|
+
expect(spy).toHaveBeenCalledWith(LABEL_REGISTRY_UPDATED, callback);
|
|
65
|
+
});
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
it("should register callback for ZONE_REGISTRY_UPDATED", async () => {
|
|
69
|
+
expect.assertions(1);
|
|
70
|
+
await hassTestRunner.run(({ event, hass }) => {
|
|
71
|
+
const spy = jest.spyOn(event, "on");
|
|
72
|
+
const callback = jest.fn();
|
|
73
|
+
hass.events.onZoneRegistryUpdate(callback);
|
|
74
|
+
expect(spy).toHaveBeenCalledWith(ZONE_REGISTRY_UPDATED, callback);
|
|
75
|
+
});
|
|
76
|
+
});
|
|
77
|
+
});
|
|
78
|
+
});
|