@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.
- package/LICENSE +21 -0
- package/README.md +120 -0
- package/dist/area-views-B-m8vwce.js +321 -0
- package/dist/camera/fetch.d.ts +15 -0
- package/dist/camera/query.d.ts +15 -0
- package/dist/camera/types.d.ts +85 -0
- package/dist/camera/webrtc.d.ts +31 -0
- package/dist/commands/service.d.ts +53 -0
- package/dist/commands/types.d.ts +113 -0
- package/dist/connection/adapter.d.ts +48 -0
- package/dist/connection/auth.d.ts +126 -0
- package/dist/connection/manager.d.ts +42 -0
- package/dist/connection/subscriptions.d.ts +25 -0
- package/dist/connection/types.d.ts +161 -0
- package/dist/core/reducers.d.ts +99 -0
- package/dist/core/store.d.ts +64 -0
- package/dist/core/types.d.ts +336 -0
- package/dist/demo/demo-data.d.ts +24 -0
- package/dist/demo/demo-provider.d.ts +33 -0
- package/dist/demo/demo-provider.js +103 -0
- package/dist/demo-data-D8JXWRpw.js +419 -0
- package/dist/entities/area-views.d.ts +25 -0
- package/dist/entities/index.d.ts +54 -0
- package/dist/entities/views.d.ts +30 -0
- package/dist/history/constants.d.ts +16 -0
- package/dist/history/fetch.d.ts +19 -0
- package/dist/history/query.d.ts +13 -0
- package/dist/history/types.d.ts +146 -0
- package/dist/index.d.ts +38 -0
- package/dist/index.js +1747 -0
- package/dist/query/area-builder.d.ts +25 -0
- package/dist/query/builder.d.ts +18 -0
- package/dist/query/helpers.d.ts +21 -0
- package/dist/query/memoization.d.ts +18 -0
- package/dist/query/projections.d.ts +74 -0
- package/dist/query/types.d.ts +260 -0
- package/dist/solid/hooks.d.ts +88 -0
- package/dist/solid/index.d.ts +10 -0
- package/dist/solid/index.js +176 -0
- package/dist/solid/indices.d.ts +14 -0
- package/dist/store-B1sbyexX.js +54 -0
- package/dist/testing/fixtures.d.ts +161 -0
- package/dist/testing/helpers.d.ts +160 -0
- package/dist/testing/index.d.ts +13 -0
- package/dist/testing/index.js +2047 -0
- package/dist/testing/mock-behaviors/base.d.ts +73 -0
- package/dist/testing/mock-behaviors/climate.d.ts +22 -0
- package/dist/testing/mock-behaviors/cover.d.ts +22 -0
- package/dist/testing/mock-behaviors/index.d.ts +11 -0
- package/dist/testing/mock-behaviors/light.d.ts +20 -0
- package/dist/testing/mock-behaviors/lock.d.ts +20 -0
- package/dist/testing/mock-behaviors/media_player.d.ts +42 -0
- package/dist/testing/mock-behaviors/registry.d.ts +55 -0
- package/dist/testing/mock-behaviors/switch.d.ts +17 -0
- package/dist/testing/mock.d.ts +191 -0
- package/dist/testing/types.d.ts +87 -0
- package/dist/types-Csog0nqk.js +10 -0
- package/dist/weather/fetch.d.ts +19 -0
- package/dist/weather/query.d.ts +23 -0
- package/dist/weather/types.d.ts +126 -0
- 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[]>;
|