@glasshome/sync-layer 0.6.0 → 0.7.1
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/calendar/track.d.ts +19 -0
- package/dist/calendar/types.d.ts +32 -0
- package/dist/core/store.d.ts +3 -0
- package/dist/index.d.ts +11 -0
- package/dist/index.js +4 -2
- package/dist/solid/hooks.d.ts +10 -0
- package/dist/solid/index.d.ts +1 -1
- package/dist/solid/index.js +20 -2
- package/dist/{statistics-C6JQBDVf.js → statistics-CtRfIF6D.js} +214 -13
- package/dist/worker/protocol.d.ts +4 -0
- package/dist/worker/worker-main.js +1 -1
- package/dist/{worker-main-DTbzEKor.js → worker-main-C-3QM4di.js} +5 -3
- package/package.json +2 -2
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import type { EntityId } from "../core/types";
|
|
2
|
+
import type { CalendarWindowOptions } from "./types";
|
|
3
|
+
/**
|
|
4
|
+
* Start tracking live events for a calendar entity. Returns the untrack
|
|
5
|
+
* function. Safe while disconnected: the subscription activates on the next
|
|
6
|
+
* `forceResubscribeCalendars()` (called by the connection layer on connect
|
|
7
|
+
* and reconnect).
|
|
8
|
+
*/
|
|
9
|
+
export declare function trackCalendarEvents(entityId: EntityId, options?: CalendarWindowOptions): () => void;
|
|
10
|
+
/**
|
|
11
|
+
* Reconcile every tracked calendar, dropping current subscription handles
|
|
12
|
+
* first. The connection layer calls this on connect and after reconnect:
|
|
13
|
+
* an unchanged window is no proof the old subscription is alive, so each
|
|
14
|
+
* tracked entity resubscribes (HA re-pushes the snapshot, refreshing the
|
|
15
|
+
* store through any missed changes).
|
|
16
|
+
*/
|
|
17
|
+
export declare function forceResubscribeCalendars(): Promise<void>;
|
|
18
|
+
/** Test-only: drop all tracking state and timers. */
|
|
19
|
+
export declare function resetCalendarTracking(): void;
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import type { EntityId } from "../core/types";
|
|
2
|
+
/**
|
|
3
|
+
* One calendar event as HA serializes it over `calendar/event/subscribe`:
|
|
4
|
+
* dates/datetimes are ISO strings (date-only for all-day events), and
|
|
5
|
+
* None-valued fields are omitted.
|
|
6
|
+
*/
|
|
7
|
+
export interface CalendarEvent {
|
|
8
|
+
start: string;
|
|
9
|
+
end: string;
|
|
10
|
+
summary: string;
|
|
11
|
+
description?: string;
|
|
12
|
+
location?: string;
|
|
13
|
+
uid?: string;
|
|
14
|
+
recurrence_id?: string;
|
|
15
|
+
rrule?: string;
|
|
16
|
+
}
|
|
17
|
+
/** Live event window for one calendar entity, held in the store. */
|
|
18
|
+
export interface CalendarEventsData {
|
|
19
|
+
entityId: EntityId;
|
|
20
|
+
events: CalendarEvent[];
|
|
21
|
+
/** True until the first snapshot for the current subscription arrives. */
|
|
22
|
+
loading: boolean;
|
|
23
|
+
error: Error | null;
|
|
24
|
+
/** Subscribed window bounds (ISO). */
|
|
25
|
+
start: string;
|
|
26
|
+
end: string;
|
|
27
|
+
}
|
|
28
|
+
/** Per-tracker window request, in whole days relative to now. */
|
|
29
|
+
export interface CalendarWindowOptions {
|
|
30
|
+
daysBehind?: number;
|
|
31
|
+
daysAhead?: number;
|
|
32
|
+
}
|
package/dist/core/store.d.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
/** Central reactive state store for sync-layer using SolidJS createStore. */
|
|
2
2
|
import type { SetStoreFunction, Store } from "solid-js/store";
|
|
3
|
+
import type { CalendarEventsData } from "../calendar/types";
|
|
3
4
|
import type { CameraStreamData } from "../camera/types";
|
|
4
5
|
import type { EntityHistoryData } from "../history/types";
|
|
5
6
|
import type { WeatherForecastsData } from "../weather/types";
|
|
@@ -55,6 +56,8 @@ export interface GlassHomeState {
|
|
|
55
56
|
history: Record<EntityId, EntityHistoryData>;
|
|
56
57
|
/** Weather forecasts data (keyed by entity ID) */
|
|
57
58
|
forecasts: Record<EntityId, WeatherForecastsData>;
|
|
59
|
+
/** Live calendar events (keyed by entity ID), maintained by calendar/track */
|
|
60
|
+
calendars: Record<EntityId, CalendarEventsData>;
|
|
58
61
|
/** Camera stream data (keyed by entity ID) */
|
|
59
62
|
streams: Record<EntityId, CameraStreamData>;
|
|
60
63
|
}
|
package/dist/index.d.ts
CHANGED
|
@@ -4,6 +4,16 @@
|
|
|
4
4
|
* Type-safe, reactive state synchronization layer for Home Assistant
|
|
5
5
|
* Uses SolidJS createStore for reactive state management.
|
|
6
6
|
*
|
|
7
|
+
* THIS ENTRY IS AN ACCESS-CONTROL SURFACE, not an ordinary barrel. The host
|
|
8
|
+
* import map serves it to every untrusted widget bundle, so anything exported
|
|
9
|
+
* here is callable by widget code. Before adding an export, ask who outside
|
|
10
|
+
* this package needs it: in-package callers use relative imports and need
|
|
11
|
+
* nothing here, host-only lifecycle belongs on claimHostApi(), and widget
|
|
12
|
+
* hooks belong on the ./solid subpath. Types are always safe; they carry no
|
|
13
|
+
* authority. scripts/check-widget-reachable-surface.ts guards this, but it is
|
|
14
|
+
* a DENYLIST of known-dangerous names and is blind to any handle invented
|
|
15
|
+
* after it was written — it cannot be relied on to catch a new mistake.
|
|
16
|
+
*
|
|
7
17
|
* @packageDocumentation
|
|
8
18
|
*/
|
|
9
19
|
export type { ConnectionOptions } from "./connection/manager";
|
|
@@ -39,6 +49,7 @@ export { fetchStatisticsDuringPeriod, normalizeStatisticTime, } from "./history/
|
|
|
39
49
|
export type { StatisticsPeriod, StatisticsQueryOptions, StatisticValue, } from "./history/statistics";
|
|
40
50
|
export type { EntityHistoryData, EntityHistoryQueryOptions, EntityHistoryResult, EntityHistoryState, HistoryData, HistoryQueryOptions, HistoryResult, HistoryStates, TimelineState, } from "./history/types";
|
|
41
51
|
export { fetchForecast, fetchForecastData, fetchForecasts } from "./weather/fetch";
|
|
52
|
+
export type { CalendarEvent, CalendarEventsData, CalendarWindowOptions } from "./calendar/types";
|
|
42
53
|
export { getForecast, getForecasts, refreshForecast, refreshForecasts } from "./weather/query";
|
|
43
54
|
export type { EntityForecastQueryOptions, ForecastQueryOptions, ForecastResult, ForecastsResult, ForecastType, WeatherForecast, WeatherForecastData, WeatherForecasts, WeatherForecastsData, } from "./weather/types";
|
|
44
55
|
export { applyDemoServiceCall, createDemoFixtures, isDemoMode, startDemoEnergyTicker, stopDemoEnergyTicker, } from "./demo/demo-provider";
|
package/dist/index.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { applyDemoServiceCall, batchEntityUpdates, batchServiceCalls, buildAreaView, buildDeviceView, buildEntityView, callService, createDemoFixtures, demoEnergyPreferences, entityViewEquals, extractDomain, fetchStatisticsDuringPeriod, forceResubscribe, getAllEntityViews, getAreaView, getAreaViews, getEntityView, getEntityViews, isDemoMode, isEnergyEntity, loadDemoData, normalizeStatisticTime, privilegedConn, registerEntity, sendCommand, setManagerConnection, setPrivilegedConn, setResubscribeHandler, setState, startDemoEnergyTicker, state, stopDemoEnergyTicker, synthesizeEnergyHistory, toggle, turnOff, turnOn, unloadDemoData, updateEntity } from "./statistics-
|
|
2
|
-
import { runHaBridgeWorker } from "./worker-main-
|
|
1
|
+
import { applyDemoServiceCall, batchEntityUpdates, batchServiceCalls, buildAreaView, buildDeviceView, buildEntityView, callService, createDemoFixtures, demoEnergyPreferences, entityViewEquals, extractDomain, fetchStatisticsDuringPeriod, forceResubscribe, forceResubscribeCalendars, getAllEntityViews, getAreaView, getAreaViews, getEntityView, getEntityViews, isDemoMode, isEnergyEntity, loadDemoData, normalizeStatisticTime, privilegedConn, registerEntity, sendCommand, setManagerConnection, setPrivilegedConn, setResubscribeHandler, setState, startDemoEnergyTicker, state, stopDemoEnergyTicker, synthesizeEnergyHistory, toggle, turnOff, turnOn, unloadDemoData, updateEntity } from "./statistics-CtRfIF6D.js";
|
|
2
|
+
import { runHaBridgeWorker } from "./worker-main-C-3QM4di.js";
|
|
3
3
|
import { createConnection, getAuth } from "home-assistant-js-websocket";
|
|
4
4
|
import { produce } from "solid-js/store";
|
|
5
5
|
|
|
@@ -2055,6 +2055,7 @@ async function attachBridgeToStore(bridge, hassUrl, mediaProxyBase) {
|
|
|
2055
2055
|
}));
|
|
2056
2056
|
await loadInitialData(bridge.conn);
|
|
2057
2057
|
await subscribeToUpdates(bridge.conn);
|
|
2058
|
+
await forceResubscribeCalendars();
|
|
2058
2059
|
}
|
|
2059
2060
|
/** Map worker connection-state messages onto the store. */
|
|
2060
2061
|
function applyBridgeConnState(state$1) {
|
|
@@ -2067,6 +2068,7 @@ function applyBridgeConnState(state$1) {
|
|
|
2067
2068
|
async function reloadAfterBridgeReconnect(bridge) {
|
|
2068
2069
|
await loadInitialData(bridge.conn);
|
|
2069
2070
|
await forceResubscribe();
|
|
2071
|
+
await forceResubscribeCalendars();
|
|
2070
2072
|
}
|
|
2071
2073
|
/** Detach on shutdown; the caller terminates the worker itself. */
|
|
2072
2074
|
function detachBridgeFromStore() {
|
package/dist/solid/hooks.d.ts
CHANGED
|
@@ -14,6 +14,7 @@ import type { HassConfig, HassUnitSystem } from "@glasshome/ha-types";
|
|
|
14
14
|
import type { Accessor, Resource } from "solid-js";
|
|
15
15
|
import type { AreaView, CameraStreamData, ConnectionState, EntityHistoryData, EntityView, GlassHomeState, HassEntity, StatisticsQueryOptions, StatisticValue, WeatherForecastsData } from "@glasshome/sync-layer";
|
|
16
16
|
import { callService, toggle, turnOff, turnOn } from "../commands/service";
|
|
17
|
+
import type { CalendarEventsData, CalendarWindowOptions } from "../calendar/types";
|
|
17
18
|
/**
|
|
18
19
|
* Get a reactive EntityView for an entity ID.
|
|
19
20
|
*
|
|
@@ -97,6 +98,15 @@ export declare function useCamera(entityId: Accessor<string> | string): {
|
|
|
97
98
|
stream: Accessor<CameraStreamData | null>;
|
|
98
99
|
refresh: () => void;
|
|
99
100
|
};
|
|
101
|
+
/**
|
|
102
|
+
* Live calendar events for one calendar entity.
|
|
103
|
+
*
|
|
104
|
+
* Tracks the entity on mount (opening or joining the shared
|
|
105
|
+
* `calendar/event/subscribe`) and untracks on cleanup; HA pushes the initial
|
|
106
|
+
* snapshot and every later change, so the accessor is reactive with no
|
|
107
|
+
* polling. `undefined` until the subscription is established.
|
|
108
|
+
*/
|
|
109
|
+
export declare function useCalendarEvents(entityId: Accessor<string> | string, options?: CalendarWindowOptions): Accessor<CalendarEventsData | undefined>;
|
|
100
110
|
/**
|
|
101
111
|
* Get reactive weather forecast data.
|
|
102
112
|
*/
|
package/dist/solid/index.d.ts
CHANGED
|
@@ -6,5 +6,5 @@
|
|
|
6
6
|
*
|
|
7
7
|
* @packageDocumentation
|
|
8
8
|
*/
|
|
9
|
-
export { useArea, useAreas, useCamera, useConnection, useCurrency, useEntities, useEntity, useEntityHistory, useEntityState, useEntityStatistics, useForecast, useHassConfig, useLocale, useService, useStore, useTemperatureUnit, useToggle, useTurnOff, useTurnOn, useUnitSystem, } from "./hooks";
|
|
9
|
+
export { useArea, useAreas, useCalendarEvents, 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,4 +1,4 @@
|
|
|
1
|
-
import { buildAreaView, callService, entityViewEquals, extractDomain, fetchStatisticsDuringPeriod, getAreaViews, getEntityView, isDemoMode, privilegedConn, registerEntity, state, toggle, turnOff, turnOn } from "../statistics-
|
|
1
|
+
import { buildAreaView, callService, entityViewEquals, extractDomain, fetchStatisticsDuringPeriod, getAreaViews, getEntityView, isDemoMode, privilegedConn, registerEntity, state, toggle, trackCalendarEvents, turnOff, turnOn } from "../statistics-CtRfIF6D.js";
|
|
2
2
|
import { createEffect, createMemo, createResource, onCleanup } from "solid-js";
|
|
3
3
|
|
|
4
4
|
//#region src/solid/hooks.ts
|
|
@@ -203,6 +203,24 @@ function useCamera(entityId) {
|
|
|
203
203
|
};
|
|
204
204
|
}
|
|
205
205
|
/**
|
|
206
|
+
* Live calendar events for one calendar entity.
|
|
207
|
+
*
|
|
208
|
+
* Tracks the entity on mount (opening or joining the shared
|
|
209
|
+
* `calendar/event/subscribe`) and untracks on cleanup; HA pushes the initial
|
|
210
|
+
* snapshot and every later change, so the accessor is reactive with no
|
|
211
|
+
* polling. `undefined` until the subscription is established.
|
|
212
|
+
*/
|
|
213
|
+
function useCalendarEvents(entityId, options) {
|
|
214
|
+
const getId = typeof entityId === "function" ? entityId : () => entityId;
|
|
215
|
+
createEffect(() => {
|
|
216
|
+
const id = getId();
|
|
217
|
+
if (!id) return;
|
|
218
|
+
const untrack = trackCalendarEvents(id, options);
|
|
219
|
+
onCleanup(untrack);
|
|
220
|
+
});
|
|
221
|
+
return createMemo(() => state.calendars[getId()]);
|
|
222
|
+
}
|
|
223
|
+
/**
|
|
206
224
|
* Get reactive weather forecast data.
|
|
207
225
|
*/
|
|
208
226
|
function useForecast(entityId) {
|
|
@@ -256,4 +274,4 @@ const byLabel = () => {
|
|
|
256
274
|
const allEntityIds = () => Object.keys(state.entities);
|
|
257
275
|
|
|
258
276
|
//#endregion
|
|
259
|
-
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 };
|
|
277
|
+
export { allEntityIds, byArea, byDevice, byDomain, byFloor, byLabel, useArea, useAreas, useCalendarEvents, useCamera, useConnection, useCurrency, useEntities, useEntity, useEntityHistory, useEntityState, useEntityStatistics, useForecast, useHassConfig, useLocale, useService, useStore, useTemperatureUnit, useToggle, useTurnOff, useTurnOn, useUnitSystem };
|
|
@@ -17,6 +17,7 @@ const initialState = {
|
|
|
17
17
|
statisticsMetadata: {},
|
|
18
18
|
history: {},
|
|
19
19
|
forecasts: {},
|
|
20
|
+
calendars: {},
|
|
20
21
|
streams: {}
|
|
21
22
|
};
|
|
22
23
|
if (globalThis.__GH_SYNC_LAYER_STORE__) console.error("[@glasshome/sync-layer] DUPLICATE store module instantiated. This means @glasshome/sync-layer was bundled into >1 chunk. Reads and writes will hit different singletons. Check vendor build externals and relative-vs-bare imports across entry points.");
|
|
@@ -25,6 +26,206 @@ const storeTuple = createStore(initialState);
|
|
|
25
26
|
const state = storeTuple[0];
|
|
26
27
|
const setState = storeTuple[1];
|
|
27
28
|
|
|
29
|
+
//#endregion
|
|
30
|
+
//#region src/core/privileged-conn.ts
|
|
31
|
+
let privileged = null;
|
|
32
|
+
/** Read the live connection. Null before connect and after disconnect. */
|
|
33
|
+
function privilegedConn() {
|
|
34
|
+
return privileged;
|
|
35
|
+
}
|
|
36
|
+
/** Set by the connection layer on connect/disconnect. Host-side callers only. */
|
|
37
|
+
function setPrivilegedConn(conn$1) {
|
|
38
|
+
privileged = conn$1;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
//#endregion
|
|
42
|
+
//#region src/calendar/track.ts
|
|
43
|
+
/**
|
|
44
|
+
* Ref-counted live calendar tracking, the per-entity analog of
|
|
45
|
+
* `connection/subscription-manager`. Each tracked calendar entity holds one
|
|
46
|
+
* upstream `calendar/event/subscribe` (HA pushes an initial snapshot, then
|
|
47
|
+
* debounced updates); trackers declare desired windows and a reconciler keeps
|
|
48
|
+
* the subscription matched to the union. Data lands in `state.calendars`;
|
|
49
|
+
* read-scoping for widget consumers arrives with WS-8 like every other store
|
|
50
|
+
* read.
|
|
51
|
+
*/
|
|
52
|
+
const DEFAULT_DAYS_BEHIND = 7;
|
|
53
|
+
const DEFAULT_DAYS_AHEAD = 35;
|
|
54
|
+
/** Debounce for untrack, so dashboard switches don't churn subscriptions. */
|
|
55
|
+
const UNTRACK_FLUSH_DELAY_MS = 5e3;
|
|
56
|
+
const DAY_MS = 24 * 60 * 60 * 1e3;
|
|
57
|
+
const trackers = /* @__PURE__ */ new Map();
|
|
58
|
+
const active = /* @__PURE__ */ new Map();
|
|
59
|
+
/** Per-entity reconcile chain; serializes subscribe/unsubscribe per calendar. */
|
|
60
|
+
const chains = /* @__PURE__ */ new Map();
|
|
61
|
+
let untrackTimer = null;
|
|
62
|
+
let rollTimer = null;
|
|
63
|
+
/** Day-quantized union of all tracker windows, so equal requests within a day
|
|
64
|
+
reconcile to the same subscription instead of churning. */
|
|
65
|
+
function windowFor(list) {
|
|
66
|
+
let behind = 0;
|
|
67
|
+
let ahead = 0;
|
|
68
|
+
for (const t of list) {
|
|
69
|
+
if (t.daysBehind > behind) behind = t.daysBehind;
|
|
70
|
+
if (t.daysAhead > ahead) ahead = t.daysAhead;
|
|
71
|
+
}
|
|
72
|
+
const dayStart = /* @__PURE__ */ new Date();
|
|
73
|
+
dayStart.setHours(0, 0, 0, 0);
|
|
74
|
+
return {
|
|
75
|
+
start: new Date(dayStart.getTime() - behind * DAY_MS).toISOString(),
|
|
76
|
+
end: new Date(dayStart.getTime() + (ahead + 1) * DAY_MS).toISOString()
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
/**
|
|
80
|
+
* Start tracking live events for a calendar entity. Returns the untrack
|
|
81
|
+
* function. Safe while disconnected: the subscription activates on the next
|
|
82
|
+
* `forceResubscribeCalendars()` (called by the connection layer on connect
|
|
83
|
+
* and reconnect).
|
|
84
|
+
*/
|
|
85
|
+
function trackCalendarEvents(entityId, options) {
|
|
86
|
+
const tracker = {
|
|
87
|
+
daysBehind: options?.daysBehind ?? DEFAULT_DAYS_BEHIND,
|
|
88
|
+
daysAhead: options?.daysAhead ?? DEFAULT_DAYS_AHEAD
|
|
89
|
+
};
|
|
90
|
+
const list = trackers.get(entityId) ?? [];
|
|
91
|
+
list.push(tracker);
|
|
92
|
+
trackers.set(entityId, list);
|
|
93
|
+
scheduleFlush$1();
|
|
94
|
+
ensureRollTimer();
|
|
95
|
+
let untracked = false;
|
|
96
|
+
return () => {
|
|
97
|
+
if (untracked) return;
|
|
98
|
+
untracked = true;
|
|
99
|
+
untrackCalendarEvents(entityId, tracker);
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
function untrackCalendarEvents(entityId, tracker) {
|
|
103
|
+
const list = trackers.get(entityId);
|
|
104
|
+
if (!list) return;
|
|
105
|
+
const i = list.indexOf(tracker);
|
|
106
|
+
if (i !== -1) list.splice(i, 1);
|
|
107
|
+
if (list.length === 0) trackers.delete(entityId);
|
|
108
|
+
if (untrackTimer) return;
|
|
109
|
+
untrackTimer = setTimeout(() => {
|
|
110
|
+
untrackTimer = null;
|
|
111
|
+
flushAll();
|
|
112
|
+
}, UNTRACK_FLUSH_DELAY_MS);
|
|
113
|
+
}
|
|
114
|
+
/**
|
|
115
|
+
* Reconcile every tracked calendar, dropping current subscription handles
|
|
116
|
+
* first. The connection layer calls this on connect and after reconnect:
|
|
117
|
+
* an unchanged window is no proof the old subscription is alive, so each
|
|
118
|
+
* tracked entity resubscribes (HA re-pushes the snapshot, refreshing the
|
|
119
|
+
* store through any missed changes).
|
|
120
|
+
*/
|
|
121
|
+
async function forceResubscribeCalendars() {
|
|
122
|
+
for (const entry of active.values()) {
|
|
123
|
+
const dead = entry.unsub;
|
|
124
|
+
entry.unsub = null;
|
|
125
|
+
if (dead) Promise.resolve().then(dead).catch(() => {});
|
|
126
|
+
}
|
|
127
|
+
await flushAll();
|
|
128
|
+
}
|
|
129
|
+
function scheduleFlush$1() {
|
|
130
|
+
queueMicrotask(() => {
|
|
131
|
+
flushAll();
|
|
132
|
+
});
|
|
133
|
+
}
|
|
134
|
+
async function flushAll() {
|
|
135
|
+
const ids = new Set([...trackers.keys(), ...active.keys()]);
|
|
136
|
+
await Promise.all([...ids].map(reconcileEntity));
|
|
137
|
+
if (trackers.size === 0) stopRollTimer();
|
|
138
|
+
}
|
|
139
|
+
function reconcileEntity(entityId) {
|
|
140
|
+
const next = (chains.get(entityId) ?? Promise.resolve()).then(() => runReconcile(entityId));
|
|
141
|
+
chains.set(entityId, next.catch(() => {}));
|
|
142
|
+
return next;
|
|
143
|
+
}
|
|
144
|
+
async function runReconcile(entityId) {
|
|
145
|
+
const list = trackers.get(entityId);
|
|
146
|
+
const current = active.get(entityId);
|
|
147
|
+
if (!list || list.length === 0) {
|
|
148
|
+
if (current) {
|
|
149
|
+
active.delete(entityId);
|
|
150
|
+
try {
|
|
151
|
+
await current.unsub?.();
|
|
152
|
+
} catch {}
|
|
153
|
+
}
|
|
154
|
+
chains.delete(entityId);
|
|
155
|
+
setState(produce((s) => {
|
|
156
|
+
delete s.calendars[entityId];
|
|
157
|
+
}));
|
|
158
|
+
return;
|
|
159
|
+
}
|
|
160
|
+
const conn$1 = privilegedConn();
|
|
161
|
+
if (!conn$1) return;
|
|
162
|
+
const win = windowFor(list);
|
|
163
|
+
if (current?.unsub && current.start === win.start && current.end === win.end) return;
|
|
164
|
+
setState(produce((s) => {
|
|
165
|
+
s.calendars[entityId] = {
|
|
166
|
+
entityId,
|
|
167
|
+
events: s.calendars[entityId]?.events ?? [],
|
|
168
|
+
loading: true,
|
|
169
|
+
error: null,
|
|
170
|
+
start: win.start,
|
|
171
|
+
end: win.end
|
|
172
|
+
};
|
|
173
|
+
}));
|
|
174
|
+
const old = current?.unsub ?? null;
|
|
175
|
+
const entry = {
|
|
176
|
+
start: win.start,
|
|
177
|
+
end: win.end,
|
|
178
|
+
unsub: null
|
|
179
|
+
};
|
|
180
|
+
active.set(entityId, entry);
|
|
181
|
+
try {
|
|
182
|
+
entry.unsub = await conn$1.subscribeMessage((msg) => {
|
|
183
|
+
setState(produce((s) => {
|
|
184
|
+
const rec = s.calendars[entityId];
|
|
185
|
+
if (!rec) return;
|
|
186
|
+
rec.events = msg.events ?? [];
|
|
187
|
+
rec.loading = false;
|
|
188
|
+
rec.error = null;
|
|
189
|
+
}));
|
|
190
|
+
}, {
|
|
191
|
+
type: "calendar/event/subscribe",
|
|
192
|
+
entity_id: entityId,
|
|
193
|
+
start: win.start,
|
|
194
|
+
end: win.end
|
|
195
|
+
});
|
|
196
|
+
} catch (err) {
|
|
197
|
+
active.delete(entityId);
|
|
198
|
+
setState(produce((s) => {
|
|
199
|
+
const rec = s.calendars[entityId];
|
|
200
|
+
if (!rec) return;
|
|
201
|
+
rec.loading = false;
|
|
202
|
+
rec.error = err instanceof Error ? err : new Error(String(err));
|
|
203
|
+
}));
|
|
204
|
+
}
|
|
205
|
+
if (old) try {
|
|
206
|
+
await old();
|
|
207
|
+
} catch {}
|
|
208
|
+
}
|
|
209
|
+
/** One timer for the whole module: at local midnight the day-quantized
|
|
210
|
+
windows shift, so a reconcile slides every subscription forward. Only
|
|
211
|
+
armed while something is tracked — an idle app holds no timer. */
|
|
212
|
+
function ensureRollTimer() {
|
|
213
|
+
if (rollTimer || trackers.size === 0) return;
|
|
214
|
+
const now$1 = /* @__PURE__ */ new Date();
|
|
215
|
+
const next = new Date(now$1);
|
|
216
|
+
next.setHours(24, 0, 30, 0);
|
|
217
|
+
rollTimer = setTimeout(() => {
|
|
218
|
+
rollTimer = null;
|
|
219
|
+
flushAll().then(ensureRollTimer);
|
|
220
|
+
}, next.getTime() - now$1.getTime());
|
|
221
|
+
}
|
|
222
|
+
function stopRollTimer() {
|
|
223
|
+
if (rollTimer) {
|
|
224
|
+
clearTimeout(rollTimer);
|
|
225
|
+
rollTimer = null;
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
|
|
28
229
|
//#endregion
|
|
29
230
|
//#region src/connection/subscription-manager.ts
|
|
30
231
|
if (globalThis.__GH_SYNC_LAYER_SUB_MGR__) console.error("[@glasshome/sync-layer] DUPLICATE subscription-manager module instantiated. registerEntity() and the connection setter will hit different singletons → subscribe_entities will never be sent. Cause: relative import of subscription-manager from a separate entry point.");
|
|
@@ -1032,6 +1233,18 @@ function createDemoFixtures() {
|
|
|
1032
1233
|
device_class: "battery",
|
|
1033
1234
|
unit_of_measurement: "%"
|
|
1034
1235
|
});
|
|
1236
|
+
add(makeEntity("sensor.electricity_maps_co2_intensity", "214", {
|
|
1237
|
+
unit_of_measurement: "gCO2eq/kWh",
|
|
1238
|
+
state_class: "measurement"
|
|
1239
|
+
}), { unit_of_measurement: "gCO2eq/kWh" });
|
|
1240
|
+
add(makeEntity("sensor.electricity_maps_fossil_fuel_percentage", "31", {
|
|
1241
|
+
unit_of_measurement: "%",
|
|
1242
|
+
state_class: "measurement"
|
|
1243
|
+
}), { unit_of_measurement: "%" });
|
|
1244
|
+
add(makeEntity("sensor.nordpool_current_price", "0.18", {
|
|
1245
|
+
unit_of_measurement: "EUR/kWh",
|
|
1246
|
+
state_class: "measurement"
|
|
1247
|
+
}), { unit_of_measurement: "EUR/kWh" });
|
|
1035
1248
|
add(makeEntity("binary_sensor.front_door", "off", { device_class: "door" }), {
|
|
1036
1249
|
device_id: "door_sensor",
|
|
1037
1250
|
area_id: "entry",
|
|
@@ -1571,18 +1784,6 @@ function applyTurnOff(e, domain) {
|
|
|
1571
1784
|
} else e.state = "off";
|
|
1572
1785
|
}
|
|
1573
1786
|
|
|
1574
|
-
//#endregion
|
|
1575
|
-
//#region src/core/privileged-conn.ts
|
|
1576
|
-
let privileged = null;
|
|
1577
|
-
/** Read the live connection. Null before connect and after disconnect. */
|
|
1578
|
-
function privilegedConn() {
|
|
1579
|
-
return privileged;
|
|
1580
|
-
}
|
|
1581
|
-
/** Set by the connection layer on connect/disconnect. Host-side callers only. */
|
|
1582
|
-
function setPrivilegedConn(conn$1) {
|
|
1583
|
-
privileged = conn$1;
|
|
1584
|
-
}
|
|
1585
|
-
|
|
1586
1787
|
//#endregion
|
|
1587
1788
|
//#region src/commands/service.ts
|
|
1588
1789
|
/**
|
|
@@ -1971,4 +2172,4 @@ async function fetchStatisticsDuringPeriod(connection, statisticIds, options) {
|
|
|
1971
2172
|
}
|
|
1972
2173
|
|
|
1973
2174
|
//#endregion
|
|
1974
|
-
export { applyDemoServiceCall, batchEntityUpdates, batchServiceCalls, buildAreaView, buildDeviceView, buildEntityView, callService, createDemoFixtures, demoEnergyPreferences, entityViewEquals, extractDomain, fetchStatisticsDuringPeriod, forceResubscribe, getAllEntityViews, getAreaView, getAreaViews, getEntityView, getEntityViews, isDemoMode, isEnergyEntity, loadDemoData, normalizeStatisticTime, privilegedConn, registerEntity, sendCommand, setManagerConnection, setPrivilegedConn, setResubscribeHandler, setState, startDemoEnergyTicker, state, stopDemoEnergyTicker, synthesizeEnergyHistory, toggle, turnOff, turnOn, unloadDemoData, updateEntity };
|
|
2175
|
+
export { applyDemoServiceCall, batchEntityUpdates, batchServiceCalls, buildAreaView, buildDeviceView, buildEntityView, callService, createDemoFixtures, demoEnergyPreferences, entityViewEquals, extractDomain, fetchStatisticsDuringPeriod, forceResubscribe, forceResubscribeCalendars, getAllEntityViews, getAreaView, getAreaViews, getEntityView, getEntityViews, isDemoMode, isEnergyEntity, loadDemoData, normalizeStatisticTime, privilegedConn, registerEntity, sendCommand, setManagerConnection, setPrivilegedConn, setResubscribeHandler, setState, startDemoEnergyTicker, state, stopDemoEnergyTicker, synthesizeEnergyHistory, toggle, trackCalendarEvents, turnOff, turnOn, unloadDemoData, updateEntity };
|
|
@@ -113,10 +113,14 @@ export interface WidgetServiceCall {
|
|
|
113
113
|
service: string;
|
|
114
114
|
data?: Record<string, unknown>;
|
|
115
115
|
target?: Record<string, unknown>;
|
|
116
|
+
/** Opt in to HA's service response (return_response). Only set for services
|
|
117
|
+
that support/require it; HA errors otherwise. */
|
|
118
|
+
returnResponse?: boolean;
|
|
116
119
|
}
|
|
117
120
|
export type WidgetServiceResult = {
|
|
118
121
|
id: number;
|
|
119
122
|
ok: true;
|
|
123
|
+
result?: unknown;
|
|
120
124
|
} | {
|
|
121
125
|
id: number;
|
|
122
126
|
ok: false;
|
|
@@ -239,16 +239,18 @@ function runHaBridgeWorker(scope) {
|
|
|
239
239
|
return;
|
|
240
240
|
}
|
|
241
241
|
try {
|
|
242
|
-
await conn.sendMessagePromise({
|
|
242
|
+
const res = await conn.sendMessagePromise({
|
|
243
243
|
type: "call_service",
|
|
244
244
|
domain: call.domain,
|
|
245
245
|
service: call.service,
|
|
246
246
|
service_data: call.data ?? {},
|
|
247
|
-
...call.target ? { target: call.target } : {}
|
|
247
|
+
...call.target ? { target: call.target } : {},
|
|
248
|
+
...call.returnResponse ? { return_response: true } : {}
|
|
248
249
|
});
|
|
249
250
|
reply({
|
|
250
251
|
id: call.id,
|
|
251
|
-
ok: true
|
|
252
|
+
ok: true,
|
|
253
|
+
...call.returnResponse ? { result: res.response } : {}
|
|
252
254
|
});
|
|
253
255
|
} catch (err) {
|
|
254
256
|
reply({
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@glasshome/sync-layer",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.7.1",
|
|
4
4
|
"description": "Type-safe, reactive state synchronization layer for Home Assistant",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -51,7 +51,7 @@
|
|
|
51
51
|
},
|
|
52
52
|
"dependencies": {
|
|
53
53
|
"home-assistant-js-websocket": "^9.6.0",
|
|
54
|
-
"@glasshome/widget-contract": "0.
|
|
54
|
+
"@glasshome/widget-contract": "0.2.0"
|
|
55
55
|
},
|
|
56
56
|
"devDependencies": {
|
|
57
57
|
"@glasshome/ha-types": "0.1.2",
|