@glasshome/sync-layer 0.6.0 → 0.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -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
+ }
@@ -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-C6JQBDVf.js";
2
- import { runHaBridgeWorker } from "./worker-main-DTbzEKor.js";
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() {
@@ -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
  */
@@ -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";
@@ -1,4 +1,4 @@
1
- import { buildAreaView, callService, entityViewEquals, extractDomain, fetchStatisticsDuringPeriod, getAreaViews, getEntityView, isDemoMode, privilegedConn, registerEntity, state, toggle, turnOff, turnOn } from "../statistics-C6JQBDVf.js";
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;
@@ -1,3 +1,3 @@
1
- import { runHaBridgeWorker } from "../worker-main-DTbzEKor.js";
1
+ import { runHaBridgeWorker } from "../worker-main-C-3QM4di.js";
2
2
 
3
3
  export { runHaBridgeWorker };
@@ -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.6.0",
3
+ "version": "0.7.0",
4
4
  "description": "Type-safe, reactive state synchronization layer for Home Assistant",
5
5
  "repository": {
6
6
  "type": "git",