@glasshome/sync-layer 0.7.2 → 0.8.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/index.d.ts +1291 -54
- package/dist/index.js +203 -304
- package/dist/service-C8H42I-Q.d.ts +459 -0
- package/dist/solid/index.d.ts +119 -6
- package/dist/solid/index.js +20 -15
- package/dist/{statistics-CtRfIF6D.js → statistics-DyCCTwv2.js} +83 -95
- package/dist/worker/worker-main.d.ts +11 -8
- package/dist/worker/worker-main.js +2 -3
- package/dist/{worker-main-C-3QM4di.js → worker-main-D_wyGAIG.js} +47 -157
- package/package.json +4 -4
- package/dist/calendar/track.d.ts +0 -19
- package/dist/calendar/types.d.ts +0 -32
- package/dist/camera/fetch.d.ts +0 -16
- package/dist/camera/query.d.ts +0 -15
- package/dist/camera/types.d.ts +0 -85
- package/dist/camera/webrtc.d.ts +0 -31
- package/dist/commands/service.d.ts +0 -53
- package/dist/commands/types.d.ts +0 -113
- package/dist/connection/adapter.d.ts +0 -48
- package/dist/connection/auth.d.ts +0 -126
- package/dist/connection/bridged.d.ts +0 -16
- package/dist/connection/manager.d.ts +0 -51
- package/dist/connection/subscription-manager.d.ts +0 -48
- package/dist/connection/subscriptions.d.ts +0 -31
- package/dist/connection/types.d.ts +0 -151
- package/dist/core/privileged-conn.d.ts +0 -29
- package/dist/core/reducers.d.ts +0 -99
- package/dist/core/store.d.ts +0 -100
- package/dist/core/types.d.ts +0 -339
- package/dist/demo/demo-data.d.ts +0 -24
- package/dist/demo/demo-provider.d.ts +0 -40
- package/dist/demo/energy-sim.d.ts +0 -112
- package/dist/energy/prefs.d.ts +0 -56
- package/dist/entities/area-views.d.ts +0 -32
- package/dist/entities/index.d.ts +0 -54
- package/dist/entities/views.d.ts +0 -35
- package/dist/history/constants.d.ts +0 -16
- package/dist/history/fetch.d.ts +0 -19
- package/dist/history/query.d.ts +0 -27
- package/dist/history/statistics.d.ts +0 -55
- package/dist/history/types.d.ts +0 -146
- package/dist/host-api.d.ts +0 -50
- package/dist/media/url.d.ts +0 -14
- package/dist/query/area-builder.d.ts +0 -25
- package/dist/query/builder.d.ts +0 -18
- package/dist/query/helpers.d.ts +0 -21
- package/dist/query/memoization.d.ts +0 -18
- package/dist/query/projections.d.ts +0 -74
- package/dist/query/types.d.ts +0 -260
- package/dist/solid/hooks.d.ts +0 -119
- package/dist/solid/indices.d.ts +0 -15
- package/dist/weather/fetch.d.ts +0 -19
- package/dist/weather/query.d.ts +0 -23
- package/dist/weather/types.d.ts +0 -126
- package/dist/worker/bridge-client.d.ts +0 -47
- package/dist/worker/enforcement.d.ts +0 -53
- package/dist/worker/protocol.d.ts +0 -129
- package/dist/worker/token-store.d.ts +0 -3
package/dist/index.js
CHANGED
|
@@ -1,8 +1,7 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { runHaBridgeWorker } from "./worker-main-
|
|
3
|
-
import
|
|
1
|
+
import { A as extractDomain, B as state, C as startDemoEnergyTicker, D as demoEnergyPreferences, E as createDemoFixtures, F as forceResubscribeCalendars, L as privilegedConn, M as registerEntity, N as setManagerConnection, O as isEnergyEntity, P as setResubscribeHandler, R as setPrivilegedConn, S as loadDemoData, T as unloadDemoData, _ as turnOff, a as getAreaView, b as applyDemoServiceCall, c as entityViewEquals, d as getEntityViews, f as batchEntityUpdates, g as toggle, h as sendCommand, i as buildDeviceView, j as forceResubscribe, k as synthesizeEnergyHistory, l as getAllEntityViews, m as callService, n as normalizeStatisticTime, o as getAreaViews, p as batchServiceCalls, r as buildAreaView, s as buildEntityView, t as fetchStatisticsDuringPeriod, u as getEntityView, v as turnOn, w as stopDemoEnergyTicker, x as isDemoMode, y as updateEntity, z as setState } from "./statistics-DyCCTwv2.js";
|
|
2
|
+
import { t as runHaBridgeWorker } from "./worker-main-D_wyGAIG.js";
|
|
3
|
+
import "home-assistant-js-websocket";
|
|
4
4
|
import { produce } from "solid-js/store";
|
|
5
|
-
|
|
6
5
|
//#region src/core/reducers.ts
|
|
7
6
|
/**
|
|
8
7
|
* Bulk update multiple entities
|
|
@@ -11,9 +10,9 @@ import { produce } from "solid-js/store";
|
|
|
11
10
|
*
|
|
12
11
|
* @param entities - Array of entity states
|
|
13
12
|
*/
|
|
14
|
-
function bulkUpdateEntities(entities
|
|
13
|
+
function bulkUpdateEntities(entities) {
|
|
15
14
|
setState(produce((s) => {
|
|
16
|
-
for (const entity
|
|
15
|
+
for (const entity of entities) s.entities[entity.entity_id] = entity;
|
|
17
16
|
}));
|
|
18
17
|
}
|
|
19
18
|
/**
|
|
@@ -71,9 +70,9 @@ function removeDevice(deviceId) {
|
|
|
71
70
|
*
|
|
72
71
|
* @param area - Area registry entry
|
|
73
72
|
*/
|
|
74
|
-
function updateArea(area
|
|
73
|
+
function updateArea(area) {
|
|
75
74
|
setState(produce((s) => {
|
|
76
|
-
s.areas[area
|
|
75
|
+
s.areas[area.id] = area;
|
|
77
76
|
}));
|
|
78
77
|
}
|
|
79
78
|
/**
|
|
@@ -136,121 +135,16 @@ function updateStatisticsMetadata(metadata) {
|
|
|
136
135
|
for (const meta of metadata) s.statisticsMetadata[meta.statistic_id] = meta;
|
|
137
136
|
}));
|
|
138
137
|
}
|
|
139
|
-
|
|
140
138
|
//#endregion
|
|
141
|
-
//#region src/
|
|
139
|
+
//#region src/history/constants.ts
|
|
142
140
|
/**
|
|
143
|
-
*
|
|
144
|
-
*
|
|
145
|
-
* This function uses `getAuth` from `home-assistant-js-websocket` which:
|
|
146
|
-
* 1. Redirects the whole page to the Home Assistant authorization page
|
|
147
|
-
* 2. Waits for user authorization
|
|
148
|
-
* 3. On return, HA redirects back to the app URL with `?auth_callback=1&code=…`
|
|
149
|
-
* 4. Exchanges the authorization code for tokens and returns an Auth object
|
|
150
|
-
*
|
|
151
|
-
* Note: For a complete OAuth flow, you typically need to:
|
|
152
|
-
* - Set up a redirect URI endpoint in your app
|
|
153
|
-
* - Handle the OAuth callback with the authorization code
|
|
154
|
-
* - Use `getAuthCode` callback to provide the code
|
|
155
|
-
*
|
|
156
|
-
* In browser environments `getAuth` handles this automatically via a full-page
|
|
157
|
-
* redirect (not a popup); native platforms supply `getAuthCode` instead.
|
|
158
|
-
*
|
|
159
|
-
* @param options - OAuth options
|
|
160
|
-
* @returns Authenticated Auth object
|
|
141
|
+
* History Constants
|
|
161
142
|
*
|
|
162
|
-
*
|
|
163
|
-
*
|
|
164
|
-
* // Basic OAuth flow (browser only)
|
|
165
|
-
* const auth = await authenticateWithOAuth({
|
|
166
|
-
* url: 'https://homeassistant.local:8123',
|
|
167
|
-
* clientId: 'https://homeassistant.local:8123',
|
|
168
|
-
* redirectUri: 'https://myapp.com/auth/callback',
|
|
169
|
-
* });
|
|
170
|
-
* ```
|
|
143
|
+
* Constants and utilities for history fetching and processing.
|
|
144
|
+
* Based on Home Assistant frontend implementation.
|
|
171
145
|
*
|
|
172
|
-
* @
|
|
173
|
-
* ```typescript
|
|
174
|
-
* // OAuth with custom code handling
|
|
175
|
-
* const auth = await authenticateWithOAuth({
|
|
176
|
-
* url: 'https://homeassistant.local:8123',
|
|
177
|
-
* clientId: 'https://homeassistant.local:8123',
|
|
178
|
-
* redirectUri: 'https://myapp.com/auth/callback',
|
|
179
|
-
* getAuthCode: async () => {
|
|
180
|
-
* // Wait for user to authorize and get code from callback
|
|
181
|
-
* return await waitForAuthCode();
|
|
182
|
-
* },
|
|
183
|
-
* });
|
|
184
|
-
* ```
|
|
146
|
+
* @packageDocumentation
|
|
185
147
|
*/
|
|
186
|
-
async function authenticateWithOAuth(options) {
|
|
187
|
-
const { url, clientId, redirectUri, authCode, getAuthCode, saveAuth, loadAuth, limitHassInstance } = options;
|
|
188
|
-
try {
|
|
189
|
-
const auth$1 = await getAuth({
|
|
190
|
-
hassUrl: url,
|
|
191
|
-
clientId: clientId ?? null,
|
|
192
|
-
redirectUrl: redirectUri,
|
|
193
|
-
authCode,
|
|
194
|
-
limitHassInstance,
|
|
195
|
-
async loadTokens() {
|
|
196
|
-
if (loadAuth) {
|
|
197
|
-
const saved = await loadAuth();
|
|
198
|
-
if (saved?.data) return saved.data;
|
|
199
|
-
}
|
|
200
|
-
return null;
|
|
201
|
-
},
|
|
202
|
-
saveTokens(tokens) {
|
|
203
|
-
if (saveAuth && tokens) {
|
|
204
|
-
const authObj = {
|
|
205
|
-
hassUrl: url,
|
|
206
|
-
clientId,
|
|
207
|
-
redirectUrl: redirectUri,
|
|
208
|
-
data: tokens,
|
|
209
|
-
async refreshAccessToken() {
|
|
210
|
-
return Promise.resolve();
|
|
211
|
-
}
|
|
212
|
-
};
|
|
213
|
-
saveAuth(authObj);
|
|
214
|
-
}
|
|
215
|
-
}
|
|
216
|
-
});
|
|
217
|
-
return auth$1;
|
|
218
|
-
} catch {}
|
|
219
|
-
let finalAuthCode = authCode;
|
|
220
|
-
if (!finalAuthCode && getAuthCode) finalAuthCode = await getAuthCode();
|
|
221
|
-
const auth = await getAuth({
|
|
222
|
-
hassUrl: url,
|
|
223
|
-
clientId: clientId ?? null,
|
|
224
|
-
redirectUrl: redirectUri,
|
|
225
|
-
authCode: finalAuthCode,
|
|
226
|
-
limitHassInstance,
|
|
227
|
-
async loadTokens() {
|
|
228
|
-
if (loadAuth) {
|
|
229
|
-
const saved = await loadAuth();
|
|
230
|
-
if (saved?.data) return saved.data;
|
|
231
|
-
}
|
|
232
|
-
return null;
|
|
233
|
-
},
|
|
234
|
-
saveTokens(tokens) {
|
|
235
|
-
if (saveAuth && tokens) {
|
|
236
|
-
const authObj = {
|
|
237
|
-
hassUrl: url,
|
|
238
|
-
clientId,
|
|
239
|
-
redirectUrl: redirectUri,
|
|
240
|
-
data: tokens,
|
|
241
|
-
async refreshAccessToken() {
|
|
242
|
-
return Promise.resolve();
|
|
243
|
-
}
|
|
244
|
-
};
|
|
245
|
-
saveAuth(authObj);
|
|
246
|
-
}
|
|
247
|
-
}
|
|
248
|
-
});
|
|
249
|
-
return auth;
|
|
250
|
-
}
|
|
251
|
-
|
|
252
|
-
//#endregion
|
|
253
|
-
//#region src/history/constants.ts
|
|
254
148
|
const DOMAINS_USE_LAST_UPDATED = [
|
|
255
149
|
"climate",
|
|
256
150
|
"humidifier",
|
|
@@ -283,10 +177,14 @@ function entityIdHistoryNeedsAttributes(entityId) {
|
|
|
283
177
|
if (!state.entities[entityId]) return NEED_ATTRIBUTE_DOMAINS.includes(domain);
|
|
284
178
|
return NEED_ATTRIBUTE_DOMAINS.includes(domain);
|
|
285
179
|
}
|
|
286
|
-
|
|
287
180
|
//#endregion
|
|
288
181
|
//#region src/history/fetch.ts
|
|
289
182
|
/**
|
|
183
|
+
* History Fetching
|
|
184
|
+
*
|
|
185
|
+
* @packageDocumentation
|
|
186
|
+
*/
|
|
187
|
+
/**
|
|
290
188
|
* Fetch history for entities during a time period
|
|
291
189
|
*/
|
|
292
190
|
async function fetchHistory(options) {
|
|
@@ -306,19 +204,17 @@ async function fetchHistory(options) {
|
|
|
306
204
|
}
|
|
307
205
|
const startTimeStr = startTime.toISOString();
|
|
308
206
|
const endTimeStr = endTime?.toISOString();
|
|
309
|
-
const noAttributes = noAttributesOverride ?? (entityIds.length === 0 ? false : !entityIds.some((entityId) => entityIdHistoryNeedsAttributes(entityId)));
|
|
310
207
|
const params = {
|
|
311
208
|
type: "history/history_during_period",
|
|
312
209
|
start_time: startTimeStr,
|
|
313
210
|
minimal_response: minimalResponse,
|
|
314
|
-
no_attributes:
|
|
211
|
+
no_attributes: noAttributesOverride ?? (entityIds.length === 0 ? false : !entityIds.some((entityId) => entityIdHistoryNeedsAttributes(entityId))),
|
|
315
212
|
include_start_time_state: includeStartTimeState,
|
|
316
213
|
significant_changes_only: significantChangesOnly
|
|
317
214
|
};
|
|
318
215
|
if (endTimeStr) params.end_time = endTimeStr;
|
|
319
216
|
if (entityIds.length !== 0) params.entity_ids = entityIds;
|
|
320
|
-
|
|
321
|
-
return response ?? {};
|
|
217
|
+
return await sendCommand(params) ?? {};
|
|
322
218
|
}
|
|
323
219
|
/**
|
|
324
220
|
* Fetch history for a single entity
|
|
@@ -326,7 +222,7 @@ async function fetchHistory(options) {
|
|
|
326
222
|
async function fetchEntityHistory(entityId, options) {
|
|
327
223
|
const { startTime, endTime, includeStartTimeState = true, significantChangesOnly = true, minimalResponse = false, noAttributes = false } = options;
|
|
328
224
|
try {
|
|
329
|
-
const
|
|
225
|
+
const entityHistory = (await fetchHistory({
|
|
330
226
|
startTime,
|
|
331
227
|
endTime,
|
|
332
228
|
entityIds: [entityId],
|
|
@@ -334,18 +230,16 @@ async function fetchEntityHistory(entityId, options) {
|
|
|
334
230
|
significantChangesOnly,
|
|
335
231
|
minimalResponse,
|
|
336
232
|
noAttributes
|
|
337
|
-
});
|
|
338
|
-
const entityHistory = historyData[entityId] ?? [];
|
|
339
|
-
const timeline = entityHistory.map((s) => ({
|
|
340
|
-
timestamp: s.lu,
|
|
341
|
-
state: s.s,
|
|
342
|
-
attributes: s.a,
|
|
343
|
-
lastChanged: s.lc,
|
|
344
|
-
lastUpdated: s.lu
|
|
345
|
-
}));
|
|
233
|
+
}))[entityId] ?? [];
|
|
346
234
|
return {
|
|
347
235
|
entityId,
|
|
348
|
-
timeline
|
|
236
|
+
timeline: entityHistory.map((s) => ({
|
|
237
|
+
timestamp: s.lu,
|
|
238
|
+
state: s.s,
|
|
239
|
+
attributes: s.a,
|
|
240
|
+
lastChanged: s.lc,
|
|
241
|
+
lastUpdated: s.lu
|
|
242
|
+
})),
|
|
349
243
|
entityHistory,
|
|
350
244
|
loading: false,
|
|
351
245
|
error: null,
|
|
@@ -374,7 +268,6 @@ function historyStateToTimeline(s) {
|
|
|
374
268
|
lastUpdated: s.lu
|
|
375
269
|
};
|
|
376
270
|
}
|
|
377
|
-
|
|
378
271
|
//#endregion
|
|
379
272
|
//#region src/history/query.ts
|
|
380
273
|
/** Hard cap on retained points per entity timeline. */
|
|
@@ -462,12 +355,11 @@ function bulkAppendHistoryPoints(points) {
|
|
|
462
355
|
for (const entityId of touched) {
|
|
463
356
|
const history = s.history[entityId];
|
|
464
357
|
if (!history) continue;
|
|
465
|
-
if (history.entityHistory.length >
|
|
466
|
-
if (history.timeline.length >
|
|
358
|
+
if (history.entityHistory.length > 5760) history.entityHistory.splice(0, history.entityHistory.length - MAX_HISTORY_POINTS);
|
|
359
|
+
if (history.timeline.length > 5760) history.timeline.splice(0, history.timeline.length - MAX_HISTORY_POINTS);
|
|
467
360
|
}
|
|
468
361
|
}));
|
|
469
362
|
}
|
|
470
|
-
|
|
471
363
|
//#endregion
|
|
472
364
|
//#region src/connection/subscriptions.ts
|
|
473
365
|
/** Registry subscription cleanup functions (entity sub managed by subscription-manager) */
|
|
@@ -479,8 +371,7 @@ function bufferMessage(message) {
|
|
|
479
371
|
messageBuffer.push(message);
|
|
480
372
|
if (!rafScheduled) {
|
|
481
373
|
rafScheduled = true;
|
|
482
|
-
|
|
483
|
-
schedule(flushMessageBuffer);
|
|
374
|
+
(typeof requestAnimationFrame === "function" ? requestAnimationFrame : queueMicrotask)(flushMessageBuffer);
|
|
484
375
|
}
|
|
485
376
|
}
|
|
486
377
|
function flushMessageBuffer() {
|
|
@@ -511,10 +402,9 @@ function flushMessageBuffer() {
|
|
|
511
402
|
async function subscribeEntities(connection, entityIds) {
|
|
512
403
|
const subscribeMsg = { type: "subscribe_entities" };
|
|
513
404
|
if (entityIds.length > 0) subscribeMsg.entity_ids = entityIds;
|
|
514
|
-
|
|
405
|
+
return await connection.subscribeMessage((message) => {
|
|
515
406
|
bufferMessage(message);
|
|
516
407
|
}, subscribeMsg);
|
|
517
|
-
return unsub;
|
|
518
408
|
}
|
|
519
409
|
/**
|
|
520
410
|
* Subscribe to all relevant events from Home Assistant
|
|
@@ -592,8 +482,7 @@ async function handleDeviceRegistryUpdate(event) {
|
|
|
592
482
|
const conn = privilegedConn();
|
|
593
483
|
if (!conn) return;
|
|
594
484
|
try {
|
|
595
|
-
const
|
|
596
|
-
const device = devices.find((d) => d.id === device_id);
|
|
485
|
+
const device = (await conn.sendMessagePromise({ type: "config/device_registry/list" })).find((d) => d.id === device_id);
|
|
597
486
|
if (device) updateDevice(device);
|
|
598
487
|
} catch (error) {
|
|
599
488
|
console.error("Error fetching device:", error);
|
|
@@ -606,9 +495,8 @@ async function handleAreaRegistryUpdate(event) {
|
|
|
606
495
|
const conn = privilegedConn();
|
|
607
496
|
if (!conn) return;
|
|
608
497
|
try {
|
|
609
|
-
const
|
|
610
|
-
|
|
611
|
-
if (area$1) updateArea(area$1);
|
|
498
|
+
const area = (await conn.sendMessagePromise({ type: "config/area_registry/list" })).find((a) => a.id === area_id);
|
|
499
|
+
if (area) updateArea(area);
|
|
612
500
|
} catch (error) {
|
|
613
501
|
console.error("Error fetching area:", error);
|
|
614
502
|
}
|
|
@@ -620,8 +508,7 @@ async function handleFloorRegistryUpdate(event) {
|
|
|
620
508
|
const conn = privilegedConn();
|
|
621
509
|
if (!conn) return;
|
|
622
510
|
try {
|
|
623
|
-
const
|
|
624
|
-
const floor = floors.find((f) => f.floor_id === floor_id);
|
|
511
|
+
const floor = (await conn.sendMessagePromise({ type: "config/floor_registry/list" })).find((f) => f.floor_id === floor_id);
|
|
625
512
|
if (floor) updateFloor(floor);
|
|
626
513
|
} catch (error) {
|
|
627
514
|
console.error("Error fetching floor:", error);
|
|
@@ -634,8 +521,7 @@ async function handleLabelRegistryUpdate(event) {
|
|
|
634
521
|
const conn = privilegedConn();
|
|
635
522
|
if (!conn) return;
|
|
636
523
|
try {
|
|
637
|
-
const
|
|
638
|
-
const label = labels.find((l) => l.label_id === label_id);
|
|
524
|
+
const label = (await conn.sendMessagePromise({ type: "config/label_registry/list" })).find((l) => l.label_id === label_id);
|
|
639
525
|
if (label) updateLabel(label);
|
|
640
526
|
} catch (error) {
|
|
641
527
|
console.error("Error fetching label:", error);
|
|
@@ -660,7 +546,7 @@ async function handleStatisticsUpdate(event) {
|
|
|
660
546
|
function timestampToIso(timestamp) {
|
|
661
547
|
if (timestamp === void 0) return (/* @__PURE__ */ new Date()).toISOString();
|
|
662
548
|
if (typeof timestamp === "string") return timestamp;
|
|
663
|
-
return new Date(timestamp * 1e3).toISOString();
|
|
549
|
+
return (/* @__PURE__ */ new Date(timestamp * 1e3)).toISOString();
|
|
664
550
|
}
|
|
665
551
|
function collectHistoryFromCompressed(points, entityId, compressed) {
|
|
666
552
|
if (!isHistoryTracked(entityId)) return;
|
|
@@ -753,10 +639,8 @@ function applyStateDiff(s, entityId, diff) {
|
|
|
753
639
|
const removals = diff["-"];
|
|
754
640
|
if (removals?.a) for (const key of removals.a) delete existing.attributes[key];
|
|
755
641
|
}
|
|
756
|
-
|
|
757
642
|
//#endregion
|
|
758
643
|
//#region src/connection/manager.ts
|
|
759
|
-
const SUSPEND_DELAY_MS = 5 * 60 * 1e3;
|
|
760
644
|
let activeConn = null;
|
|
761
645
|
let heartbeatInterval = null;
|
|
762
646
|
let suspendTimeout = null;
|
|
@@ -834,7 +718,7 @@ async function loadInitialData(conn) {
|
|
|
834
718
|
setState(produce((s) => {
|
|
835
719
|
for (const device of deviceRegistry) s.devices[device.id] = device;
|
|
836
720
|
for (const areaApi of areaRegistry) {
|
|
837
|
-
const area
|
|
721
|
+
const area = {
|
|
838
722
|
id: areaApi.area_id || areaApi.id,
|
|
839
723
|
name: areaApi.name,
|
|
840
724
|
normalized_name: areaApi.normalized_name || areaApi.name.toLowerCase().replace(/\s+/g, "_"),
|
|
@@ -845,10 +729,10 @@ async function loadInitialData(conn) {
|
|
|
845
729
|
labels: Array.isArray(areaApi.labels) ? areaApi.labels : [],
|
|
846
730
|
picture: areaApi.picture ?? null,
|
|
847
731
|
temperature_entity_id: areaApi.temperature_entity_id ?? null,
|
|
848
|
-
created_at: typeof areaApi.created_at === "number" ? new Date(areaApi.created_at * 1e3).toISOString() : areaApi.created_at,
|
|
849
|
-
modified_at: typeof areaApi.modified_at === "number" ? new Date(areaApi.modified_at * 1e3).toISOString() : areaApi.modified_at
|
|
732
|
+
created_at: typeof areaApi.created_at === "number" ? (/* @__PURE__ */ new Date(areaApi.created_at * 1e3)).toISOString() : areaApi.created_at,
|
|
733
|
+
modified_at: typeof areaApi.modified_at === "number" ? (/* @__PURE__ */ new Date(areaApi.modified_at * 1e3)).toISOString() : areaApi.modified_at
|
|
850
734
|
};
|
|
851
|
-
s.areas[area
|
|
735
|
+
s.areas[area.id] = area;
|
|
852
736
|
}
|
|
853
737
|
for (const floor of floorRegistry) s.floors[floor.floor_id] = floor;
|
|
854
738
|
for (const label of labelRegistry) s.labels[label.label_id] = label;
|
|
@@ -858,7 +742,6 @@ async function loadInitialData(conn) {
|
|
|
858
742
|
throw error;
|
|
859
743
|
}
|
|
860
744
|
}
|
|
861
|
-
|
|
862
745
|
//#endregion
|
|
863
746
|
//#region src/query/area-builder.ts
|
|
864
747
|
var AreaQueryBuilderImpl = class AreaQueryBuilderImpl {
|
|
@@ -897,8 +780,7 @@ var AreaQueryBuilderImpl = class AreaQueryBuilderImpl {
|
|
|
897
780
|
return this.execute();
|
|
898
781
|
}
|
|
899
782
|
first() {
|
|
900
|
-
|
|
901
|
-
return results[0];
|
|
783
|
+
return this.execute()[0];
|
|
902
784
|
}
|
|
903
785
|
subscribe(callback) {
|
|
904
786
|
callback(this.get());
|
|
@@ -911,15 +793,15 @@ var AreaQueryBuilderImpl = class AreaQueryBuilderImpl {
|
|
|
911
793
|
});
|
|
912
794
|
}
|
|
913
795
|
execute() {
|
|
914
|
-
let areas
|
|
915
|
-
for (const filter of this.queryState.filters) if (filter.type === "floor") areas
|
|
916
|
-
else if (filter.type === "label") areas
|
|
917
|
-
else if (filter.type === "predicate") areas
|
|
918
|
-
if (this.queryState.sorts.length > 0) areas
|
|
919
|
-
return areas
|
|
920
|
-
}
|
|
921
|
-
sortAreas(areas
|
|
922
|
-
return [...areas
|
|
796
|
+
let areas = getAreaViews();
|
|
797
|
+
for (const filter of this.queryState.filters) if (filter.type === "floor") areas = areas.filter((area) => area.floorId === filter.value);
|
|
798
|
+
else if (filter.type === "label") areas = areas.filter((area) => area.labels.includes(filter.value));
|
|
799
|
+
else if (filter.type === "predicate") areas = areas.filter(filter.value);
|
|
800
|
+
if (this.queryState.sorts.length > 0) areas = this.sortAreas(areas);
|
|
801
|
+
return areas;
|
|
802
|
+
}
|
|
803
|
+
sortAreas(areas) {
|
|
804
|
+
return [...areas].sort((a, b) => {
|
|
923
805
|
for (const sort of this.queryState.sorts) {
|
|
924
806
|
const aValue = this.getNestedValue(a, sort.key);
|
|
925
807
|
const bValue = this.getNestedValue(b, sort.key);
|
|
@@ -951,10 +833,18 @@ function areas() {
|
|
|
951
833
|
function area(areaId) {
|
|
952
834
|
return getAreaView(areaId);
|
|
953
835
|
}
|
|
954
|
-
|
|
955
836
|
//#endregion
|
|
956
837
|
//#region src/entities/index.ts
|
|
957
838
|
/**
|
|
839
|
+
* Entity Index Queries
|
|
840
|
+
*
|
|
841
|
+
* Provides utilities for querying entities by various criteria.
|
|
842
|
+
* In the SolidJS port, indices are NOT stored in state -- they are
|
|
843
|
+
* computed on the fly here (and will be derived reactively in Plan 03).
|
|
844
|
+
*
|
|
845
|
+
* @packageDocumentation
|
|
846
|
+
*/
|
|
847
|
+
/**
|
|
958
848
|
* Get all entity IDs
|
|
959
849
|
*/
|
|
960
850
|
function getAllEntityIds() {
|
|
@@ -976,10 +866,7 @@ function getEntitiesViaDevices(areaId) {
|
|
|
976
866
|
const entityIds = [];
|
|
977
867
|
for (const device of devicesInArea) {
|
|
978
868
|
const deviceEntities = getEntitiesByDevice(device.id);
|
|
979
|
-
for (const entityId of deviceEntities)
|
|
980
|
-
const registry = state.entityRegistry[entityId];
|
|
981
|
-
if (!registry?.area_id) entityIds.push(entityId);
|
|
982
|
-
}
|
|
869
|
+
for (const entityId of deviceEntities) if (!state.entityRegistry[entityId]?.area_id) entityIds.push(entityId);
|
|
983
870
|
}
|
|
984
871
|
return entityIds;
|
|
985
872
|
}
|
|
@@ -992,7 +879,7 @@ function getEntitiesByArea(areaId) {
|
|
|
992
879
|
const directEntityIds = [];
|
|
993
880
|
for (const [entityId, entry] of Object.entries(state.entityRegistry)) if (entry.area_id === areaId) directEntityIds.push(entityId);
|
|
994
881
|
const deviceEntityIds = getEntitiesViaDevices(areaId);
|
|
995
|
-
return Array.from(new Set([...directEntityIds, ...deviceEntityIds]));
|
|
882
|
+
return Array.from(/* @__PURE__ */ new Set([...directEntityIds, ...deviceEntityIds]));
|
|
996
883
|
}
|
|
997
884
|
/**
|
|
998
885
|
* Get entities by device
|
|
@@ -1007,9 +894,9 @@ function getEntitiesByDevice(deviceId) {
|
|
|
1007
894
|
*/
|
|
1008
895
|
function getEntitiesByFloor(floorId) {
|
|
1009
896
|
const entityIds = [];
|
|
1010
|
-
const areasOnFloor = Object.values(state.areas).filter((area
|
|
1011
|
-
for (const area
|
|
1012
|
-
const areaEntities = getEntitiesByArea(area
|
|
897
|
+
const areasOnFloor = Object.values(state.areas).filter((area) => area.floor_id === floorId);
|
|
898
|
+
for (const area of areasOnFloor) {
|
|
899
|
+
const areaEntities = getEntitiesByArea(area.id);
|
|
1013
900
|
entityIds.push(...areaEntities);
|
|
1014
901
|
}
|
|
1015
902
|
return entityIds;
|
|
@@ -1022,7 +909,6 @@ function getEntitiesByLabel(labelId) {
|
|
|
1022
909
|
for (const [entityId, entry] of Object.entries(state.entityRegistry)) if (entry.labels?.includes(labelId)) entityIds.push(entityId);
|
|
1023
910
|
return entityIds;
|
|
1024
911
|
}
|
|
1025
|
-
|
|
1026
912
|
//#endregion
|
|
1027
913
|
//#region src/query/memoization.ts
|
|
1028
914
|
/**
|
|
@@ -1043,7 +929,6 @@ function buildQuerySignature(query) {
|
|
|
1043
929
|
function memoizedExecute(_signature, compute) {
|
|
1044
930
|
return compute();
|
|
1045
931
|
}
|
|
1046
|
-
|
|
1047
932
|
//#endregion
|
|
1048
933
|
//#region src/query/projections.ts
|
|
1049
934
|
/**
|
|
@@ -1052,8 +937,8 @@ function memoizedExecute(_signature, compute) {
|
|
|
1052
937
|
* @param entities - Array of entity views
|
|
1053
938
|
* @returns Array of entity IDs
|
|
1054
939
|
*/
|
|
1055
|
-
function projectToIds(entities
|
|
1056
|
-
return entities
|
|
940
|
+
function projectToIds(entities) {
|
|
941
|
+
return entities.map((e) => e.id);
|
|
1057
942
|
}
|
|
1058
943
|
/**
|
|
1059
944
|
* Pick specific fields from entities
|
|
@@ -1062,10 +947,10 @@ function projectToIds(entities$1) {
|
|
|
1062
947
|
* @param keys - Array of field names to include
|
|
1063
948
|
* @returns Array of objects with only specified fields
|
|
1064
949
|
*/
|
|
1065
|
-
function projectPick(entities
|
|
1066
|
-
return entities
|
|
950
|
+
function projectPick(entities, keys) {
|
|
951
|
+
return entities.map((entity) => {
|
|
1067
952
|
const result = {};
|
|
1068
|
-
for (const key of keys) result[key] = entity
|
|
953
|
+
for (const key of keys) result[key] = entity[key];
|
|
1069
954
|
return result;
|
|
1070
955
|
});
|
|
1071
956
|
}
|
|
@@ -1078,8 +963,8 @@ function projectPick(entities$1, keys) {
|
|
|
1078
963
|
* @param key - Field name or dot-path to extract
|
|
1079
964
|
* @returns Array of field values
|
|
1080
965
|
*/
|
|
1081
|
-
function projectPluck(entities
|
|
1082
|
-
return entities
|
|
966
|
+
function projectPluck(entities, key) {
|
|
967
|
+
return entities.map((entity) => getNestedValue(entity, key));
|
|
1083
968
|
}
|
|
1084
969
|
/**
|
|
1085
970
|
* Map entities using a custom function
|
|
@@ -1088,8 +973,8 @@ function projectPluck(entities$1, key) {
|
|
|
1088
973
|
* @param mapper - Function to transform each entity
|
|
1089
974
|
* @returns Array of mapped values
|
|
1090
975
|
*/
|
|
1091
|
-
function projectMap(entities
|
|
1092
|
-
return entities
|
|
976
|
+
function projectMap(entities, mapper) {
|
|
977
|
+
return entities.map(mapper);
|
|
1093
978
|
}
|
|
1094
979
|
/**
|
|
1095
980
|
* Get nested value from object using dot-notation
|
|
@@ -1108,12 +993,11 @@ function getNestedValue(obj, path) {
|
|
|
1108
993
|
const parts = path.split(".");
|
|
1109
994
|
let current = obj;
|
|
1110
995
|
for (const part of parts) {
|
|
1111
|
-
if (current === null || current === void 0) return
|
|
996
|
+
if (current === null || current === void 0) return;
|
|
1112
997
|
current = current[part];
|
|
1113
998
|
}
|
|
1114
999
|
return current;
|
|
1115
1000
|
}
|
|
1116
|
-
|
|
1117
1001
|
//#endregion
|
|
1118
1002
|
//#region src/query/builder.ts
|
|
1119
1003
|
var EntitiesQueryBuilderImpl = class EntitiesQueryBuilderImpl {
|
|
@@ -1204,12 +1088,10 @@ var EntitiesQueryBuilderImpl = class EntitiesQueryBuilderImpl {
|
|
|
1204
1088
|
return this.clone({ distinct: key });
|
|
1205
1089
|
}
|
|
1206
1090
|
get() {
|
|
1207
|
-
|
|
1208
|
-
return memoizedExecute(signature, () => this.execute());
|
|
1091
|
+
return memoizedExecute(buildQuerySignature(this.queryState), () => this.execute());
|
|
1209
1092
|
}
|
|
1210
1093
|
first() {
|
|
1211
|
-
|
|
1212
|
-
return results[0];
|
|
1094
|
+
return this.get()[0];
|
|
1213
1095
|
}
|
|
1214
1096
|
subscribe(callback) {
|
|
1215
1097
|
callback(this.get());
|
|
@@ -1223,14 +1105,14 @@ var EntitiesQueryBuilderImpl = class EntitiesQueryBuilderImpl {
|
|
|
1223
1105
|
}
|
|
1224
1106
|
execute() {
|
|
1225
1107
|
const entityIds = this.getEntityIdsFromIndices();
|
|
1226
|
-
let entities
|
|
1227
|
-
for (const filter of this.queryState.predicateFilters) entities
|
|
1228
|
-
if (this.queryState.sorts.length > 0) entities
|
|
1229
|
-
if (this.queryState.distinct) entities
|
|
1230
|
-
if (this.queryState.skip !== void 0) entities
|
|
1231
|
-
if (this.queryState.take !== void 0) entities
|
|
1232
|
-
if (this.queryState.projection) return this.applyProjection(entities
|
|
1233
|
-
return entities
|
|
1108
|
+
let entities = getEntityViews(entityIds);
|
|
1109
|
+
for (const filter of this.queryState.predicateFilters) entities = entities.filter(filter);
|
|
1110
|
+
if (this.queryState.sorts.length > 0) entities = this.sortEntities(entities);
|
|
1111
|
+
if (this.queryState.distinct) entities = this.applyDistinct(entities);
|
|
1112
|
+
if (this.queryState.skip !== void 0) entities = entities.slice(this.queryState.skip);
|
|
1113
|
+
if (this.queryState.take !== void 0) entities = entities.slice(0, this.queryState.take);
|
|
1114
|
+
if (this.queryState.projection) return this.applyProjection(entities);
|
|
1115
|
+
return entities;
|
|
1234
1116
|
}
|
|
1235
1117
|
getEntityIdsFromIndices() {
|
|
1236
1118
|
if (this.queryState.indexFilters.length === 0) return getAllEntityIds();
|
|
@@ -1257,8 +1139,8 @@ var EntitiesQueryBuilderImpl = class EntitiesQueryBuilderImpl {
|
|
|
1257
1139
|
}
|
|
1258
1140
|
return result;
|
|
1259
1141
|
}
|
|
1260
|
-
sortEntities(entities
|
|
1261
|
-
const sorted = [...entities
|
|
1142
|
+
sortEntities(entities) {
|
|
1143
|
+
const sorted = [...entities];
|
|
1262
1144
|
sorted.sort((a, b) => {
|
|
1263
1145
|
for (const sort of this.queryState.sorts) {
|
|
1264
1146
|
const aVal = getNestedValue(a, sort.key);
|
|
@@ -1278,32 +1160,33 @@ var EntitiesQueryBuilderImpl = class EntitiesQueryBuilderImpl {
|
|
|
1278
1160
|
if (typeof a === "number" && typeof b === "number") return a - b;
|
|
1279
1161
|
return String(a).localeCompare(String(b));
|
|
1280
1162
|
}
|
|
1281
|
-
applyProjection(entities
|
|
1282
|
-
if (!this.queryState.projection) return entities
|
|
1163
|
+
applyProjection(entities) {
|
|
1164
|
+
if (!this.queryState.projection) return entities;
|
|
1283
1165
|
switch (this.queryState.projection.type) {
|
|
1284
|
-
case "ids": return projectToIds(entities
|
|
1285
|
-
case "pick": return projectPick(entities
|
|
1286
|
-
case "pluck": return projectPluck(entities
|
|
1287
|
-
case "map": return projectMap(entities
|
|
1288
|
-
default: return entities
|
|
1166
|
+
case "ids": return projectToIds(entities);
|
|
1167
|
+
case "pick": return projectPick(entities, this.queryState.projection.keys);
|
|
1168
|
+
case "pluck": return projectPluck(entities, this.queryState.projection.key);
|
|
1169
|
+
case "map": return projectMap(entities, this.queryState.projection.mapper);
|
|
1170
|
+
default: return entities;
|
|
1289
1171
|
}
|
|
1290
1172
|
}
|
|
1291
|
-
applyDistinct(entities
|
|
1292
|
-
if (!this.queryState.distinct) return entities
|
|
1173
|
+
applyDistinct(entities) {
|
|
1174
|
+
if (!this.queryState.distinct) return entities;
|
|
1293
1175
|
const seen = /* @__PURE__ */ new Set();
|
|
1294
1176
|
const result = [];
|
|
1295
|
-
for (const entity
|
|
1296
|
-
const value = getNestedValue(entity
|
|
1177
|
+
for (const entity of entities) {
|
|
1178
|
+
const value = getNestedValue(entity, this.queryState.distinct);
|
|
1297
1179
|
const key = JSON.stringify(value);
|
|
1298
1180
|
if (!seen.has(key)) {
|
|
1299
1181
|
seen.add(key);
|
|
1300
|
-
result.push(entity
|
|
1182
|
+
result.push(entity);
|
|
1301
1183
|
}
|
|
1302
1184
|
}
|
|
1303
1185
|
return result;
|
|
1304
1186
|
}
|
|
1305
1187
|
};
|
|
1306
1188
|
var EntityQueryBuilderImpl = class {
|
|
1189
|
+
entityId;
|
|
1307
1190
|
constructor(entityId) {
|
|
1308
1191
|
this.entityId = entityId;
|
|
1309
1192
|
}
|
|
@@ -1331,7 +1214,6 @@ function entities() {
|
|
|
1331
1214
|
function entity(entityId) {
|
|
1332
1215
|
return new EntityQueryBuilderImpl(entityId);
|
|
1333
1216
|
}
|
|
1334
|
-
|
|
1335
1217
|
//#endregion
|
|
1336
1218
|
//#region src/query/helpers.ts
|
|
1337
1219
|
/**
|
|
@@ -1353,9 +1235,13 @@ function entitiesByAreaAndDomain(areaId, domain) {
|
|
|
1353
1235
|
function entitiesWhere(predicate) {
|
|
1354
1236
|
return entities().where(predicate);
|
|
1355
1237
|
}
|
|
1356
|
-
|
|
1357
1238
|
//#endregion
|
|
1358
1239
|
//#region src/camera/fetch.ts
|
|
1240
|
+
/**
|
|
1241
|
+
* Camera Stream Fetching
|
|
1242
|
+
*
|
|
1243
|
+
* @packageDocumentation
|
|
1244
|
+
*/
|
|
1359
1245
|
/** Public HLS test stream for demo/disconnected mode */
|
|
1360
1246
|
const DEMO_HLS_STREAM = "https://test-streams.mux.dev/x36xhzz/x36xhzz.m3u8";
|
|
1361
1247
|
/**
|
|
@@ -1365,12 +1251,11 @@ const DEMO_HLS_STREAM = "https://test-streams.mux.dev/x36xhzz/x36xhzz.m3u8";
|
|
|
1365
1251
|
async function fetchStream(entityId, format = "hls") {
|
|
1366
1252
|
if (isDemoMode()) return format === "hls" ? DEMO_HLS_STREAM : null;
|
|
1367
1253
|
if (!privilegedConn()) return null;
|
|
1368
|
-
|
|
1254
|
+
return (await sendCommand({
|
|
1369
1255
|
type: "camera/stream",
|
|
1370
1256
|
entity_id: entityId,
|
|
1371
1257
|
format
|
|
1372
|
-
});
|
|
1373
|
-
return response?.url ?? null;
|
|
1258
|
+
}))?.url ?? null;
|
|
1374
1259
|
}
|
|
1375
1260
|
/**
|
|
1376
1261
|
* Transform relative stream URL to absolute URL
|
|
@@ -1384,8 +1269,7 @@ function transformStreamUrl(streamUrl) {
|
|
|
1384
1269
|
console.warn("[fetchStreamData] No Home Assistant URL available to resolve relative stream URL");
|
|
1385
1270
|
return streamUrl;
|
|
1386
1271
|
}
|
|
1387
|
-
|
|
1388
|
-
return `${baseUrl}${streamUrl}`;
|
|
1272
|
+
return `${hassUrl.replace(/\/$/, "")}${streamUrl}`;
|
|
1389
1273
|
}
|
|
1390
1274
|
return streamUrl;
|
|
1391
1275
|
}
|
|
@@ -1395,49 +1279,48 @@ function transformStreamUrl(streamUrl) {
|
|
|
1395
1279
|
async function fetchStreamData(entityId, options = {}) {
|
|
1396
1280
|
const { format = "hls" } = options;
|
|
1397
1281
|
try {
|
|
1398
|
-
const streamUrl = await fetchStream(entityId, format);
|
|
1399
|
-
const absoluteStreamUrl = transformStreamUrl(streamUrl);
|
|
1400
|
-
const stream = {
|
|
1401
|
-
url: absoluteStreamUrl,
|
|
1402
|
-
loading: false,
|
|
1403
|
-
error: null,
|
|
1404
|
-
lastFetched: Date.now(),
|
|
1405
|
-
format,
|
|
1406
|
-
expiresAt: null
|
|
1407
|
-
};
|
|
1408
1282
|
return {
|
|
1409
1283
|
entityId,
|
|
1410
|
-
stream
|
|
1284
|
+
stream: {
|
|
1285
|
+
url: transformStreamUrl(await fetchStream(entityId, format)),
|
|
1286
|
+
loading: false,
|
|
1287
|
+
error: null,
|
|
1288
|
+
lastFetched: Date.now(),
|
|
1289
|
+
format,
|
|
1290
|
+
expiresAt: null
|
|
1291
|
+
},
|
|
1411
1292
|
loading: false,
|
|
1412
1293
|
error: null,
|
|
1413
1294
|
lastFetched: Date.now()
|
|
1414
1295
|
};
|
|
1415
1296
|
} catch (error) {
|
|
1416
|
-
const stream = {
|
|
1417
|
-
url: null,
|
|
1418
|
-
loading: false,
|
|
1419
|
-
error: error instanceof Error ? error : new Error(String(error)),
|
|
1420
|
-
lastFetched: null,
|
|
1421
|
-
format,
|
|
1422
|
-
expiresAt: null
|
|
1423
|
-
};
|
|
1424
1297
|
return {
|
|
1425
1298
|
entityId,
|
|
1426
|
-
stream
|
|
1299
|
+
stream: {
|
|
1300
|
+
url: null,
|
|
1301
|
+
loading: false,
|
|
1302
|
+
error: error instanceof Error ? error : new Error(String(error)),
|
|
1303
|
+
lastFetched: null,
|
|
1304
|
+
format,
|
|
1305
|
+
expiresAt: null
|
|
1306
|
+
},
|
|
1427
1307
|
loading: false,
|
|
1428
1308
|
error: error instanceof Error ? error : new Error(String(error)),
|
|
1429
1309
|
lastFetched: null
|
|
1430
1310
|
};
|
|
1431
1311
|
}
|
|
1432
1312
|
}
|
|
1433
|
-
|
|
1434
1313
|
//#endregion
|
|
1435
1314
|
//#region src/camera/query.ts
|
|
1436
|
-
|
|
1315
|
+
/**
|
|
1316
|
+
* Camera Stream Query Builder
|
|
1317
|
+
*
|
|
1318
|
+
* @packageDocumentation
|
|
1319
|
+
*/
|
|
1320
|
+
const DEFAULT_REFRESH_INTERVAL = 24e4;
|
|
1437
1321
|
function needsStreamRefresh(streamData, refreshInterval = DEFAULT_REFRESH_INTERVAL) {
|
|
1438
1322
|
if (!streamData || !streamData.lastFetched) return true;
|
|
1439
|
-
|
|
1440
|
-
return age >= refreshInterval;
|
|
1323
|
+
return Date.now() - streamData.lastFetched >= refreshInterval;
|
|
1441
1324
|
}
|
|
1442
1325
|
/**
|
|
1443
1326
|
* Get stream for a camera entity
|
|
@@ -1450,9 +1333,9 @@ async function getStream(entityId, options = {}) {
|
|
|
1450
1333
|
format,
|
|
1451
1334
|
autoRefresh,
|
|
1452
1335
|
refreshInterval
|
|
1453
|
-
}).then((streamData
|
|
1336
|
+
}).then((streamData) => {
|
|
1454
1337
|
setState(produce((s) => {
|
|
1455
|
-
s.streams[entityId] = streamData
|
|
1338
|
+
s.streams[entityId] = streamData;
|
|
1456
1339
|
}));
|
|
1457
1340
|
});
|
|
1458
1341
|
return cached;
|
|
@@ -1476,7 +1359,6 @@ async function refreshStream(entityId, format = "hls") {
|
|
|
1476
1359
|
}));
|
|
1477
1360
|
return getStream(entityId, { format });
|
|
1478
1361
|
}
|
|
1479
|
-
|
|
1480
1362
|
//#endregion
|
|
1481
1363
|
//#region src/energy/prefs.ts
|
|
1482
1364
|
function isRecord(value) {
|
|
@@ -1513,7 +1395,6 @@ async function fetchEnergyPreferences(connection) {
|
|
|
1513
1395
|
device_consumption: devices
|
|
1514
1396
|
};
|
|
1515
1397
|
}
|
|
1516
|
-
|
|
1517
1398
|
//#endregion
|
|
1518
1399
|
//#region src/media/url.ts
|
|
1519
1400
|
/**
|
|
@@ -1537,7 +1418,6 @@ function hassMediaUrl(path) {
|
|
|
1537
1418
|
if (base) return `${base}?path=${encodeURIComponent(path)}`;
|
|
1538
1419
|
return state.hassUrl ? `${state.hassUrl}${path}` : path;
|
|
1539
1420
|
}
|
|
1540
|
-
|
|
1541
1421
|
//#endregion
|
|
1542
1422
|
//#region src/camera/webrtc.ts
|
|
1543
1423
|
/**
|
|
@@ -1559,7 +1439,7 @@ async function getWebRtcClientConfig(entityId) {
|
|
|
1559
1439
|
*/
|
|
1560
1440
|
function startWebRtcSession(entityId, sdpOffer, onCandidate) {
|
|
1561
1441
|
const conn = privilegedConn();
|
|
1562
|
-
if (!conn) return Promise.reject(new Error("Not connected"));
|
|
1442
|
+
if (!conn) return Promise.reject(/* @__PURE__ */ new Error("Not connected"));
|
|
1563
1443
|
return new Promise((resolve, reject) => {
|
|
1564
1444
|
const session = {
|
|
1565
1445
|
sessionId: null,
|
|
@@ -1569,7 +1449,7 @@ function startWebRtcSession(entityId, sdpOffer, onCandidate) {
|
|
|
1569
1449
|
const timeout = setTimeout(() => {
|
|
1570
1450
|
if (!answered) {
|
|
1571
1451
|
session.unsubscribe?.();
|
|
1572
|
-
reject(new Error("WebRTC offer timed out"));
|
|
1452
|
+
reject(/* @__PURE__ */ new Error("WebRTC offer timed out"));
|
|
1573
1453
|
}
|
|
1574
1454
|
}, 15e3);
|
|
1575
1455
|
conn.subscribeMessage((event) => {
|
|
@@ -1594,7 +1474,6 @@ function startWebRtcSession(entityId, sdpOffer, onCandidate) {
|
|
|
1594
1474
|
clearTimeout(timeout);
|
|
1595
1475
|
session.unsubscribe?.();
|
|
1596
1476
|
reject(new Error(event.message || event.code || "WebRTC offer failed"));
|
|
1597
|
-
break;
|
|
1598
1477
|
}
|
|
1599
1478
|
}, {
|
|
1600
1479
|
type: "camera/webrtc/offer",
|
|
@@ -1621,17 +1500,21 @@ async function sendWebRtcCandidate(entityId, sessionId, candidate) {
|
|
|
1621
1500
|
candidate
|
|
1622
1501
|
});
|
|
1623
1502
|
}
|
|
1624
|
-
|
|
1625
1503
|
//#endregion
|
|
1626
1504
|
//#region src/weather/fetch.ts
|
|
1627
1505
|
/**
|
|
1506
|
+
* Weather Forecast Fetching
|
|
1507
|
+
*
|
|
1508
|
+
* @packageDocumentation
|
|
1509
|
+
*/
|
|
1510
|
+
/**
|
|
1628
1511
|
* In demo mode, derive forecasts from the entity's `forecast` attribute.
|
|
1629
1512
|
* Hourly and daily are synthesised from the same source (daily entries
|
|
1630
1513
|
* expanded into 12 hourly steps for the chart).
|
|
1631
1514
|
*/
|
|
1632
1515
|
function buildDemoForecast(entityId, type) {
|
|
1633
|
-
const entity
|
|
1634
|
-
const raw = entity
|
|
1516
|
+
const entity = state.entities[entityId];
|
|
1517
|
+
const raw = entity?.attributes?.forecast ?? [];
|
|
1635
1518
|
if (raw.length === 0) return [];
|
|
1636
1519
|
if (type === "daily" || type === "twice_daily") return raw.map((d) => {
|
|
1637
1520
|
const templow = d.templow;
|
|
@@ -1659,7 +1542,7 @@ function buildDemoForecast(entityId, type) {
|
|
|
1659
1542
|
out.push({
|
|
1660
1543
|
datetime: dt.toISOString(),
|
|
1661
1544
|
temperature: Math.round((mid + Math.sin(phase) * swing) * 10) / 10,
|
|
1662
|
-
condition: today?.condition ?? entity
|
|
1545
|
+
condition: today?.condition ?? entity?.state ?? "cloudy"
|
|
1663
1546
|
});
|
|
1664
1547
|
}
|
|
1665
1548
|
return out;
|
|
@@ -1677,7 +1560,7 @@ async function fetchForecast(entityId, type) {
|
|
|
1677
1560
|
const timeout = setTimeout(() => {
|
|
1678
1561
|
if (!resolved && unsubscribe) {
|
|
1679
1562
|
resolved = true;
|
|
1680
|
-
unsubscribe().then(() => reject(new Error("Forecast subscription timeout"))).catch(() => reject(new Error("Forecast subscription timeout")));
|
|
1563
|
+
unsubscribe().then(() => reject(/* @__PURE__ */ new Error("Forecast subscription timeout"))).catch(() => reject(/* @__PURE__ */ new Error("Forecast subscription timeout")));
|
|
1681
1564
|
}
|
|
1682
1565
|
}, 1e4);
|
|
1683
1566
|
conn.subscribeMessage((event) => {
|
|
@@ -1740,11 +1623,10 @@ async function fetchForecasts(entityId, types) {
|
|
|
1740
1623
|
*/
|
|
1741
1624
|
async function fetchForecastData(entityId, type) {
|
|
1742
1625
|
try {
|
|
1743
|
-
const forecast = await fetchForecast(entityId, type);
|
|
1744
1626
|
return {
|
|
1745
1627
|
entityId,
|
|
1746
1628
|
type,
|
|
1747
|
-
forecast,
|
|
1629
|
+
forecast: await fetchForecast(entityId, type),
|
|
1748
1630
|
loading: false,
|
|
1749
1631
|
error: null,
|
|
1750
1632
|
lastFetched: Date.now()
|
|
@@ -1760,10 +1642,14 @@ async function fetchForecastData(entityId, type) {
|
|
|
1760
1642
|
};
|
|
1761
1643
|
}
|
|
1762
1644
|
}
|
|
1763
|
-
|
|
1764
1645
|
//#endregion
|
|
1765
1646
|
//#region src/weather/query.ts
|
|
1766
1647
|
/**
|
|
1648
|
+
* Weather Forecast Query Builder
|
|
1649
|
+
*
|
|
1650
|
+
* @packageDocumentation
|
|
1651
|
+
*/
|
|
1652
|
+
/**
|
|
1767
1653
|
* Get forecast for a single entity and type
|
|
1768
1654
|
*/
|
|
1769
1655
|
async function getForecast(entityId, type) {
|
|
@@ -1878,12 +1764,11 @@ async function refreshForecasts(entityId, types) {
|
|
|
1878
1764
|
}));
|
|
1879
1765
|
return getForecasts(entityId, types);
|
|
1880
1766
|
}
|
|
1881
|
-
|
|
1882
1767
|
//#endregion
|
|
1883
1768
|
//#region src/worker/bridge-client.ts
|
|
1884
|
-
var
|
|
1769
|
+
var BridgeInvalidAuthError = class extends Error {
|
|
1885
1770
|
constructor() {
|
|
1886
|
-
super("
|
|
1771
|
+
super("Home Assistant rejected the connection; an admin needs to sign in to Home Assistant again");
|
|
1887
1772
|
}
|
|
1888
1773
|
};
|
|
1889
1774
|
function createHaBridge(worker, events = {}) {
|
|
@@ -1907,12 +1792,13 @@ function createHaBridge(worker, events = {}) {
|
|
|
1907
1792
|
switch (msg.k) {
|
|
1908
1793
|
case "connect_result":
|
|
1909
1794
|
if (msg.ok) pendingConnect?.resolve();
|
|
1910
|
-
else pendingConnect?.reject(msg.
|
|
1795
|
+
else pendingConnect?.reject(msg.reason === "invalid_auth" ? new BridgeInvalidAuthError() : new Error(msg.error ?? "Connect failed"));
|
|
1911
1796
|
pendingConnect = null;
|
|
1912
1797
|
break;
|
|
1913
1798
|
case "conn":
|
|
1914
1799
|
connState = msg.state;
|
|
1915
1800
|
events.onConnState?.(msg.state);
|
|
1801
|
+
if (msg.reason === "invalid_auth") events.onInvalidAuth?.();
|
|
1916
1802
|
break;
|
|
1917
1803
|
case "ready_after_reconnect":
|
|
1918
1804
|
events.onReadyAfterReconnect?.();
|
|
@@ -1925,17 +1811,14 @@ function createHaBridge(worker, events = {}) {
|
|
|
1925
1811
|
else pending.reject(new Error(msg.error ?? "Request failed"));
|
|
1926
1812
|
break;
|
|
1927
1813
|
}
|
|
1928
|
-
case "event":
|
|
1814
|
+
case "event":
|
|
1929
1815
|
subCallbacks.get(msg.id)?.(msg.payload);
|
|
1930
1816
|
for (const listener of rawListeners) listener(msg.payload);
|
|
1931
1817
|
break;
|
|
1932
|
-
}
|
|
1933
1818
|
case "sub_end":
|
|
1934
1819
|
subCallbacks.delete(msg.id);
|
|
1935
1820
|
break;
|
|
1936
|
-
case "denial":
|
|
1937
|
-
events.onDenial?.(msg);
|
|
1938
|
-
break;
|
|
1821
|
+
case "denial": events.onDenial?.(msg);
|
|
1939
1822
|
}
|
|
1940
1823
|
};
|
|
1941
1824
|
function sendMessagePromise(message) {
|
|
@@ -1970,24 +1853,6 @@ function createHaBridge(worker, events = {}) {
|
|
|
1970
1853
|
});
|
|
1971
1854
|
};
|
|
1972
1855
|
}
|
|
1973
|
-
const conn = {
|
|
1974
|
-
connect: () => Promise.resolve(),
|
|
1975
|
-
disconnect: () => post({ k: "disconnect" }),
|
|
1976
|
-
get connected$() {
|
|
1977
|
-
return connState === "connected";
|
|
1978
|
-
},
|
|
1979
|
-
authenticate: () => Promise.resolve(),
|
|
1980
|
-
get authState() {
|
|
1981
|
-
return connState === "connected" ? "authenticated" : "pending";
|
|
1982
|
-
},
|
|
1983
|
-
subscribeEvents: (callback, eventType) => Promise.resolve(subscribe("events", callback, eventType)),
|
|
1984
|
-
subscribeMessage: (callback, message) => Promise.resolve(subscribe("message", callback, void 0, message)),
|
|
1985
|
-
sendMessagePromise,
|
|
1986
|
-
addEventListener: (handler) => {
|
|
1987
|
-
rawListeners.add(handler);
|
|
1988
|
-
return () => rawListeners.delete(handler);
|
|
1989
|
-
}
|
|
1990
|
-
};
|
|
1991
1856
|
return {
|
|
1992
1857
|
connect(opts) {
|
|
1993
1858
|
return new Promise((resolve, reject) => {
|
|
@@ -1998,7 +1863,6 @@ function createHaBridge(worker, events = {}) {
|
|
|
1998
1863
|
post({
|
|
1999
1864
|
k: "connect",
|
|
2000
1865
|
url: opts.url,
|
|
2001
|
-
mode: opts.mode,
|
|
2002
1866
|
proxyWsPath: opts.proxyWsPath,
|
|
2003
1867
|
proxyTicket: opts.proxyTicket
|
|
2004
1868
|
});
|
|
@@ -2006,8 +1870,24 @@ function createHaBridge(worker, events = {}) {
|
|
|
2006
1870
|
},
|
|
2007
1871
|
disconnect: () => post({ k: "disconnect" }),
|
|
2008
1872
|
reconnect: () => post({ k: "reconnect" }),
|
|
2009
|
-
|
|
2010
|
-
|
|
1873
|
+
conn: {
|
|
1874
|
+
connect: () => Promise.resolve(),
|
|
1875
|
+
disconnect: () => post({ k: "disconnect" }),
|
|
1876
|
+
get connected$() {
|
|
1877
|
+
return connState === "connected";
|
|
1878
|
+
},
|
|
1879
|
+
authenticate: () => Promise.resolve(),
|
|
1880
|
+
get authState() {
|
|
1881
|
+
return connState === "connected" ? "authenticated" : "pending";
|
|
1882
|
+
},
|
|
1883
|
+
subscribeEvents: (callback, eventType) => Promise.resolve(subscribe("events", callback, eventType)),
|
|
1884
|
+
subscribeMessage: (callback, message) => Promise.resolve(subscribe("message", callback, void 0, message)),
|
|
1885
|
+
sendMessagePromise,
|
|
1886
|
+
addEventListener: (handler) => {
|
|
1887
|
+
rawListeners.add(handler);
|
|
1888
|
+
return () => rawListeners.delete(handler);
|
|
1889
|
+
}
|
|
1890
|
+
},
|
|
2011
1891
|
registerWidget(widgetId, caps) {
|
|
2012
1892
|
const channel = new MessageChannel();
|
|
2013
1893
|
post({
|
|
@@ -2042,7 +1922,6 @@ function createHaBridge(worker, events = {}) {
|
|
|
2042
1922
|
}
|
|
2043
1923
|
};
|
|
2044
1924
|
}
|
|
2045
|
-
|
|
2046
1925
|
//#endregion
|
|
2047
1926
|
//#region src/connection/bridged.ts
|
|
2048
1927
|
/**
|
|
@@ -2066,10 +1945,10 @@ async function attachBridgeToStore(bridge, hassUrl, mediaProxyBase) {
|
|
|
2066
1945
|
await forceResubscribeCalendars();
|
|
2067
1946
|
}
|
|
2068
1947
|
/** Map worker connection-state messages onto the store. */
|
|
2069
|
-
function applyBridgeConnState(state
|
|
1948
|
+
function applyBridgeConnState(state) {
|
|
2070
1949
|
setState(produce((s) => {
|
|
2071
|
-
s.connectionState = state
|
|
2072
|
-
if (state
|
|
1950
|
+
s.connectionState = state;
|
|
1951
|
+
if (state === "connected") s.connectionError = null;
|
|
2073
1952
|
}));
|
|
2074
1953
|
}
|
|
2075
1954
|
/** Re-sync after the worker reports a completed reconnect. */
|
|
@@ -2085,16 +1964,38 @@ function detachBridgeFromStore() {
|
|
|
2085
1964
|
s.connectionState = "disconnected";
|
|
2086
1965
|
}));
|
|
2087
1966
|
}
|
|
2088
|
-
|
|
2089
1967
|
//#endregion
|
|
2090
1968
|
//#region src/host-api.ts
|
|
1969
|
+
/**
|
|
1970
|
+
* One-shot handoff of the host-only surface.
|
|
1971
|
+
*
|
|
1972
|
+
* The problem this solves: the host import map serves this package's entry to
|
|
1973
|
+
* every widget bundle, so everything exported there is callable by untrusted
|
|
1974
|
+
* widget code. But the host needs privileged operations from the same package —
|
|
1975
|
+
* connect the bridge, authenticate, disconnect — and it cannot import them
|
|
1976
|
+
* through a private subpath, because the store must be ONE instance shared with
|
|
1977
|
+
* widgets. An unserved subpath gets bundled into the host and forks the store
|
|
1978
|
+
* (finding 45); a served one is reachable by widgets again (finding 46). A
|
|
1979
|
+
* subpath cannot be both, so hiding is not available.
|
|
1980
|
+
*
|
|
1981
|
+
* So the surface stays on the shared entry and is protected by ORDER instead of
|
|
1982
|
+
* by visibility: `claimHostApi()` yields the API to its first caller and null to
|
|
1983
|
+
* everyone after. The host claims it while booting; widget bundles are fetched
|
|
1984
|
+
* and evaluated later, by which point there is nothing left to take.
|
|
1985
|
+
*
|
|
1986
|
+
* Why these members are dangerous in a widget's hands:
|
|
1987
|
+
* - attachBridgeToStore / createHaBridge: install a connection, i.e. replace the
|
|
1988
|
+
* host's link with one the widget controls — every subsequent service call is
|
|
1989
|
+
* intercepted and every entity the dashboard renders can be fabricated.
|
|
1990
|
+
* - disconnect / detachBridgeFromStore: drop the household's connection.
|
|
1991
|
+
* - loadDemoData / unloadDemoData: overwrite live state with fixtures.
|
|
1992
|
+
*/
|
|
2091
1993
|
let unclaimed = {
|
|
2092
1994
|
attachBridgeToStore,
|
|
2093
1995
|
detachBridgeFromStore,
|
|
2094
1996
|
applyBridgeConnState,
|
|
2095
1997
|
reloadAfterBridgeReconnect,
|
|
2096
1998
|
createHaBridge,
|
|
2097
|
-
authenticateWithOAuth,
|
|
2098
1999
|
disconnect,
|
|
2099
2000
|
loadDemoData,
|
|
2100
2001
|
unloadDemoData
|
|
@@ -2112,10 +2013,8 @@ function claimHostApi() {
|
|
|
2112
2013
|
unclaimed = null;
|
|
2113
2014
|
return api;
|
|
2114
2015
|
}
|
|
2115
|
-
|
|
2116
2016
|
//#endregion
|
|
2117
2017
|
//#region src/index.ts
|
|
2118
2018
|
const VERSION = "0.2.1";
|
|
2119
|
-
|
|
2120
2019
|
//#endregion
|
|
2121
|
-
export {
|
|
2020
|
+
export { BridgeInvalidAuthError, DOMAINS_USE_LAST_UPDATED, LINE_ATTRIBUTES_TO_KEEP, MAX_HISTORY_POINTS, NEED_ATTRIBUTE_DOMAINS, VERSION, appendHistoryPoint, applyDemoServiceCall, area, areas, batchEntityUpdates, batchServiceCalls, buildAreaView, buildDeviceView, buildEntityView, bulkAppendHistoryPoints, callService, claimHostApi, createDemoFixtures, entities, entitiesByAreaAndDomain, entitiesWhere, entity, entityIdHistoryNeedsAttributes, entityViewEquals, extractDomain, fetchEnergyPreferences, fetchEntityHistory, fetchForecast, fetchForecastData, fetchForecasts, fetchHistory, fetchStatisticsDuringPeriod, fetchStream, fetchStreamData, getAllEntityViews, getAreaView, getAreaViews, getConnectionState, getEntityView, getEntityViews, getForecast, getForecasts, getStream, getWebRtcClientConfig, hassMediaUrl, historyStateToTimeline, isConnected, isDemoMode, isHistoryTracked, normalizeStatisticTime, refreshForecast, refreshForecasts, refreshStream, registerEntity, runHaBridgeWorker, searchEntities, sendCommand, sendWebRtcCandidate, startDemoEnergyTicker, startWebRtcSession, state, stopDemoEnergyTicker, toggle, trackEntityHistory, turnOff, turnOn, untrackEntityHistory, updateEntity };
|