@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,300 @@
|
|
|
1
|
+
import dayjs from "dayjs";
|
|
2
|
+
|
|
3
|
+
import { ANY_ENTITY, ENTITY_STATE } from "../helpers";
|
|
4
|
+
import { hassTestRunner } from "../mock_assistant";
|
|
5
|
+
|
|
6
|
+
describe("References", () => {
|
|
7
|
+
afterEach(async () => {
|
|
8
|
+
await hassTestRunner.teardown();
|
|
9
|
+
jest.restoreAllMocks();
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
describe("loading", function () {
|
|
13
|
+
describe("refBy.id", () => {
|
|
14
|
+
it("can grab references by id", async () => {
|
|
15
|
+
expect.assertions(2);
|
|
16
|
+
await hassTestRunner.run(({ lifecycle, hass }) => {
|
|
17
|
+
lifecycle.onReady(() => {
|
|
18
|
+
const sensor = hass.refBy.id("sensor.magic");
|
|
19
|
+
expect(sensor).toBeDefined();
|
|
20
|
+
expect(sensor.state).toBe("unavailable");
|
|
21
|
+
});
|
|
22
|
+
});
|
|
23
|
+
});
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
describe("domain", () => {
|
|
27
|
+
it("load references by domain", async () => {
|
|
28
|
+
expect.assertions(1);
|
|
29
|
+
await hassTestRunner.run(({ lifecycle, hass }) => {
|
|
30
|
+
lifecycle.onReady(() => {
|
|
31
|
+
const sensor = hass.refBy.domain("sensor");
|
|
32
|
+
expect(sensor.length).toBe(8);
|
|
33
|
+
});
|
|
34
|
+
});
|
|
35
|
+
});
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
describe("unique_id", () => {
|
|
39
|
+
it("load references by unique_id", async () => {
|
|
40
|
+
expect.assertions(1);
|
|
41
|
+
await hassTestRunner.run(({ lifecycle, hass }) => {
|
|
42
|
+
lifecycle.onReady(() => {
|
|
43
|
+
const sensor = hass.refBy.unique_id(
|
|
44
|
+
"e1806fdc93296bbd5ab42967003cd38729ff9ba6cfeefc3e15a03ad01ac894fe",
|
|
45
|
+
);
|
|
46
|
+
expect(sensor?.entity_id).toBe("sensor.magic");
|
|
47
|
+
});
|
|
48
|
+
});
|
|
49
|
+
});
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
describe("label", () => {
|
|
53
|
+
it("load references by label", async () => {
|
|
54
|
+
expect.assertions(1);
|
|
55
|
+
await hassTestRunner.run(({ lifecycle, hass }) => {
|
|
56
|
+
lifecycle.onReady(() => {
|
|
57
|
+
const list = hass.refBy.label("synapse");
|
|
58
|
+
expect(list.length).toBe(7);
|
|
59
|
+
});
|
|
60
|
+
});
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
it("load references by label limiting by domain", async () => {
|
|
64
|
+
expect.assertions(1);
|
|
65
|
+
await hassTestRunner.run(({ lifecycle, hass }) => {
|
|
66
|
+
lifecycle.onReady(() => {
|
|
67
|
+
const list = hass.refBy.label("synapse", "light");
|
|
68
|
+
expect(list.length).toBe(0);
|
|
69
|
+
});
|
|
70
|
+
});
|
|
71
|
+
});
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
describe("area", () => {
|
|
75
|
+
it("load references by area", async () => {
|
|
76
|
+
expect.assertions(2);
|
|
77
|
+
await hassTestRunner.run(({ lifecycle, hass }) => {
|
|
78
|
+
lifecycle.onReady(() => {
|
|
79
|
+
const bedroom = hass.refBy.area("bedroom");
|
|
80
|
+
const kitchen = hass.refBy.area("kitchen");
|
|
81
|
+
expect(bedroom.length).toBe(2);
|
|
82
|
+
expect(kitchen.length).toBe(1);
|
|
83
|
+
});
|
|
84
|
+
});
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
it("load references by area limiting by domain", async () => {
|
|
88
|
+
expect.assertions(2);
|
|
89
|
+
await hassTestRunner.run(({ lifecycle, hass }) => {
|
|
90
|
+
lifecycle.onReady(() => {
|
|
91
|
+
const bedroom = hass.refBy.area("bedroom", "light");
|
|
92
|
+
const kitchen = hass.refBy.area("kitchen", "light");
|
|
93
|
+
expect(bedroom.length).toBe(1);
|
|
94
|
+
expect(kitchen.length).toBe(0);
|
|
95
|
+
});
|
|
96
|
+
});
|
|
97
|
+
});
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
describe("device", () => {
|
|
101
|
+
it("load references by device", async () => {
|
|
102
|
+
expect.assertions(1);
|
|
103
|
+
await hassTestRunner.run(({ lifecycle, hass }) => {
|
|
104
|
+
lifecycle.onReady(() => {
|
|
105
|
+
const synapse = hass.refBy.device("308e39cf50a9fc6c30b4110724ed1f2e");
|
|
106
|
+
expect(synapse.length).toBe(9);
|
|
107
|
+
});
|
|
108
|
+
});
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
it("load references by device limiting by domain", async () => {
|
|
112
|
+
expect.assertions(1);
|
|
113
|
+
await hassTestRunner.run(({ lifecycle, hass }) => {
|
|
114
|
+
lifecycle.onReady(() => {
|
|
115
|
+
const synapse = hass.refBy.device("308e39cf50a9fc6c30b4110724ed1f2e", "light");
|
|
116
|
+
expect(synapse.length).toBe(0);
|
|
117
|
+
});
|
|
118
|
+
});
|
|
119
|
+
});
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
describe("platform", () => {
|
|
123
|
+
it("load references by platform", async () => {
|
|
124
|
+
expect.assertions(1);
|
|
125
|
+
await hassTestRunner.run(({ lifecycle, hass }) => {
|
|
126
|
+
lifecycle.onReady(() => {
|
|
127
|
+
const synapse = hass.refBy.platform("synapse");
|
|
128
|
+
expect(synapse.length).toBe(7);
|
|
129
|
+
});
|
|
130
|
+
});
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
it("load references by platform limiting by domain", async () => {
|
|
134
|
+
expect.assertions(1);
|
|
135
|
+
await hassTestRunner.run(({ lifecycle, hass }) => {
|
|
136
|
+
lifecycle.onReady(() => {
|
|
137
|
+
const synapse = hass.refBy.platform("synapse", "light");
|
|
138
|
+
expect(synapse.length).toBe(0);
|
|
139
|
+
});
|
|
140
|
+
});
|
|
141
|
+
});
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
describe("floor", () => {
|
|
145
|
+
it("load references by floor", async () => {
|
|
146
|
+
expect.assertions(1);
|
|
147
|
+
await hassTestRunner.run(({ lifecycle, hass }) => {
|
|
148
|
+
lifecycle.onReady(() => {
|
|
149
|
+
const synapse = hass.refBy.floor("downstairs");
|
|
150
|
+
expect(synapse.length).toBe(3);
|
|
151
|
+
});
|
|
152
|
+
});
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
it("load references by floor limiting by domain", async () => {
|
|
156
|
+
expect.assertions(1);
|
|
157
|
+
await hassTestRunner.run(({ lifecycle, hass }) => {
|
|
158
|
+
lifecycle.onReady(() => {
|
|
159
|
+
const synapse = hass.refBy.floor("downstairs", "light");
|
|
160
|
+
expect(synapse.length).toBe(0);
|
|
161
|
+
});
|
|
162
|
+
});
|
|
163
|
+
});
|
|
164
|
+
});
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
describe("functionality", () => {
|
|
168
|
+
describe("operators", () => {
|
|
169
|
+
it("has", async () => {
|
|
170
|
+
expect.assertions(15);
|
|
171
|
+
await hassTestRunner.run(({ lifecycle, hass }) => {
|
|
172
|
+
lifecycle.onReady(() => {
|
|
173
|
+
const entity = hass.refBy.id("switch.bedroom_lamp");
|
|
174
|
+
// always there stuff
|
|
175
|
+
expect("does not exist" in entity).toBe(false);
|
|
176
|
+
[
|
|
177
|
+
"attributes",
|
|
178
|
+
"entity_id",
|
|
179
|
+
"history",
|
|
180
|
+
"last",
|
|
181
|
+
"nextState",
|
|
182
|
+
"once",
|
|
183
|
+
"onUpdate",
|
|
184
|
+
"previous",
|
|
185
|
+
"removeAllListeners",
|
|
186
|
+
"state",
|
|
187
|
+
"waitForState",
|
|
188
|
+
].forEach(property => expect(property in entity).toBe(true));
|
|
189
|
+
// service calls exist too
|
|
190
|
+
["toggle", "turn_off", "turn_on"].forEach(method =>
|
|
191
|
+
expect(method in entity).toBe(true),
|
|
192
|
+
);
|
|
193
|
+
});
|
|
194
|
+
});
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
it("ownKeys", async () => {
|
|
198
|
+
expect.assertions(1);
|
|
199
|
+
await hassTestRunner.run(({ lifecycle, hass }) => {
|
|
200
|
+
lifecycle.onReady(() => {
|
|
201
|
+
const entity = hass.refBy.id("switch.bedroom_lamp");
|
|
202
|
+
const keys = Object.keys(entity);
|
|
203
|
+
// note: each section sorted alphabetically
|
|
204
|
+
expect(keys).toEqual([
|
|
205
|
+
// hard coded
|
|
206
|
+
"entity_id",
|
|
207
|
+
"state",
|
|
208
|
+
"attributes",
|
|
209
|
+
"last_changed",
|
|
210
|
+
"last_reported",
|
|
211
|
+
"last_updated",
|
|
212
|
+
"context",
|
|
213
|
+
"history",
|
|
214
|
+
"last",
|
|
215
|
+
"nextState",
|
|
216
|
+
"once",
|
|
217
|
+
"onUpdate",
|
|
218
|
+
"previous",
|
|
219
|
+
"removeAllListeners",
|
|
220
|
+
"waitForState",
|
|
221
|
+
// services
|
|
222
|
+
"toggle",
|
|
223
|
+
"turn_off",
|
|
224
|
+
"turn_on",
|
|
225
|
+
]);
|
|
226
|
+
});
|
|
227
|
+
});
|
|
228
|
+
});
|
|
229
|
+
describe("set", () => {
|
|
230
|
+
it("state", () => {
|
|
231
|
+
// stub
|
|
232
|
+
});
|
|
233
|
+
it("attributes", () => {
|
|
234
|
+
// stub
|
|
235
|
+
});
|
|
236
|
+
it("everything else", () => {
|
|
237
|
+
// stub
|
|
238
|
+
});
|
|
239
|
+
});
|
|
240
|
+
});
|
|
241
|
+
|
|
242
|
+
describe("get", () => {
|
|
243
|
+
it("references have attributes", async () => {
|
|
244
|
+
expect.assertions(2);
|
|
245
|
+
await hassTestRunner.run(({ lifecycle, hass }) => {
|
|
246
|
+
lifecycle.onReady(() => {
|
|
247
|
+
const sensor = hass.refBy.id("sensor.magic");
|
|
248
|
+
expect("attributes" in sensor).toBe(true);
|
|
249
|
+
expect(sensor.attributes).toEqual(expect.objectContaining({ friendly_name: "magic" }));
|
|
250
|
+
});
|
|
251
|
+
});
|
|
252
|
+
});
|
|
253
|
+
|
|
254
|
+
// mental note: legacy test
|
|
255
|
+
// better covered by the ownKeys & has operator tests now
|
|
256
|
+
it("references do not return random attributes", async () => {
|
|
257
|
+
expect.assertions(1);
|
|
258
|
+
await hassTestRunner.run(({ lifecycle, hass }) => {
|
|
259
|
+
lifecycle.onReady(() => {
|
|
260
|
+
const sensor = hass.refBy.id("sensor.magic");
|
|
261
|
+
// @ts-expect-error it's the test
|
|
262
|
+
expect(sensor.foo).toBeUndefined();
|
|
263
|
+
});
|
|
264
|
+
});
|
|
265
|
+
});
|
|
266
|
+
|
|
267
|
+
it("references provide last_* as dayjs", async () => {
|
|
268
|
+
expect.assertions(3);
|
|
269
|
+
await hassTestRunner.run(({ lifecycle, hass }) => {
|
|
270
|
+
lifecycle.onReady(() => {
|
|
271
|
+
const sensor = hass.refBy.id("sensor.magic");
|
|
272
|
+
expect(sensor.last_changed instanceof dayjs).toBe(true);
|
|
273
|
+
expect(sensor.last_reported instanceof dayjs).toBe(true);
|
|
274
|
+
expect(sensor.last_updated instanceof dayjs).toBe(true);
|
|
275
|
+
});
|
|
276
|
+
});
|
|
277
|
+
});
|
|
278
|
+
|
|
279
|
+
it("passes through history calls", async () => {
|
|
280
|
+
expect.assertions(2);
|
|
281
|
+
await hassTestRunner.run(({ lifecycle, hass }) => {
|
|
282
|
+
lifecycle.onReady(async () => {
|
|
283
|
+
const result = [] as ENTITY_STATE<ANY_ENTITY>[];
|
|
284
|
+
const spy = jest
|
|
285
|
+
.spyOn(hass.fetch, "fetchEntityHistory")
|
|
286
|
+
.mockImplementation(async () => result);
|
|
287
|
+
const from = new Date();
|
|
288
|
+
const to = new Date();
|
|
289
|
+
const entity_id = "sensor.magic";
|
|
290
|
+
|
|
291
|
+
const entity = hass.refBy.id(entity_id);
|
|
292
|
+
const out = await entity.history(from, to);
|
|
293
|
+
expect(spy).toHaveBeenCalledWith(entity_id, from, to);
|
|
294
|
+
expect(out).toBe(result);
|
|
295
|
+
});
|
|
296
|
+
});
|
|
297
|
+
});
|
|
298
|
+
});
|
|
299
|
+
});
|
|
300
|
+
});
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import { hassTestRunner } from "../mock_assistant";
|
|
2
|
+
|
|
3
|
+
describe("Websocket", () => {
|
|
4
|
+
afterEach(async () => {
|
|
5
|
+
await hassTestRunner.teardown();
|
|
6
|
+
jest.restoreAllMocks();
|
|
7
|
+
});
|
|
8
|
+
|
|
9
|
+
describe("API Interactions", () => {
|
|
10
|
+
it("should emit events onConnect", async () => {
|
|
11
|
+
expect.assertions(1);
|
|
12
|
+
await hassTestRunner.run(({ lifecycle, hass }) => {
|
|
13
|
+
let hit = false;
|
|
14
|
+
hass.socket.onConnect(() => (hit = true));
|
|
15
|
+
lifecycle.onReady(() => {
|
|
16
|
+
expect(hit).toBe(true);
|
|
17
|
+
});
|
|
18
|
+
});
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
it("should emit a socket message with subscribeEvents", async () => {
|
|
22
|
+
expect.assertions(1);
|
|
23
|
+
await hassTestRunner.run(({ lifecycle, hass, context }) => {
|
|
24
|
+
const spy = jest
|
|
25
|
+
.spyOn(hass.socket, "sendMessage")
|
|
26
|
+
.mockImplementation(async () => undefined);
|
|
27
|
+
lifecycle.onReady(async () => {
|
|
28
|
+
await hass.socket.subscribe({
|
|
29
|
+
context,
|
|
30
|
+
event_type: "test",
|
|
31
|
+
exec: () => {},
|
|
32
|
+
});
|
|
33
|
+
expect(spy).toHaveBeenCalledWith(
|
|
34
|
+
expect.objectContaining({
|
|
35
|
+
event_type: "test",
|
|
36
|
+
type: "subscribe_events",
|
|
37
|
+
}),
|
|
38
|
+
);
|
|
39
|
+
});
|
|
40
|
+
});
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
it("should emit a socket message with fireEvent", async () => {
|
|
44
|
+
expect.assertions(1);
|
|
45
|
+
await hassTestRunner.run(({ lifecycle, hass }) => {
|
|
46
|
+
const spy = jest
|
|
47
|
+
.spyOn(hass.socket, "sendMessage")
|
|
48
|
+
.mockImplementation(async () => undefined);
|
|
49
|
+
lifecycle.onReady(async () => {
|
|
50
|
+
const data = { example: "data" };
|
|
51
|
+
await hass.socket.fireEvent("test_event", data);
|
|
52
|
+
expect(spy).toHaveBeenCalledWith(
|
|
53
|
+
expect.objectContaining({
|
|
54
|
+
event_data: data,
|
|
55
|
+
event_type: "test_event",
|
|
56
|
+
type: "fire_event",
|
|
57
|
+
}),
|
|
58
|
+
);
|
|
59
|
+
});
|
|
60
|
+
});
|
|
61
|
+
});
|
|
62
|
+
});
|
|
63
|
+
});
|
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
import { CronExpression, SECOND, sleep } from "@digital-alchemy/core";
|
|
2
|
+
import dayjs from "dayjs";
|
|
3
|
+
|
|
4
|
+
import { hassTestRunner } from "../mock_assistant";
|
|
5
|
+
|
|
6
|
+
describe("Workflows", () => {
|
|
7
|
+
beforeAll(() => {
|
|
8
|
+
hassTestRunner.appendService(({ hass, scheduler }) => {
|
|
9
|
+
scheduler.cron({
|
|
10
|
+
async exec() {
|
|
11
|
+
await hass.call.switch.turn_on({
|
|
12
|
+
entity_id: "switch.bedroom_lamp",
|
|
13
|
+
});
|
|
14
|
+
},
|
|
15
|
+
schedule: CronExpression.EVERY_DAY_AT_8PM,
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
const entity = hass.refBy.id("sensor.magic");
|
|
19
|
+
entity.onUpdate(async () => {
|
|
20
|
+
const action = entity.state === "test" ? "turn_on" : "turn_off";
|
|
21
|
+
await hass.call.switch[action]({
|
|
22
|
+
entity_id: "switch.porch_light",
|
|
23
|
+
});
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
entity.onUpdate(async (new_state, old_state) => {
|
|
27
|
+
if (old_state.state === "away" && new_state.state === "here") {
|
|
28
|
+
await hass.call.switch.turn_on({
|
|
29
|
+
entity_id: "switch.living_room_mood_lights",
|
|
30
|
+
});
|
|
31
|
+
}
|
|
32
|
+
});
|
|
33
|
+
});
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
afterEach(async () => {
|
|
37
|
+
await hassTestRunner.teardown();
|
|
38
|
+
jest.restoreAllMocks();
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
describe("Event and Response", () => {
|
|
42
|
+
it("should be able to trigger a workflow", async () => {
|
|
43
|
+
expect.assertions(2);
|
|
44
|
+
await hassTestRunner.run(({ mock_assistant, hass, lifecycle }) => {
|
|
45
|
+
lifecycle.onReady(async () => {
|
|
46
|
+
const turnOn = jest.spyOn(hass.call.switch, "turn_on");
|
|
47
|
+
const turnOff = jest.spyOn(hass.call.switch, "turn_off");
|
|
48
|
+
await mock_assistant.events.emitEntityUpdate("sensor.magic", {
|
|
49
|
+
state: "test",
|
|
50
|
+
});
|
|
51
|
+
await mock_assistant.events.emitEntityUpdate("sensor.magic", {
|
|
52
|
+
state: "foo",
|
|
53
|
+
});
|
|
54
|
+
expect(turnOn).toHaveBeenCalledTimes(1);
|
|
55
|
+
expect(turnOff).toHaveBeenCalledTimes(1);
|
|
56
|
+
});
|
|
57
|
+
});
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
it("should be able to trigger a from an initial state", async () => {
|
|
61
|
+
expect.assertions(1);
|
|
62
|
+
await hassTestRunner.run(({ mock_assistant, hass, lifecycle }) => {
|
|
63
|
+
mock_assistant.fixtures.setState({
|
|
64
|
+
"sensor.magic": {
|
|
65
|
+
state: "away",
|
|
66
|
+
},
|
|
67
|
+
});
|
|
68
|
+
lifecycle.onReady(async () => {
|
|
69
|
+
const turnOn = jest.spyOn(hass.call.switch, "turn_on");
|
|
70
|
+
await mock_assistant.events.emitEntityUpdate("sensor.magic", {
|
|
71
|
+
state: "here",
|
|
72
|
+
});
|
|
73
|
+
await sleep(1);
|
|
74
|
+
expect(turnOn).toHaveBeenCalledWith({
|
|
75
|
+
entity_id: "switch.living_room_mood_lights",
|
|
76
|
+
});
|
|
77
|
+
});
|
|
78
|
+
});
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
it("should not trigger a from an invalid initial state", async () => {
|
|
82
|
+
expect.assertions(1);
|
|
83
|
+
await hassTestRunner.run(({ mock_assistant, hass, lifecycle }) => {
|
|
84
|
+
mock_assistant.fixtures.setState({
|
|
85
|
+
"sensor.magic": {
|
|
86
|
+
state: "mars",
|
|
87
|
+
},
|
|
88
|
+
});
|
|
89
|
+
lifecycle.onReady(async () => {
|
|
90
|
+
const turnOn = jest.spyOn(hass.call.switch, "turn_on");
|
|
91
|
+
await mock_assistant.events.emitEntityUpdate("sensor.magic", {
|
|
92
|
+
state: "here",
|
|
93
|
+
});
|
|
94
|
+
expect(turnOn).not.toHaveBeenCalledWith({
|
|
95
|
+
entity_id: "switch.living_room_mood_lights",
|
|
96
|
+
});
|
|
97
|
+
});
|
|
98
|
+
});
|
|
99
|
+
});
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
describe("Timers", () => {
|
|
103
|
+
beforeEach(() => {
|
|
104
|
+
jest.useFakeTimers();
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
afterEach(() => {
|
|
108
|
+
jest.useRealTimers();
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
it("should run at 3PM", async () => {
|
|
112
|
+
expect.assertions(1);
|
|
113
|
+
jest.setSystemTime(dayjs("2024-01-01 19:59:59").toDate());
|
|
114
|
+
jest.runOnlyPendingTimersAsync();
|
|
115
|
+
await hassTestRunner.run(({ hass, lifecycle }) => {
|
|
116
|
+
lifecycle.onReady(() => {
|
|
117
|
+
const turnOn = jest.spyOn(hass.call.switch, "turn_on");
|
|
118
|
+
jest.advanceTimersByTime(2 * SECOND);
|
|
119
|
+
expect(turnOn).toHaveBeenCalledWith({
|
|
120
|
+
entity_id: "switch.bedroom_lamp",
|
|
121
|
+
});
|
|
122
|
+
});
|
|
123
|
+
});
|
|
124
|
+
});
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
describe("Call", () => {
|
|
128
|
+
const EXPECTED_KEYS = [
|
|
129
|
+
"persistent_notification",
|
|
130
|
+
"homeassistant",
|
|
131
|
+
"system_log",
|
|
132
|
+
"logger",
|
|
133
|
+
"recorder",
|
|
134
|
+
"person",
|
|
135
|
+
"frontend",
|
|
136
|
+
"cloud",
|
|
137
|
+
"ffmpeg",
|
|
138
|
+
"tts",
|
|
139
|
+
"scene",
|
|
140
|
+
"timer",
|
|
141
|
+
"input_number",
|
|
142
|
+
"conversation",
|
|
143
|
+
"input_select",
|
|
144
|
+
"zone",
|
|
145
|
+
"input_button",
|
|
146
|
+
"script",
|
|
147
|
+
"automation",
|
|
148
|
+
"logbook",
|
|
149
|
+
"input_boolean",
|
|
150
|
+
"button",
|
|
151
|
+
"switch",
|
|
152
|
+
"input_datetime",
|
|
153
|
+
"backup",
|
|
154
|
+
"shopping_list",
|
|
155
|
+
"counter",
|
|
156
|
+
"schedule",
|
|
157
|
+
"input_text",
|
|
158
|
+
"synapse",
|
|
159
|
+
"todo",
|
|
160
|
+
"notify",
|
|
161
|
+
"calendar",
|
|
162
|
+
];
|
|
163
|
+
it("does not allow set", async () => {
|
|
164
|
+
expect.assertions(1);
|
|
165
|
+
await hassTestRunner.run(({ hass }) => {
|
|
166
|
+
try {
|
|
167
|
+
// @ts-expect-error testing
|
|
168
|
+
hass.call.button = {};
|
|
169
|
+
} catch (error) {
|
|
170
|
+
expect(error).toBeDefined();
|
|
171
|
+
}
|
|
172
|
+
});
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
it("provides keys via ownKeys", async () => {
|
|
176
|
+
expect.assertions(1);
|
|
177
|
+
await hassTestRunner.run(({ hass, lifecycle }) => {
|
|
178
|
+
lifecycle.onReady(() => {
|
|
179
|
+
const keys = Object.keys(hass.call);
|
|
180
|
+
expect(keys).toEqual(EXPECTED_KEYS);
|
|
181
|
+
});
|
|
182
|
+
});
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
it("does has correctly", async () => {
|
|
186
|
+
expect.assertions(34);
|
|
187
|
+
await hassTestRunner.run(({ hass, lifecycle }) => {
|
|
188
|
+
lifecycle.onReady(() => {
|
|
189
|
+
EXPECTED_KEYS.forEach(i => expect(i in hass.call).toBe(true));
|
|
190
|
+
expect("unknown_property" in hass.call).toBe(false);
|
|
191
|
+
});
|
|
192
|
+
});
|
|
193
|
+
});
|
|
194
|
+
});
|
|
195
|
+
});
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
import { sleep } from "@digital-alchemy/core";
|
|
2
|
+
|
|
3
|
+
import { ZONE_REGISTRY_UPDATED, ZoneDetails } from "../helpers";
|
|
4
|
+
import { hassTestRunner } from "../mock_assistant";
|
|
5
|
+
|
|
6
|
+
describe("Zone", () => {
|
|
7
|
+
afterEach(async () => {
|
|
8
|
+
await hassTestRunner.teardown();
|
|
9
|
+
jest.restoreAllMocks();
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
const EXAMPLE_ZONE = {
|
|
13
|
+
icon: "",
|
|
14
|
+
latitude: 0,
|
|
15
|
+
longitude: 0,
|
|
16
|
+
name: "Test",
|
|
17
|
+
passive: true,
|
|
18
|
+
} as ZoneDetails;
|
|
19
|
+
|
|
20
|
+
describe("Lifecycle", () => {
|
|
21
|
+
it("should force values to be available before ready", async () => {
|
|
22
|
+
expect.assertions(1);
|
|
23
|
+
await hassTestRunner.run(({ lifecycle, hass }) => {
|
|
24
|
+
const spy = jest
|
|
25
|
+
.spyOn(hass.socket, "sendMessage")
|
|
26
|
+
.mockImplementation(async () => [EXAMPLE_ZONE]);
|
|
27
|
+
lifecycle.onReady(async () => {
|
|
28
|
+
await hass.zone.list();
|
|
29
|
+
expect(spy).toHaveBeenCalledWith(expect.objectContaining({ type: "zone/list" }));
|
|
30
|
+
});
|
|
31
|
+
});
|
|
32
|
+
});
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
describe("API", () => {
|
|
36
|
+
describe("Formatting", () => {
|
|
37
|
+
it("should call list properly", async () => {
|
|
38
|
+
expect.assertions(1);
|
|
39
|
+
await hassTestRunner.run(({ lifecycle, hass }) => {
|
|
40
|
+
const spy = jest.spyOn(hass.socket, "sendMessage").mockImplementation(async () => []);
|
|
41
|
+
lifecycle.onReady(async () => {
|
|
42
|
+
await hass.zone.list();
|
|
43
|
+
expect(spy).toHaveBeenCalledWith(expect.objectContaining({ type: "zone/list" }));
|
|
44
|
+
});
|
|
45
|
+
});
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
it("should call create properly", async () => {
|
|
49
|
+
expect.assertions(1);
|
|
50
|
+
await hassTestRunner.run(({ lifecycle, hass, event }) => {
|
|
51
|
+
const spy = jest
|
|
52
|
+
.spyOn(hass.socket, "sendMessage")
|
|
53
|
+
.mockImplementation(async () => undefined);
|
|
54
|
+
lifecycle.onReady(async () => {
|
|
55
|
+
setImmediate(() => event.emit(ZONE_REGISTRY_UPDATED));
|
|
56
|
+
await hass.zone.create(EXAMPLE_ZONE);
|
|
57
|
+
|
|
58
|
+
expect(spy).toHaveBeenCalledWith({
|
|
59
|
+
type: "zone/create",
|
|
60
|
+
...EXAMPLE_ZONE,
|
|
61
|
+
});
|
|
62
|
+
});
|
|
63
|
+
});
|
|
64
|
+
});
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
describe("Order of operations", () => {
|
|
68
|
+
it("should debounce updates properly", async () => {
|
|
69
|
+
expect.assertions(1);
|
|
70
|
+
await hassTestRunner.run(({ lifecycle, hass }) => {
|
|
71
|
+
jest.spyOn(hass.socket, "sendMessage").mockImplementation(async () => undefined);
|
|
72
|
+
let counter = 0;
|
|
73
|
+
hass.events.onZoneRegistryUpdate(() => counter++);
|
|
74
|
+
lifecycle.onReady(async () => {
|
|
75
|
+
setImmediate(async () => {
|
|
76
|
+
hass.socket.socketEvents.emit("zone_registry_updated");
|
|
77
|
+
await sleep(5);
|
|
78
|
+
hass.socket.socketEvents.emit("zone_registry_updated");
|
|
79
|
+
await sleep(5);
|
|
80
|
+
hass.socket.socketEvents.emit("zone_registry_updated");
|
|
81
|
+
await sleep(75);
|
|
82
|
+
hass.socket.socketEvents.emit("zone_registry_updated");
|
|
83
|
+
});
|
|
84
|
+
await sleep(200);
|
|
85
|
+
expect(counter).toBe(2);
|
|
86
|
+
});
|
|
87
|
+
});
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
it("should wait for an update before returning when creating", async () => {
|
|
91
|
+
expect.assertions(1);
|
|
92
|
+
await hassTestRunner.run(({ lifecycle, hass, event }) => {
|
|
93
|
+
jest.spyOn(hass.socket, "sendMessage").mockImplementation(async () => undefined);
|
|
94
|
+
lifecycle.onReady(async () => {
|
|
95
|
+
const response = hass.zone.create(EXAMPLE_ZONE);
|
|
96
|
+
let order = "";
|
|
97
|
+
setTimeout(() => {
|
|
98
|
+
order += "a";
|
|
99
|
+
event.emit(ZONE_REGISTRY_UPDATED);
|
|
100
|
+
}, 5);
|
|
101
|
+
await response;
|
|
102
|
+
order += "b";
|
|
103
|
+
expect(order).toEqual("ab");
|
|
104
|
+
});
|
|
105
|
+
});
|
|
106
|
+
});
|
|
107
|
+
});
|
|
108
|
+
});
|
|
109
|
+
});
|