@glasshome/sync-layer 0.1.13 → 0.1.14

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
@@ -26,14 +26,18 @@ export { resetStore, setState, state } from "./core/store";
26
26
  export type { CallServiceRequest, CoreEventType, Domain, EntityCategory, EntityRegistryEntry, EventStateChangedData, HAEvent, 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";
package/dist/index.js CHANGED
@@ -807,6 +807,8 @@ function historyStateToTimeline(s) {
807
807
 
808
808
  //#endregion
809
809
  //#region src/history/query.ts
810
+ /** Hard cap on retained points per entity timeline. */
811
+ const MAX_HISTORY_POINTS = 5760;
810
812
  /** Set of entity IDs with active history tracking. */
811
813
  const trackedEntities = /* @__PURE__ */ new Set();
812
814
  /** Check if an entity has active history tracking. */
@@ -860,6 +862,7 @@ function appendHistoryPoint(entityId, stateValue, attributes, lastUpdated, lastC
860
862
  function bulkAppendHistoryPoints(points) {
861
863
  if (points.length === 0) return;
862
864
  setState(produce((s) => {
865
+ const touched = /* @__PURE__ */ new Set();
863
866
  for (const { entityId, stateValue, attributes, lastUpdated, lastChanged } of points) {
864
867
  const history = s.history[entityId];
865
868
  if (!history) continue;
@@ -876,6 +879,13 @@ function bulkAppendHistoryPoints(points) {
876
879
  lastChanged,
877
880
  lastUpdated
878
881
  });
882
+ touched.add(entityId);
883
+ }
884
+ for (const entityId of touched) {
885
+ const history = s.history[entityId];
886
+ if (!history) continue;
887
+ if (history.entityHistory.length > MAX_HISTORY_POINTS) history.entityHistory.splice(0, history.entityHistory.length - MAX_HISTORY_POINTS);
888
+ if (history.timeline.length > MAX_HISTORY_POINTS) history.timeline.splice(0, history.timeline.length - MAX_HISTORY_POINTS);
879
889
  }
880
890
  }));
881
891
  }
@@ -2164,6 +2174,35 @@ async function refreshStream(entityId, format = "hls") {
2164
2174
  return getStream(entityId, { format });
2165
2175
  }
2166
2176
 
2177
+ //#endregion
2178
+ //#region src/energy/prefs.ts
2179
+ function isRecord(value) {
2180
+ return typeof value === "object" && value !== null;
2181
+ }
2182
+ /**
2183
+ * Fetch the HA energy dashboard preferences.
2184
+ *
2185
+ * Returns `null` when no energy dashboard is configured (HA raises an error
2186
+ * for `energy/get_prefs` in that case); callers treat `null` as "not
2187
+ * configured".
2188
+ */
2189
+ async function fetchEnergyPreferences(connection) {
2190
+ let response;
2191
+ try {
2192
+ response = await connection.sendMessagePromise({ type: "energy/get_prefs" });
2193
+ } catch {
2194
+ return null;
2195
+ }
2196
+ if (!isRecord(response)) return null;
2197
+ const sources = Array.isArray(response.energy_sources) ? response.energy_sources.filter(isRecord) : [];
2198
+ const devices = Array.isArray(response.device_consumption) ? response.device_consumption.filter(isRecord) : [];
2199
+ return {
2200
+ ...response,
2201
+ energy_sources: sources,
2202
+ device_consumption: devices
2203
+ };
2204
+ }
2205
+
2167
2206
  //#endregion
2168
2207
  //#region src/camera/webrtc.ts
2169
2208
  /**
@@ -2248,6 +2287,70 @@ async function sendWebRtcCandidate(entityId, sessionId, candidate) {
2248
2287
  });
2249
2288
  }
2250
2289
 
2290
+ //#endregion
2291
+ //#region src/history/statistics.ts
2292
+ const STATISTIC_TYPES = [
2293
+ "mean",
2294
+ "sum",
2295
+ "min",
2296
+ "max",
2297
+ "state",
2298
+ "change"
2299
+ ];
2300
+ /**
2301
+ * HA returns start/end as ISO strings or ms numbers depending on version.
2302
+ * Normalize both shapes to ms epoch numbers; unparseable values become NaN.
2303
+ */
2304
+ function normalizeStatisticTime(value) {
2305
+ if (typeof value === "number") return value;
2306
+ if (typeof value === "string") return new Date(value).getTime();
2307
+ return NaN;
2308
+ }
2309
+ function normalizeNumber(value) {
2310
+ return typeof value === "number" ? value : void 0;
2311
+ }
2312
+ function normalizeStatisticValue(raw) {
2313
+ const value = {
2314
+ start: normalizeStatisticTime(raw.start),
2315
+ end: normalizeStatisticTime(raw.end)
2316
+ };
2317
+ const mean = normalizeNumber(raw.mean);
2318
+ if (mean !== void 0) value.mean = mean;
2319
+ const sum = normalizeNumber(raw.sum);
2320
+ if (sum !== void 0) value.sum = sum;
2321
+ const min = normalizeNumber(raw.min);
2322
+ if (min !== void 0) value.min = min;
2323
+ const max = normalizeNumber(raw.max);
2324
+ if (max !== void 0) value.max = max;
2325
+ const stateVal = normalizeNumber(raw.state);
2326
+ if (stateVal !== void 0) value.state = stateVal;
2327
+ const change = normalizeNumber(raw.change);
2328
+ if (change !== void 0) value.change = change;
2329
+ return value;
2330
+ }
2331
+ /**
2332
+ * Fetch long-term statistics for one or more statistic ids during a period.
2333
+ *
2334
+ * Statistic ids with no data are returned as empty arrays so callers never
2335
+ * hit `undefined`.
2336
+ */
2337
+ async function fetchStatisticsDuringPeriod(connection, statisticIds, options) {
2338
+ const { startTime, endTime, period } = options;
2339
+ const params = {
2340
+ type: "recorder/statistics_during_period",
2341
+ start_time: startTime.toISOString(),
2342
+ statistic_ids: statisticIds,
2343
+ period,
2344
+ types: STATISTIC_TYPES
2345
+ };
2346
+ if (endTime) params.end_time = endTime.toISOString();
2347
+ const response = await connection.sendMessagePromise(params);
2348
+ const result = {};
2349
+ for (const id of statisticIds) result[id] = [];
2350
+ if (response && typeof response === "object") for (const [id, buckets] of Object.entries(response)) result[id] = Array.isArray(buckets) ? buckets.map(normalizeStatisticValue) : [];
2351
+ return result;
2352
+ }
2353
+
2251
2354
  //#endregion
2252
2355
  //#region src/weather/fetch.ts
2253
2356
  /**
@@ -2510,4 +2613,4 @@ async function refreshForecasts(entityId, types) {
2510
2613
  const VERSION = "0.1.0";
2511
2614
 
2512
2615
  //#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 };
2616
+ 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, startWebRtcSession, state, toggle, trackEntityHistory, turnOff, turnOn, unloadDemoData, untrackEntityHistory, updateEntity };
@@ -10,8 +10,8 @@
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 { Accessor, Resource } from "solid-js";
14
+ import type { AreaView, CameraStreamData, ConnectionState, EntityHistoryData, EntityView, GlassHomeState, HassEntity, StatisticsQueryOptions, StatisticValue, WeatherForecastsData } from "@glasshome/sync-layer";
15
15
  import { callService, toggle, turnOff, turnOn } from "@glasshome/sync-layer";
16
16
  /**
17
17
  * Get a reactive EntityView for an entity ID.
@@ -71,6 +71,14 @@ export declare function useArea(areaId: Accessor<string> | string): Accessor<Are
71
71
  * this hook simply reads it reactively.
72
72
  */
73
73
  export declare function useEntityHistory(entityId: Accessor<string> | string): Accessor<EntityHistoryData | undefined>;
74
+ /**
75
+ * Get reactive long-term statistics for a statistic id.
76
+ *
77
+ * Fetches via `recorder/statistics_during_period` and re-fetches whenever the
78
+ * statistic id or options change. The returned Resource exposes `.loading`
79
+ * and `.error` alongside the value accessor.
80
+ */
81
+ export declare function useEntityStatistics(statisticId: Accessor<string> | string, options: Accessor<StatisticsQueryOptions> | StatisticsQueryOptions): Resource<StatisticValue[]>;
74
82
  /**
75
83
  * Get reactive camera stream data.
76
84
  */
@@ -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, useEntities, useEntity, useEntityHistory, useEntityState, useEntityStatistics, useForecast, useService, useStore, useToggle, useTurnOff, useTurnOn, } 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, registerEntity, state, toggle, turnOff, turnOn } from "@glasshome/sync-layer";
3
3
 
4
4
  //#region src/solid/hooks.ts
5
5
  /**
@@ -152,6 +152,28 @@ function useEntityHistory(entityId) {
152
152
  return createMemo(() => state.history[getId()]);
153
153
  }
154
154
  /**
155
+ * Get reactive long-term statistics for a statistic id.
156
+ *
157
+ * Fetches via `recorder/statistics_during_period` and re-fetches whenever the
158
+ * statistic id or options change. The returned Resource exposes `.loading`
159
+ * and `.error` alongside the value accessor.
160
+ */
161
+ function useEntityStatistics(statisticId, options) {
162
+ const getId = typeof statisticId === "function" ? statisticId : () => statisticId;
163
+ const getOptions = typeof options === "function" ? options : () => options;
164
+ const [data] = createResource(() => ({
165
+ id: getId(),
166
+ options: getOptions()
167
+ }), async ({ id, options: opts }) => {
168
+ if (!id) return [];
169
+ const conn = state.conn;
170
+ if (!conn) return [];
171
+ const result = await fetchStatisticsDuringPeriod(conn, [id], opts);
172
+ return result[id] ?? [];
173
+ });
174
+ return data;
175
+ }
176
+ /**
155
177
  * Get reactive camera stream data.
156
178
  */
157
179
  function useCamera(entityId) {
@@ -215,4 +237,4 @@ const byLabel = () => {
215
237
  const allEntityIds = () => Object.keys(state.entities);
216
238
 
217
239
  //#endregion
218
- export { allEntityIds, byArea, byDevice, byDomain, byFloor, byLabel, useArea, useAreas, useCamera, useConnection, useEntities, useEntity, useEntityHistory, useEntityState, useForecast, useService, useStore, useToggle, useTurnOff, useTurnOn };
240
+ export { allEntityIds, byArea, byDevice, byDomain, byFloor, byLabel, useArea, useAreas, useCamera, useConnection, useEntities, useEntity, useEntityHistory, useEntityState, useEntityStatistics, useForecast, useService, useStore, useToggle, useTurnOff, useTurnOn };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@glasshome/sync-layer",
3
- "version": "0.1.13",
3
+ "version": "0.1.14",
4
4
  "description": "Type-safe, reactive state synchronization layer for Home Assistant",
5
5
  "repository": {
6
6
  "type": "git",