@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.
- package/dist/core/store.d.ts +2 -0
- package/dist/demo/demo-provider.d.ts +7 -0
- package/dist/demo/energy-sim.d.ts +106 -0
- package/dist/demo-data-C_sa1VWX.js +1149 -0
- package/dist/energy/prefs.d.ts +56 -0
- package/dist/history/query.d.ts +2 -0
- package/dist/history/statistics.d.ts +55 -0
- package/dist/index.d.ts +7 -3
- package/dist/index.js +201 -4
- package/dist/solid/hooks.d.ts +21 -2
- package/dist/solid/index.d.ts +1 -1
- package/dist/solid/index.js +52 -3
- package/dist/testing/index.js +23 -2
- package/dist/testing/mock.d.ts +6 -1
- package/package.json +3 -3
- package/dist/demo-data-D1ofmMa4.js +0 -608
|
@@ -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 {};
|
package/dist/history/query.d.ts
CHANGED
|
@@ -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-
|
|
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 };
|
package/dist/solid/hooks.d.ts
CHANGED
|
@@ -10,8 +10,9 @@
|
|
|
10
10
|
*
|
|
11
11
|
* @packageDocumentation
|
|
12
12
|
*/
|
|
13
|
-
import type {
|
|
14
|
-
import type {
|
|
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
|
*/
|
package/dist/solid/index.d.ts
CHANGED
|
@@ -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";
|
package/dist/solid/index.js
CHANGED
|
@@ -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 };
|
package/dist/testing/index.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { createDemoFixtures, state } from "../demo-data-
|
|
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": {
|
package/dist/testing/mock.d.ts
CHANGED
|
@@ -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
|
*/
|