@digital-alchemy/hass 24.10.1 → 24.11.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/dist/dynamic.d.ts +3 -1
- package/dist/extensions/events.extension.d.ts +1 -1
- package/dist/extensions/events.extension.js +26 -7
- package/dist/extensions/events.extension.js.map +1 -1
- package/dist/extensions/internal.extension.js +1 -1
- package/dist/extensions/internal.extension.js.map +1 -1
- package/dist/extensions/reference.extension.d.ts +51 -1
- package/dist/extensions/reference.extension.js +293 -181
- package/dist/extensions/reference.extension.js.map +1 -1
- package/dist/extensions/websocket-api.extension.js +4 -3
- package/dist/extensions/websocket-api.extension.js.map +1 -1
- package/dist/hass.module.d.ts +3 -3
- package/dist/hass.module.js +3 -3
- package/dist/hass.module.js.map +1 -1
- package/dist/helpers/entity-state.helper.d.ts +11 -7
- package/dist/helpers/interfaces.helper.d.ts +10 -10
- package/dist/helpers/interfaces.helper.js.map +1 -1
- package/dist/helpers/utility.helper.d.ts +5 -0
- package/dist/helpers/utility.helper.js +5 -0
- package/dist/helpers/utility.helper.js.map +1 -1
- package/dist/mock_assistant/mock-assistant.module.d.ts +4 -4
- package/dist/mock_assistant/mock-assistant.module.js +7 -0
- package/dist/mock_assistant/mock-assistant.module.js.map +1 -1
- package/dist/testing/fetch-api.spec.js +13 -13
- package/dist/testing/fetch-api.spec.js.map +1 -1
- package/package.json +27 -28
- package/scripts/test.sh +1 -1
- package/src/dynamic.ts +82 -77
- package/src/extensions/events.extension.ts +26 -10
- package/src/extensions/internal.extension.ts +1 -1
- package/src/extensions/reference.extension.ts +323 -201
- package/src/extensions/websocket-api.extension.ts +4 -3
- package/src/hass.module.ts +4 -4
- package/src/helpers/entity-state.helper.ts +11 -6
- package/src/helpers/interfaces.helper.ts +10 -9
- package/src/helpers/utility.helper.ts +8 -0
- package/src/mock_assistant/mock-assistant.module.ts +7 -0
- package/src/testing/fetch-api.spec.ts +13 -13
|
@@ -2,7 +2,7 @@ import { DOWN, is, NONE, sleep, TAnyFunction, TServiceParams, UP } from "@digita
|
|
|
2
2
|
import dayjs, { Dayjs } from "dayjs";
|
|
3
3
|
import { Get } from "type-fest";
|
|
4
4
|
|
|
5
|
-
import { SERVICE_LIST_UPDATED } from "..";
|
|
5
|
+
import { SERVICE_LIST_UPDATED, SOCKET_CONNECTED } from "..";
|
|
6
6
|
import {
|
|
7
7
|
TAreaId,
|
|
8
8
|
TDeviceId,
|
|
@@ -26,15 +26,65 @@ import {
|
|
|
26
26
|
PICK_FROM_FLOOR,
|
|
27
27
|
PICK_FROM_LABEL,
|
|
28
28
|
PICK_FROM_PLATFORM,
|
|
29
|
+
RemoveCallback,
|
|
29
30
|
} from "../helpers";
|
|
30
31
|
|
|
31
|
-
|
|
32
|
+
/**
|
|
33
|
+
* ## Overview
|
|
34
|
+
*
|
|
35
|
+
* This service is intended for the creation of type safe entity references based on entity id.
|
|
36
|
+
*
|
|
37
|
+
* ```typescript
|
|
38
|
+
* const ref = hass.refBy.id("switch.example");
|
|
39
|
+
* ```
|
|
40
|
+
*
|
|
41
|
+
* These contain specialized type definitions that gets looked up by entity id, making typescript report info about the desired entity.
|
|
42
|
+
*
|
|
43
|
+
* ## Capabilities
|
|
44
|
+
*
|
|
45
|
+
* ### Event Listeners
|
|
46
|
+
*
|
|
47
|
+
* Run a callback in response to
|
|
48
|
+
* - `.onUpdate((new_state, old_state) => ...)`
|
|
49
|
+
* - `.once((new_state, old_state) => ...)`
|
|
50
|
+
*
|
|
51
|
+
* ### State lookups
|
|
52
|
+
*
|
|
53
|
+
* - `.nextState(timeout?)`
|
|
54
|
+
* - `.waitForState(state, timeout?)`
|
|
55
|
+
* - `.previous.state` | `.previous.attributes`
|
|
56
|
+
*
|
|
57
|
+
* ### Service Calling
|
|
58
|
+
*
|
|
59
|
+
* For services that appear on the same domain as the provided entity, the service call can be made directly from the reference.
|
|
60
|
+
*
|
|
61
|
+
* > Ex: `switch.example` calling `switch.turn_on`
|
|
62
|
+
*
|
|
63
|
+
* ```typescript
|
|
64
|
+
* ref.turn_on();
|
|
65
|
+
* ```
|
|
66
|
+
*
|
|
67
|
+
* The `entity_id` property that would normally be required is automatically provided by the proxy.
|
|
68
|
+
*
|
|
69
|
+
* ### Property Lookups
|
|
70
|
+
*
|
|
71
|
+
* - `.state`
|
|
72
|
+
* - `.entity_id`
|
|
73
|
+
* - `.attributes`
|
|
74
|
+
*
|
|
75
|
+
* ## Garbage Collection
|
|
76
|
+
*
|
|
77
|
+
* - `.removeAllListeners()`
|
|
78
|
+
*
|
|
79
|
+
* The reference may call the remove all active event listeners and timers at any time.
|
|
80
|
+
* It will interrupt any timers (nextState/waitForState), as well as detach any event listeners
|
|
81
|
+
*/
|
|
82
|
+
export function ReferenceService({
|
|
32
83
|
hass,
|
|
33
84
|
logger,
|
|
34
85
|
internal,
|
|
35
86
|
event,
|
|
36
87
|
}: TServiceParams): HassReferenceService {
|
|
37
|
-
const ENTITY_PROXIES = new Map<ANY_ENTITY, ByIdProxy<ANY_ENTITY>>();
|
|
38
88
|
// #MARK:proxyGetLogic
|
|
39
89
|
function proxyGetLogic<ENTITY extends ANY_ENTITY = ANY_ENTITY, PROPERTY extends string = string>(
|
|
40
90
|
entity: ENTITY,
|
|
@@ -64,238 +114,307 @@ export function ReferenceExtension({
|
|
|
64
114
|
}
|
|
65
115
|
|
|
66
116
|
// #MARK: byId
|
|
117
|
+
// ! Calls to this function MUST ALWAYS go through `hass.refBy.id`
|
|
118
|
+
// never call this function directly 💧
|
|
67
119
|
function byId<ENTITY_ID extends ANY_ENTITY>(entity_id: ENTITY_ID): ByIdProxy<ENTITY_ID> {
|
|
68
120
|
const entity_domain = domain(entity_id) as ALL_SERVICE_DOMAINS;
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
121
|
+
const { ...thing } = hass.entity.getCurrentState(entity_id) as ByIdProxy<ENTITY_ID>;
|
|
122
|
+
let loaded = false;
|
|
123
|
+
|
|
124
|
+
function keys() {
|
|
125
|
+
const entityDomain = domain(entity_id);
|
|
126
|
+
return [
|
|
127
|
+
"attributes",
|
|
128
|
+
"entity_id",
|
|
129
|
+
"history",
|
|
130
|
+
"last",
|
|
131
|
+
"nextState",
|
|
132
|
+
"once",
|
|
133
|
+
"onUpdate",
|
|
134
|
+
"previous",
|
|
135
|
+
"removeAllListeners",
|
|
136
|
+
"state",
|
|
137
|
+
"waitForState",
|
|
138
|
+
...hass.configure
|
|
139
|
+
.getServices()
|
|
140
|
+
.filter(({ domain }) => domain === entityDomain)
|
|
141
|
+
.flatMap(i => Object.keys(i.services))
|
|
142
|
+
.sort((a, b) => (a > b ? UP : DOWN)),
|
|
143
|
+
];
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
function appendKeys(force = false) {
|
|
147
|
+
if (loaded && !force) {
|
|
148
|
+
return;
|
|
93
149
|
}
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
// @ts-ignore
|
|
103
|
-
keys().forEach(i => (thing[i] ??= () => {}));
|
|
104
|
-
if (!is.empty(hass.configure.getServices())) {
|
|
105
|
-
loaded = true;
|
|
106
|
-
}
|
|
150
|
+
// Not gonna build types for this, and ts-expect-error fails in jest
|
|
151
|
+
// This is a weird hack for an obscure feature, so sue me
|
|
152
|
+
//
|
|
153
|
+
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
|
154
|
+
// @ts-ignore
|
|
155
|
+
keys().forEach(i => (thing[i] ??= () => {}));
|
|
156
|
+
if (!is.empty(hass.configure.getServices())) {
|
|
157
|
+
loaded = true;
|
|
107
158
|
}
|
|
108
|
-
|
|
109
|
-
ENTITY_PROXIES.set(
|
|
110
|
-
entity_id,
|
|
111
|
-
// just because you can't do generics properly....
|
|
112
|
-
new Proxy(thing, {
|
|
113
|
-
// things that shouldn't be needed: this extract
|
|
114
|
-
// eslint-disable-next-line sonarjs/function-return-type
|
|
115
|
-
get: (_, property: Extract<keyof ByIdProxy<ENTITY_ID>, string>) => {
|
|
116
|
-
switch (property) {
|
|
117
|
-
// * onUpdate
|
|
118
|
-
case "onUpdate": {
|
|
119
|
-
return (callback: TAnyFunction) => {
|
|
120
|
-
const removableCallback = async (
|
|
121
|
-
a: ENTITY_STATE<ENTITY_ID>,
|
|
122
|
-
b: ENTITY_STATE<ENTITY_ID>,
|
|
123
|
-
) => await internal.safeExec(async () => callback(a, b, remove));
|
|
124
|
-
function remove() {
|
|
125
|
-
event.removeListener(entity_id, removableCallback);
|
|
126
|
-
}
|
|
159
|
+
}
|
|
127
160
|
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
};
|
|
131
|
-
}
|
|
161
|
+
event.on(SERVICE_LIST_UPDATED, () => appendKeys(true));
|
|
162
|
+
const listeners = new Set<() => void>();
|
|
132
163
|
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
164
|
+
// just because you can't do generics properly....
|
|
165
|
+
return new Proxy(thing, {
|
|
166
|
+
// things that shouldn't be needed: this extract
|
|
167
|
+
// eslint-disable-next-line sonarjs/function-return-type
|
|
168
|
+
get: (_, property: Extract<keyof ByIdProxy<ENTITY_ID>, string>) => {
|
|
169
|
+
switch (property) {
|
|
170
|
+
// #MARK: onUpdate
|
|
171
|
+
case "onUpdate": {
|
|
172
|
+
return (callback: TAnyFunction) => {
|
|
173
|
+
const removableCallback = async (
|
|
174
|
+
new_state: ENTITY_STATE<ENTITY_ID>,
|
|
175
|
+
old_state: ENTITY_STATE<ENTITY_ID>,
|
|
176
|
+
) => await internal.safeExec(async () => callback(new_state, old_state, remove));
|
|
177
|
+
function remove() {
|
|
178
|
+
event.removeListener(entity_id, removableCallback);
|
|
179
|
+
event.removeListener(SOCKET_CONNECTED, removableCallback);
|
|
180
|
+
listeners.delete(remove);
|
|
181
|
+
logger.trace({ entity_id }, "remove [onUpdate] listener");
|
|
138
182
|
}
|
|
139
183
|
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
return await hass.fetch.fetchEntityHistory(entity_id, from, to);
|
|
144
|
-
};
|
|
145
|
-
}
|
|
184
|
+
event.on(entity_id, removableCallback);
|
|
185
|
+
event.on(SOCKET_CONNECTED, removableCallback);
|
|
186
|
+
listeners.add(remove);
|
|
146
187
|
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
event.once(entity_id, async (a, b) => callback(a, b));
|
|
151
|
-
}
|
|
188
|
+
return is.removeFn(remove);
|
|
189
|
+
};
|
|
190
|
+
}
|
|
152
191
|
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
192
|
+
// #MARK: addListener
|
|
193
|
+
case "addListener": {
|
|
194
|
+
return function (remove: RemoveCallback) {
|
|
195
|
+
listeners.add(remove);
|
|
196
|
+
};
|
|
197
|
+
}
|
|
157
198
|
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
199
|
+
// #MARK: removeAllListeners
|
|
200
|
+
case "removeAllListeners": {
|
|
201
|
+
return function () {
|
|
202
|
+
// remove will delete from set
|
|
203
|
+
listeners.forEach(remove => remove());
|
|
204
|
+
};
|
|
205
|
+
}
|
|
162
206
|
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
done(entity satisfies ENTITY_STATE<ENTITY_ID>);
|
|
170
|
-
done = undefined;
|
|
171
|
-
}
|
|
172
|
-
};
|
|
173
|
-
event.once(entity_id, complete);
|
|
174
|
-
if (is.number(timeout) && timeout > NONE) {
|
|
175
|
-
await sleep(timeout);
|
|
176
|
-
if (done) {
|
|
177
|
-
logger.debug({ entity_id, name: "nextState", timeout }, "timed out");
|
|
178
|
-
done(undefined);
|
|
179
|
-
done = undefined;
|
|
180
|
-
event.removeListener(entity_id, complete);
|
|
181
|
-
}
|
|
182
|
-
}
|
|
183
|
-
});
|
|
184
|
-
}
|
|
207
|
+
// #MARK: history
|
|
208
|
+
case "history": {
|
|
209
|
+
return async function (from: Dayjs | Date, to: Dayjs | Date) {
|
|
210
|
+
return await hass.fetch.fetchEntityHistory(entity_id, from, to);
|
|
211
|
+
};
|
|
212
|
+
}
|
|
185
213
|
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
},
|
|
198
|
-
`state did not match`,
|
|
199
|
-
);
|
|
200
|
-
return;
|
|
201
|
-
}
|
|
202
|
-
if (done) {
|
|
203
|
-
done(entity satisfies ENTITY_STATE<ENTITY_ID>);
|
|
204
|
-
done = undefined;
|
|
205
|
-
event.removeListener(entity_id, complete);
|
|
206
|
-
}
|
|
207
|
-
};
|
|
208
|
-
event.on(entity_id, complete);
|
|
209
|
-
if (is.number(timeout) && timeout > NONE) {
|
|
210
|
-
await sleep(timeout);
|
|
211
|
-
if (done) {
|
|
212
|
-
logger.debug({ entity_id, name: "waitForState", timeout }, "timed out");
|
|
213
|
-
done(undefined);
|
|
214
|
-
done = undefined;
|
|
215
|
-
event.removeListener(entity_id, complete);
|
|
216
|
-
}
|
|
217
|
-
}
|
|
218
|
-
});
|
|
219
|
-
}
|
|
220
|
-
}
|
|
221
|
-
if (hass.configure.isService(entity_domain, property)) {
|
|
222
|
-
return async function (data = {}) {
|
|
223
|
-
// @ts-expect-error it's fine
|
|
224
|
-
return await hass.call[entity_domain][property]({
|
|
225
|
-
entity_id,
|
|
226
|
-
...data,
|
|
227
|
-
});
|
|
214
|
+
// #MARK: once
|
|
215
|
+
case "once": {
|
|
216
|
+
return (callback: TAnyFunction) => {
|
|
217
|
+
const remove = () => {
|
|
218
|
+
event.removeListener(entity_id, wrapped);
|
|
219
|
+
listeners.delete(remove);
|
|
220
|
+
logger.trace({ entity_id }, "remove [once] listener");
|
|
221
|
+
};
|
|
222
|
+
const wrapped = async (a: ENTITY_STATE<ENTITY_ID>, b: ENTITY_STATE<ENTITY_ID>) => {
|
|
223
|
+
listeners.delete(remove);
|
|
224
|
+
callback(a, b);
|
|
228
225
|
};
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
226
|
+
listeners.add(remove);
|
|
227
|
+
event.once(entity_id, wrapped);
|
|
228
|
+
return is.removeFn(remove);
|
|
229
|
+
};
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
// #MARK: entity_id
|
|
233
|
+
case "entity_id": {
|
|
234
|
+
return entity_id;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
// #MARK: previous
|
|
238
|
+
case "previous": {
|
|
239
|
+
return hass.entity.previousState(entity_id);
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
// #MARK: nextState
|
|
243
|
+
case "nextState": {
|
|
244
|
+
return async (timeout?: number) =>
|
|
245
|
+
await new Promise<ENTITY_STATE<ENTITY_ID>>(async done => {
|
|
246
|
+
// - set up cleanup function
|
|
247
|
+
const remove = () => {
|
|
248
|
+
listeners.delete(remove);
|
|
249
|
+
event.removeListener(entity_id, complete);
|
|
250
|
+
done = undefined;
|
|
251
|
+
logger.trace({ entity_id }, "remove [nextState] listener");
|
|
252
|
+
if (wait) {
|
|
253
|
+
logger.trace({ entity_id }, "stopping [nextState] race timer");
|
|
254
|
+
wait.kill("stop");
|
|
255
|
+
}
|
|
256
|
+
};
|
|
257
|
+
listeners.add(remove);
|
|
258
|
+
|
|
259
|
+
// - add wrapper & make friendly with race
|
|
260
|
+
const complete = (entity: ENTITY_STATE<ENTITY_ID>) => {
|
|
261
|
+
if (done) {
|
|
262
|
+
done(entity satisfies ENTITY_STATE<ENTITY_ID>);
|
|
263
|
+
done = undefined;
|
|
264
|
+
}
|
|
265
|
+
};
|
|
266
|
+
|
|
267
|
+
// - attach single run listener
|
|
268
|
+
event.once(entity_id, complete);
|
|
269
|
+
|
|
270
|
+
// - race!
|
|
271
|
+
let wait: ReturnType<typeof sleep>;
|
|
272
|
+
if (is.number(timeout) && timeout > NONE) {
|
|
273
|
+
// keep track of sleep so it can be cleaned up also
|
|
274
|
+
wait = sleep(timeout);
|
|
275
|
+
await wait;
|
|
276
|
+
wait = undefined;
|
|
277
|
+
if (done) {
|
|
278
|
+
logger.debug({ entity_id, name: "nextState", timeout }, "timed out");
|
|
279
|
+
done(undefined);
|
|
280
|
+
remove();
|
|
281
|
+
}
|
|
282
|
+
}
|
|
248
283
|
});
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
// #MARK: waitForState
|
|
287
|
+
case "waitForState": {
|
|
288
|
+
return async (state: string | number, timeout?: number) =>
|
|
289
|
+
await new Promise<ENTITY_STATE<ENTITY_ID>>(async done => {
|
|
290
|
+
const remove = () => {
|
|
291
|
+
done = undefined;
|
|
292
|
+
listeners.delete(remove);
|
|
293
|
+
done = undefined;
|
|
294
|
+
logger.trace({ entity_id }, "remove [waitForState] listener");
|
|
295
|
+
if (wait) {
|
|
296
|
+
logger.trace({ entity_id }, "stopping [waitForState] race timer");
|
|
297
|
+
|
|
298
|
+
wait.kill("stop");
|
|
299
|
+
}
|
|
300
|
+
event.removeListener(entity_id, complete);
|
|
301
|
+
};
|
|
302
|
+
listeners.add(remove);
|
|
303
|
+
|
|
304
|
+
const complete = (entity: ENTITY_STATE<ENTITY_ID>) => {
|
|
305
|
+
if (entity.state !== state) {
|
|
306
|
+
logger.trace(
|
|
307
|
+
{
|
|
308
|
+
expected: state,
|
|
309
|
+
incoming: entity.state,
|
|
310
|
+
name: "waitForState",
|
|
311
|
+
},
|
|
312
|
+
`state did not match`,
|
|
313
|
+
);
|
|
314
|
+
return;
|
|
315
|
+
}
|
|
316
|
+
if (done) {
|
|
317
|
+
done(entity satisfies ENTITY_STATE<ENTITY_ID>);
|
|
318
|
+
remove();
|
|
319
|
+
}
|
|
320
|
+
};
|
|
321
|
+
|
|
322
|
+
event.on(entity_id, complete);
|
|
323
|
+
let wait: ReturnType<typeof sleep>;
|
|
324
|
+
if (is.number(timeout) && timeout > NONE) {
|
|
325
|
+
wait = sleep(timeout);
|
|
326
|
+
await wait;
|
|
327
|
+
wait = undefined;
|
|
328
|
+
if (done) {
|
|
329
|
+
logger.debug({ entity_id, name: "waitForState", timeout }, "timed out");
|
|
330
|
+
done(undefined);
|
|
331
|
+
remove();
|
|
332
|
+
}
|
|
333
|
+
}
|
|
265
334
|
});
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
// #MARK: service calls
|
|
338
|
+
if (hass.configure.isService(entity_domain, property)) {
|
|
339
|
+
return async function (data = {}) {
|
|
340
|
+
// @ts-expect-error it's fine
|
|
341
|
+
return await hass.call[entity_domain][property]({
|
|
342
|
+
entity_id,
|
|
343
|
+
...data,
|
|
344
|
+
});
|
|
345
|
+
};
|
|
346
|
+
}
|
|
347
|
+
return proxyGetLogic(entity_id, property);
|
|
348
|
+
},
|
|
349
|
+
// #MARK: has
|
|
350
|
+
has(_, property: string) {
|
|
351
|
+
appendKeys();
|
|
352
|
+
return property in thing;
|
|
353
|
+
},
|
|
354
|
+
// #MARK: ownKeys
|
|
355
|
+
ownKeys() {
|
|
356
|
+
appendKeys();
|
|
357
|
+
return Object.keys(thing);
|
|
358
|
+
},
|
|
359
|
+
// #MARK: set
|
|
360
|
+
set(_, property: Extract<keyof ByIdProxy<ENTITY_ID>, string>, value: unknown) {
|
|
361
|
+
// * state
|
|
362
|
+
if (property === "state") {
|
|
363
|
+
setImmediate(async () => {
|
|
364
|
+
logger.debug({ entity_id, state: value }, `emitting set state via rest`);
|
|
365
|
+
await hass.fetch.updateEntity(entity_id, {
|
|
366
|
+
state: value as string | number,
|
|
367
|
+
});
|
|
368
|
+
});
|
|
369
|
+
return true;
|
|
370
|
+
}
|
|
371
|
+
// * attributes
|
|
372
|
+
if (property === "attributes") {
|
|
373
|
+
if (!is.object(value)) {
|
|
374
|
+
logger.error(`can only provide objects as attributes`);
|
|
269
375
|
return false;
|
|
270
|
-
}
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
376
|
+
}
|
|
377
|
+
setImmediate(async () => {
|
|
378
|
+
logger.debug(
|
|
379
|
+
{ attributes: Object.keys(value), entity_id },
|
|
380
|
+
`updating attributes via rest`,
|
|
381
|
+
);
|
|
382
|
+
await hass.fetch.updateEntity(entity_id, {
|
|
383
|
+
attributes: value,
|
|
384
|
+
});
|
|
385
|
+
});
|
|
386
|
+
return true;
|
|
387
|
+
}
|
|
388
|
+
logger.error({ entity_id, property }, `cannot set property on entity`);
|
|
389
|
+
return false;
|
|
390
|
+
},
|
|
391
|
+
});
|
|
275
392
|
}
|
|
276
393
|
|
|
394
|
+
// #MARK: <return>
|
|
277
395
|
return {
|
|
278
396
|
area: <AREA extends TAreaId, DOMAINS extends TRawDomains = TRawDomains>(
|
|
279
397
|
area: AREA,
|
|
280
398
|
...domains: DOMAINS[]
|
|
281
399
|
): ByIdProxy<PICK_FROM_AREA<AREA, DOMAINS>>[] =>
|
|
282
|
-
hass.idBy.area<AREA, DOMAINS>(area, ...domains).map(id =>
|
|
400
|
+
hass.idBy.area<AREA, DOMAINS>(area, ...domains).map(id => hass.refBy.id(id)),
|
|
283
401
|
|
|
284
402
|
device: <DEVICE extends TDeviceId, DOMAINS extends TRawDomains = TRawDomains>(
|
|
285
403
|
device: DEVICE,
|
|
286
404
|
...domains: DOMAINS[]
|
|
287
405
|
): ByIdProxy<PICK_FROM_DEVICE<DEVICE, DOMAINS>>[] =>
|
|
288
|
-
hass.idBy.device<DEVICE, DOMAINS>(device, ...domains).map(id =>
|
|
406
|
+
hass.idBy.device<DEVICE, DOMAINS>(device, ...domains).map(id => hass.refBy.id(id)),
|
|
289
407
|
|
|
290
408
|
domain: <DOMAIN extends TRawDomains = TRawDomains>(
|
|
291
409
|
domain: DOMAIN,
|
|
292
|
-
): ByIdProxy<PICK_ENTITY<DOMAIN>>[] =>
|
|
410
|
+
): ByIdProxy<PICK_ENTITY<DOMAIN>>[] =>
|
|
411
|
+
hass.idBy.domain<DOMAIN>(domain).map(id => hass.refBy.id(id)),
|
|
293
412
|
|
|
294
413
|
floor: <FLOOR extends TFloorId, DOMAINS extends TRawDomains = TRawDomains>(
|
|
295
414
|
floor: FLOOR,
|
|
296
415
|
...domains: DOMAINS[]
|
|
297
416
|
): ByIdProxy<PICK_FROM_FLOOR<FLOOR, DOMAINS>>[] =>
|
|
298
|
-
hass.idBy.floor<FLOOR, DOMAINS>(floor, ...domains).map(id =>
|
|
417
|
+
hass.idBy.floor<FLOOR, DOMAINS>(floor, ...domains).map(id => hass.refBy.id(id)),
|
|
299
418
|
|
|
300
419
|
id: byId,
|
|
301
420
|
|
|
@@ -303,13 +422,13 @@ export function ReferenceExtension({
|
|
|
303
422
|
label: LABEL,
|
|
304
423
|
...domains: DOMAINS[]
|
|
305
424
|
): ByIdProxy<PICK_FROM_LABEL<LABEL, DOMAINS>>[] =>
|
|
306
|
-
hass.idBy.label<LABEL, DOMAINS>(label, ...domains).map(id =>
|
|
425
|
+
hass.idBy.label<LABEL, DOMAINS>(label, ...domains).map(id => hass.refBy.id(id)),
|
|
307
426
|
|
|
308
427
|
platform: <PLATFORM extends TPlatformId, DOMAINS extends TRawDomains = TRawDomains>(
|
|
309
428
|
platform: PLATFORM,
|
|
310
429
|
...domains: DOMAINS[]
|
|
311
430
|
): ByIdProxy<PICK_FROM_PLATFORM<PLATFORM, DOMAINS>>[] =>
|
|
312
|
-
hass.idBy.platform<PLATFORM, DOMAINS>(platform, ...domains).map(id =>
|
|
431
|
+
hass.idBy.platform<PLATFORM, DOMAINS>(platform, ...domains).map(id => hass.refBy.id(id)),
|
|
313
432
|
|
|
314
433
|
unique_id: <
|
|
315
434
|
UNIQUE_ID extends TUniqueId,
|
|
@@ -322,9 +441,12 @@ export function ReferenceExtension({
|
|
|
322
441
|
): ByIdProxy<ENTITY_ID> => {
|
|
323
442
|
const id = hass.idBy.unique_id<UNIQUE_ID, ENTITY_ID>(unique_id);
|
|
324
443
|
if (!id) {
|
|
444
|
+
// mental note:
|
|
445
|
+
// this is technically fixable, but would require emitting internal events
|
|
446
|
+
// for both entity_id & unique_id
|
|
325
447
|
return undefined;
|
|
326
448
|
}
|
|
327
|
-
return
|
|
449
|
+
return hass.refBy.id(id);
|
|
328
450
|
},
|
|
329
451
|
};
|
|
330
452
|
}
|
|
@@ -493,10 +493,10 @@ export function WebsocketAPI({
|
|
|
493
493
|
} else {
|
|
494
494
|
socketEvents.on(event, callback);
|
|
495
495
|
}
|
|
496
|
-
return () => {
|
|
496
|
+
return is.removeFn(() => {
|
|
497
497
|
logger.trace({ context, event, name: onEvent }, `removing socket event listener`);
|
|
498
498
|
socketEvents.removeListener(event, callback);
|
|
499
|
-
};
|
|
499
|
+
});
|
|
500
500
|
}
|
|
501
501
|
|
|
502
502
|
// #MARK: subscribe
|
|
@@ -506,7 +506,7 @@ export function WebsocketAPI({
|
|
|
506
506
|
exec,
|
|
507
507
|
}: SocketSubscribeOptions<EVENT>) {
|
|
508
508
|
await hass.socket.sendMessage({ event_type, type: "subscribe_events" });
|
|
509
|
-
hass.socket.onEvent({
|
|
509
|
+
return hass.socket.onEvent({
|
|
510
510
|
context,
|
|
511
511
|
event: event_type,
|
|
512
512
|
exec,
|
|
@@ -527,6 +527,7 @@ export function WebsocketAPI({
|
|
|
527
527
|
// attach anyways, for restarts or whatever
|
|
528
528
|
}
|
|
529
529
|
event.on(SOCKET_CONNECTED, wrapped);
|
|
530
|
+
return is.removeFn(() => event.removeListener(SOCKET_CONNECTED, wrapped));
|
|
530
531
|
}
|
|
531
532
|
|
|
532
533
|
// #MARK: return object
|
package/src/hass.module.ts
CHANGED
|
@@ -7,13 +7,13 @@ import {
|
|
|
7
7
|
Configure,
|
|
8
8
|
Device,
|
|
9
9
|
EntityManager,
|
|
10
|
-
|
|
10
|
+
EventsService,
|
|
11
11
|
FetchAPI,
|
|
12
12
|
FetchInternals,
|
|
13
13
|
Floor,
|
|
14
14
|
IDByExtension,
|
|
15
15
|
Label,
|
|
16
|
-
|
|
16
|
+
ReferenceService,
|
|
17
17
|
Registry,
|
|
18
18
|
WebsocketAPI,
|
|
19
19
|
Zone,
|
|
@@ -158,7 +158,7 @@ export const LIB_HASS = CreateLibrary({
|
|
|
158
158
|
/**
|
|
159
159
|
* named event attachments
|
|
160
160
|
*/
|
|
161
|
-
events:
|
|
161
|
+
events: EventsService,
|
|
162
162
|
|
|
163
163
|
/**
|
|
164
164
|
* rest api commands
|
|
@@ -188,7 +188,7 @@ export const LIB_HASS = CreateLibrary({
|
|
|
188
188
|
/**
|
|
189
189
|
* obtain references to entities
|
|
190
190
|
*/
|
|
191
|
-
refBy:
|
|
191
|
+
refBy: ReferenceService,
|
|
192
192
|
|
|
193
193
|
/**
|
|
194
194
|
* interact with the home assistant registry
|