@glasshome/sync-layer 0.8.1 → 0.9.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 +4 -3
- package/dist/index.js +10 -151
- package/dist/solid/index.d.ts +4 -3
- package/dist/solid/index.js +9 -3
- package/dist/{statistics-DyCCTwv2.js → statistics-C5j1DLGM.js} +504 -130
- package/dist/worker/worker-main.js +1 -1
- package/dist/{worker-main-D_wyGAIG.js → worker-main-DamaVb7B.js} +8 -1
- package/package.json +3 -3
package/dist/index.d.ts
CHANGED
|
@@ -862,7 +862,7 @@ declare function entitiesWhere(predicate: (entity: EntityView) => boolean): Enti
|
|
|
862
862
|
//#region src/camera/fetch.d.ts
|
|
863
863
|
/**
|
|
864
864
|
* Fetch stream for a camera entity.
|
|
865
|
-
* Returns
|
|
865
|
+
* Returns the dashboard's own demo HLS feed in demo mode.
|
|
866
866
|
*/
|
|
867
867
|
declare function fetchStream(entityId: EntityId, format?: StreamFormat): Promise<string | null>;
|
|
868
868
|
/**
|
|
@@ -1207,6 +1207,7 @@ declare function applyDemoServiceCall(_domain: string, service: string, _service
|
|
|
1207
1207
|
//#endregion
|
|
1208
1208
|
//#region src/worker/protocol.d.ts
|
|
1209
1209
|
type ConnState = "connected" | "disconnected" | "reconnecting";
|
|
1210
|
+
type ConnReason = "invalid_auth" | "ping_timeout";
|
|
1210
1211
|
//#endregion
|
|
1211
1212
|
//#region src/worker/bridge-client.d.ts
|
|
1212
1213
|
/**
|
|
@@ -1225,7 +1226,7 @@ interface BridgeConnectOptions {
|
|
|
1225
1226
|
proxyTicket?: string;
|
|
1226
1227
|
}
|
|
1227
1228
|
interface BridgeEvents {
|
|
1228
|
-
onConnState?: (state: ConnState) => void;
|
|
1229
|
+
onConnState?: (state: ConnState, reason?: ConnReason) => void;
|
|
1229
1230
|
onReadyAfterReconnect?: () => void;
|
|
1230
1231
|
onInvalidAuth?: () => void;
|
|
1231
1232
|
onDenial?: (denial: {
|
|
@@ -1264,7 +1265,7 @@ declare function createHaBridge(worker: Worker, events?: BridgeEvents): HaBridge
|
|
|
1264
1265
|
*/
|
|
1265
1266
|
declare function attachBridgeToStore(bridge: HaBridge, hassUrl: string, mediaProxyBase?: string): Promise<void>;
|
|
1266
1267
|
/** Map worker connection-state messages onto the store. */
|
|
1267
|
-
declare function applyBridgeConnState(state: "connected" | "disconnected" | "reconnecting"): void;
|
|
1268
|
+
declare function applyBridgeConnState(state: "connecting" | "connected" | "disconnected" | "reconnecting"): void;
|
|
1268
1269
|
/** Re-sync after the worker reports a completed reconnect. */
|
|
1269
1270
|
declare function reloadAfterBridgeReconnect(bridge: HaBridge): Promise<void>;
|
|
1270
1271
|
/** Detach on shutdown; the caller terminates the worker itself. */
|
package/dist/index.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { A as
|
|
2
|
-
import { t as runHaBridgeWorker } from "./worker-main-
|
|
1
|
+
import { A as isDemoMode, B as extractDomain, C as setManagerConnection, D as fetchForecastData, E as fetchForecast, F as createDemoFixtures, G as setState, I as demoAssetUrl, K as state, L as demoEnergyPreferences, M as startDemoEnergyTicker, N as stopDemoEnergyTicker, O as fetchForecasts, P as unloadDemoData, R as isEnergyEntity, S as registerEntity, U as privilegedConn, V as forceResubscribeCalendars, W as setPrivilegedConn, _ as toggle, a as getAreaView, b as updateEntity, c as entityViewEquals, d as getEntityViews, f as numericSensorHistory, g as sendCommand, h as callService, i as buildDeviceView, j as loadDemoData, k as applyDemoServiceCall, l as getAllEntityViews, m as batchServiceCalls, n as normalizeStatisticTime, o as getAreaViews, p as batchEntityUpdates, r as buildAreaView, s as buildEntityView, t as fetchStatisticsDuringPeriod, u as getEntityView, v as turnOff, w as setResubscribeHandler, x as forceResubscribe, y as turnOn, z as synthesizeEnergyHistory } from "./statistics-C5j1DLGM.js";
|
|
2
|
+
import { t as runHaBridgeWorker } from "./worker-main-DamaVb7B.js";
|
|
3
3
|
import "home-assistant-js-websocket";
|
|
4
4
|
import { produce } from "solid-js/store";
|
|
5
5
|
//#region src/core/reducers.ts
|
|
@@ -193,8 +193,9 @@ async function fetchHistory(options) {
|
|
|
193
193
|
const endMs = (endTime ?? /* @__PURE__ */ new Date()).getTime();
|
|
194
194
|
const result = {};
|
|
195
195
|
for (const id of entityIds) {
|
|
196
|
-
|
|
197
|
-
|
|
196
|
+
const points = isEnergyEntity(id) ? synthesizeEnergyHistory(id, startTime.getTime(), endMs) : numericSensorHistory(id, startTime.getTime(), endMs);
|
|
197
|
+
if (!points) continue;
|
|
198
|
+
result[id] = points.map((p) => ({
|
|
198
199
|
s: p.s,
|
|
199
200
|
a: {},
|
|
200
201
|
lu: p.lu
|
|
@@ -1242,14 +1243,14 @@ function entitiesWhere(predicate) {
|
|
|
1242
1243
|
*
|
|
1243
1244
|
* @packageDocumentation
|
|
1244
1245
|
*/
|
|
1245
|
-
/**
|
|
1246
|
-
const DEMO_HLS_STREAM = "
|
|
1246
|
+
/** The demo's front door feed, served by the dashboard itself so the demo never reaches a third party. */
|
|
1247
|
+
const DEMO_HLS_STREAM = "/demo/camera/front-door.m3u8";
|
|
1247
1248
|
/**
|
|
1248
1249
|
* Fetch stream for a camera entity.
|
|
1249
|
-
* Returns
|
|
1250
|
+
* Returns the dashboard's own demo HLS feed in demo mode.
|
|
1250
1251
|
*/
|
|
1251
1252
|
async function fetchStream(entityId, format = "hls") {
|
|
1252
|
-
if (isDemoMode()) return format === "hls" ? DEMO_HLS_STREAM : null;
|
|
1253
|
+
if (isDemoMode()) return format === "hls" ? demoAssetUrl(DEMO_HLS_STREAM) : null;
|
|
1253
1254
|
if (!privilegedConn()) return null;
|
|
1254
1255
|
return (await sendCommand({
|
|
1255
1256
|
type: "camera/stream",
|
|
@@ -1501,148 +1502,6 @@ async function sendWebRtcCandidate(entityId, sessionId, candidate) {
|
|
|
1501
1502
|
});
|
|
1502
1503
|
}
|
|
1503
1504
|
//#endregion
|
|
1504
|
-
//#region src/weather/fetch.ts
|
|
1505
|
-
/**
|
|
1506
|
-
* Weather Forecast Fetching
|
|
1507
|
-
*
|
|
1508
|
-
* @packageDocumentation
|
|
1509
|
-
*/
|
|
1510
|
-
/**
|
|
1511
|
-
* In demo mode, derive forecasts from the entity's `forecast` attribute.
|
|
1512
|
-
* Hourly and daily are synthesised from the same source (daily entries
|
|
1513
|
-
* expanded into 12 hourly steps for the chart).
|
|
1514
|
-
*/
|
|
1515
|
-
function buildDemoForecast(entityId, type) {
|
|
1516
|
-
const entity = state.entities[entityId];
|
|
1517
|
-
const raw = entity?.attributes?.forecast ?? [];
|
|
1518
|
-
if (raw.length === 0) return [];
|
|
1519
|
-
if (type === "daily" || type === "twice_daily") return raw.map((d) => {
|
|
1520
|
-
const templow = d.templow;
|
|
1521
|
-
return {
|
|
1522
|
-
...d,
|
|
1523
|
-
temp_high: d.temp_high ?? d.temperature,
|
|
1524
|
-
temp_low: d.temp_low ?? templow
|
|
1525
|
-
};
|
|
1526
|
-
});
|
|
1527
|
-
const start = /* @__PURE__ */ new Date();
|
|
1528
|
-
const today = raw[0];
|
|
1529
|
-
const tomorrow = raw[1] ?? today;
|
|
1530
|
-
const tHi = today?.temperature ?? 18;
|
|
1531
|
-
const tLo = today?.templow ?? tHi - 5;
|
|
1532
|
-
const tNext = tomorrow?.temperature ?? tHi;
|
|
1533
|
-
const out = [];
|
|
1534
|
-
for (let i = 0; i < 24; i++) {
|
|
1535
|
-
const dt = new Date(start.getTime() + i * 60 * 60 * 1e3);
|
|
1536
|
-
const hour = dt.getHours();
|
|
1537
|
-
const dayMix = i < 12 ? 0 : (i - 12) / 12;
|
|
1538
|
-
const baseHi = tHi * (1 - dayMix) + tNext * dayMix;
|
|
1539
|
-
const phase = (hour - 6) / 24 * Math.PI * 2;
|
|
1540
|
-
const swing = (baseHi - tLo) / 2;
|
|
1541
|
-
const mid = (baseHi + tLo) / 2;
|
|
1542
|
-
out.push({
|
|
1543
|
-
datetime: dt.toISOString(),
|
|
1544
|
-
temperature: Math.round((mid + Math.sin(phase) * swing) * 10) / 10,
|
|
1545
|
-
condition: today?.condition ?? entity?.state ?? "cloudy"
|
|
1546
|
-
});
|
|
1547
|
-
}
|
|
1548
|
-
return out;
|
|
1549
|
-
}
|
|
1550
|
-
/**
|
|
1551
|
-
* Fetch forecast for a single entity and type
|
|
1552
|
-
*/
|
|
1553
|
-
async function fetchForecast(entityId, type) {
|
|
1554
|
-
if (isDemoMode()) return buildDemoForecast(entityId, type);
|
|
1555
|
-
const conn = privilegedConn();
|
|
1556
|
-
if (!conn) throw new Error("Not connected");
|
|
1557
|
-
return new Promise((resolve, reject) => {
|
|
1558
|
-
let resolved = false;
|
|
1559
|
-
let unsubscribe = null;
|
|
1560
|
-
const timeout = setTimeout(() => {
|
|
1561
|
-
if (!resolved && unsubscribe) {
|
|
1562
|
-
resolved = true;
|
|
1563
|
-
unsubscribe().then(() => reject(/* @__PURE__ */ new Error("Forecast subscription timeout"))).catch(() => reject(/* @__PURE__ */ new Error("Forecast subscription timeout")));
|
|
1564
|
-
}
|
|
1565
|
-
}, 1e4);
|
|
1566
|
-
conn.subscribeMessage((event) => {
|
|
1567
|
-
if (resolved) return;
|
|
1568
|
-
if (event && typeof event === "object" && "forecast" in event) {
|
|
1569
|
-
resolved = true;
|
|
1570
|
-
clearTimeout(timeout);
|
|
1571
|
-
const forecast = event.forecast;
|
|
1572
|
-
const forecastArray = Array.isArray(forecast) ? forecast : [];
|
|
1573
|
-
if (unsubscribe) unsubscribe().then(() => resolve(forecastArray)).catch(() => resolve(forecastArray));
|
|
1574
|
-
else resolve(forecastArray);
|
|
1575
|
-
}
|
|
1576
|
-
}, {
|
|
1577
|
-
type: "weather/subscribe_forecast",
|
|
1578
|
-
entity_id: entityId,
|
|
1579
|
-
forecast_type: type === "twice_daily" ? "twice_daily" : type
|
|
1580
|
-
}).then((unsubFn) => {
|
|
1581
|
-
unsubscribe = unsubFn;
|
|
1582
|
-
}).catch((error) => {
|
|
1583
|
-
resolved = true;
|
|
1584
|
-
clearTimeout(timeout);
|
|
1585
|
-
reject(error);
|
|
1586
|
-
});
|
|
1587
|
-
});
|
|
1588
|
-
}
|
|
1589
|
-
/**
|
|
1590
|
-
* Fetch forecasts for a single entity and multiple types
|
|
1591
|
-
*/
|
|
1592
|
-
async function fetchForecasts(entityId, types) {
|
|
1593
|
-
const forecasts = {};
|
|
1594
|
-
const loading = {};
|
|
1595
|
-
const errors = {};
|
|
1596
|
-
const lastFetched = {};
|
|
1597
|
-
const fetchPromises = types.map(async (type) => {
|
|
1598
|
-
loading[type] = true;
|
|
1599
|
-
errors[type] = null;
|
|
1600
|
-
try {
|
|
1601
|
-
const forecast = await fetchForecast(entityId, type);
|
|
1602
|
-
forecasts[type] = forecast;
|
|
1603
|
-
lastFetched[type] = Date.now();
|
|
1604
|
-
loading[type] = false;
|
|
1605
|
-
} catch (error) {
|
|
1606
|
-
errors[type] = error instanceof Error ? error : new Error(String(error));
|
|
1607
|
-
forecasts[type] = [];
|
|
1608
|
-
loading[type] = false;
|
|
1609
|
-
lastFetched[type] = null;
|
|
1610
|
-
}
|
|
1611
|
-
});
|
|
1612
|
-
await Promise.all(fetchPromises);
|
|
1613
|
-
return {
|
|
1614
|
-
entityId,
|
|
1615
|
-
forecasts,
|
|
1616
|
-
loading,
|
|
1617
|
-
errors,
|
|
1618
|
-
lastFetched
|
|
1619
|
-
};
|
|
1620
|
-
}
|
|
1621
|
-
/**
|
|
1622
|
-
* Fetch forecast data with error handling
|
|
1623
|
-
*/
|
|
1624
|
-
async function fetchForecastData(entityId, type) {
|
|
1625
|
-
try {
|
|
1626
|
-
return {
|
|
1627
|
-
entityId,
|
|
1628
|
-
type,
|
|
1629
|
-
forecast: await fetchForecast(entityId, type),
|
|
1630
|
-
loading: false,
|
|
1631
|
-
error: null,
|
|
1632
|
-
lastFetched: Date.now()
|
|
1633
|
-
};
|
|
1634
|
-
} catch (error) {
|
|
1635
|
-
return {
|
|
1636
|
-
entityId,
|
|
1637
|
-
type,
|
|
1638
|
-
forecast: [],
|
|
1639
|
-
loading: false,
|
|
1640
|
-
error: error instanceof Error ? error : new Error(String(error)),
|
|
1641
|
-
lastFetched: null
|
|
1642
|
-
};
|
|
1643
|
-
}
|
|
1644
|
-
}
|
|
1645
|
-
//#endregion
|
|
1646
1505
|
//#region src/weather/query.ts
|
|
1647
1506
|
/**
|
|
1648
1507
|
* Weather Forecast Query Builder
|
|
@@ -1797,7 +1656,7 @@ function createHaBridge(worker, events = {}) {
|
|
|
1797
1656
|
break;
|
|
1798
1657
|
case "conn":
|
|
1799
1658
|
connState = msg.state;
|
|
1800
|
-
events.onConnState?.(msg.state);
|
|
1659
|
+
events.onConnState?.(msg.state, msg.reason);
|
|
1801
1660
|
if (msg.reason === "invalid_auth") events.onInvalidAuth?.();
|
|
1802
1661
|
break;
|
|
1803
1662
|
case "ready_after_reconnect":
|
package/dist/solid/index.d.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { a as toggle, g as CalendarWindowOptions, h as CalendarEventsData, o as turnOff, r as callService, s as turnOn } from "../service-C8H42I-Q.js";
|
|
2
2
|
import { Accessor, Resource } from "solid-js";
|
|
3
3
|
import { HassConfig, HassUnitSystem } from "@glasshome/ha-types";
|
|
4
|
-
import { AreaView, CameraStreamData, ConnectionState, EntityHistoryData, EntityView, GlassHomeState, HassEntity, StatisticValue, StatisticsQueryOptions, WeatherForecastsData } from "@glasshome/sync-layer";
|
|
4
|
+
import { AreaView, CameraStreamData, ConnectionState, EntityHistoryData, EntityView, ForecastType, GlassHomeState, HassEntity, StatisticValue, StatisticsQueryOptions, WeatherForecastsData } from "@glasshome/sync-layer";
|
|
5
5
|
//#region src/solid/hooks.d.ts
|
|
6
6
|
/**
|
|
7
7
|
* Get a reactive EntityView for an entity ID.
|
|
@@ -96,9 +96,10 @@ declare function useCamera(entityId: Accessor<string> | string): {
|
|
|
96
96
|
*/
|
|
97
97
|
declare function useCalendarEvents(entityId: Accessor<string> | string, options?: CalendarWindowOptions): Accessor<CalendarEventsData | undefined>;
|
|
98
98
|
/**
|
|
99
|
-
* Get reactive weather forecast data.
|
|
99
|
+
* Get reactive weather forecast data. With `types`, the forecast stays live
|
|
100
|
+
* while the caller is mounted: Home Assistant pushes each new one, no polling.
|
|
100
101
|
*/
|
|
101
|
-
declare function useForecast(entityId: Accessor<string> | string): Accessor<WeatherForecastsData | undefined>;
|
|
102
|
+
declare function useForecast(entityId: Accessor<string> | string, types?: ForecastType[]): Accessor<WeatherForecastsData | undefined>;
|
|
102
103
|
/**
|
|
103
104
|
* Escape hatch for direct store access with a selector.
|
|
104
105
|
*
|
package/dist/solid/index.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { A as
|
|
1
|
+
import { A as isDemoMode, B as extractDomain, H as trackCalendarEvents, K as state, S as registerEntity, T as trackForecast, U as privilegedConn, _ as toggle, c as entityViewEquals, h as callService, o as getAreaViews, r as buildAreaView, t as fetchStatisticsDuringPeriod, u as getEntityView, v as turnOff, y as turnOn } from "../statistics-C5j1DLGM.js";
|
|
2
2
|
import { createEffect, createMemo, createResource, onCleanup } from "solid-js";
|
|
3
3
|
//#region src/solid/hooks.ts
|
|
4
4
|
/**
|
|
@@ -228,10 +228,16 @@ function useCalendarEvents(entityId, options) {
|
|
|
228
228
|
return createMemo(() => state.calendars[getId()]);
|
|
229
229
|
}
|
|
230
230
|
/**
|
|
231
|
-
* Get reactive weather forecast data.
|
|
231
|
+
* Get reactive weather forecast data. With `types`, the forecast stays live
|
|
232
|
+
* while the caller is mounted: Home Assistant pushes each new one, no polling.
|
|
232
233
|
*/
|
|
233
|
-
function useForecast(entityId) {
|
|
234
|
+
function useForecast(entityId, types) {
|
|
234
235
|
const getId = typeof entityId === "function" ? entityId : () => entityId;
|
|
236
|
+
createEffect(() => {
|
|
237
|
+
const id = getId();
|
|
238
|
+
if (!id || !types) return;
|
|
239
|
+
for (const type of types) onCleanup(trackForecast(id, type));
|
|
240
|
+
});
|
|
235
241
|
return createMemo(() => state.forecasts[getId()]);
|
|
236
242
|
}
|
|
237
243
|
/**
|
|
@@ -49,13 +49,13 @@ function setPrivilegedConn(conn) {
|
|
|
49
49
|
const DEFAULT_DAYS_BEHIND = 7;
|
|
50
50
|
const DEFAULT_DAYS_AHEAD = 35;
|
|
51
51
|
/** Debounce for untrack, so dashboard switches don't churn subscriptions. */
|
|
52
|
-
const UNTRACK_FLUSH_DELAY_MS = 5e3;
|
|
53
|
-
const DAY_MS = 864e5;
|
|
52
|
+
const UNTRACK_FLUSH_DELAY_MS$1 = 5e3;
|
|
53
|
+
const DAY_MS$1 = 864e5;
|
|
54
54
|
const trackers = /* @__PURE__ */ new Map();
|
|
55
|
-
const active = /* @__PURE__ */ new Map();
|
|
55
|
+
const active$1 = /* @__PURE__ */ new Map();
|
|
56
56
|
/** Per-entity reconcile chain; serializes subscribe/unsubscribe per calendar. */
|
|
57
|
-
const chains = /* @__PURE__ */ new Map();
|
|
58
|
-
let untrackTimer = null;
|
|
57
|
+
const chains$1 = /* @__PURE__ */ new Map();
|
|
58
|
+
let untrackTimer$1 = null;
|
|
59
59
|
let rollTimer = null;
|
|
60
60
|
/** Day-quantized union of all tracker windows, so equal requests within a day
|
|
61
61
|
reconcile to the same subscription instead of churning. */
|
|
@@ -69,8 +69,8 @@ function windowFor(list) {
|
|
|
69
69
|
const dayStart = /* @__PURE__ */ new Date();
|
|
70
70
|
dayStart.setHours(0, 0, 0, 0);
|
|
71
71
|
return {
|
|
72
|
-
start: (/* @__PURE__ */ new Date(dayStart.getTime() - behind * DAY_MS)).toISOString(),
|
|
73
|
-
end: new Date(dayStart.getTime() + (ahead + 1) * DAY_MS).toISOString()
|
|
72
|
+
start: (/* @__PURE__ */ new Date(dayStart.getTime() - behind * DAY_MS$1)).toISOString(),
|
|
73
|
+
end: new Date(dayStart.getTime() + (ahead + 1) * DAY_MS$1).toISOString()
|
|
74
74
|
};
|
|
75
75
|
}
|
|
76
76
|
/**
|
|
@@ -102,11 +102,11 @@ function untrackCalendarEvents(entityId, tracker) {
|
|
|
102
102
|
const i = list.indexOf(tracker);
|
|
103
103
|
if (i !== -1) list.splice(i, 1);
|
|
104
104
|
if (list.length === 0) trackers.delete(entityId);
|
|
105
|
-
if (untrackTimer) return;
|
|
106
|
-
untrackTimer = setTimeout(() => {
|
|
107
|
-
untrackTimer = null;
|
|
108
|
-
flushAll();
|
|
109
|
-
}, UNTRACK_FLUSH_DELAY_MS);
|
|
105
|
+
if (untrackTimer$1) return;
|
|
106
|
+
untrackTimer$1 = setTimeout(() => {
|
|
107
|
+
untrackTimer$1 = null;
|
|
108
|
+
flushAll$1();
|
|
109
|
+
}, UNTRACK_FLUSH_DELAY_MS$1);
|
|
110
110
|
}
|
|
111
111
|
/**
|
|
112
112
|
* Reconcile every tracked calendar, dropping current subscription handles
|
|
@@ -116,39 +116,39 @@ function untrackCalendarEvents(entityId, tracker) {
|
|
|
116
116
|
* store through any missed changes).
|
|
117
117
|
*/
|
|
118
118
|
async function forceResubscribeCalendars() {
|
|
119
|
-
for (const entry of active.values()) {
|
|
119
|
+
for (const entry of active$1.values()) {
|
|
120
120
|
const dead = entry.unsub;
|
|
121
121
|
entry.unsub = null;
|
|
122
122
|
if (dead) Promise.resolve().then(dead).catch(() => {});
|
|
123
123
|
}
|
|
124
|
-
await flushAll();
|
|
124
|
+
await flushAll$1();
|
|
125
125
|
}
|
|
126
126
|
function scheduleFlush$1() {
|
|
127
127
|
queueMicrotask(() => {
|
|
128
|
-
flushAll();
|
|
128
|
+
flushAll$1();
|
|
129
129
|
});
|
|
130
130
|
}
|
|
131
|
-
async function flushAll() {
|
|
132
|
-
const ids = /* @__PURE__ */ new Set([...trackers.keys(), ...active.keys()]);
|
|
131
|
+
async function flushAll$1() {
|
|
132
|
+
const ids = /* @__PURE__ */ new Set([...trackers.keys(), ...active$1.keys()]);
|
|
133
133
|
await Promise.all([...ids].map(reconcileEntity));
|
|
134
134
|
if (trackers.size === 0) stopRollTimer();
|
|
135
135
|
}
|
|
136
136
|
function reconcileEntity(entityId) {
|
|
137
|
-
const next = (chains.get(entityId) ?? Promise.resolve()).then(() => runReconcile(entityId));
|
|
138
|
-
chains.set(entityId, next.catch(() => {}));
|
|
137
|
+
const next = (chains$1.get(entityId) ?? Promise.resolve()).then(() => runReconcile$1(entityId));
|
|
138
|
+
chains$1.set(entityId, next.catch(() => {}));
|
|
139
139
|
return next;
|
|
140
140
|
}
|
|
141
|
-
async function runReconcile(entityId) {
|
|
141
|
+
async function runReconcile$1(entityId) {
|
|
142
142
|
const list = trackers.get(entityId);
|
|
143
|
-
const current = active.get(entityId);
|
|
143
|
+
const current = active$1.get(entityId);
|
|
144
144
|
if (!list || list.length === 0) {
|
|
145
145
|
if (current) {
|
|
146
|
-
active.delete(entityId);
|
|
146
|
+
active$1.delete(entityId);
|
|
147
147
|
try {
|
|
148
148
|
await current.unsub?.();
|
|
149
149
|
} catch {}
|
|
150
150
|
}
|
|
151
|
-
chains.delete(entityId);
|
|
151
|
+
chains$1.delete(entityId);
|
|
152
152
|
setState(produce((s) => {
|
|
153
153
|
delete s.calendars[entityId];
|
|
154
154
|
}));
|
|
@@ -174,7 +174,7 @@ async function runReconcile(entityId) {
|
|
|
174
174
|
end: win.end,
|
|
175
175
|
unsub: null
|
|
176
176
|
};
|
|
177
|
-
active.set(entityId, entry);
|
|
177
|
+
active$1.set(entityId, entry);
|
|
178
178
|
try {
|
|
179
179
|
entry.unsub = await conn.subscribeMessage((msg) => {
|
|
180
180
|
setState(produce((s) => {
|
|
@@ -191,7 +191,7 @@ async function runReconcile(entityId) {
|
|
|
191
191
|
end: win.end
|
|
192
192
|
});
|
|
193
193
|
} catch (err) {
|
|
194
|
-
active.delete(entityId);
|
|
194
|
+
active$1.delete(entityId);
|
|
195
195
|
setState(produce((s) => {
|
|
196
196
|
const rec = s.calendars[entityId];
|
|
197
197
|
if (!rec) return;
|
|
@@ -213,7 +213,7 @@ function ensureRollTimer() {
|
|
|
213
213
|
next.setHours(24, 0, 30, 0);
|
|
214
214
|
rollTimer = setTimeout(() => {
|
|
215
215
|
rollTimer = null;
|
|
216
|
-
flushAll().then(ensureRollTimer);
|
|
216
|
+
flushAll$1().then(ensureRollTimer);
|
|
217
217
|
}, next.getTime() - now.getTime());
|
|
218
218
|
}
|
|
219
219
|
function stopRollTimer() {
|
|
@@ -223,102 +223,6 @@ function stopRollTimer() {
|
|
|
223
223
|
}
|
|
224
224
|
}
|
|
225
225
|
//#endregion
|
|
226
|
-
//#region src/connection/subscription-manager.ts
|
|
227
|
-
if (globalThis.__GH_SYNC_LAYER_SUB_MGR__) console.error("[@glasshome/sync-layer] DUPLICATE subscription-manager module instantiated. registerEntity() and the connection setter will hit different singletons → subscribe_entities will never be sent. Cause: relative import of subscription-manager from a separate entry point.");
|
|
228
|
-
globalThis.__GH_SYNC_LAYER_SUB_MGR__ = true;
|
|
229
|
-
/** Ref counts — how many consumers need each entity */
|
|
230
|
-
const refCounts = /* @__PURE__ */ new Map();
|
|
231
|
-
/** Current HA subscription unsub function */
|
|
232
|
-
let currentEntityUnsub = null;
|
|
233
|
-
/** Snapshot of entity IDs in the current HA subscription */
|
|
234
|
-
let subscribedIds = /* @__PURE__ */ new Set();
|
|
235
|
-
/** Connection reference for resubscribing */
|
|
236
|
-
let conn = null;
|
|
237
|
-
/** Pending flush timer (microtask for register, timeout for unregister) */
|
|
238
|
-
let flushScheduled = false;
|
|
239
|
-
/** Delayed unregister timeout */
|
|
240
|
-
let unregisterTimeout = null;
|
|
241
|
-
/** Callback invoked on resubscribe — set by subscriptions.ts */
|
|
242
|
-
let onResubscribe = null;
|
|
243
|
-
/** Delay before unregistered entities are actually removed from subscription */
|
|
244
|
-
const UNREGISTER_DELAY_MS = 5e3;
|
|
245
|
-
/** Guard against concurrent flush() calls */
|
|
246
|
-
let flushInFlight = false;
|
|
247
|
-
/**
|
|
248
|
-
* Register an entity for live updates. Returns an unregister function.
|
|
249
|
-
* Call from useEntity/useEntities hooks via onCleanup.
|
|
250
|
-
*/
|
|
251
|
-
function registerEntity(entityId) {
|
|
252
|
-
refCounts.set(entityId, (refCounts.get(entityId) ?? 0) + 1);
|
|
253
|
-
scheduleFlush();
|
|
254
|
-
return () => unregisterEntity(entityId);
|
|
255
|
-
}
|
|
256
|
-
/**
|
|
257
|
-
* Set the connection reference used for resubscribing.
|
|
258
|
-
*/
|
|
259
|
-
function setManagerConnection(connection) {
|
|
260
|
-
conn = connection;
|
|
261
|
-
if (refCounts.size > 0) scheduleFlush();
|
|
262
|
-
}
|
|
263
|
-
/**
|
|
264
|
-
* Set the resubscribe callback. Called by subscriptions.ts to wire
|
|
265
|
-
* the manager into the actual WS subscription lifecycle.
|
|
266
|
-
*/
|
|
267
|
-
function setResubscribeHandler(handler) {
|
|
268
|
-
onResubscribe = handler;
|
|
269
|
-
}
|
|
270
|
-
/**
|
|
271
|
-
* Force a resubscribe with the current active set.
|
|
272
|
-
* Used on reconnect.
|
|
273
|
-
*/
|
|
274
|
-
async function forceResubscribe() {
|
|
275
|
-
await flush();
|
|
276
|
-
}
|
|
277
|
-
function unregisterEntity(entityId) {
|
|
278
|
-
const count = (refCounts.get(entityId) ?? 1) - 1;
|
|
279
|
-
if (count <= 0) refCounts.delete(entityId);
|
|
280
|
-
else refCounts.set(entityId, count);
|
|
281
|
-
scheduleDelayedFlush();
|
|
282
|
-
}
|
|
283
|
-
function scheduleFlush() {
|
|
284
|
-
if (flushScheduled) return;
|
|
285
|
-
flushScheduled = true;
|
|
286
|
-
queueMicrotask(() => {
|
|
287
|
-
flushScheduled = false;
|
|
288
|
-
flush();
|
|
289
|
-
});
|
|
290
|
-
}
|
|
291
|
-
function scheduleDelayedFlush() {
|
|
292
|
-
if (unregisterTimeout) return;
|
|
293
|
-
unregisterTimeout = setTimeout(() => {
|
|
294
|
-
unregisterTimeout = null;
|
|
295
|
-
flush();
|
|
296
|
-
}, UNREGISTER_DELAY_MS);
|
|
297
|
-
}
|
|
298
|
-
async function flush() {
|
|
299
|
-
if (!conn || !onResubscribe) return;
|
|
300
|
-
if (flushInFlight) return;
|
|
301
|
-
flushInFlight = true;
|
|
302
|
-
try {
|
|
303
|
-
const newIds = new Set(refCounts.keys());
|
|
304
|
-
if (setsEqual(newIds, subscribedIds)) return;
|
|
305
|
-
const oldUnsub = currentEntityUnsub;
|
|
306
|
-
const entityIds = [...newIds];
|
|
307
|
-
currentEntityUnsub = await onResubscribe(conn, entityIds);
|
|
308
|
-
subscribedIds = newIds;
|
|
309
|
-
if (oldUnsub) try {
|
|
310
|
-
await oldUnsub();
|
|
311
|
-
} catch {}
|
|
312
|
-
} finally {
|
|
313
|
-
flushInFlight = false;
|
|
314
|
-
}
|
|
315
|
-
}
|
|
316
|
-
function setsEqual(a, b) {
|
|
317
|
-
if (a.size !== b.size) return false;
|
|
318
|
-
for (const id of a) if (!b.has(id)) return false;
|
|
319
|
-
return true;
|
|
320
|
-
}
|
|
321
|
-
//#endregion
|
|
322
226
|
//#region src/core/types.ts
|
|
323
227
|
/**
|
|
324
228
|
* Extract domain from entity ID
|
|
@@ -327,6 +231,9 @@ function extractDomain(entityId) {
|
|
|
327
231
|
return entityId.split(".")[0] ?? "";
|
|
328
232
|
}
|
|
329
233
|
//#endregion
|
|
234
|
+
//#region src/demo/demo-album-cover.ts
|
|
235
|
+
const DEMO_ALBUM_COVER = "data:image/webp;base64,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";
|
|
236
|
+
//#endregion
|
|
330
237
|
//#region src/demo/energy-sim.ts
|
|
331
238
|
const SOLAR_PEAK_W = 5200;
|
|
332
239
|
const SUNRISE_HOUR = 6.5;
|
|
@@ -397,7 +304,10 @@ function solarPower(lt) {
|
|
|
397
304
|
}
|
|
398
305
|
/** Coarse solar elevation in degrees for sun.sun attributes. */
|
|
399
306
|
function sunElevation(lt) {
|
|
400
|
-
if (lt.hourFrac <= SUNRISE_HOUR || lt.hourFrac >= SUNSET_HOUR)
|
|
307
|
+
if (lt.hourFrac <= SUNRISE_HOUR || lt.hourFrac >= SUNSET_HOUR) {
|
|
308
|
+
const hoursFromHorizon = Math.min(Math.abs(lt.hourFrac - SUNRISE_HOUR), Math.abs(lt.hourFrac - SUNSET_HOUR), Math.abs(lt.hourFrac + 24 - SUNSET_HOUR));
|
|
309
|
+
return -12 * Math.min(1, hoursFromHorizon / 1.5);
|
|
310
|
+
}
|
|
401
311
|
const frac = (lt.hourFrac - SUNRISE_HOUR) / 14;
|
|
402
312
|
return 60 * Math.sin(frac * Math.PI);
|
|
403
313
|
}
|
|
@@ -588,7 +498,8 @@ function sunEvents(timestampMs) {
|
|
|
588
498
|
return {
|
|
589
499
|
nextRising: new Date(nextRising).toISOString(),
|
|
590
500
|
nextSetting: new Date(nextSetting).toISOString(),
|
|
591
|
-
elevation: Math.round(sunElevation(lt) * 100) / 100
|
|
501
|
+
elevation: Math.round(sunElevation(lt) * 100) / 100,
|
|
502
|
+
rising: lt.hourFrac < 27 / 2 || lt.hourFrac >= 25.5
|
|
592
503
|
};
|
|
593
504
|
}
|
|
594
505
|
/** The energy sensor entity ids the simulation drives. */
|
|
@@ -763,6 +674,9 @@ function demoEnergyPreferences() {
|
|
|
763
674
|
}
|
|
764
675
|
//#endregion
|
|
765
676
|
//#region src/demo/demo-data.ts
|
|
677
|
+
function demoAssetUrl(path) {
|
|
678
|
+
return globalThis.location ? new URL(path, globalThis.location.origin).href : path;
|
|
679
|
+
}
|
|
766
680
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
767
681
|
let contextCounter = 0;
|
|
768
682
|
function ctx() {
|
|
@@ -977,6 +891,18 @@ function makeWeatherEntity(w) {
|
|
|
977
891
|
condition: "rainy",
|
|
978
892
|
temperature: w.temp - 2,
|
|
979
893
|
templow: w.low - 2
|
|
894
|
+
},
|
|
895
|
+
{
|
|
896
|
+
datetime: day(5),
|
|
897
|
+
condition: "cloudy",
|
|
898
|
+
temperature: w.temp - 1,
|
|
899
|
+
templow: w.low - 2
|
|
900
|
+
},
|
|
901
|
+
{
|
|
902
|
+
datetime: day(6),
|
|
903
|
+
condition: "partlycloudy",
|
|
904
|
+
temperature: w.temp + 1,
|
|
905
|
+
templow: w.low
|
|
980
906
|
}
|
|
981
907
|
]
|
|
982
908
|
});
|
|
@@ -1214,6 +1140,24 @@ function createDemoFixtures() {
|
|
|
1214
1140
|
unit_of_measurement: "%",
|
|
1215
1141
|
state_class: "measurement"
|
|
1216
1142
|
}), { unit_of_measurement: "%" });
|
|
1143
|
+
for (const [zone, co2, fossil] of [[
|
|
1144
|
+
"de",
|
|
1145
|
+
"384",
|
|
1146
|
+
"52"
|
|
1147
|
+
], [
|
|
1148
|
+
"pl",
|
|
1149
|
+
"692",
|
|
1150
|
+
"81"
|
|
1151
|
+
]]) {
|
|
1152
|
+
add(makeEntity(`sensor.electricity_maps_co2_intensity_${zone}`, co2, {
|
|
1153
|
+
unit_of_measurement: "gCO2eq/kWh",
|
|
1154
|
+
state_class: "measurement"
|
|
1155
|
+
}), { unit_of_measurement: "gCO2eq/kWh" });
|
|
1156
|
+
add(makeEntity(`sensor.electricity_maps_fossil_fuel_percentage_${zone}`, fossil, {
|
|
1157
|
+
unit_of_measurement: "%",
|
|
1158
|
+
state_class: "measurement"
|
|
1159
|
+
}), { unit_of_measurement: "%" });
|
|
1160
|
+
}
|
|
1217
1161
|
add(makeEntity("sensor.nordpool_current_price", "0.18", {
|
|
1218
1162
|
unit_of_measurement: "EUR/kWh",
|
|
1219
1163
|
state_class: "measurement"
|
|
@@ -1383,6 +1327,9 @@ function createDemoFixtures() {
|
|
|
1383
1327
|
area_id: "entry",
|
|
1384
1328
|
supported_features: 1
|
|
1385
1329
|
});
|
|
1330
|
+
add(makeEntity("lock.side_gate", "unlocked", {}), { supported_features: 1 });
|
|
1331
|
+
add(makeEntity("binary_sensor.patio_door", "on", { device_class: "door" }), { device_class: "door" });
|
|
1332
|
+
add(makeEntity("binary_sensor.motion_kitchen", "on", { device_class: "motion" }), { device_class: "motion" });
|
|
1386
1333
|
add(makeEntity("switch.coffee_machine", "off", { device_class: "outlet" }), {
|
|
1387
1334
|
area_id: "kitchen",
|
|
1388
1335
|
device_class: "outlet"
|
|
@@ -1395,17 +1342,18 @@ function createDemoFixtures() {
|
|
|
1395
1342
|
media_title: "Lo-fi Beats",
|
|
1396
1343
|
media_artist: "Chill Station",
|
|
1397
1344
|
media_content_type: "music",
|
|
1345
|
+
entity_picture: DEMO_ALBUM_COVER,
|
|
1398
1346
|
volume_level: .4,
|
|
1399
1347
|
is_volume_muted: false,
|
|
1400
|
-
supported_features:
|
|
1348
|
+
supported_features: 152509
|
|
1401
1349
|
}), {
|
|
1402
1350
|
device_id: "speaker_lr",
|
|
1403
1351
|
area_id: "living_room",
|
|
1404
|
-
supported_features:
|
|
1352
|
+
supported_features: 152509
|
|
1405
1353
|
});
|
|
1406
1354
|
for (const w of WEATHER_FIXTURES) add(makeWeatherEntity(w), { platform: "met" });
|
|
1407
1355
|
add(makeEntity("camera.front_door_camera", "streaming", {
|
|
1408
|
-
entity_picture: "/
|
|
1356
|
+
entity_picture: demoAssetUrl("/demo/camera/front-door.webp"),
|
|
1409
1357
|
frontend_stream_type: "hls",
|
|
1410
1358
|
access_token: "demo-token"
|
|
1411
1359
|
}), {
|
|
@@ -1458,7 +1406,8 @@ function createDemoFixtures() {
|
|
|
1458
1406
|
friendly_name: "Sun",
|
|
1459
1407
|
elevation: sun.elevation,
|
|
1460
1408
|
next_rising: sun.nextRising,
|
|
1461
|
-
next_setting: sun.nextSetting
|
|
1409
|
+
next_setting: sun.nextSetting,
|
|
1410
|
+
rising: sun.rising
|
|
1462
1411
|
}), {});
|
|
1463
1412
|
for (const s of [
|
|
1464
1413
|
{
|
|
@@ -1634,6 +1583,7 @@ function tickEnergy() {
|
|
|
1634
1583
|
sunEntity.attributes.elevation = sun.elevation;
|
|
1635
1584
|
sunEntity.attributes.next_rising = sun.nextRising;
|
|
1636
1585
|
sunEntity.attributes.next_setting = sun.nextSetting;
|
|
1586
|
+
sunEntity.attributes.rising = sun.rising;
|
|
1637
1587
|
}
|
|
1638
1588
|
}));
|
|
1639
1589
|
}
|
|
@@ -1763,6 +1713,368 @@ function applyTurnOff(e, domain) {
|
|
|
1763
1713
|
} else e.state = "off";
|
|
1764
1714
|
}
|
|
1765
1715
|
//#endregion
|
|
1716
|
+
//#region src/weather/fetch.ts
|
|
1717
|
+
/**
|
|
1718
|
+
* Weather Forecast Fetching
|
|
1719
|
+
*
|
|
1720
|
+
* @packageDocumentation
|
|
1721
|
+
*/
|
|
1722
|
+
const DEMO_RAIN_CHANCE = {
|
|
1723
|
+
sunny: 0,
|
|
1724
|
+
"clear-night": 0,
|
|
1725
|
+
partlycloudy: 10,
|
|
1726
|
+
cloudy: 20,
|
|
1727
|
+
windy: 10,
|
|
1728
|
+
fog: 10,
|
|
1729
|
+
rainy: 80,
|
|
1730
|
+
pouring: 95,
|
|
1731
|
+
lightning: 60,
|
|
1732
|
+
"lightning-rainy": 85,
|
|
1733
|
+
snowy: 70,
|
|
1734
|
+
"snowy-rainy": 75,
|
|
1735
|
+
hail: 70,
|
|
1736
|
+
exceptional: 40
|
|
1737
|
+
};
|
|
1738
|
+
/**
|
|
1739
|
+
* In demo mode, derive forecasts from the entity's `forecast` attribute.
|
|
1740
|
+
* Hourly and daily are synthesised from the same source (daily entries
|
|
1741
|
+
* expanded into 24 hourly steps).
|
|
1742
|
+
*/
|
|
1743
|
+
function buildDemoForecast(entityId, type) {
|
|
1744
|
+
const entity = state.entities[entityId];
|
|
1745
|
+
const raw = entity?.attributes?.forecast ?? [];
|
|
1746
|
+
if (raw.length === 0) return [];
|
|
1747
|
+
if (type === "daily" || type === "twice_daily") return raw.map((d) => {
|
|
1748
|
+
const chance = DEMO_RAIN_CHANCE[d.condition ?? ""] ?? 10;
|
|
1749
|
+
const templow = d.templow;
|
|
1750
|
+
return {
|
|
1751
|
+
...d,
|
|
1752
|
+
temp_high: d.temp_high ?? d.temperature,
|
|
1753
|
+
temp_low: d.temp_low ?? templow,
|
|
1754
|
+
precipitation_probability: d.precipitation_probability ?? chance
|
|
1755
|
+
};
|
|
1756
|
+
});
|
|
1757
|
+
const start = /* @__PURE__ */ new Date();
|
|
1758
|
+
start.setMinutes(0, 0, 0);
|
|
1759
|
+
const today = raw[0];
|
|
1760
|
+
const tomorrow = raw[1] ?? today;
|
|
1761
|
+
const tHi = today?.temperature ?? 18;
|
|
1762
|
+
const tLo = today?.templow ?? tHi - 5;
|
|
1763
|
+
const tNext = tomorrow?.temperature ?? tHi;
|
|
1764
|
+
const out = [];
|
|
1765
|
+
for (let i = 0; i < 24; i++) {
|
|
1766
|
+
const dt = new Date(start.getTime() + i * 60 * 60 * 1e3);
|
|
1767
|
+
const hour = dt.getHours();
|
|
1768
|
+
const dayMix = i < 12 ? 0 : (i - 12) / 12;
|
|
1769
|
+
const baseHi = tHi * (1 - dayMix) + tNext * dayMix;
|
|
1770
|
+
const phase = (hour - 6) / 24 * Math.PI * 2;
|
|
1771
|
+
const swing = (baseHi - tLo) / 2;
|
|
1772
|
+
const mid = (baseHi + tLo) / 2;
|
|
1773
|
+
const source = i < 8 ? today : tomorrow;
|
|
1774
|
+
const night = hour < 6 || hour >= 21;
|
|
1775
|
+
const condition = source?.condition ?? entity?.state ?? "cloudy";
|
|
1776
|
+
out.push({
|
|
1777
|
+
datetime: dt.toISOString(),
|
|
1778
|
+
temperature: Math.round((mid + Math.sin(phase) * swing) * 10) / 10,
|
|
1779
|
+
condition: night && condition === "sunny" ? "clear-night" : condition,
|
|
1780
|
+
precipitation_probability: DEMO_RAIN_CHANCE[condition] ?? 10
|
|
1781
|
+
});
|
|
1782
|
+
}
|
|
1783
|
+
return out;
|
|
1784
|
+
}
|
|
1785
|
+
/**
|
|
1786
|
+
* Fetch forecast for a single entity and type
|
|
1787
|
+
*/
|
|
1788
|
+
async function fetchForecast(entityId, type) {
|
|
1789
|
+
if (isDemoMode()) return buildDemoForecast(entityId, type);
|
|
1790
|
+
const conn = privilegedConn();
|
|
1791
|
+
if (!conn) throw new Error("Not connected");
|
|
1792
|
+
return new Promise((resolve, reject) => {
|
|
1793
|
+
let resolved = false;
|
|
1794
|
+
let unsubscribe = null;
|
|
1795
|
+
const timeout = setTimeout(() => {
|
|
1796
|
+
if (!resolved && unsubscribe) {
|
|
1797
|
+
resolved = true;
|
|
1798
|
+
unsubscribe().then(() => reject(/* @__PURE__ */ new Error("Forecast subscription timeout"))).catch(() => reject(/* @__PURE__ */ new Error("Forecast subscription timeout")));
|
|
1799
|
+
}
|
|
1800
|
+
}, 1e4);
|
|
1801
|
+
conn.subscribeMessage((event) => {
|
|
1802
|
+
if (resolved) return;
|
|
1803
|
+
if (event && typeof event === "object" && "forecast" in event) {
|
|
1804
|
+
resolved = true;
|
|
1805
|
+
clearTimeout(timeout);
|
|
1806
|
+
const forecast = event.forecast;
|
|
1807
|
+
const forecastArray = Array.isArray(forecast) ? forecast : [];
|
|
1808
|
+
if (unsubscribe) unsubscribe().then(() => resolve(forecastArray)).catch(() => resolve(forecastArray));
|
|
1809
|
+
else resolve(forecastArray);
|
|
1810
|
+
}
|
|
1811
|
+
}, {
|
|
1812
|
+
type: "weather/subscribe_forecast",
|
|
1813
|
+
entity_id: entityId,
|
|
1814
|
+
forecast_type: type === "twice_daily" ? "twice_daily" : type
|
|
1815
|
+
}).then((unsubFn) => {
|
|
1816
|
+
unsubscribe = unsubFn;
|
|
1817
|
+
}).catch((error) => {
|
|
1818
|
+
resolved = true;
|
|
1819
|
+
clearTimeout(timeout);
|
|
1820
|
+
reject(error);
|
|
1821
|
+
});
|
|
1822
|
+
});
|
|
1823
|
+
}
|
|
1824
|
+
/**
|
|
1825
|
+
* Fetch forecasts for a single entity and multiple types
|
|
1826
|
+
*/
|
|
1827
|
+
async function fetchForecasts(entityId, types) {
|
|
1828
|
+
const forecasts = {};
|
|
1829
|
+
const loading = {};
|
|
1830
|
+
const errors = {};
|
|
1831
|
+
const lastFetched = {};
|
|
1832
|
+
const fetchPromises = types.map(async (type) => {
|
|
1833
|
+
loading[type] = true;
|
|
1834
|
+
errors[type] = null;
|
|
1835
|
+
try {
|
|
1836
|
+
const forecast = await fetchForecast(entityId, type);
|
|
1837
|
+
forecasts[type] = forecast;
|
|
1838
|
+
lastFetched[type] = Date.now();
|
|
1839
|
+
loading[type] = false;
|
|
1840
|
+
} catch (error) {
|
|
1841
|
+
errors[type] = error instanceof Error ? error : new Error(String(error));
|
|
1842
|
+
forecasts[type] = [];
|
|
1843
|
+
loading[type] = false;
|
|
1844
|
+
lastFetched[type] = null;
|
|
1845
|
+
}
|
|
1846
|
+
});
|
|
1847
|
+
await Promise.all(fetchPromises);
|
|
1848
|
+
return {
|
|
1849
|
+
entityId,
|
|
1850
|
+
forecasts,
|
|
1851
|
+
loading,
|
|
1852
|
+
errors,
|
|
1853
|
+
lastFetched
|
|
1854
|
+
};
|
|
1855
|
+
}
|
|
1856
|
+
/**
|
|
1857
|
+
* Fetch forecast data with error handling
|
|
1858
|
+
*/
|
|
1859
|
+
async function fetchForecastData(entityId, type) {
|
|
1860
|
+
try {
|
|
1861
|
+
return {
|
|
1862
|
+
entityId,
|
|
1863
|
+
type,
|
|
1864
|
+
forecast: await fetchForecast(entityId, type),
|
|
1865
|
+
loading: false,
|
|
1866
|
+
error: null,
|
|
1867
|
+
lastFetched: Date.now()
|
|
1868
|
+
};
|
|
1869
|
+
} catch (error) {
|
|
1870
|
+
return {
|
|
1871
|
+
entityId,
|
|
1872
|
+
type,
|
|
1873
|
+
forecast: [],
|
|
1874
|
+
loading: false,
|
|
1875
|
+
error: error instanceof Error ? error : new Error(String(error)),
|
|
1876
|
+
lastFetched: null
|
|
1877
|
+
};
|
|
1878
|
+
}
|
|
1879
|
+
}
|
|
1880
|
+
//#endregion
|
|
1881
|
+
//#region src/weather/track.ts
|
|
1882
|
+
/**
|
|
1883
|
+
* Ref-counted live forecasts, the weather analog of `calendar/track`. Each
|
|
1884
|
+
* tracked (entity, type) holds one `weather/subscribe_forecast`; Home Assistant
|
|
1885
|
+
* pushes the current forecast, then every new one. Data lands in
|
|
1886
|
+
* `state.forecasts`, the same place `getForecasts` writes.
|
|
1887
|
+
*/
|
|
1888
|
+
/** Debounce for untrack, so dashboard switches don't churn subscriptions. */
|
|
1889
|
+
const UNTRACK_FLUSH_DELAY_MS = 5e3;
|
|
1890
|
+
const counts = /* @__PURE__ */ new Map();
|
|
1891
|
+
const active = /* @__PURE__ */ new Map();
|
|
1892
|
+
const chains = /* @__PURE__ */ new Map();
|
|
1893
|
+
let untrackTimer = null;
|
|
1894
|
+
const keyOf = (entityId, type) => `${entityId}|${type}`;
|
|
1895
|
+
function parse(key) {
|
|
1896
|
+
const i = key.lastIndexOf("|");
|
|
1897
|
+
return [key.slice(0, i), key.slice(i + 1)];
|
|
1898
|
+
}
|
|
1899
|
+
/**
|
|
1900
|
+
* Keep a forecast live while tracked. Returns the untrack function. Safe while
|
|
1901
|
+
* disconnected: the subscription starts on the next `forceResubscribeForecasts()`.
|
|
1902
|
+
*/
|
|
1903
|
+
function trackForecast(entityId, type) {
|
|
1904
|
+
const key = keyOf(entityId, type);
|
|
1905
|
+
counts.set(key, (counts.get(key) ?? 0) + 1);
|
|
1906
|
+
queueMicrotask(() => {
|
|
1907
|
+
reconcile$1(key);
|
|
1908
|
+
});
|
|
1909
|
+
let untracked = false;
|
|
1910
|
+
return () => {
|
|
1911
|
+
if (untracked) return;
|
|
1912
|
+
untracked = true;
|
|
1913
|
+
const n = (counts.get(key) ?? 1) - 1;
|
|
1914
|
+
if (n > 0) counts.set(key, n);
|
|
1915
|
+
else counts.delete(key);
|
|
1916
|
+
if (untrackTimer) return;
|
|
1917
|
+
untrackTimer = setTimeout(() => {
|
|
1918
|
+
untrackTimer = null;
|
|
1919
|
+
flushAll();
|
|
1920
|
+
}, UNTRACK_FLUSH_DELAY_MS);
|
|
1921
|
+
};
|
|
1922
|
+
}
|
|
1923
|
+
async function flushAll() {
|
|
1924
|
+
const keys = /* @__PURE__ */ new Set([...counts.keys(), ...active.keys()]);
|
|
1925
|
+
await Promise.all([...keys].map(reconcile$1));
|
|
1926
|
+
}
|
|
1927
|
+
function reconcile$1(key) {
|
|
1928
|
+
const next = (chains.get(key) ?? Promise.resolve()).then(() => runReconcile(key));
|
|
1929
|
+
chains.set(key, next.catch(() => {}));
|
|
1930
|
+
return next;
|
|
1931
|
+
}
|
|
1932
|
+
function write(entityId, type, forecast, error) {
|
|
1933
|
+
setState(produce((s) => {
|
|
1934
|
+
const rec = s.forecasts[entityId] ??= {
|
|
1935
|
+
entityId,
|
|
1936
|
+
forecasts: {},
|
|
1937
|
+
loading: {},
|
|
1938
|
+
errors: {},
|
|
1939
|
+
lastFetched: {}
|
|
1940
|
+
};
|
|
1941
|
+
if (forecast) {
|
|
1942
|
+
rec.forecasts[type] = forecast;
|
|
1943
|
+
rec.lastFetched[type] = Date.now();
|
|
1944
|
+
}
|
|
1945
|
+
rec.loading[type] = false;
|
|
1946
|
+
rec.errors[type] = error;
|
|
1947
|
+
}));
|
|
1948
|
+
}
|
|
1949
|
+
async function runReconcile(key) {
|
|
1950
|
+
const [entityId, type] = parse(key);
|
|
1951
|
+
const current = active.get(key);
|
|
1952
|
+
if (!counts.has(key)) {
|
|
1953
|
+
active.delete(key);
|
|
1954
|
+
chains.delete(key);
|
|
1955
|
+
try {
|
|
1956
|
+
await current?.unsub?.();
|
|
1957
|
+
} catch {}
|
|
1958
|
+
return;
|
|
1959
|
+
}
|
|
1960
|
+
if (current?.unsub) return;
|
|
1961
|
+
if (isDemoMode()) {
|
|
1962
|
+
active.set(key, { unsub: () => {} });
|
|
1963
|
+
write(entityId, type, buildDemoForecast(entityId, type), null);
|
|
1964
|
+
return;
|
|
1965
|
+
}
|
|
1966
|
+
const conn = privilegedConn();
|
|
1967
|
+
if (!conn) return;
|
|
1968
|
+
const entry = { unsub: null };
|
|
1969
|
+
active.set(key, entry);
|
|
1970
|
+
try {
|
|
1971
|
+
entry.unsub = await conn.subscribeMessage((msg) => write(entityId, type, Array.isArray(msg?.forecast) ? msg.forecast : [], null), {
|
|
1972
|
+
type: "weather/subscribe_forecast",
|
|
1973
|
+
entity_id: entityId,
|
|
1974
|
+
forecast_type: type
|
|
1975
|
+
});
|
|
1976
|
+
} catch (err) {
|
|
1977
|
+
active.delete(key);
|
|
1978
|
+
write(entityId, type, null, err instanceof Error ? err : new Error(String(err)));
|
|
1979
|
+
}
|
|
1980
|
+
}
|
|
1981
|
+
//#endregion
|
|
1982
|
+
//#region src/connection/subscription-manager.ts
|
|
1983
|
+
if (globalThis.__GH_SYNC_LAYER_SUB_MGR__) console.error("[@glasshome/sync-layer] DUPLICATE subscription-manager module instantiated. registerEntity() and the connection setter will hit different singletons → subscribe_entities will never be sent. Cause: relative import of subscription-manager from a separate entry point.");
|
|
1984
|
+
globalThis.__GH_SYNC_LAYER_SUB_MGR__ = true;
|
|
1985
|
+
/** Ref counts — how many consumers need each entity */
|
|
1986
|
+
const refCounts = /* @__PURE__ */ new Map();
|
|
1987
|
+
/** Current HA subscription unsub function */
|
|
1988
|
+
let currentEntityUnsub = null;
|
|
1989
|
+
/** Snapshot of entity IDs in the current HA subscription */
|
|
1990
|
+
let subscribedIds = /* @__PURE__ */ new Set();
|
|
1991
|
+
/** Connection reference for resubscribing */
|
|
1992
|
+
let conn = null;
|
|
1993
|
+
/** Pending flush timer (microtask for register, timeout for unregister) */
|
|
1994
|
+
let flushScheduled = false;
|
|
1995
|
+
/** Delayed unregister timeout */
|
|
1996
|
+
let unregisterTimeout = null;
|
|
1997
|
+
/** Callback invoked on resubscribe — set by subscriptions.ts */
|
|
1998
|
+
let onResubscribe = null;
|
|
1999
|
+
/** Delay before unregistered entities are actually removed from subscription */
|
|
2000
|
+
const UNREGISTER_DELAY_MS = 5e3;
|
|
2001
|
+
/** Guard against concurrent flush() calls */
|
|
2002
|
+
let flushInFlight = false;
|
|
2003
|
+
/**
|
|
2004
|
+
* Register an entity for live updates. Returns an unregister function.
|
|
2005
|
+
* Call from useEntity/useEntities hooks via onCleanup.
|
|
2006
|
+
*/
|
|
2007
|
+
function registerEntity(entityId) {
|
|
2008
|
+
refCounts.set(entityId, (refCounts.get(entityId) ?? 0) + 1);
|
|
2009
|
+
scheduleFlush();
|
|
2010
|
+
return () => unregisterEntity(entityId);
|
|
2011
|
+
}
|
|
2012
|
+
/**
|
|
2013
|
+
* Set the connection reference used for resubscribing.
|
|
2014
|
+
*/
|
|
2015
|
+
function setManagerConnection(connection) {
|
|
2016
|
+
conn = connection;
|
|
2017
|
+
if (refCounts.size > 0) scheduleFlush();
|
|
2018
|
+
}
|
|
2019
|
+
/**
|
|
2020
|
+
* Set the resubscribe callback. Called by subscriptions.ts to wire
|
|
2021
|
+
* the manager into the actual WS subscription lifecycle.
|
|
2022
|
+
*/
|
|
2023
|
+
function setResubscribeHandler(handler) {
|
|
2024
|
+
onResubscribe = handler;
|
|
2025
|
+
}
|
|
2026
|
+
/**
|
|
2027
|
+
* Force a resubscribe with the current active set.
|
|
2028
|
+
* Used on reconnect.
|
|
2029
|
+
*/
|
|
2030
|
+
async function forceResubscribe() {
|
|
2031
|
+
await flush();
|
|
2032
|
+
}
|
|
2033
|
+
function unregisterEntity(entityId) {
|
|
2034
|
+
const count = (refCounts.get(entityId) ?? 1) - 1;
|
|
2035
|
+
if (count <= 0) refCounts.delete(entityId);
|
|
2036
|
+
else refCounts.set(entityId, count);
|
|
2037
|
+
scheduleDelayedFlush();
|
|
2038
|
+
}
|
|
2039
|
+
function scheduleFlush() {
|
|
2040
|
+
if (flushScheduled) return;
|
|
2041
|
+
flushScheduled = true;
|
|
2042
|
+
queueMicrotask(() => {
|
|
2043
|
+
flushScheduled = false;
|
|
2044
|
+
flush();
|
|
2045
|
+
});
|
|
2046
|
+
}
|
|
2047
|
+
function scheduleDelayedFlush() {
|
|
2048
|
+
if (unregisterTimeout) return;
|
|
2049
|
+
unregisterTimeout = setTimeout(() => {
|
|
2050
|
+
unregisterTimeout = null;
|
|
2051
|
+
flush();
|
|
2052
|
+
}, UNREGISTER_DELAY_MS);
|
|
2053
|
+
}
|
|
2054
|
+
async function flush() {
|
|
2055
|
+
if (!conn || !onResubscribe) return;
|
|
2056
|
+
if (flushInFlight) return;
|
|
2057
|
+
flushInFlight = true;
|
|
2058
|
+
try {
|
|
2059
|
+
const newIds = new Set(refCounts.keys());
|
|
2060
|
+
if (setsEqual(newIds, subscribedIds)) return;
|
|
2061
|
+
const oldUnsub = currentEntityUnsub;
|
|
2062
|
+
const entityIds = [...newIds];
|
|
2063
|
+
currentEntityUnsub = await onResubscribe(conn, entityIds);
|
|
2064
|
+
subscribedIds = newIds;
|
|
2065
|
+
if (oldUnsub) try {
|
|
2066
|
+
await oldUnsub();
|
|
2067
|
+
} catch {}
|
|
2068
|
+
} finally {
|
|
2069
|
+
flushInFlight = false;
|
|
2070
|
+
}
|
|
2071
|
+
}
|
|
2072
|
+
function setsEqual(a, b) {
|
|
2073
|
+
if (a.size !== b.size) return false;
|
|
2074
|
+
for (const id of a) if (!b.has(id)) return false;
|
|
2075
|
+
return true;
|
|
2076
|
+
}
|
|
2077
|
+
//#endregion
|
|
1766
2078
|
//#region src/commands/service.ts
|
|
1767
2079
|
/**
|
|
1768
2080
|
* Call a Home Assistant service
|
|
@@ -1861,6 +2173,68 @@ async function batchEntityUpdates(updates) {
|
|
|
1861
2173
|
return Promise.all(updates.map((u) => updateEntity(u.entityId, u.updates)));
|
|
1862
2174
|
}
|
|
1863
2175
|
//#endregion
|
|
2176
|
+
//#region src/demo/sensor-history.ts
|
|
2177
|
+
const DAY_MS = 864e5;
|
|
2178
|
+
function seedOf(entityId) {
|
|
2179
|
+
let h = 0;
|
|
2180
|
+
for (let i = 0; i < entityId.length; i++) h = h * 31 + entityId.charCodeAt(i) | 0;
|
|
2181
|
+
return Math.abs(h) % 1e3 / 1e3;
|
|
2182
|
+
}
|
|
2183
|
+
/**
|
|
2184
|
+
* A slow daily swing plus a faster ripple, pinned so the last point is the reading
|
|
2185
|
+
* the demo shows now. Deterministic per entity, so previews repeat.
|
|
2186
|
+
*/
|
|
2187
|
+
function synthesizeSensorHistory(entityId, current, startMs, endMs, stepMs = 9e5) {
|
|
2188
|
+
const amplitude = Math.max(Math.abs(current) * .08, .6);
|
|
2189
|
+
const phase = seedOf(entityId) * 2 * Math.PI;
|
|
2190
|
+
const wave = (t) => Math.sin(2 * Math.PI * t / DAY_MS + phase) + .15 * Math.sin(2 * Math.PI * t * 3 / DAY_MS + 2 * phase);
|
|
2191
|
+
const decimals = Number.isInteger(current) ? 1 : 2;
|
|
2192
|
+
const points = [];
|
|
2193
|
+
for (let t = startMs; t <= endMs; t += stepMs) {
|
|
2194
|
+
const value = current + amplitude * (wave(t) - wave(endMs));
|
|
2195
|
+
points.push({
|
|
2196
|
+
lu: Math.round(t / 1e3),
|
|
2197
|
+
s: value.toFixed(decimals)
|
|
2198
|
+
});
|
|
2199
|
+
}
|
|
2200
|
+
const last = points.at(-1);
|
|
2201
|
+
if (last) last.s = String(current);
|
|
2202
|
+
return points;
|
|
2203
|
+
}
|
|
2204
|
+
function currentReading(id) {
|
|
2205
|
+
if (!id.startsWith("sensor.")) return void 0;
|
|
2206
|
+
const current = Number(state.entities[id]?.state);
|
|
2207
|
+
return Number.isFinite(current) ? current : void 0;
|
|
2208
|
+
}
|
|
2209
|
+
/** History for a demo sensor with a numeric reading; undefined for anything else. */
|
|
2210
|
+
function numericSensorHistory(id, startMs, endMs) {
|
|
2211
|
+
const current = currentReading(id);
|
|
2212
|
+
return current === void 0 ? void 0 : synthesizeSensorHistory(id, current, startMs, endMs);
|
|
2213
|
+
}
|
|
2214
|
+
/** Hour or day buckets of the same curve, shaped like recorder statistics. */
|
|
2215
|
+
function numericSensorStatistics(id, startMs, endMs, period) {
|
|
2216
|
+
const points = numericSensorHistory(id, startMs, endMs);
|
|
2217
|
+
if (!points) return [];
|
|
2218
|
+
const span = period === "hour" ? 36e5 : DAY_MS;
|
|
2219
|
+
const buckets = [];
|
|
2220
|
+
for (let start = startMs; start < endMs; start += span) {
|
|
2221
|
+
const end = start + span;
|
|
2222
|
+
const values = points.filter((p) => p.lu * 1e3 >= start && p.lu * 1e3 < end).map((p) => Number(p.s));
|
|
2223
|
+
if (values.length === 0) continue;
|
|
2224
|
+
const mean = values.reduce((a, b) => a + b, 0) / values.length;
|
|
2225
|
+
buckets.push({
|
|
2226
|
+
start,
|
|
2227
|
+
end,
|
|
2228
|
+
mean,
|
|
2229
|
+
min: Math.min(...values),
|
|
2230
|
+
max: Math.max(...values),
|
|
2231
|
+
sum: 0,
|
|
2232
|
+
change: 0
|
|
2233
|
+
});
|
|
2234
|
+
}
|
|
2235
|
+
return buckets;
|
|
2236
|
+
}
|
|
2237
|
+
//#endregion
|
|
1864
2238
|
//#region src/entities/views.ts
|
|
1865
2239
|
/**
|
|
1866
2240
|
* Get icon for an entity from registry or attributes
|
|
@@ -2142,7 +2516,7 @@ async function fetchStatisticsDuringPeriod(connection, statisticIds, options) {
|
|
|
2142
2516
|
const result = {};
|
|
2143
2517
|
const statPeriod = period === "hour" || period === "day" ? period : "day";
|
|
2144
2518
|
const endMs = (endTime ?? /* @__PURE__ */ new Date()).getTime();
|
|
2145
|
-
for (const id of statisticIds) result[id] = isEnergyEntity(id) ? synthesizeEnergyStatistics(id, startTime.getTime(), endMs, statPeriod) :
|
|
2519
|
+
for (const id of statisticIds) result[id] = isEnergyEntity(id) ? synthesizeEnergyStatistics(id, startTime.getTime(), endMs, statPeriod) : numericSensorStatistics(id, startTime.getTime(), endMs, statPeriod);
|
|
2146
2520
|
return result;
|
|
2147
2521
|
}
|
|
2148
2522
|
const params = {
|
|
@@ -2160,4 +2534,4 @@ async function fetchStatisticsDuringPeriod(connection, statisticIds, options) {
|
|
|
2160
2534
|
return result;
|
|
2161
2535
|
}
|
|
2162
2536
|
//#endregion
|
|
2163
|
-
export {
|
|
2537
|
+
export { isDemoMode as A, extractDomain as B, setManagerConnection as C, fetchForecastData as D, fetchForecast as E, createDemoFixtures as F, setState as G, trackCalendarEvents as H, demoAssetUrl as I, state as K, demoEnergyPreferences as L, startDemoEnergyTicker as M, stopDemoEnergyTicker as N, fetchForecasts as O, unloadDemoData as P, isEnergyEntity as R, registerEntity as S, trackForecast as T, privilegedConn as U, forceResubscribeCalendars as V, setPrivilegedConn as W, toggle as _, getAreaView as a, updateEntity as b, entityViewEquals as c, getEntityViews as d, numericSensorHistory as f, sendCommand as g, callService as h, buildDeviceView as i, loadDemoData as j, applyDemoServiceCall as k, getAllEntityViews as l, batchServiceCalls as m, normalizeStatisticTime as n, getAreaViews as o, batchEntityUpdates as p, buildAreaView as r, buildEntityView as s, fetchStatisticsDuringPeriod as t, getEntityView as u, turnOff as v, setResubscribeHandler as w, forceResubscribe as x, turnOn as y, synthesizeEnergyHistory as z };
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import { t as runHaBridgeWorker } from "../worker-main-
|
|
1
|
+
import { t as runHaBridgeWorker } from "../worker-main-DamaVb7B.js";
|
|
2
2
|
export { runHaBridgeWorker };
|
|
@@ -214,7 +214,14 @@ function runHaBridgeWorker(scope) {
|
|
|
214
214
|
stopHeartbeat();
|
|
215
215
|
heartbeat = setInterval(() => {
|
|
216
216
|
const timer = new Promise((_, reject) => setTimeout(() => reject(/* @__PURE__ */ new Error("ping timeout")), HEARTBEAT_TIMEOUT_MS));
|
|
217
|
-
Promise.race([c.ping(), timer]).catch(() =>
|
|
217
|
+
Promise.race([c.ping(), timer]).catch(() => {
|
|
218
|
+
post({
|
|
219
|
+
k: "conn",
|
|
220
|
+
state: "reconnecting",
|
|
221
|
+
reason: "ping_timeout"
|
|
222
|
+
});
|
|
223
|
+
c.reconnect();
|
|
224
|
+
});
|
|
218
225
|
}, HEARTBEAT_INTERVAL_MS);
|
|
219
226
|
}
|
|
220
227
|
function stopHeartbeat() {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@glasshome/sync-layer",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.9.0",
|
|
4
4
|
"description": "Type-safe, reactive state synchronization layer for Home Assistant",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -51,11 +51,11 @@
|
|
|
51
51
|
},
|
|
52
52
|
"dependencies": {
|
|
53
53
|
"home-assistant-js-websocket": "^9.6.0",
|
|
54
|
-
"@glasshome/widget-contract": "0.
|
|
54
|
+
"@glasshome/widget-contract": "0.3.0"
|
|
55
55
|
},
|
|
56
56
|
"devDependencies": {
|
|
57
57
|
"@glasshome/ha-types": "0.1.2",
|
|
58
|
-
"@types/bun": "
|
|
58
|
+
"@types/bun": "^1.4.0",
|
|
59
59
|
"solid-js": "^1.9.0",
|
|
60
60
|
"tsdown": "^0.22.14",
|
|
61
61
|
"typescript": "^7.0.2"
|