@glasshome/sync-layer 0.1.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.
Files changed (61) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +120 -0
  3. package/dist/area-views-B-m8vwce.js +321 -0
  4. package/dist/camera/fetch.d.ts +15 -0
  5. package/dist/camera/query.d.ts +15 -0
  6. package/dist/camera/types.d.ts +85 -0
  7. package/dist/camera/webrtc.d.ts +31 -0
  8. package/dist/commands/service.d.ts +53 -0
  9. package/dist/commands/types.d.ts +113 -0
  10. package/dist/connection/adapter.d.ts +48 -0
  11. package/dist/connection/auth.d.ts +126 -0
  12. package/dist/connection/manager.d.ts +42 -0
  13. package/dist/connection/subscriptions.d.ts +25 -0
  14. package/dist/connection/types.d.ts +161 -0
  15. package/dist/core/reducers.d.ts +99 -0
  16. package/dist/core/store.d.ts +64 -0
  17. package/dist/core/types.d.ts +336 -0
  18. package/dist/demo/demo-data.d.ts +24 -0
  19. package/dist/demo/demo-provider.d.ts +33 -0
  20. package/dist/demo/demo-provider.js +103 -0
  21. package/dist/demo-data-D8JXWRpw.js +419 -0
  22. package/dist/entities/area-views.d.ts +25 -0
  23. package/dist/entities/index.d.ts +54 -0
  24. package/dist/entities/views.d.ts +30 -0
  25. package/dist/history/constants.d.ts +16 -0
  26. package/dist/history/fetch.d.ts +19 -0
  27. package/dist/history/query.d.ts +13 -0
  28. package/dist/history/types.d.ts +146 -0
  29. package/dist/index.d.ts +38 -0
  30. package/dist/index.js +1747 -0
  31. package/dist/query/area-builder.d.ts +25 -0
  32. package/dist/query/builder.d.ts +18 -0
  33. package/dist/query/helpers.d.ts +21 -0
  34. package/dist/query/memoization.d.ts +18 -0
  35. package/dist/query/projections.d.ts +74 -0
  36. package/dist/query/types.d.ts +260 -0
  37. package/dist/solid/hooks.d.ts +88 -0
  38. package/dist/solid/index.d.ts +10 -0
  39. package/dist/solid/index.js +176 -0
  40. package/dist/solid/indices.d.ts +14 -0
  41. package/dist/store-B1sbyexX.js +54 -0
  42. package/dist/testing/fixtures.d.ts +161 -0
  43. package/dist/testing/helpers.d.ts +160 -0
  44. package/dist/testing/index.d.ts +13 -0
  45. package/dist/testing/index.js +2047 -0
  46. package/dist/testing/mock-behaviors/base.d.ts +73 -0
  47. package/dist/testing/mock-behaviors/climate.d.ts +22 -0
  48. package/dist/testing/mock-behaviors/cover.d.ts +22 -0
  49. package/dist/testing/mock-behaviors/index.d.ts +11 -0
  50. package/dist/testing/mock-behaviors/light.d.ts +20 -0
  51. package/dist/testing/mock-behaviors/lock.d.ts +20 -0
  52. package/dist/testing/mock-behaviors/media_player.d.ts +42 -0
  53. package/dist/testing/mock-behaviors/registry.d.ts +55 -0
  54. package/dist/testing/mock-behaviors/switch.d.ts +17 -0
  55. package/dist/testing/mock.d.ts +191 -0
  56. package/dist/testing/types.d.ts +87 -0
  57. package/dist/types-Csog0nqk.js +10 -0
  58. package/dist/weather/fetch.d.ts +19 -0
  59. package/dist/weather/query.d.ts +23 -0
  60. package/dist/weather/types.d.ts +126 -0
  61. package/package.json +85 -0
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 GlassHome Labs
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,120 @@
1
+ # @glasshome/sync-layer
2
+
3
+ Type-safe, reactive state synchronization layer for Home Assistant.
4
+
5
+ Provides connection management, entity querying, service calls, and reactive state via a SolidJS store — all with full TypeScript types powered by `@glasshome/ha-types`.
6
+
7
+ ## Install
8
+
9
+ ```bash
10
+ npm install @glasshome/sync-layer @glasshome/ha-types
11
+ # or
12
+ bun add @glasshome/sync-layer @glasshome/ha-types
13
+ ```
14
+
15
+ ## Quick Start
16
+
17
+ ```typescript
18
+ import {
19
+ initConnection,
20
+ getConnection,
21
+ state,
22
+ entity,
23
+ entities,
24
+ callService,
25
+ } from "@glasshome/sync-layer";
26
+
27
+ // Connect to Home Assistant
28
+ await initConnection({
29
+ hassUrl: "http://homeassistant.local:8123",
30
+ token: "YOUR_LONG_LIVED_ACCESS_TOKEN",
31
+ });
32
+
33
+ // Access reactive state
34
+ console.log(state.connectionState); // "connected"
35
+
36
+ // Query a single entity
37
+ const light = entity("light.living_room").get();
38
+
39
+ // Query multiple entities by domain
40
+ const allLights = entities().domain("light").list();
41
+
42
+ // Call a service
43
+ await callService("light", "turn_on", {
44
+ entity_id: "light.living_room",
45
+ brightness: 200,
46
+ });
47
+ ```
48
+
49
+ ## Subpath Imports
50
+
51
+ ### `@glasshome/sync-layer/solid`
52
+
53
+ SolidJS hooks for reactive UI integration. Requires `solid-js` as a peer dependency.
54
+
55
+ ```typescript
56
+ import {
57
+ useEntity,
58
+ useEntities,
59
+ useConnection,
60
+ useStore,
61
+ useService,
62
+ useToggle,
63
+ useArea,
64
+ useCamera,
65
+ useEntityHistory,
66
+ useForecast,
67
+ } from "@glasshome/sync-layer/solid";
68
+
69
+ // Reactive entity access in SolidJS components
70
+ const light = useEntity("light.living_room");
71
+ const connection = useConnection();
72
+ const toggle = useToggle("light.living_room");
73
+ ```
74
+
75
+ ### `@glasshome/sync-layer/testing`
76
+
77
+ Mock utilities for testing without a live Home Assistant instance.
78
+
79
+ ```typescript
80
+ import {
81
+ MockConnection,
82
+ createEntity,
83
+ createArea,
84
+ createBasicFixtures,
85
+ simulateStateChange,
86
+ waitForConnection,
87
+ getStoreSnapshot,
88
+ } from "@glasshome/sync-layer/testing";
89
+
90
+ const mock = new MockConnection();
91
+ await mock.connect();
92
+
93
+ simulateStateChange("light.test", { state: "on" });
94
+ ```
95
+
96
+ ### `@glasshome/sync-layer/demo`
97
+
98
+ Demo mode provider for loading fixture data without a real backend.
99
+
100
+ ```typescript
101
+ import {
102
+ loadDemoData,
103
+ unloadDemoData,
104
+ isDemoMode,
105
+ applyDemoServiceCall,
106
+ } from "@glasshome/sync-layer/demo";
107
+
108
+ await loadDemoData(); // Populates store with demo entities
109
+ ```
110
+
111
+ ## Peer Dependencies
112
+
113
+ | Package | Required | Notes |
114
+ |---------|----------|-------|
115
+ | `@glasshome/ha-types` | Yes | TypeScript types for Home Assistant entities, services, and events |
116
+ | `solid-js` | Only for `./solid` subpath | SolidJS reactive primitives used by the hooks adapter |
117
+
118
+ ## License
119
+
120
+ MIT
@@ -0,0 +1,321 @@
1
+ import { state } from "./store-B1sbyexX.js";
2
+ import { extractDomain } from "./types-Csog0nqk.js";
3
+ import { callService } from "home-assistant-js-websocket";
4
+
5
+ //#region src/commands/service.ts
6
+ /**
7
+ * Call a Home Assistant service
8
+ */
9
+ async function callService$1(domain, service, serviceData = {}, target = {}) {
10
+ const connection = state.conn;
11
+ if (!connection) throw new Error("Not connected to Home Assistant. Call initConnection() first.");
12
+ try {
13
+ const mockConnection = connection;
14
+ if (mockConnection && typeof mockConnection.callService === "function") await mockConnection.callService(domain, service, serviceData, target);
15
+ else await callService(connection, domain, service, serviceData, target);
16
+ } catch (error) {
17
+ throw new Error(`Service call failed: ${error.message || String(error)}`);
18
+ }
19
+ }
20
+ /**
21
+ * Turn on entity or entities
22
+ */
23
+ async function turnOn(entityId, serviceData = {}) {
24
+ const firstEntityId = Array.isArray(entityId) ? entityId[0] : entityId;
25
+ if (!firstEntityId) throw new Error("Entity ID is required");
26
+ const domain = firstEntityId.split(".")[0];
27
+ if (!domain) throw new Error(`Invalid entity ID: ${firstEntityId}`);
28
+ await callService$1(domain, "turn_on", serviceData, { entity_id: entityId });
29
+ }
30
+ /**
31
+ * Turn off entity or entities
32
+ */
33
+ async function turnOff(entityId, serviceData = {}) {
34
+ const firstEntityId = Array.isArray(entityId) ? entityId[0] : entityId;
35
+ if (!firstEntityId) throw new Error("Entity ID is required");
36
+ const domain = firstEntityId.split(".")[0];
37
+ if (!domain) throw new Error(`Invalid entity ID: ${firstEntityId}`);
38
+ await callService$1(domain, "turn_off", serviceData, { entity_id: entityId });
39
+ }
40
+ /**
41
+ * Toggle entity or entities
42
+ */
43
+ async function toggle(entityId, serviceData = {}) {
44
+ const firstEntityId = Array.isArray(entityId) ? entityId[0] : entityId;
45
+ if (!firstEntityId) throw new Error("Entity ID is required");
46
+ const domain = firstEntityId.split(".")[0];
47
+ if (!domain) throw new Error(`Invalid entity ID: ${firstEntityId}`);
48
+ await callService$1(domain, "toggle", serviceData, { entity_id: entityId });
49
+ }
50
+ /**
51
+ * Update entity registry entry
52
+ */
53
+ async function updateEntity(entityId, updates) {
54
+ const connection = state.conn;
55
+ if (!connection) throw new Error("Not connected to Home Assistant. Call initConnection() first.");
56
+ try {
57
+ const result = await connection.sendMessagePromise({
58
+ type: "config/entity_registry/update",
59
+ entity_id: entityId,
60
+ ...updates
61
+ });
62
+ if (result && typeof result === "object" && "_mockError" in result) throw new Error(`Entity registry entry not found: ${entityId}`);
63
+ return result;
64
+ } catch (error) {
65
+ if (error?.message && error.message.includes("Entity registry entry not found")) throw error;
66
+ throw new Error(`Entity update failed: ${error.message || String(error)}`);
67
+ }
68
+ }
69
+ /**
70
+ * Send a generic WebSocket command
71
+ */
72
+ async function sendCommand(command) {
73
+ const connection = state.conn;
74
+ if (!connection) throw new Error("Not connected");
75
+ try {
76
+ return await connection.sendMessagePromise(command);
77
+ } catch (error) {
78
+ throw new Error(`Command failed: ${error.message || String(error)}`);
79
+ }
80
+ }
81
+ /**
82
+ * Execute multiple service calls in parallel
83
+ */
84
+ async function batchServiceCalls(calls) {
85
+ await Promise.all(calls.map((call) => callService$1(call.domain, call.service, call.serviceData, call.target)));
86
+ }
87
+ /**
88
+ * Execute multiple entity updates in parallel
89
+ */
90
+ async function batchEntityUpdates(updates) {
91
+ return Promise.all(updates.map((u) => updateEntity(u.entityId, u.updates)));
92
+ }
93
+
94
+ //#endregion
95
+ //#region src/entities/index.ts
96
+ /**
97
+ * Get all entity IDs
98
+ */
99
+ function getAllEntityIds() {
100
+ return Object.keys(state.entities);
101
+ }
102
+ /**
103
+ * Get entities by domain
104
+ */
105
+ function getEntitiesByDomain(domain) {
106
+ return Object.keys(state.entities).filter((id) => extractDomain(id) === domain);
107
+ }
108
+ /**
109
+ * Get entities via devices in area
110
+ *
111
+ * Only includes entities that inherit area from device (entity.area_id is null).
112
+ */
113
+ function getEntitiesViaDevices(areaId) {
114
+ const devicesInArea = Object.values(state.devices).filter((device) => device.area_id === areaId);
115
+ const entityIds = [];
116
+ for (const device of devicesInArea) {
117
+ const deviceEntities = getEntitiesByDevice(device.id);
118
+ for (const entityId of deviceEntities) {
119
+ const registry = state.entityRegistry[entityId];
120
+ if (!registry?.area_id) entityIds.push(entityId);
121
+ }
122
+ }
123
+ return entityIds;
124
+ }
125
+ /**
126
+ * Get entities by area
127
+ *
128
+ * Returns entities with direct area_id assignments and entities that inherit their area from devices.
129
+ */
130
+ function getEntitiesByArea(areaId) {
131
+ const directEntityIds = [];
132
+ for (const [entityId, entry] of Object.entries(state.entityRegistry)) if (entry.area_id === areaId) directEntityIds.push(entityId);
133
+ const deviceEntityIds = getEntitiesViaDevices(areaId);
134
+ return Array.from(new Set([...directEntityIds, ...deviceEntityIds]));
135
+ }
136
+ /**
137
+ * Get entities by device
138
+ */
139
+ function getEntitiesByDevice(deviceId) {
140
+ const entityIds = [];
141
+ for (const [entityId, entry] of Object.entries(state.entityRegistry)) if (entry.device_id === deviceId) entityIds.push(entityId);
142
+ return entityIds;
143
+ }
144
+ /**
145
+ * Get entities by floor
146
+ */
147
+ function getEntitiesByFloor(floorId) {
148
+ const entityIds = [];
149
+ const areasOnFloor = Object.values(state.areas).filter((area) => area.floor_id === floorId);
150
+ for (const area of areasOnFloor) {
151
+ const areaEntities = getEntitiesByArea(area.id);
152
+ entityIds.push(...areaEntities);
153
+ }
154
+ return entityIds;
155
+ }
156
+ /**
157
+ * Get entities by label
158
+ */
159
+ function getEntitiesByLabel(labelId) {
160
+ const entityIds = [];
161
+ for (const [entityId, entry] of Object.entries(state.entityRegistry)) if (entry.labels?.includes(labelId)) entityIds.push(entityId);
162
+ return entityIds;
163
+ }
164
+
165
+ //#endregion
166
+ //#region src/entities/views.ts
167
+ /**
168
+ * Get icon for an entity from registry or attributes
169
+ */
170
+ function resolveIcon(entity, registry) {
171
+ if (registry?.icon) return {
172
+ icon: registry.icon,
173
+ source: "registry"
174
+ };
175
+ if (entity.attributes?.icon) return {
176
+ icon: entity.attributes.icon,
177
+ source: "attribute"
178
+ };
179
+ return null;
180
+ }
181
+ /**
182
+ * Build an EntityView from runtime state and registry metadata
183
+ */
184
+ function buildEntityView(entity, registry) {
185
+ const domain = extractDomain(entity.entity_id);
186
+ const friendlyName = entity.attributes.friendly_name ?? registry?.name ?? entity.entity_id;
187
+ const name = registry?.name ?? friendlyName;
188
+ const iconResolution = resolveIcon(entity, registry);
189
+ let areaId = registry?.area_id ?? null;
190
+ if (areaId === null && registry?.device_id) {
191
+ const device = state.devices[registry.device_id];
192
+ if (device) areaId = device.area_id ?? null;
193
+ }
194
+ return {
195
+ id: entity.entity_id,
196
+ domain,
197
+ state: entity.state,
198
+ attributes: entity.attributes,
199
+ lastChanged: new Date(entity.last_changed),
200
+ lastUpdated: new Date(entity.last_updated),
201
+ context: {
202
+ id: entity.context.id,
203
+ parentId: entity.context.parent_id,
204
+ userId: entity.context.user_id
205
+ },
206
+ name,
207
+ friendlyName,
208
+ areaId,
209
+ deviceId: registry?.device_id ?? null,
210
+ platform: registry?.platform ?? "unknown",
211
+ uniqueId: registry?.unique_id ?? null,
212
+ isDisabled: registry?.disabled_by !== null && registry?.disabled_by !== void 0,
213
+ isHidden: registry?.hidden_by !== null && registry?.hidden_by !== void 0,
214
+ icon: iconResolution?.icon ?? null,
215
+ iconSource: iconResolution?.source ?? "default",
216
+ entityCategory: registry?.entity_category ?? null,
217
+ labels: registry?.labels ?? [],
218
+ aliases: registry?.aliases ?? [],
219
+ deviceClass: registry?.device_class ?? entity.attributes.device_class ?? null,
220
+ unitOfMeasurement: registry?.unit_of_measurement ?? entity.attributes.unit_of_measurement ?? null,
221
+ supportedFeatures: registry?.supported_features ?? entity.attributes.supported_features
222
+ };
223
+ }
224
+ /**
225
+ * Get or build an EntityView
226
+ */
227
+ function getEntityView(entityId) {
228
+ const entity = state.entities[entityId];
229
+ if (!entity) return void 0;
230
+ const registry = state.entityRegistry[entityId];
231
+ return buildEntityView(entity, registry);
232
+ }
233
+ /**
234
+ * Get multiple entity views
235
+ */
236
+ function getEntityViews(entityIds) {
237
+ const views = [];
238
+ for (const id of entityIds) {
239
+ const view = getEntityView(id);
240
+ if (view) views.push(view);
241
+ }
242
+ return views;
243
+ }
244
+ /**
245
+ * Get all entity views
246
+ */
247
+ function getAllEntityViews() {
248
+ return getEntityViews(Object.keys(state.entities));
249
+ }
250
+
251
+ //#endregion
252
+ //#region src/entities/area-views.ts
253
+ /**
254
+ * Build a DeviceView from registry data
255
+ */
256
+ function buildDeviceView(device) {
257
+ return {
258
+ id: device.id,
259
+ name: device.name ?? "",
260
+ nameByUser: device.name_by_user ?? null,
261
+ manufacturer: device.manufacturer ?? null,
262
+ model: device.model ?? null,
263
+ swVersion: device.sw_version ?? null,
264
+ areaId: device.area_id ?? null,
265
+ configEntries: device.config_entries ?? [],
266
+ connections: device.connections ?? [],
267
+ identifiers: device.identifiers ?? [],
268
+ disabledBy: device.disabled_by ?? null
269
+ };
270
+ }
271
+ /**
272
+ * Build an AreaView from registry data using query engine
273
+ */
274
+ function buildAreaView(areaId) {
275
+ const area = state.areas[areaId];
276
+ if (!area) throw new Error(`Area ${areaId} not found`);
277
+ const directEntityIds = [];
278
+ for (const [entityId, entry] of Object.entries(state.entityRegistry)) if (entry.area_id === areaId) directEntityIds.push(entityId);
279
+ const deviceEntityIds = getEntitiesViaDevices(areaId);
280
+ const allEntityIds = Array.from(new Set([...directEntityIds, ...deviceEntityIds]));
281
+ const areaEntities = allEntityIds.length > 0 ? getEntityViews(allEntityIds) : [];
282
+ const devicesInArea = Object.values(state.devices).filter((device) => device.area_id === areaId);
283
+ const devices = devicesInArea.map((device) => buildDeviceView(device));
284
+ return {
285
+ id: areaId,
286
+ name: area.name,
287
+ normalizedName: area.normalized_name ?? area.name.toLowerCase(),
288
+ aliases: Array.isArray(area.aliases) ? area.aliases : [],
289
+ floorId: area.floor_id ?? null,
290
+ icon: area.icon ?? null,
291
+ picture: area.picture ?? null,
292
+ labels: Array.isArray(area.labels) ? area.labels : [],
293
+ temperatureEntityId: area.temperature_entity_id ?? null,
294
+ humidityEntityId: area.humidity_entity_id ?? null,
295
+ createdAt: area.created_at ?? (/* @__PURE__ */ new Date()).toISOString(),
296
+ modifiedAt: area.modified_at ?? (/* @__PURE__ */ new Date()).toISOString(),
297
+ entities: areaEntities,
298
+ devices,
299
+ entityIds: allEntityIds,
300
+ deviceIds: devices.map((d) => d.id)
301
+ };
302
+ }
303
+ /**
304
+ * Get all area views
305
+ */
306
+ function getAreaViews() {
307
+ return Object.keys(state.areas).map((areaId) => buildAreaView(areaId));
308
+ }
309
+ /**
310
+ * Get area view
311
+ */
312
+ function getAreaView(areaId) {
313
+ try {
314
+ return buildAreaView(areaId);
315
+ } catch {
316
+ return void 0;
317
+ }
318
+ }
319
+
320
+ //#endregion
321
+ export { batchEntityUpdates, batchServiceCalls, buildAreaView, buildDeviceView, buildEntityView, callService$1 as callService, getAllEntityIds, getAllEntityViews, getAreaView, getAreaViews, getEntitiesByArea, getEntitiesByDevice, getEntitiesByDomain, getEntitiesByFloor, getEntitiesByLabel, getEntityView, getEntityViews, sendCommand, toggle, turnOff, turnOn, updateEntity };
@@ -0,0 +1,15 @@
1
+ /**
2
+ * Camera Stream Fetching
3
+ *
4
+ * @packageDocumentation
5
+ */
6
+ import type { EntityId } from "../core/types";
7
+ import type { CameraStreamData, StreamFormat, StreamQueryOptions } from "./types";
8
+ /**
9
+ * Fetch stream for a camera entity
10
+ */
11
+ export declare function fetchStream(entityId: EntityId, format?: StreamFormat): Promise<string | null>;
12
+ /**
13
+ * Fetch stream data for a camera entity
14
+ */
15
+ export declare function fetchStreamData(entityId: EntityId, options?: StreamQueryOptions): Promise<CameraStreamData>;
@@ -0,0 +1,15 @@
1
+ /**
2
+ * Camera Stream Query Builder
3
+ *
4
+ * @packageDocumentation
5
+ */
6
+ import type { EntityId } from "../core/types";
7
+ import type { CameraStreamData, EntityStreamQueryOptions, StreamFormat } from "./types";
8
+ /**
9
+ * Get stream for a camera entity
10
+ */
11
+ export declare function getStream(entityId: EntityId, options?: EntityStreamQueryOptions): Promise<CameraStreamData>;
12
+ /**
13
+ * Refresh stream for a camera entity
14
+ */
15
+ export declare function refreshStream(entityId: EntityId, format?: StreamFormat): Promise<CameraStreamData>;
@@ -0,0 +1,85 @@
1
+ /**
2
+ * Camera Types
3
+ *
4
+ * Type definitions for camera stream data.
5
+ *
6
+ * @packageDocumentation
7
+ */
8
+ import type { EntityId } from "../core/types";
9
+ /**
10
+ * Stream format
11
+ * Supported stream formats from Home Assistant Camera API
12
+ */
13
+ export type StreamFormat = "hls" | "webrtc" | "mjpeg";
14
+ /**
15
+ * Camera stream data
16
+ */
17
+ export interface CameraStream {
18
+ /** Stream URL */
19
+ url: string | null;
20
+ /** Loading state */
21
+ loading: boolean;
22
+ /** Error if any */
23
+ error: Error | null;
24
+ /** Last fetched timestamp */
25
+ lastFetched: number | null;
26
+ /** Stream format */
27
+ format: StreamFormat;
28
+ /** Stream expiration time (if available) */
29
+ expiresAt?: number | null;
30
+ }
31
+ /**
32
+ * Camera stream data for an entity
33
+ */
34
+ export interface CameraStreamData {
35
+ /** Entity ID */
36
+ entityId: EntityId;
37
+ /** Stream data */
38
+ stream: CameraStream;
39
+ /** Loading state */
40
+ loading: boolean;
41
+ /** Error if any */
42
+ error: Error | null;
43
+ /** Last fetched timestamp */
44
+ lastFetched: number | null;
45
+ }
46
+ /**
47
+ * Camera stream data for multiple entities
48
+ */
49
+ export interface CameraStreams {
50
+ /** Entity ID -> Stream data */
51
+ [entityId: EntityId]: CameraStreamData;
52
+ }
53
+ /**
54
+ * Options for stream queries
55
+ */
56
+ export interface StreamQueryOptions {
57
+ /** Stream format (default: "hls") */
58
+ format?: StreamFormat;
59
+ /** Auto-refresh stream before expiration */
60
+ autoRefresh?: boolean;
61
+ /** Stream refresh interval in milliseconds (default: 5 minutes) */
62
+ refreshInterval?: number;
63
+ }
64
+ /**
65
+ * Single entity stream query options
66
+ */
67
+ export interface EntityStreamQueryOptions {
68
+ /** Stream format (default: "hls") */
69
+ format?: StreamFormat;
70
+ /** Auto-refresh stream before expiration */
71
+ autoRefresh?: boolean;
72
+ /** Stream refresh interval in milliseconds (default: 5 minutes) */
73
+ refreshInterval?: number;
74
+ }
75
+ /**
76
+ * Stream query result
77
+ */
78
+ export interface StreamResult {
79
+ /** Stream data */
80
+ data: CameraStreamData | null;
81
+ /** Loading state */
82
+ loading: boolean;
83
+ /** Error if any */
84
+ error: Error | null;
85
+ }
@@ -0,0 +1,31 @@
1
+ import type { EntityId } from "../core/types";
2
+ /** Minimal RTC types for sync-layer (no DOM lib dependency) */
3
+ type RtcConfiguration = Record<string, unknown>;
4
+ type RtcIceCandidateInit = Record<string, unknown>;
5
+ interface WebRtcClientConfig {
6
+ configuration: RtcConfiguration;
7
+ dataChannel?: string;
8
+ }
9
+ interface WebRtcSession {
10
+ sessionId: string | null;
11
+ unsubscribe: (() => Promise<void>) | null;
12
+ }
13
+ /**
14
+ * Get WebRTC client configuration (STUN/TURN servers) from HA
15
+ */
16
+ export declare function getWebRtcClientConfig(entityId: EntityId): Promise<WebRtcClientConfig>;
17
+ /**
18
+ * Start WebRTC session with HA.
19
+ * Returns a promise that resolves when the answer SDP is received.
20
+ * ICE candidates from HA are forwarded to the provided callback.
21
+ * Local ICE candidates should be sent via sendWebRtcCandidate().
22
+ */
23
+ export declare function startWebRtcSession(entityId: EntityId, sdpOffer: string, onCandidate: (candidate: RtcIceCandidateInit) => void): Promise<{
24
+ answer: string;
25
+ session: WebRtcSession;
26
+ }>;
27
+ /**
28
+ * Send a local ICE candidate to HA
29
+ */
30
+ export declare function sendWebRtcCandidate(entityId: EntityId, sessionId: string, candidate: RtcIceCandidateInit): Promise<void>;
31
+ export {};
@@ -0,0 +1,53 @@
1
+ /**
2
+ * Service calls and commands
3
+ *
4
+ * Type-safe wrappers for Home Assistant service calls and commands.
5
+ *
6
+ * @packageDocumentation
7
+ */
8
+ import type { Domain, ServiceCall, ServiceName, WsCommandType } from "@glasshome/ha-types";
9
+ import type { EntityId } from "../core/types";
10
+ import type { EntityUpdateFields, ServiceTarget } from "./types";
11
+ /**
12
+ * Call a Home Assistant service
13
+ */
14
+ export declare function callService(domain: Domain, service: ServiceName<Domain>, serviceData?: ServiceCall<Domain, ServiceName<Domain>>, target?: ServiceTarget): Promise<void>;
15
+ /**
16
+ * Turn on entity or entities
17
+ */
18
+ export declare function turnOn(entityId: EntityId | EntityId[], serviceData?: Record<string, any>): Promise<void>;
19
+ /**
20
+ * Turn off entity or entities
21
+ */
22
+ export declare function turnOff(entityId: EntityId | EntityId[], serviceData?: Record<string, any>): Promise<void>;
23
+ /**
24
+ * Toggle entity or entities
25
+ */
26
+ export declare function toggle(entityId: EntityId | EntityId[], serviceData?: Record<string, any>): Promise<void>;
27
+ /**
28
+ * Update entity registry entry
29
+ */
30
+ export declare function updateEntity(entityId: EntityId, updates: EntityUpdateFields): Promise<any>;
31
+ /**
32
+ * Send a generic WebSocket command
33
+ */
34
+ export declare function sendCommand<T = unknown>(command: {
35
+ type: WsCommandType | string;
36
+ [key: string]: unknown;
37
+ }): Promise<T>;
38
+ /**
39
+ * Execute multiple service calls in parallel
40
+ */
41
+ export declare function batchServiceCalls(calls: Array<{
42
+ domain: Domain;
43
+ service: ServiceName<Domain>;
44
+ serviceData?: ServiceCall<Domain, ServiceName<Domain>>;
45
+ target?: ServiceTarget;
46
+ }>): Promise<void>;
47
+ /**
48
+ * Execute multiple entity updates in parallel
49
+ */
50
+ export declare function batchEntityUpdates(updates: Array<{
51
+ entityId: EntityId;
52
+ updates: EntityUpdateFields;
53
+ }>): Promise<any[]>;