@glasshome/sync-layer 0.1.13 → 0.1.15

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.
@@ -0,0 +1,56 @@
1
+ /**
2
+ * Energy Preferences Fetching
3
+ *
4
+ * Reads HA's energy dashboard config via `energy/get_prefs`.
5
+ *
6
+ * @packageDocumentation
7
+ */
8
+ import type { SyncLayerConnection } from "../connection/types";
9
+ /**
10
+ * An energy source entry. Only the fields the widgets read are typed; the
11
+ * rest is passthrough. HA emits one of several shapes keyed by `type`:
12
+ * - grid: `flow_from[].stat_energy_from`, `flow_to[].stat_energy_to`
13
+ * - solar: `stat_energy_from`
14
+ * - battery: `stat_energy_from`, `stat_energy_to`
15
+ */
16
+ export interface EnergySource {
17
+ type: "grid" | "solar" | "battery" | "gas" | "water" | string;
18
+ stat_energy_from?: string;
19
+ stat_energy_to?: string;
20
+ flow_from?: Array<{
21
+ stat_energy_from: string;
22
+ [key: string]: unknown;
23
+ }>;
24
+ flow_to?: Array<{
25
+ stat_energy_to: string;
26
+ [key: string]: unknown;
27
+ }>;
28
+ [key: string]: unknown;
29
+ }
30
+ /** A device tracked on the energy dashboard. */
31
+ export interface EnergyDeviceConsumption {
32
+ stat_consumption: string;
33
+ name?: string;
34
+ [key: string]: unknown;
35
+ }
36
+ /**
37
+ * HA energy dashboard preferences.
38
+ *
39
+ * Based on HA frontend: EnergyPreferences.
40
+ */
41
+ export interface EnergyPreferences {
42
+ energy_sources: EnergySource[];
43
+ device_consumption: EnergyDeviceConsumption[];
44
+ [key: string]: unknown;
45
+ }
46
+ /** Connection surface used by energy prefs fetching. */
47
+ type EnergyConnection = Pick<SyncLayerConnection, "sendMessagePromise">;
48
+ /**
49
+ * Fetch the HA energy dashboard preferences.
50
+ *
51
+ * Returns `null` when no energy dashboard is configured (HA raises an error
52
+ * for `energy/get_prefs` in that case); callers treat `null` as "not
53
+ * configured".
54
+ */
55
+ export declare function fetchEnergyPreferences(connection: EnergyConnection): Promise<EnergyPreferences | null>;
56
+ export {};
@@ -7,6 +7,8 @@ export interface HistoryPoint {
7
7
  lastUpdated: number;
8
8
  lastChanged?: number;
9
9
  }
10
+ /** Hard cap on retained points per entity timeline. */
11
+ export declare const MAX_HISTORY_POINTS = 5760;
10
12
  /** Check if an entity has active history tracking. */
11
13
  export declare function isHistoryTracked(entityId: EntityId): boolean;
12
14
  /** Start tracking history for an entity. Fetches initial backfill. */
@@ -0,0 +1,55 @@
1
+ /**
2
+ * Long-term Statistics Fetching
3
+ *
4
+ * Reads HA's recorder long-term statistics via `recorder/statistics_during_period`.
5
+ *
6
+ * @packageDocumentation
7
+ */
8
+ import type { SyncLayerConnection } from "../connection/types";
9
+ /**
10
+ * A single statistics bucket for one statistic id.
11
+ *
12
+ * HA 2023.6+ omits columns that were not requested and may omit null
13
+ * columns, so every value field is optional. `start`/`end` are normalized
14
+ * to ms epoch numbers regardless of the version's wire format.
15
+ */
16
+ export interface StatisticValue {
17
+ /** Bucket start (ms epoch) */
18
+ start: number;
19
+ /** Bucket end (ms epoch) */
20
+ end: number;
21
+ mean?: number;
22
+ sum?: number;
23
+ min?: number;
24
+ max?: number;
25
+ state?: number;
26
+ change?: number;
27
+ }
28
+ /** Aggregation period for statistics buckets. */
29
+ export type StatisticsPeriod = "5minute" | "hour" | "day" | "week" | "month";
30
+ /**
31
+ * Options for {@link fetchStatisticsDuringPeriod}.
32
+ */
33
+ export interface StatisticsQueryOptions {
34
+ /** Start time (required) */
35
+ startTime: Date;
36
+ /** End time (optional, defaults to now on the HA side) */
37
+ endTime?: Date;
38
+ /** Aggregation period */
39
+ period: StatisticsPeriod;
40
+ }
41
+ /** Connection surface used by statistics fetching. */
42
+ type StatisticsConnection = Pick<SyncLayerConnection, "sendMessagePromise">;
43
+ /**
44
+ * HA returns start/end as ISO strings or ms numbers depending on version.
45
+ * Normalize both shapes to ms epoch numbers; unparseable values become NaN.
46
+ */
47
+ export declare function normalizeStatisticTime(value: unknown): number;
48
+ /**
49
+ * Fetch long-term statistics for one or more statistic ids during a period.
50
+ *
51
+ * Statistic ids with no data are returned as empty arrays so callers never
52
+ * hit `undefined`.
53
+ */
54
+ export declare function fetchStatisticsDuringPeriod(connection: StatisticsConnection, statisticIds: string[], options: StatisticsQueryOptions): Promise<Record<string, StatisticValue[]>>;
55
+ export {};
package/dist/index.d.ts CHANGED
@@ -23,20 +23,24 @@ export type { GlassHomeState } from "./core/store";
23
23
  export type { AreaId, AreaView, ConnectionState, DeviceId, DeviceView, EntityDomain, EntityId, EntityView, HassEntity, } from "./core/types";
24
24
  export { extractDomain } from "./core/types";
25
25
  export { resetStore, setState, state } from "./core/store";
26
- export type { CallServiceRequest, CoreEventType, Domain, EntityCategory, EntityRegistryEntry, EventStateChangedData, HAEvent, RegistryEntryDisabler, RegistryEntryHider, ServiceCall, ServiceName, WsCommandType, WsResult, } from "@glasshome/ha-types";
26
+ export type { CallServiceRequest, CoreEventType, Domain, EntityCategory, EntityRegistryEntry, EventStateChangedData, HAEvent, HassConfig, HassUnitSystem, RegistryEntryDisabler, RegistryEntryHider, ServiceCall, ServiceName, WsCommandType, WsResult, } from "@glasshome/ha-types";
27
27
  export { fetchStream, fetchStreamData } from "./camera/fetch";
28
28
  export { getStream, refreshStream } from "./camera/query";
29
+ export { fetchEnergyPreferences } from "./energy/prefs";
30
+ export type { EnergyDeviceConsumption, EnergyPreferences, EnergySource, } from "./energy/prefs";
29
31
  export type { CameraStream, CameraStreamData, CameraStreams, EntityStreamQueryOptions, StreamFormat, StreamQueryOptions, StreamResult, } from "./camera/types";
30
32
  export { getWebRtcClientConfig, sendWebRtcCandidate, startWebRtcSession, } from "./camera/webrtc";
31
33
  export { buildAreaView, buildDeviceView, getAreaView, getAreaViews, } from "./entities/area-views";
32
34
  export { buildEntityView, entityViewEquals, getAllEntityViews, getEntityView, getEntityViews, } from "./entities/views";
33
35
  export { DOMAINS_USE_LAST_UPDATED, entityIdHistoryNeedsAttributes, LINE_ATTRIBUTES_TO_KEEP, NEED_ATTRIBUTE_DOMAINS, } from "./history/constants";
34
36
  export { fetchEntityHistory, fetchHistory, historyStateToTimeline } from "./history/fetch";
35
- export { appendHistoryPoint, bulkAppendHistoryPoints, isHistoryTracked, trackEntityHistory, untrackEntityHistory, } from "./history/query";
37
+ export { appendHistoryPoint, bulkAppendHistoryPoints, isHistoryTracked, MAX_HISTORY_POINTS, trackEntityHistory, untrackEntityHistory, } from "./history/query";
36
38
  export type { HistoryPoint } from "./history/query";
39
+ export { fetchStatisticsDuringPeriod, normalizeStatisticTime, } from "./history/statistics";
40
+ export type { StatisticsPeriod, StatisticsQueryOptions, StatisticValue, } from "./history/statistics";
37
41
  export type { EntityHistoryData, EntityHistoryQueryOptions, EntityHistoryResult, EntityHistoryState, HistoryData, HistoryQueryOptions, HistoryResult, HistoryStates, TimelineState, } from "./history/types";
38
42
  export { fetchForecast, fetchForecastData, fetchForecasts } from "./weather/fetch";
39
43
  export { getForecast, getForecasts, refreshForecast, refreshForecasts } from "./weather/query";
40
44
  export type { EntityForecastQueryOptions, ForecastQueryOptions, ForecastResult, ForecastsResult, ForecastType, WeatherForecast, WeatherForecastData, WeatherForecasts, WeatherForecastsData, } from "./weather/types";
41
- export { applyDemoServiceCall, createDemoFixtures, isDemoMode, loadDemoData, unloadDemoData, } from "./demo/demo-provider";
45
+ export { applyDemoServiceCall, createDemoFixtures, isDemoMode, loadDemoData, startDemoEnergyTicker, stopDemoEnergyTicker, unloadDemoData, } from "./demo/demo-provider";
42
46
  export declare const VERSION = "0.1.0";
package/dist/index.js CHANGED
@@ -1,4 +1,4 @@
1
- import { createDemoFixtures, resetStore, setState, state } from "./demo-data-D1ofmMa4.js";
1
+ import { ENERGY_ENTITY_IDS, createDemoFixtures, demoEnergyPreferences, energyEntityValue, formatEnergyState, isEnergyEntity, isSunUp, resetStore, setState, simulateEnergy, state, sunEvents, synthesizeEnergyHistory, synthesizeEnergyStatistics } from "./demo-data-C_sa1VWX.js";
2
2
  import { callService as callService$1, createConnection, createLongLivedTokenAuth, getAuth } from "home-assistant-js-websocket";
3
3
  import { produce, reconcile } from "solid-js/store";
4
4
 
@@ -524,11 +524,13 @@ async function loadDemoData() {
524
524
  setState("hassUrl", "https://demo.home-assistant.local");
525
525
  setState("connectionState", "connected");
526
526
  _isDemoMode = true;
527
+ startDemoEnergyTicker();
527
528
  }
528
529
  /**
529
530
  * Unload demo data and reset to disconnected state.
530
531
  */
531
532
  function unloadDemoData() {
533
+ stopDemoEnergyTicker();
532
534
  setState("entities", reconcile({}));
533
535
  setState("entityRegistry", reconcile({}));
534
536
  setState("areas", reconcile({}));
@@ -537,6 +539,54 @@ function unloadDemoData() {
537
539
  setState("connectionState", "disconnected");
538
540
  _isDemoMode = false;
539
541
  }
542
+ const ENERGY_TICK_MS = 5e3;
543
+ let _energyTicker = null;
544
+ /** Write the current energy sample into the store as string states (like real HA). */
545
+ function tickEnergy() {
546
+ const nowMs = Date.now();
547
+ const sample = simulateEnergy(nowMs);
548
+ const sun = sunEvents(nowMs);
549
+ const nowIso = new Date(nowMs).toISOString();
550
+ setState(produce((s) => {
551
+ for (const entityId of ENERGY_ENTITY_IDS) {
552
+ const e = s.entities[entityId];
553
+ if (!e) continue;
554
+ const value = energyEntityValue(entityId, sample);
555
+ if (value === void 0) continue;
556
+ const next = formatEnergyState(entityId, value);
557
+ if (e.state !== next) e.last_changed = nowIso;
558
+ e.state = next;
559
+ e.last_updated = nowIso;
560
+ }
561
+ const sunEntity = s.entities["sun.sun"];
562
+ if (sunEntity) {
563
+ const nextState = isSunUp(nowMs) ? "above_horizon" : "below_horizon";
564
+ if (sunEntity.state !== nextState) sunEntity.last_changed = nowIso;
565
+ sunEntity.state = nextState;
566
+ sunEntity.last_updated = nowIso;
567
+ sunEntity.attributes.elevation = sun.elevation;
568
+ sunEntity.attributes.next_rising = sun.nextRising;
569
+ sunEntity.attributes.next_setting = sun.nextSetting;
570
+ }
571
+ }));
572
+ }
573
+ /**
574
+ * Start the demo energy ticker. Updates the energy sensors and `sun.sun`
575
+ * every 5 seconds. Idempotent: clears any prior interval first. SSR-safe.
576
+ */
577
+ function startDemoEnergyTicker() {
578
+ stopDemoEnergyTicker();
579
+ if (typeof setInterval !== "function") return;
580
+ tickEnergy();
581
+ _energyTicker = setInterval(tickEnergy, ENERGY_TICK_MS);
582
+ }
583
+ /** Stop the demo energy ticker if running. */
584
+ function stopDemoEnergyTicker() {
585
+ if (_energyTicker !== null) {
586
+ clearInterval(_energyTicker);
587
+ _energyTicker = null;
588
+ }
589
+ }
540
590
  /**
541
591
  * Apply an optimistic state update for demo mode.
542
592
  *
@@ -734,6 +784,19 @@ function entityIdHistoryNeedsAttributes(entityId) {
734
784
  */
735
785
  async function fetchHistory(options) {
736
786
  const { startTime, endTime, entityIds = [], includeStartTimeState = true, significantChangesOnly = true, minimalResponse = true, noAttributes: noAttributesOverride } = options;
787
+ if (isDemoMode()) {
788
+ const endMs = (endTime ?? /* @__PURE__ */ new Date()).getTime();
789
+ const result = {};
790
+ for (const id of entityIds) {
791
+ if (!isEnergyEntity(id)) continue;
792
+ result[id] = synthesizeEnergyHistory(id, startTime.getTime(), endMs).map((p) => ({
793
+ s: p.s,
794
+ a: {},
795
+ lu: p.lu
796
+ }));
797
+ }
798
+ return result;
799
+ }
737
800
  const startTimeStr = startTime.toISOString();
738
801
  const endTimeStr = endTime?.toISOString();
739
802
  const noAttributes = noAttributesOverride ?? (entityIds.length === 0 ? false : !entityIds.some((entityId) => entityIdHistoryNeedsAttributes(entityId)));
@@ -807,6 +870,8 @@ function historyStateToTimeline(s) {
807
870
 
808
871
  //#endregion
809
872
  //#region src/history/query.ts
873
+ /** Hard cap on retained points per entity timeline. */
874
+ const MAX_HISTORY_POINTS = 5760;
810
875
  /** Set of entity IDs with active history tracking. */
811
876
  const trackedEntities = /* @__PURE__ */ new Set();
812
877
  /** Check if an entity has active history tracking. */
@@ -860,6 +925,7 @@ function appendHistoryPoint(entityId, stateValue, attributes, lastUpdated, lastC
860
925
  function bulkAppendHistoryPoints(points) {
861
926
  if (points.length === 0) return;
862
927
  setState(produce((s) => {
928
+ const touched = /* @__PURE__ */ new Set();
863
929
  for (const { entityId, stateValue, attributes, lastUpdated, lastChanged } of points) {
864
930
  const history = s.history[entityId];
865
931
  if (!history) continue;
@@ -876,6 +942,13 @@ function bulkAppendHistoryPoints(points) {
876
942
  lastChanged,
877
943
  lastUpdated
878
944
  });
945
+ touched.add(entityId);
946
+ }
947
+ for (const entityId of touched) {
948
+ const history = s.history[entityId];
949
+ if (!history) continue;
950
+ if (history.entityHistory.length > MAX_HISTORY_POINTS) history.entityHistory.splice(0, history.entityHistory.length - MAX_HISTORY_POINTS);
951
+ if (history.timeline.length > MAX_HISTORY_POINTS) history.timeline.splice(0, history.timeline.length - MAX_HISTORY_POINTS);
879
952
  }
880
953
  }));
881
954
  }
@@ -959,6 +1032,10 @@ async function subscribeToUpdates(conn$1) {
959
1032
  handleStatisticsUpdate(event);
960
1033
  }, "recorder_5min_statistics_generated");
961
1034
  registrySubscriptions.push(() => statisticsSub());
1035
+ const coreConfigSub = await conn$1.subscribeEvents(() => {
1036
+ handleCoreConfigUpdate();
1037
+ }, "core_config_updated");
1038
+ registrySubscriptions.push(() => coreConfigSub());
962
1039
  }
963
1040
  /**
964
1041
  * Clean up all active subscriptions
@@ -968,6 +1045,16 @@ async function cleanupSubscriptions() {
968
1045
  for (const unsub of subscriptions) await unsub();
969
1046
  registrySubscriptions = [];
970
1047
  }
1048
+ async function handleCoreConfigUpdate() {
1049
+ const conn$1 = state.conn;
1050
+ if (!conn$1) return;
1051
+ try {
1052
+ const config = await conn$1.sendMessagePromise({ type: "get_config" });
1053
+ if (config) setState("config", config);
1054
+ } catch (error) {
1055
+ console.error("Error refreshing core config:", error);
1056
+ }
1057
+ }
971
1058
  async function handleRegistryUpdate(event) {
972
1059
  const { action, entity_id } = event.data;
973
1060
  if (action === "create" || action === "update") {
@@ -1335,16 +1422,18 @@ function setupEventHandlers(conn$1, options) {
1335
1422
  }
1336
1423
  async function loadInitialData(conn$1) {
1337
1424
  try {
1338
- const [states, entityRegistry, deviceRegistry, areaRegistry, floorRegistry, labelRegistry] = await Promise.all([
1425
+ const [states, entityRegistry, deviceRegistry, areaRegistry, floorRegistry, labelRegistry, config] = await Promise.all([
1339
1426
  conn$1.sendMessagePromise({ type: "get_states" }),
1340
1427
  conn$1.sendMessagePromise({ type: "config/entity_registry/list" }),
1341
1428
  conn$1.sendMessagePromise({ type: "config/device_registry/list" }),
1342
1429
  conn$1.sendMessagePromise({ type: "config/area_registry/list" }),
1343
1430
  conn$1.sendMessagePromise({ type: "config/floor_registry/list" }),
1344
- conn$1.sendMessagePromise({ type: "config/label_registry/list" })
1431
+ conn$1.sendMessagePromise({ type: "config/label_registry/list" }),
1432
+ conn$1.sendMessagePromise({ type: "get_config" })
1345
1433
  ]);
1346
1434
  bulkUpdateEntities(states);
1347
1435
  bulkUpdateEntityRegistry(entityRegistry);
1436
+ setState("config", config);
1348
1437
  setState(produce((s) => {
1349
1438
  for (const device of deviceRegistry) s.devices[device.id] = device;
1350
1439
  for (const areaApi of areaRegistry) {
@@ -2164,6 +2253,43 @@ async function refreshStream(entityId, format = "hls") {
2164
2253
  return getStream(entityId, { format });
2165
2254
  }
2166
2255
 
2256
+ //#endregion
2257
+ //#region src/energy/prefs.ts
2258
+ function isRecord(value) {
2259
+ return typeof value === "object" && value !== null;
2260
+ }
2261
+ /**
2262
+ * Fetch the HA energy dashboard preferences.
2263
+ *
2264
+ * Returns `null` when no energy dashboard is configured (HA raises an error
2265
+ * for `energy/get_prefs` in that case); callers treat `null` as "not
2266
+ * configured".
2267
+ */
2268
+ async function fetchEnergyPreferences(connection) {
2269
+ if (isDemoMode()) {
2270
+ const prefs = demoEnergyPreferences();
2271
+ return {
2272
+ ...prefs,
2273
+ energy_sources: prefs.energy_sources,
2274
+ device_consumption: prefs.device_consumption
2275
+ };
2276
+ }
2277
+ let response;
2278
+ try {
2279
+ response = await connection.sendMessagePromise({ type: "energy/get_prefs" });
2280
+ } catch {
2281
+ return null;
2282
+ }
2283
+ if (!isRecord(response)) return null;
2284
+ const sources = Array.isArray(response.energy_sources) ? response.energy_sources.filter(isRecord) : [];
2285
+ const devices = Array.isArray(response.device_consumption) ? response.device_consumption.filter(isRecord) : [];
2286
+ return {
2287
+ ...response,
2288
+ energy_sources: sources,
2289
+ device_consumption: devices
2290
+ };
2291
+ }
2292
+
2167
2293
  //#endregion
2168
2294
  //#region src/camera/webrtc.ts
2169
2295
  /**
@@ -2248,6 +2374,77 @@ async function sendWebRtcCandidate(entityId, sessionId, candidate) {
2248
2374
  });
2249
2375
  }
2250
2376
 
2377
+ //#endregion
2378
+ //#region src/history/statistics.ts
2379
+ const STATISTIC_TYPES = [
2380
+ "mean",
2381
+ "sum",
2382
+ "min",
2383
+ "max",
2384
+ "state",
2385
+ "change"
2386
+ ];
2387
+ /**
2388
+ * HA returns start/end as ISO strings or ms numbers depending on version.
2389
+ * Normalize both shapes to ms epoch numbers; unparseable values become NaN.
2390
+ */
2391
+ function normalizeStatisticTime(value) {
2392
+ if (typeof value === "number") return value;
2393
+ if (typeof value === "string") return new Date(value).getTime();
2394
+ return NaN;
2395
+ }
2396
+ function normalizeNumber(value) {
2397
+ return typeof value === "number" ? value : void 0;
2398
+ }
2399
+ function normalizeStatisticValue(raw) {
2400
+ const value = {
2401
+ start: normalizeStatisticTime(raw.start),
2402
+ end: normalizeStatisticTime(raw.end)
2403
+ };
2404
+ const mean = normalizeNumber(raw.mean);
2405
+ if (mean !== void 0) value.mean = mean;
2406
+ const sum = normalizeNumber(raw.sum);
2407
+ if (sum !== void 0) value.sum = sum;
2408
+ const min = normalizeNumber(raw.min);
2409
+ if (min !== void 0) value.min = min;
2410
+ const max = normalizeNumber(raw.max);
2411
+ if (max !== void 0) value.max = max;
2412
+ const stateVal = normalizeNumber(raw.state);
2413
+ if (stateVal !== void 0) value.state = stateVal;
2414
+ const change = normalizeNumber(raw.change);
2415
+ if (change !== void 0) value.change = change;
2416
+ return value;
2417
+ }
2418
+ /**
2419
+ * Fetch long-term statistics for one or more statistic ids during a period.
2420
+ *
2421
+ * Statistic ids with no data are returned as empty arrays so callers never
2422
+ * hit `undefined`.
2423
+ */
2424
+ async function fetchStatisticsDuringPeriod(connection, statisticIds, options) {
2425
+ const { startTime, endTime, period } = options;
2426
+ if (isDemoMode()) {
2427
+ const result$1 = {};
2428
+ const statPeriod = period === "hour" || period === "day" ? period : "day";
2429
+ const endMs = (endTime ?? /* @__PURE__ */ new Date()).getTime();
2430
+ for (const id of statisticIds) result$1[id] = isEnergyEntity(id) ? synthesizeEnergyStatistics(id, startTime.getTime(), endMs, statPeriod) : [];
2431
+ return result$1;
2432
+ }
2433
+ const params = {
2434
+ type: "recorder/statistics_during_period",
2435
+ start_time: startTime.toISOString(),
2436
+ statistic_ids: statisticIds,
2437
+ period,
2438
+ types: STATISTIC_TYPES
2439
+ };
2440
+ if (endTime) params.end_time = endTime.toISOString();
2441
+ const response = await connection.sendMessagePromise(params);
2442
+ const result = {};
2443
+ for (const id of statisticIds) result[id] = [];
2444
+ if (response && typeof response === "object") for (const [id, buckets] of Object.entries(response)) result[id] = Array.isArray(buckets) ? buckets.map(normalizeStatisticValue) : [];
2445
+ return result;
2446
+ }
2447
+
2251
2448
  //#endregion
2252
2449
  //#region src/weather/fetch.ts
2253
2450
  /**
@@ -2510,4 +2707,4 @@ async function refreshForecasts(entityId, types) {
2510
2707
  const VERSION = "0.1.0";
2511
2708
 
2512
2709
  //#endregion
2513
- export { DOMAINS_USE_LAST_UPDATED, LINE_ATTRIBUTES_TO_KEEP, NEED_ATTRIBUTE_DOMAINS, VERSION, addDebugIncomingMessageListener, addDebugOutgoingMessageListener, appendHistoryPoint, applyDemoServiceCall, area, areas, authenticateWithOAuth, authenticateWithToken, batchEntityUpdates, batchServiceCalls, buildAreaView, buildDeviceView, buildEntityView, bulkAppendHistoryPoints, callService, createDemoFixtures, disconnect, entities, entitiesByAreaAndDomain, entitiesWhere, entity, entityIdHistoryNeedsAttributes, entityViewEquals, extractDomain, fetchEntityHistory, fetchForecast, fetchForecastData, fetchForecasts, fetchHistory, fetchStream, fetchStreamData, getAllEntityViews, getAreaView, getAreaViews, getConnection, getConnectionState, getEntityView, getEntityViews, getForecast, getForecasts, getStream, getWebRtcClientConfig, historyStateToTimeline, initConnection, isAuthValid, isConnected, isDemoMode, isHistoryTracked, loadDemoData, refreshAuth, refreshForecast, refreshForecasts, refreshStream, registerEntity, resetStore, searchEntities, sendCommand, sendWebRtcCandidate, setState, startWebRtcSession, state, toggle, trackEntityHistory, turnOff, turnOn, unloadDemoData, untrackEntityHistory, updateEntity };
2710
+ export { DOMAINS_USE_LAST_UPDATED, LINE_ATTRIBUTES_TO_KEEP, MAX_HISTORY_POINTS, NEED_ATTRIBUTE_DOMAINS, VERSION, addDebugIncomingMessageListener, addDebugOutgoingMessageListener, appendHistoryPoint, applyDemoServiceCall, area, areas, authenticateWithOAuth, authenticateWithToken, batchEntityUpdates, batchServiceCalls, buildAreaView, buildDeviceView, buildEntityView, bulkAppendHistoryPoints, callService, createDemoFixtures, disconnect, entities, entitiesByAreaAndDomain, entitiesWhere, entity, entityIdHistoryNeedsAttributes, entityViewEquals, extractDomain, fetchEnergyPreferences, fetchEntityHistory, fetchForecast, fetchForecastData, fetchForecasts, fetchHistory, fetchStatisticsDuringPeriod, fetchStream, fetchStreamData, getAllEntityViews, getAreaView, getAreaViews, getConnection, getConnectionState, getEntityView, getEntityViews, getForecast, getForecasts, getStream, getWebRtcClientConfig, historyStateToTimeline, initConnection, isAuthValid, isConnected, isDemoMode, isHistoryTracked, loadDemoData, normalizeStatisticTime, refreshAuth, refreshForecast, refreshForecasts, refreshStream, registerEntity, resetStore, searchEntities, sendCommand, sendWebRtcCandidate, setState, startDemoEnergyTicker, startWebRtcSession, state, stopDemoEnergyTicker, toggle, trackEntityHistory, turnOff, turnOn, unloadDemoData, untrackEntityHistory, updateEntity };
@@ -10,8 +10,9 @@
10
10
  *
11
11
  * @packageDocumentation
12
12
  */
13
- import type { Accessor } from "solid-js";
14
- import type { AreaView, CameraStreamData, ConnectionState, EntityHistoryData, EntityView, GlassHomeState, HassEntity, WeatherForecastsData } from "@glasshome/sync-layer";
13
+ import type { HassConfig, HassUnitSystem } from "@glasshome/ha-types";
14
+ import type { Accessor, Resource } from "solid-js";
15
+ import type { AreaView, CameraStreamData, ConnectionState, EntityHistoryData, EntityView, GlassHomeState, HassEntity, StatisticsQueryOptions, StatisticValue, WeatherForecastsData } from "@glasshome/sync-layer";
15
16
  import { callService, toggle, turnOff, turnOn } from "@glasshome/sync-layer";
16
17
  /**
17
18
  * Get a reactive EntityView for an entity ID.
@@ -39,6 +40,16 @@ export declare function useConnection(): {
39
40
  status: Accessor<ConnectionState>;
40
41
  isConnected: Accessor<boolean>;
41
42
  };
43
+ /** Reactive Home Assistant core config (null until loaded). */
44
+ export declare function useHassConfig(): Accessor<HassConfig | null>;
45
+ /** Reactive HA unit system (length/mass/temperature/volume), or null until loaded. */
46
+ export declare function useUnitSystem(): Accessor<HassUnitSystem | null>;
47
+ /** Reactive temperature unit from HA config, including the degree sign. Defaults to "°C". */
48
+ export declare function useTemperatureUnit(): Accessor<string>;
49
+ /** Reactive UI locale (BCP 47 tag) from HA config. Defaults to "en". */
50
+ export declare function useLocale(): Accessor<string>;
51
+ /** Reactive currency (ISO 4217) from HA config. Defaults to "USD". */
52
+ export declare function useCurrency(): Accessor<string>;
42
53
  /**
43
54
  * Get all service call functions.
44
55
  */
@@ -71,6 +82,14 @@ export declare function useArea(areaId: Accessor<string> | string): Accessor<Are
71
82
  * this hook simply reads it reactively.
72
83
  */
73
84
  export declare function useEntityHistory(entityId: Accessor<string> | string): Accessor<EntityHistoryData | undefined>;
85
+ /**
86
+ * Get reactive long-term statistics for a statistic id.
87
+ *
88
+ * Fetches via `recorder/statistics_during_period` and re-fetches whenever the
89
+ * statistic id or options change. The returned Resource exposes `.loading`
90
+ * and `.error` alongside the value accessor.
91
+ */
92
+ export declare function useEntityStatistics(statisticId: Accessor<string> | string, options: Accessor<StatisticsQueryOptions> | StatisticsQueryOptions): Resource<StatisticValue[]>;
74
93
  /**
75
94
  * Get reactive camera stream data.
76
95
  */
@@ -6,5 +6,5 @@
6
6
  *
7
7
  * @packageDocumentation
8
8
  */
9
- export { useArea, useAreas, useCamera, useConnection, useEntities, useEntity, useEntityHistory, useEntityState, useForecast, useService, useStore, useToggle, useTurnOff, useTurnOn, } from "./hooks";
9
+ export { useArea, useAreas, useCamera, useConnection, useCurrency, useEntities, useEntity, useEntityHistory, useEntityState, useEntityStatistics, useForecast, useHassConfig, useLocale, useService, useStore, useTemperatureUnit, useToggle, useTurnOff, useTurnOn, useUnitSystem, } from "./hooks";
10
10
  export { allEntityIds, byArea, byDevice, byDomain, byFloor, byLabel, } from "./indices";
@@ -1,5 +1,5 @@
1
- import { createEffect, createMemo, onCleanup } from "solid-js";
2
- import { buildAreaView, callService, entityViewEquals, extractDomain, getEntityView, registerEntity, state, toggle, turnOff, turnOn } from "@glasshome/sync-layer";
1
+ import { createEffect, createMemo, createResource, onCleanup } from "solid-js";
2
+ import { buildAreaView, callService, entityViewEquals, extractDomain, fetchStatisticsDuringPeriod, getEntityView, isDemoMode, registerEntity, state, toggle, turnOff, turnOn } from "@glasshome/sync-layer";
3
3
 
4
4
  //#region src/solid/hooks.ts
5
5
  /**
@@ -69,6 +69,26 @@ function useConnection() {
69
69
  isConnected
70
70
  };
71
71
  }
72
+ /** Reactive Home Assistant core config (null until loaded). */
73
+ function useHassConfig() {
74
+ return createMemo(() => state.config);
75
+ }
76
+ /** Reactive HA unit system (length/mass/temperature/volume), or null until loaded. */
77
+ function useUnitSystem() {
78
+ return createMemo(() => state.config?.unit_system ?? null);
79
+ }
80
+ /** Reactive temperature unit from HA config, including the degree sign. Defaults to "°C". */
81
+ function useTemperatureUnit() {
82
+ return createMemo(() => state.config?.unit_system.temperature ?? "°C");
83
+ }
84
+ /** Reactive UI locale (BCP 47 tag) from HA config. Defaults to "en". */
85
+ function useLocale() {
86
+ return createMemo(() => state.config?.language || "en");
87
+ }
88
+ /** Reactive currency (ISO 4217) from HA config. Defaults to "USD". */
89
+ function useCurrency() {
90
+ return createMemo(() => state.config?.currency || "USD");
91
+ }
72
92
  /**
73
93
  * Get all service call functions.
74
94
  */
@@ -152,6 +172,35 @@ function useEntityHistory(entityId) {
152
172
  return createMemo(() => state.history[getId()]);
153
173
  }
154
174
  /**
175
+ * Connection stub for demo mode: the statistics fetcher's demo branch never
176
+ * sends a message, so this only satisfies the signature.
177
+ */
178
+ const DEMO_STATS_CONNECTION = { sendMessagePromise() {
179
+ return Promise.reject(new Error("demo mode: no connection"));
180
+ } };
181
+ /**
182
+ * Get reactive long-term statistics for a statistic id.
183
+ *
184
+ * Fetches via `recorder/statistics_during_period` and re-fetches whenever the
185
+ * statistic id or options change. The returned Resource exposes `.loading`
186
+ * and `.error` alongside the value accessor.
187
+ */
188
+ function useEntityStatistics(statisticId, options) {
189
+ const getId = typeof statisticId === "function" ? statisticId : () => statisticId;
190
+ const getOptions = typeof options === "function" ? options : () => options;
191
+ const [data] = createResource(() => ({
192
+ id: getId(),
193
+ options: getOptions()
194
+ }), async ({ id, options: opts }) => {
195
+ if (!id) return [];
196
+ const conn = state.conn;
197
+ if (!conn && !isDemoMode()) return [];
198
+ const result = await fetchStatisticsDuringPeriod(conn ?? DEMO_STATS_CONNECTION, [id], opts);
199
+ return result[id] ?? [];
200
+ });
201
+ return data;
202
+ }
203
+ /**
155
204
  * Get reactive camera stream data.
156
205
  */
157
206
  function useCamera(entityId) {
@@ -215,4 +264,4 @@ const byLabel = () => {
215
264
  const allEntityIds = () => Object.keys(state.entities);
216
265
 
217
266
  //#endregion
218
- export { allEntityIds, byArea, byDevice, byDomain, byFloor, byLabel, useArea, useAreas, useCamera, useConnection, useEntities, useEntity, useEntityHistory, useEntityState, useForecast, useService, useStore, useToggle, useTurnOff, useTurnOn };
267
+ export { allEntityIds, byArea, byDevice, byDomain, byFloor, byLabel, useArea, useAreas, useCamera, useConnection, useCurrency, useEntities, useEntity, useEntityHistory, useEntityState, useEntityStatistics, useForecast, useHassConfig, useLocale, useService, useStore, useTemperatureUnit, useToggle, useTurnOff, useTurnOn, useUnitSystem };
@@ -1,4 +1,4 @@
1
- import { createDemoFixtures, state } from "../demo-data-D1ofmMa4.js";
1
+ import { createDemoFixtures, state } from "../demo-data-C_sa1VWX.js";
2
2
 
3
3
  //#region src/testing/mock-behaviors/base.ts
4
4
  /**
@@ -601,6 +601,7 @@ var MockConnection = class {
601
601
  messageIdCounter = 1;
602
602
  autoReact;
603
603
  reactionDelay;
604
+ configOverride;
604
605
  serviceCalls = [];
605
606
  sentCommands = [];
606
607
  constructor(fixtures, latency = 0, options) {
@@ -608,6 +609,25 @@ var MockConnection = class {
608
609
  this.latency = latency;
609
610
  this.autoReact = options?.autoReact ?? true;
610
611
  this.reactionDelay = options?.reactionDelay ?? 0;
612
+ this.configOverride = options?.config ?? {};
613
+ }
614
+ /** Replace the core config and fire core_config_updated (simulates HA Region change). */
615
+ setConfig(config) {
616
+ this.configOverride = {
617
+ ...this.configOverride,
618
+ ...config
619
+ };
620
+ this.emitEvent({
621
+ event_type: "core_config_updated",
622
+ data: {},
623
+ origin: "LOCAL",
624
+ time_fired: (/* @__PURE__ */ new Date(0)).toISOString(),
625
+ context: {
626
+ id: "mock",
627
+ parent_id: null,
628
+ user_id: null
629
+ }
630
+ });
611
631
  }
612
632
  /**
613
633
  * Get connected state
@@ -807,7 +827,8 @@ var MockConnection = class {
807
827
  internal_url: null,
808
828
  currency: "USD",
809
829
  country: null,
810
- language: "en"
830
+ language: "en",
831
+ ...this.configOverride
811
832
  };
812
833
  case "get_services": return {};
813
834
  case "fire_event": {
@@ -5,7 +5,7 @@
5
5
  *
6
6
  * @packageDocumentation
7
7
  */
8
- import type { EntityRegistryEntry, HAEvent } from "@glasshome/ha-types";
8
+ import type { EntityRegistryEntry, HassConfig, HAEvent } from "@glasshome/ha-types";
9
9
  import type { SyncLayerConnection } from "../connection/types";
10
10
  import type { HassEntity } from "../core/types";
11
11
  import type { AreaRegistryEntry, DeviceRegistryEntry, Fixtures } from "./types";
@@ -30,6 +30,7 @@ export declare class MockConnection implements SyncLayerConnection {
30
30
  private messageIdCounter;
31
31
  private autoReact;
32
32
  private reactionDelay;
33
+ private configOverride;
33
34
  serviceCalls: Array<{
34
35
  domain: string;
35
36
  service: string;
@@ -47,7 +48,11 @@ export declare class MockConnection implements SyncLayerConnection {
47
48
  autoReact?: boolean;
48
49
  /** Delay before emitting entity update events after service calls in ms (default: 0) */
49
50
  reactionDelay?: number;
51
+ /** Override fields of the get_config response (unit_system, language, currency, etc.) */
52
+ config?: Partial<HassConfig>;
50
53
  });
54
+ /** Replace the core config and fire core_config_updated (simulates HA Region change). */
55
+ setConfig(config: Partial<HassConfig>): void;
51
56
  /**
52
57
  * Get connected state
53
58
  */