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