@glasshome/sync-layer 0.7.1 → 0.8.0
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 +215 -308
- 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 +3 -3
- 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,28 +268,35 @@ 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. */
|
|
381
274
|
const MAX_HISTORY_POINTS = 5760;
|
|
382
|
-
/**
|
|
383
|
-
|
|
275
|
+
/** Live tracker count per entity. Ref-counted because a timeline is shared: two
|
|
276
|
+
* widgets on the same entity must not let the first unmount delete the store
|
|
277
|
+
* data the second is still rendering (appendHistoryPoint would then silently
|
|
278
|
+
* drop every update and the survivor's chart would freeze). */
|
|
279
|
+
const trackedEntities = /* @__PURE__ */ new Map();
|
|
384
280
|
/** Check if an entity has active history tracking. */
|
|
385
281
|
function isHistoryTracked(entityId) {
|
|
386
282
|
return trackedEntities.has(entityId);
|
|
387
283
|
}
|
|
388
284
|
/** Start tracking history for an entity. Fetches initial backfill. */
|
|
389
285
|
async function trackEntityHistory(entityId, options) {
|
|
390
|
-
trackedEntities.
|
|
286
|
+
trackedEntities.set(entityId, (trackedEntities.get(entityId) ?? 0) + 1);
|
|
391
287
|
const historyData = await fetchEntityHistory(entityId, options);
|
|
392
288
|
setState(produce((s) => {
|
|
393
289
|
s.history[entityId] = historyData;
|
|
394
290
|
}));
|
|
395
291
|
return historyData;
|
|
396
292
|
}
|
|
397
|
-
/**
|
|
293
|
+
/** Release one tracker. The entity's data is cleared only when the last one goes. */
|
|
398
294
|
function untrackEntityHistory(entityId) {
|
|
295
|
+
const remaining = (trackedEntities.get(entityId) ?? 0) - 1;
|
|
296
|
+
if (remaining > 0) {
|
|
297
|
+
trackedEntities.set(entityId, remaining);
|
|
298
|
+
return;
|
|
299
|
+
}
|
|
399
300
|
trackedEntities.delete(entityId);
|
|
400
301
|
setState(produce((s) => {
|
|
401
302
|
delete s.history[entityId];
|
|
@@ -454,12 +355,11 @@ function bulkAppendHistoryPoints(points) {
|
|
|
454
355
|
for (const entityId of touched) {
|
|
455
356
|
const history = s.history[entityId];
|
|
456
357
|
if (!history) continue;
|
|
457
|
-
if (history.entityHistory.length >
|
|
458
|
-
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);
|
|
459
360
|
}
|
|
460
361
|
}));
|
|
461
362
|
}
|
|
462
|
-
|
|
463
363
|
//#endregion
|
|
464
364
|
//#region src/connection/subscriptions.ts
|
|
465
365
|
/** Registry subscription cleanup functions (entity sub managed by subscription-manager) */
|
|
@@ -471,8 +371,7 @@ function bufferMessage(message) {
|
|
|
471
371
|
messageBuffer.push(message);
|
|
472
372
|
if (!rafScheduled) {
|
|
473
373
|
rafScheduled = true;
|
|
474
|
-
|
|
475
|
-
schedule(flushMessageBuffer);
|
|
374
|
+
(typeof requestAnimationFrame === "function" ? requestAnimationFrame : queueMicrotask)(flushMessageBuffer);
|
|
476
375
|
}
|
|
477
376
|
}
|
|
478
377
|
function flushMessageBuffer() {
|
|
@@ -503,10 +402,9 @@ function flushMessageBuffer() {
|
|
|
503
402
|
async function subscribeEntities(connection, entityIds) {
|
|
504
403
|
const subscribeMsg = { type: "subscribe_entities" };
|
|
505
404
|
if (entityIds.length > 0) subscribeMsg.entity_ids = entityIds;
|
|
506
|
-
|
|
405
|
+
return await connection.subscribeMessage((message) => {
|
|
507
406
|
bufferMessage(message);
|
|
508
407
|
}, subscribeMsg);
|
|
509
|
-
return unsub;
|
|
510
408
|
}
|
|
511
409
|
/**
|
|
512
410
|
* Subscribe to all relevant events from Home Assistant
|
|
@@ -584,8 +482,7 @@ async function handleDeviceRegistryUpdate(event) {
|
|
|
584
482
|
const conn = privilegedConn();
|
|
585
483
|
if (!conn) return;
|
|
586
484
|
try {
|
|
587
|
-
const
|
|
588
|
-
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);
|
|
589
486
|
if (device) updateDevice(device);
|
|
590
487
|
} catch (error) {
|
|
591
488
|
console.error("Error fetching device:", error);
|
|
@@ -598,9 +495,8 @@ async function handleAreaRegistryUpdate(event) {
|
|
|
598
495
|
const conn = privilegedConn();
|
|
599
496
|
if (!conn) return;
|
|
600
497
|
try {
|
|
601
|
-
const
|
|
602
|
-
|
|
603
|
-
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);
|
|
604
500
|
} catch (error) {
|
|
605
501
|
console.error("Error fetching area:", error);
|
|
606
502
|
}
|
|
@@ -612,8 +508,7 @@ async function handleFloorRegistryUpdate(event) {
|
|
|
612
508
|
const conn = privilegedConn();
|
|
613
509
|
if (!conn) return;
|
|
614
510
|
try {
|
|
615
|
-
const
|
|
616
|
-
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);
|
|
617
512
|
if (floor) updateFloor(floor);
|
|
618
513
|
} catch (error) {
|
|
619
514
|
console.error("Error fetching floor:", error);
|
|
@@ -626,8 +521,7 @@ async function handleLabelRegistryUpdate(event) {
|
|
|
626
521
|
const conn = privilegedConn();
|
|
627
522
|
if (!conn) return;
|
|
628
523
|
try {
|
|
629
|
-
const
|
|
630
|
-
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);
|
|
631
525
|
if (label) updateLabel(label);
|
|
632
526
|
} catch (error) {
|
|
633
527
|
console.error("Error fetching label:", error);
|
|
@@ -652,7 +546,7 @@ async function handleStatisticsUpdate(event) {
|
|
|
652
546
|
function timestampToIso(timestamp) {
|
|
653
547
|
if (timestamp === void 0) return (/* @__PURE__ */ new Date()).toISOString();
|
|
654
548
|
if (typeof timestamp === "string") return timestamp;
|
|
655
|
-
return new Date(timestamp * 1e3).toISOString();
|
|
549
|
+
return (/* @__PURE__ */ new Date(timestamp * 1e3)).toISOString();
|
|
656
550
|
}
|
|
657
551
|
function collectHistoryFromCompressed(points, entityId, compressed) {
|
|
658
552
|
if (!isHistoryTracked(entityId)) return;
|
|
@@ -745,10 +639,8 @@ function applyStateDiff(s, entityId, diff) {
|
|
|
745
639
|
const removals = diff["-"];
|
|
746
640
|
if (removals?.a) for (const key of removals.a) delete existing.attributes[key];
|
|
747
641
|
}
|
|
748
|
-
|
|
749
642
|
//#endregion
|
|
750
643
|
//#region src/connection/manager.ts
|
|
751
|
-
const SUSPEND_DELAY_MS = 5 * 60 * 1e3;
|
|
752
644
|
let activeConn = null;
|
|
753
645
|
let heartbeatInterval = null;
|
|
754
646
|
let suspendTimeout = null;
|
|
@@ -826,7 +718,7 @@ async function loadInitialData(conn) {
|
|
|
826
718
|
setState(produce((s) => {
|
|
827
719
|
for (const device of deviceRegistry) s.devices[device.id] = device;
|
|
828
720
|
for (const areaApi of areaRegistry) {
|
|
829
|
-
const area
|
|
721
|
+
const area = {
|
|
830
722
|
id: areaApi.area_id || areaApi.id,
|
|
831
723
|
name: areaApi.name,
|
|
832
724
|
normalized_name: areaApi.normalized_name || areaApi.name.toLowerCase().replace(/\s+/g, "_"),
|
|
@@ -837,10 +729,10 @@ async function loadInitialData(conn) {
|
|
|
837
729
|
labels: Array.isArray(areaApi.labels) ? areaApi.labels : [],
|
|
838
730
|
picture: areaApi.picture ?? null,
|
|
839
731
|
temperature_entity_id: areaApi.temperature_entity_id ?? null,
|
|
840
|
-
created_at: typeof areaApi.created_at === "number" ? new Date(areaApi.created_at * 1e3).toISOString() : areaApi.created_at,
|
|
841
|
-
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
|
|
842
734
|
};
|
|
843
|
-
s.areas[area
|
|
735
|
+
s.areas[area.id] = area;
|
|
844
736
|
}
|
|
845
737
|
for (const floor of floorRegistry) s.floors[floor.floor_id] = floor;
|
|
846
738
|
for (const label of labelRegistry) s.labels[label.label_id] = label;
|
|
@@ -850,7 +742,6 @@ async function loadInitialData(conn) {
|
|
|
850
742
|
throw error;
|
|
851
743
|
}
|
|
852
744
|
}
|
|
853
|
-
|
|
854
745
|
//#endregion
|
|
855
746
|
//#region src/query/area-builder.ts
|
|
856
747
|
var AreaQueryBuilderImpl = class AreaQueryBuilderImpl {
|
|
@@ -889,8 +780,7 @@ var AreaQueryBuilderImpl = class AreaQueryBuilderImpl {
|
|
|
889
780
|
return this.execute();
|
|
890
781
|
}
|
|
891
782
|
first() {
|
|
892
|
-
|
|
893
|
-
return results[0];
|
|
783
|
+
return this.execute()[0];
|
|
894
784
|
}
|
|
895
785
|
subscribe(callback) {
|
|
896
786
|
callback(this.get());
|
|
@@ -903,15 +793,15 @@ var AreaQueryBuilderImpl = class AreaQueryBuilderImpl {
|
|
|
903
793
|
});
|
|
904
794
|
}
|
|
905
795
|
execute() {
|
|
906
|
-
let areas
|
|
907
|
-
for (const filter of this.queryState.filters) if (filter.type === "floor") areas
|
|
908
|
-
else if (filter.type === "label") areas
|
|
909
|
-
else if (filter.type === "predicate") areas
|
|
910
|
-
if (this.queryState.sorts.length > 0) areas
|
|
911
|
-
return areas
|
|
912
|
-
}
|
|
913
|
-
sortAreas(areas
|
|
914
|
-
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) => {
|
|
915
805
|
for (const sort of this.queryState.sorts) {
|
|
916
806
|
const aValue = this.getNestedValue(a, sort.key);
|
|
917
807
|
const bValue = this.getNestedValue(b, sort.key);
|
|
@@ -943,10 +833,18 @@ function areas() {
|
|
|
943
833
|
function area(areaId) {
|
|
944
834
|
return getAreaView(areaId);
|
|
945
835
|
}
|
|
946
|
-
|
|
947
836
|
//#endregion
|
|
948
837
|
//#region src/entities/index.ts
|
|
949
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
|
+
/**
|
|
950
848
|
* Get all entity IDs
|
|
951
849
|
*/
|
|
952
850
|
function getAllEntityIds() {
|
|
@@ -968,10 +866,7 @@ function getEntitiesViaDevices(areaId) {
|
|
|
968
866
|
const entityIds = [];
|
|
969
867
|
for (const device of devicesInArea) {
|
|
970
868
|
const deviceEntities = getEntitiesByDevice(device.id);
|
|
971
|
-
for (const entityId of deviceEntities)
|
|
972
|
-
const registry = state.entityRegistry[entityId];
|
|
973
|
-
if (!registry?.area_id) entityIds.push(entityId);
|
|
974
|
-
}
|
|
869
|
+
for (const entityId of deviceEntities) if (!state.entityRegistry[entityId]?.area_id) entityIds.push(entityId);
|
|
975
870
|
}
|
|
976
871
|
return entityIds;
|
|
977
872
|
}
|
|
@@ -984,7 +879,7 @@ function getEntitiesByArea(areaId) {
|
|
|
984
879
|
const directEntityIds = [];
|
|
985
880
|
for (const [entityId, entry] of Object.entries(state.entityRegistry)) if (entry.area_id === areaId) directEntityIds.push(entityId);
|
|
986
881
|
const deviceEntityIds = getEntitiesViaDevices(areaId);
|
|
987
|
-
return Array.from(new Set([...directEntityIds, ...deviceEntityIds]));
|
|
882
|
+
return Array.from(/* @__PURE__ */ new Set([...directEntityIds, ...deviceEntityIds]));
|
|
988
883
|
}
|
|
989
884
|
/**
|
|
990
885
|
* Get entities by device
|
|
@@ -999,9 +894,9 @@ function getEntitiesByDevice(deviceId) {
|
|
|
999
894
|
*/
|
|
1000
895
|
function getEntitiesByFloor(floorId) {
|
|
1001
896
|
const entityIds = [];
|
|
1002
|
-
const areasOnFloor = Object.values(state.areas).filter((area
|
|
1003
|
-
for (const area
|
|
1004
|
-
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);
|
|
1005
900
|
entityIds.push(...areaEntities);
|
|
1006
901
|
}
|
|
1007
902
|
return entityIds;
|
|
@@ -1014,7 +909,6 @@ function getEntitiesByLabel(labelId) {
|
|
|
1014
909
|
for (const [entityId, entry] of Object.entries(state.entityRegistry)) if (entry.labels?.includes(labelId)) entityIds.push(entityId);
|
|
1015
910
|
return entityIds;
|
|
1016
911
|
}
|
|
1017
|
-
|
|
1018
912
|
//#endregion
|
|
1019
913
|
//#region src/query/memoization.ts
|
|
1020
914
|
/**
|
|
@@ -1035,7 +929,6 @@ function buildQuerySignature(query) {
|
|
|
1035
929
|
function memoizedExecute(_signature, compute) {
|
|
1036
930
|
return compute();
|
|
1037
931
|
}
|
|
1038
|
-
|
|
1039
932
|
//#endregion
|
|
1040
933
|
//#region src/query/projections.ts
|
|
1041
934
|
/**
|
|
@@ -1044,8 +937,8 @@ function memoizedExecute(_signature, compute) {
|
|
|
1044
937
|
* @param entities - Array of entity views
|
|
1045
938
|
* @returns Array of entity IDs
|
|
1046
939
|
*/
|
|
1047
|
-
function projectToIds(entities
|
|
1048
|
-
return entities
|
|
940
|
+
function projectToIds(entities) {
|
|
941
|
+
return entities.map((e) => e.id);
|
|
1049
942
|
}
|
|
1050
943
|
/**
|
|
1051
944
|
* Pick specific fields from entities
|
|
@@ -1054,10 +947,10 @@ function projectToIds(entities$1) {
|
|
|
1054
947
|
* @param keys - Array of field names to include
|
|
1055
948
|
* @returns Array of objects with only specified fields
|
|
1056
949
|
*/
|
|
1057
|
-
function projectPick(entities
|
|
1058
|
-
return entities
|
|
950
|
+
function projectPick(entities, keys) {
|
|
951
|
+
return entities.map((entity) => {
|
|
1059
952
|
const result = {};
|
|
1060
|
-
for (const key of keys) result[key] = entity
|
|
953
|
+
for (const key of keys) result[key] = entity[key];
|
|
1061
954
|
return result;
|
|
1062
955
|
});
|
|
1063
956
|
}
|
|
@@ -1070,8 +963,8 @@ function projectPick(entities$1, keys) {
|
|
|
1070
963
|
* @param key - Field name or dot-path to extract
|
|
1071
964
|
* @returns Array of field values
|
|
1072
965
|
*/
|
|
1073
|
-
function projectPluck(entities
|
|
1074
|
-
return entities
|
|
966
|
+
function projectPluck(entities, key) {
|
|
967
|
+
return entities.map((entity) => getNestedValue(entity, key));
|
|
1075
968
|
}
|
|
1076
969
|
/**
|
|
1077
970
|
* Map entities using a custom function
|
|
@@ -1080,8 +973,8 @@ function projectPluck(entities$1, key) {
|
|
|
1080
973
|
* @param mapper - Function to transform each entity
|
|
1081
974
|
* @returns Array of mapped values
|
|
1082
975
|
*/
|
|
1083
|
-
function projectMap(entities
|
|
1084
|
-
return entities
|
|
976
|
+
function projectMap(entities, mapper) {
|
|
977
|
+
return entities.map(mapper);
|
|
1085
978
|
}
|
|
1086
979
|
/**
|
|
1087
980
|
* Get nested value from object using dot-notation
|
|
@@ -1100,12 +993,11 @@ function getNestedValue(obj, path) {
|
|
|
1100
993
|
const parts = path.split(".");
|
|
1101
994
|
let current = obj;
|
|
1102
995
|
for (const part of parts) {
|
|
1103
|
-
if (current === null || current === void 0) return
|
|
996
|
+
if (current === null || current === void 0) return;
|
|
1104
997
|
current = current[part];
|
|
1105
998
|
}
|
|
1106
999
|
return current;
|
|
1107
1000
|
}
|
|
1108
|
-
|
|
1109
1001
|
//#endregion
|
|
1110
1002
|
//#region src/query/builder.ts
|
|
1111
1003
|
var EntitiesQueryBuilderImpl = class EntitiesQueryBuilderImpl {
|
|
@@ -1196,12 +1088,10 @@ var EntitiesQueryBuilderImpl = class EntitiesQueryBuilderImpl {
|
|
|
1196
1088
|
return this.clone({ distinct: key });
|
|
1197
1089
|
}
|
|
1198
1090
|
get() {
|
|
1199
|
-
|
|
1200
|
-
return memoizedExecute(signature, () => this.execute());
|
|
1091
|
+
return memoizedExecute(buildQuerySignature(this.queryState), () => this.execute());
|
|
1201
1092
|
}
|
|
1202
1093
|
first() {
|
|
1203
|
-
|
|
1204
|
-
return results[0];
|
|
1094
|
+
return this.get()[0];
|
|
1205
1095
|
}
|
|
1206
1096
|
subscribe(callback) {
|
|
1207
1097
|
callback(this.get());
|
|
@@ -1215,14 +1105,14 @@ var EntitiesQueryBuilderImpl = class EntitiesQueryBuilderImpl {
|
|
|
1215
1105
|
}
|
|
1216
1106
|
execute() {
|
|
1217
1107
|
const entityIds = this.getEntityIdsFromIndices();
|
|
1218
|
-
let entities
|
|
1219
|
-
for (const filter of this.queryState.predicateFilters) entities
|
|
1220
|
-
if (this.queryState.sorts.length > 0) entities
|
|
1221
|
-
if (this.queryState.distinct) entities
|
|
1222
|
-
if (this.queryState.skip !== void 0) entities
|
|
1223
|
-
if (this.queryState.take !== void 0) entities
|
|
1224
|
-
if (this.queryState.projection) return this.applyProjection(entities
|
|
1225
|
-
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;
|
|
1226
1116
|
}
|
|
1227
1117
|
getEntityIdsFromIndices() {
|
|
1228
1118
|
if (this.queryState.indexFilters.length === 0) return getAllEntityIds();
|
|
@@ -1249,8 +1139,8 @@ var EntitiesQueryBuilderImpl = class EntitiesQueryBuilderImpl {
|
|
|
1249
1139
|
}
|
|
1250
1140
|
return result;
|
|
1251
1141
|
}
|
|
1252
|
-
sortEntities(entities
|
|
1253
|
-
const sorted = [...entities
|
|
1142
|
+
sortEntities(entities) {
|
|
1143
|
+
const sorted = [...entities];
|
|
1254
1144
|
sorted.sort((a, b) => {
|
|
1255
1145
|
for (const sort of this.queryState.sorts) {
|
|
1256
1146
|
const aVal = getNestedValue(a, sort.key);
|
|
@@ -1270,32 +1160,33 @@ var EntitiesQueryBuilderImpl = class EntitiesQueryBuilderImpl {
|
|
|
1270
1160
|
if (typeof a === "number" && typeof b === "number") return a - b;
|
|
1271
1161
|
return String(a).localeCompare(String(b));
|
|
1272
1162
|
}
|
|
1273
|
-
applyProjection(entities
|
|
1274
|
-
if (!this.queryState.projection) return entities
|
|
1163
|
+
applyProjection(entities) {
|
|
1164
|
+
if (!this.queryState.projection) return entities;
|
|
1275
1165
|
switch (this.queryState.projection.type) {
|
|
1276
|
-
case "ids": return projectToIds(entities
|
|
1277
|
-
case "pick": return projectPick(entities
|
|
1278
|
-
case "pluck": return projectPluck(entities
|
|
1279
|
-
case "map": return projectMap(entities
|
|
1280
|
-
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;
|
|
1281
1171
|
}
|
|
1282
1172
|
}
|
|
1283
|
-
applyDistinct(entities
|
|
1284
|
-
if (!this.queryState.distinct) return entities
|
|
1173
|
+
applyDistinct(entities) {
|
|
1174
|
+
if (!this.queryState.distinct) return entities;
|
|
1285
1175
|
const seen = /* @__PURE__ */ new Set();
|
|
1286
1176
|
const result = [];
|
|
1287
|
-
for (const entity
|
|
1288
|
-
const value = getNestedValue(entity
|
|
1177
|
+
for (const entity of entities) {
|
|
1178
|
+
const value = getNestedValue(entity, this.queryState.distinct);
|
|
1289
1179
|
const key = JSON.stringify(value);
|
|
1290
1180
|
if (!seen.has(key)) {
|
|
1291
1181
|
seen.add(key);
|
|
1292
|
-
result.push(entity
|
|
1182
|
+
result.push(entity);
|
|
1293
1183
|
}
|
|
1294
1184
|
}
|
|
1295
1185
|
return result;
|
|
1296
1186
|
}
|
|
1297
1187
|
};
|
|
1298
1188
|
var EntityQueryBuilderImpl = class {
|
|
1189
|
+
entityId;
|
|
1299
1190
|
constructor(entityId) {
|
|
1300
1191
|
this.entityId = entityId;
|
|
1301
1192
|
}
|
|
@@ -1323,7 +1214,6 @@ function entities() {
|
|
|
1323
1214
|
function entity(entityId) {
|
|
1324
1215
|
return new EntityQueryBuilderImpl(entityId);
|
|
1325
1216
|
}
|
|
1326
|
-
|
|
1327
1217
|
//#endregion
|
|
1328
1218
|
//#region src/query/helpers.ts
|
|
1329
1219
|
/**
|
|
@@ -1345,9 +1235,13 @@ function entitiesByAreaAndDomain(areaId, domain) {
|
|
|
1345
1235
|
function entitiesWhere(predicate) {
|
|
1346
1236
|
return entities().where(predicate);
|
|
1347
1237
|
}
|
|
1348
|
-
|
|
1349
1238
|
//#endregion
|
|
1350
1239
|
//#region src/camera/fetch.ts
|
|
1240
|
+
/**
|
|
1241
|
+
* Camera Stream Fetching
|
|
1242
|
+
*
|
|
1243
|
+
* @packageDocumentation
|
|
1244
|
+
*/
|
|
1351
1245
|
/** Public HLS test stream for demo/disconnected mode */
|
|
1352
1246
|
const DEMO_HLS_STREAM = "https://test-streams.mux.dev/x36xhzz/x36xhzz.m3u8";
|
|
1353
1247
|
/**
|
|
@@ -1357,12 +1251,11 @@ const DEMO_HLS_STREAM = "https://test-streams.mux.dev/x36xhzz/x36xhzz.m3u8";
|
|
|
1357
1251
|
async function fetchStream(entityId, format = "hls") {
|
|
1358
1252
|
if (isDemoMode()) return format === "hls" ? DEMO_HLS_STREAM : null;
|
|
1359
1253
|
if (!privilegedConn()) return null;
|
|
1360
|
-
|
|
1254
|
+
return (await sendCommand({
|
|
1361
1255
|
type: "camera/stream",
|
|
1362
1256
|
entity_id: entityId,
|
|
1363
1257
|
format
|
|
1364
|
-
});
|
|
1365
|
-
return response?.url ?? null;
|
|
1258
|
+
}))?.url ?? null;
|
|
1366
1259
|
}
|
|
1367
1260
|
/**
|
|
1368
1261
|
* Transform relative stream URL to absolute URL
|
|
@@ -1376,8 +1269,7 @@ function transformStreamUrl(streamUrl) {
|
|
|
1376
1269
|
console.warn("[fetchStreamData] No Home Assistant URL available to resolve relative stream URL");
|
|
1377
1270
|
return streamUrl;
|
|
1378
1271
|
}
|
|
1379
|
-
|
|
1380
|
-
return `${baseUrl}${streamUrl}`;
|
|
1272
|
+
return `${hassUrl.replace(/\/$/, "")}${streamUrl}`;
|
|
1381
1273
|
}
|
|
1382
1274
|
return streamUrl;
|
|
1383
1275
|
}
|
|
@@ -1387,49 +1279,48 @@ function transformStreamUrl(streamUrl) {
|
|
|
1387
1279
|
async function fetchStreamData(entityId, options = {}) {
|
|
1388
1280
|
const { format = "hls" } = options;
|
|
1389
1281
|
try {
|
|
1390
|
-
const streamUrl = await fetchStream(entityId, format);
|
|
1391
|
-
const absoluteStreamUrl = transformStreamUrl(streamUrl);
|
|
1392
|
-
const stream = {
|
|
1393
|
-
url: absoluteStreamUrl,
|
|
1394
|
-
loading: false,
|
|
1395
|
-
error: null,
|
|
1396
|
-
lastFetched: Date.now(),
|
|
1397
|
-
format,
|
|
1398
|
-
expiresAt: null
|
|
1399
|
-
};
|
|
1400
1282
|
return {
|
|
1401
1283
|
entityId,
|
|
1402
|
-
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
|
+
},
|
|
1403
1292
|
loading: false,
|
|
1404
1293
|
error: null,
|
|
1405
1294
|
lastFetched: Date.now()
|
|
1406
1295
|
};
|
|
1407
1296
|
} catch (error) {
|
|
1408
|
-
const stream = {
|
|
1409
|
-
url: null,
|
|
1410
|
-
loading: false,
|
|
1411
|
-
error: error instanceof Error ? error : new Error(String(error)),
|
|
1412
|
-
lastFetched: null,
|
|
1413
|
-
format,
|
|
1414
|
-
expiresAt: null
|
|
1415
|
-
};
|
|
1416
1297
|
return {
|
|
1417
1298
|
entityId,
|
|
1418
|
-
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
|
+
},
|
|
1419
1307
|
loading: false,
|
|
1420
1308
|
error: error instanceof Error ? error : new Error(String(error)),
|
|
1421
1309
|
lastFetched: null
|
|
1422
1310
|
};
|
|
1423
1311
|
}
|
|
1424
1312
|
}
|
|
1425
|
-
|
|
1426
1313
|
//#endregion
|
|
1427
1314
|
//#region src/camera/query.ts
|
|
1428
|
-
|
|
1315
|
+
/**
|
|
1316
|
+
* Camera Stream Query Builder
|
|
1317
|
+
*
|
|
1318
|
+
* @packageDocumentation
|
|
1319
|
+
*/
|
|
1320
|
+
const DEFAULT_REFRESH_INTERVAL = 24e4;
|
|
1429
1321
|
function needsStreamRefresh(streamData, refreshInterval = DEFAULT_REFRESH_INTERVAL) {
|
|
1430
1322
|
if (!streamData || !streamData.lastFetched) return true;
|
|
1431
|
-
|
|
1432
|
-
return age >= refreshInterval;
|
|
1323
|
+
return Date.now() - streamData.lastFetched >= refreshInterval;
|
|
1433
1324
|
}
|
|
1434
1325
|
/**
|
|
1435
1326
|
* Get stream for a camera entity
|
|
@@ -1442,9 +1333,9 @@ async function getStream(entityId, options = {}) {
|
|
|
1442
1333
|
format,
|
|
1443
1334
|
autoRefresh,
|
|
1444
1335
|
refreshInterval
|
|
1445
|
-
}).then((streamData
|
|
1336
|
+
}).then((streamData) => {
|
|
1446
1337
|
setState(produce((s) => {
|
|
1447
|
-
s.streams[entityId] = streamData
|
|
1338
|
+
s.streams[entityId] = streamData;
|
|
1448
1339
|
}));
|
|
1449
1340
|
});
|
|
1450
1341
|
return cached;
|
|
@@ -1468,7 +1359,6 @@ async function refreshStream(entityId, format = "hls") {
|
|
|
1468
1359
|
}));
|
|
1469
1360
|
return getStream(entityId, { format });
|
|
1470
1361
|
}
|
|
1471
|
-
|
|
1472
1362
|
//#endregion
|
|
1473
1363
|
//#region src/energy/prefs.ts
|
|
1474
1364
|
function isRecord(value) {
|
|
@@ -1505,7 +1395,6 @@ async function fetchEnergyPreferences(connection) {
|
|
|
1505
1395
|
device_consumption: devices
|
|
1506
1396
|
};
|
|
1507
1397
|
}
|
|
1508
|
-
|
|
1509
1398
|
//#endregion
|
|
1510
1399
|
//#region src/media/url.ts
|
|
1511
1400
|
/**
|
|
@@ -1529,7 +1418,6 @@ function hassMediaUrl(path) {
|
|
|
1529
1418
|
if (base) return `${base}?path=${encodeURIComponent(path)}`;
|
|
1530
1419
|
return state.hassUrl ? `${state.hassUrl}${path}` : path;
|
|
1531
1420
|
}
|
|
1532
|
-
|
|
1533
1421
|
//#endregion
|
|
1534
1422
|
//#region src/camera/webrtc.ts
|
|
1535
1423
|
/**
|
|
@@ -1551,7 +1439,7 @@ async function getWebRtcClientConfig(entityId) {
|
|
|
1551
1439
|
*/
|
|
1552
1440
|
function startWebRtcSession(entityId, sdpOffer, onCandidate) {
|
|
1553
1441
|
const conn = privilegedConn();
|
|
1554
|
-
if (!conn) return Promise.reject(new Error("Not connected"));
|
|
1442
|
+
if (!conn) return Promise.reject(/* @__PURE__ */ new Error("Not connected"));
|
|
1555
1443
|
return new Promise((resolve, reject) => {
|
|
1556
1444
|
const session = {
|
|
1557
1445
|
sessionId: null,
|
|
@@ -1561,7 +1449,7 @@ function startWebRtcSession(entityId, sdpOffer, onCandidate) {
|
|
|
1561
1449
|
const timeout = setTimeout(() => {
|
|
1562
1450
|
if (!answered) {
|
|
1563
1451
|
session.unsubscribe?.();
|
|
1564
|
-
reject(new Error("WebRTC offer timed out"));
|
|
1452
|
+
reject(/* @__PURE__ */ new Error("WebRTC offer timed out"));
|
|
1565
1453
|
}
|
|
1566
1454
|
}, 15e3);
|
|
1567
1455
|
conn.subscribeMessage((event) => {
|
|
@@ -1586,7 +1474,6 @@ function startWebRtcSession(entityId, sdpOffer, onCandidate) {
|
|
|
1586
1474
|
clearTimeout(timeout);
|
|
1587
1475
|
session.unsubscribe?.();
|
|
1588
1476
|
reject(new Error(event.message || event.code || "WebRTC offer failed"));
|
|
1589
|
-
break;
|
|
1590
1477
|
}
|
|
1591
1478
|
}, {
|
|
1592
1479
|
type: "camera/webrtc/offer",
|
|
@@ -1613,17 +1500,21 @@ async function sendWebRtcCandidate(entityId, sessionId, candidate) {
|
|
|
1613
1500
|
candidate
|
|
1614
1501
|
});
|
|
1615
1502
|
}
|
|
1616
|
-
|
|
1617
1503
|
//#endregion
|
|
1618
1504
|
//#region src/weather/fetch.ts
|
|
1619
1505
|
/**
|
|
1506
|
+
* Weather Forecast Fetching
|
|
1507
|
+
*
|
|
1508
|
+
* @packageDocumentation
|
|
1509
|
+
*/
|
|
1510
|
+
/**
|
|
1620
1511
|
* In demo mode, derive forecasts from the entity's `forecast` attribute.
|
|
1621
1512
|
* Hourly and daily are synthesised from the same source (daily entries
|
|
1622
1513
|
* expanded into 12 hourly steps for the chart).
|
|
1623
1514
|
*/
|
|
1624
1515
|
function buildDemoForecast(entityId, type) {
|
|
1625
|
-
const entity
|
|
1626
|
-
const raw = entity
|
|
1516
|
+
const entity = state.entities[entityId];
|
|
1517
|
+
const raw = entity?.attributes?.forecast ?? [];
|
|
1627
1518
|
if (raw.length === 0) return [];
|
|
1628
1519
|
if (type === "daily" || type === "twice_daily") return raw.map((d) => {
|
|
1629
1520
|
const templow = d.templow;
|
|
@@ -1651,7 +1542,7 @@ function buildDemoForecast(entityId, type) {
|
|
|
1651
1542
|
out.push({
|
|
1652
1543
|
datetime: dt.toISOString(),
|
|
1653
1544
|
temperature: Math.round((mid + Math.sin(phase) * swing) * 10) / 10,
|
|
1654
|
-
condition: today?.condition ?? entity
|
|
1545
|
+
condition: today?.condition ?? entity?.state ?? "cloudy"
|
|
1655
1546
|
});
|
|
1656
1547
|
}
|
|
1657
1548
|
return out;
|
|
@@ -1669,7 +1560,7 @@ async function fetchForecast(entityId, type) {
|
|
|
1669
1560
|
const timeout = setTimeout(() => {
|
|
1670
1561
|
if (!resolved && unsubscribe) {
|
|
1671
1562
|
resolved = true;
|
|
1672
|
-
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")));
|
|
1673
1564
|
}
|
|
1674
1565
|
}, 1e4);
|
|
1675
1566
|
conn.subscribeMessage((event) => {
|
|
@@ -1732,11 +1623,10 @@ async function fetchForecasts(entityId, types) {
|
|
|
1732
1623
|
*/
|
|
1733
1624
|
async function fetchForecastData(entityId, type) {
|
|
1734
1625
|
try {
|
|
1735
|
-
const forecast = await fetchForecast(entityId, type);
|
|
1736
1626
|
return {
|
|
1737
1627
|
entityId,
|
|
1738
1628
|
type,
|
|
1739
|
-
forecast,
|
|
1629
|
+
forecast: await fetchForecast(entityId, type),
|
|
1740
1630
|
loading: false,
|
|
1741
1631
|
error: null,
|
|
1742
1632
|
lastFetched: Date.now()
|
|
@@ -1752,10 +1642,14 @@ async function fetchForecastData(entityId, type) {
|
|
|
1752
1642
|
};
|
|
1753
1643
|
}
|
|
1754
1644
|
}
|
|
1755
|
-
|
|
1756
1645
|
//#endregion
|
|
1757
1646
|
//#region src/weather/query.ts
|
|
1758
1647
|
/**
|
|
1648
|
+
* Weather Forecast Query Builder
|
|
1649
|
+
*
|
|
1650
|
+
* @packageDocumentation
|
|
1651
|
+
*/
|
|
1652
|
+
/**
|
|
1759
1653
|
* Get forecast for a single entity and type
|
|
1760
1654
|
*/
|
|
1761
1655
|
async function getForecast(entityId, type) {
|
|
@@ -1870,12 +1764,11 @@ async function refreshForecasts(entityId, types) {
|
|
|
1870
1764
|
}));
|
|
1871
1765
|
return getForecasts(entityId, types);
|
|
1872
1766
|
}
|
|
1873
|
-
|
|
1874
1767
|
//#endregion
|
|
1875
1768
|
//#region src/worker/bridge-client.ts
|
|
1876
|
-
var
|
|
1769
|
+
var BridgeInvalidAuthError = class extends Error {
|
|
1877
1770
|
constructor() {
|
|
1878
|
-
super("
|
|
1771
|
+
super("Home Assistant rejected the connection; an admin needs to sign in to Home Assistant again");
|
|
1879
1772
|
}
|
|
1880
1773
|
};
|
|
1881
1774
|
function createHaBridge(worker, events = {}) {
|
|
@@ -1899,12 +1792,13 @@ function createHaBridge(worker, events = {}) {
|
|
|
1899
1792
|
switch (msg.k) {
|
|
1900
1793
|
case "connect_result":
|
|
1901
1794
|
if (msg.ok) pendingConnect?.resolve();
|
|
1902
|
-
else pendingConnect?.reject(msg.
|
|
1795
|
+
else pendingConnect?.reject(msg.reason === "invalid_auth" ? new BridgeInvalidAuthError() : new Error(msg.error ?? "Connect failed"));
|
|
1903
1796
|
pendingConnect = null;
|
|
1904
1797
|
break;
|
|
1905
1798
|
case "conn":
|
|
1906
1799
|
connState = msg.state;
|
|
1907
1800
|
events.onConnState?.(msg.state);
|
|
1801
|
+
if (msg.reason === "invalid_auth") events.onInvalidAuth?.();
|
|
1908
1802
|
break;
|
|
1909
1803
|
case "ready_after_reconnect":
|
|
1910
1804
|
events.onReadyAfterReconnect?.();
|
|
@@ -1917,17 +1811,14 @@ function createHaBridge(worker, events = {}) {
|
|
|
1917
1811
|
else pending.reject(new Error(msg.error ?? "Request failed"));
|
|
1918
1812
|
break;
|
|
1919
1813
|
}
|
|
1920
|
-
case "event":
|
|
1814
|
+
case "event":
|
|
1921
1815
|
subCallbacks.get(msg.id)?.(msg.payload);
|
|
1922
1816
|
for (const listener of rawListeners) listener(msg.payload);
|
|
1923
1817
|
break;
|
|
1924
|
-
}
|
|
1925
1818
|
case "sub_end":
|
|
1926
1819
|
subCallbacks.delete(msg.id);
|
|
1927
1820
|
break;
|
|
1928
|
-
case "denial":
|
|
1929
|
-
events.onDenial?.(msg);
|
|
1930
|
-
break;
|
|
1821
|
+
case "denial": events.onDenial?.(msg);
|
|
1931
1822
|
}
|
|
1932
1823
|
};
|
|
1933
1824
|
function sendMessagePromise(message) {
|
|
@@ -1962,24 +1853,6 @@ function createHaBridge(worker, events = {}) {
|
|
|
1962
1853
|
});
|
|
1963
1854
|
};
|
|
1964
1855
|
}
|
|
1965
|
-
const conn = {
|
|
1966
|
-
connect: () => Promise.resolve(),
|
|
1967
|
-
disconnect: () => post({ k: "disconnect" }),
|
|
1968
|
-
get connected$() {
|
|
1969
|
-
return connState === "connected";
|
|
1970
|
-
},
|
|
1971
|
-
authenticate: () => Promise.resolve(),
|
|
1972
|
-
get authState() {
|
|
1973
|
-
return connState === "connected" ? "authenticated" : "pending";
|
|
1974
|
-
},
|
|
1975
|
-
subscribeEvents: (callback, eventType) => Promise.resolve(subscribe("events", callback, eventType)),
|
|
1976
|
-
subscribeMessage: (callback, message) => Promise.resolve(subscribe("message", callback, void 0, message)),
|
|
1977
|
-
sendMessagePromise,
|
|
1978
|
-
addEventListener: (handler) => {
|
|
1979
|
-
rawListeners.add(handler);
|
|
1980
|
-
return () => rawListeners.delete(handler);
|
|
1981
|
-
}
|
|
1982
|
-
};
|
|
1983
1856
|
return {
|
|
1984
1857
|
connect(opts) {
|
|
1985
1858
|
return new Promise((resolve, reject) => {
|
|
@@ -1990,7 +1863,6 @@ function createHaBridge(worker, events = {}) {
|
|
|
1990
1863
|
post({
|
|
1991
1864
|
k: "connect",
|
|
1992
1865
|
url: opts.url,
|
|
1993
|
-
mode: opts.mode,
|
|
1994
1866
|
proxyWsPath: opts.proxyWsPath,
|
|
1995
1867
|
proxyTicket: opts.proxyTicket
|
|
1996
1868
|
});
|
|
@@ -1998,8 +1870,24 @@ function createHaBridge(worker, events = {}) {
|
|
|
1998
1870
|
},
|
|
1999
1871
|
disconnect: () => post({ k: "disconnect" }),
|
|
2000
1872
|
reconnect: () => post({ k: "reconnect" }),
|
|
2001
|
-
|
|
2002
|
-
|
|
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
|
+
},
|
|
2003
1891
|
registerWidget(widgetId, caps) {
|
|
2004
1892
|
const channel = new MessageChannel();
|
|
2005
1893
|
post({
|
|
@@ -2034,7 +1922,6 @@ function createHaBridge(worker, events = {}) {
|
|
|
2034
1922
|
}
|
|
2035
1923
|
};
|
|
2036
1924
|
}
|
|
2037
|
-
|
|
2038
1925
|
//#endregion
|
|
2039
1926
|
//#region src/connection/bridged.ts
|
|
2040
1927
|
/**
|
|
@@ -2058,10 +1945,10 @@ async function attachBridgeToStore(bridge, hassUrl, mediaProxyBase) {
|
|
|
2058
1945
|
await forceResubscribeCalendars();
|
|
2059
1946
|
}
|
|
2060
1947
|
/** Map worker connection-state messages onto the store. */
|
|
2061
|
-
function applyBridgeConnState(state
|
|
1948
|
+
function applyBridgeConnState(state) {
|
|
2062
1949
|
setState(produce((s) => {
|
|
2063
|
-
s.connectionState = state
|
|
2064
|
-
if (state
|
|
1950
|
+
s.connectionState = state;
|
|
1951
|
+
if (state === "connected") s.connectionError = null;
|
|
2065
1952
|
}));
|
|
2066
1953
|
}
|
|
2067
1954
|
/** Re-sync after the worker reports a completed reconnect. */
|
|
@@ -2077,16 +1964,38 @@ function detachBridgeFromStore() {
|
|
|
2077
1964
|
s.connectionState = "disconnected";
|
|
2078
1965
|
}));
|
|
2079
1966
|
}
|
|
2080
|
-
|
|
2081
1967
|
//#endregion
|
|
2082
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
|
+
*/
|
|
2083
1993
|
let unclaimed = {
|
|
2084
1994
|
attachBridgeToStore,
|
|
2085
1995
|
detachBridgeFromStore,
|
|
2086
1996
|
applyBridgeConnState,
|
|
2087
1997
|
reloadAfterBridgeReconnect,
|
|
2088
1998
|
createHaBridge,
|
|
2089
|
-
authenticateWithOAuth,
|
|
2090
1999
|
disconnect,
|
|
2091
2000
|
loadDemoData,
|
|
2092
2001
|
unloadDemoData
|
|
@@ -2104,10 +2013,8 @@ function claimHostApi() {
|
|
|
2104
2013
|
unclaimed = null;
|
|
2105
2014
|
return api;
|
|
2106
2015
|
}
|
|
2107
|
-
|
|
2108
2016
|
//#endregion
|
|
2109
2017
|
//#region src/index.ts
|
|
2110
2018
|
const VERSION = "0.2.1";
|
|
2111
|
-
|
|
2112
2019
|
//#endregion
|
|
2113
|
-
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 };
|