@glasshome/sync-layer 0.1.0 → 0.1.2

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.
@@ -1,6 +1,6 @@
1
1
  import { setState, state } from "../store-B1sbyexX.js";
2
- import { extractDomain } from "../types-Csog0nqk.js";
3
- import { createDemoFixtures } from "../demo-data-D8JXWRpw.js";
2
+ import { extractDomain } from "../types-CxMcKkJQ.js";
3
+ import { createDemoFixtures } from "../demo-data-Bir7Px_e.js";
4
4
  import { produce, reconcile } from "solid-js/store";
5
5
 
6
6
  //#region src/demo/demo-provider.ts
package/dist/index.d.ts CHANGED
@@ -19,7 +19,8 @@ export type { AreaQueryBuilder, EntitiesQueryBuilder, EntityQueryBuilder, Unsubs
19
19
  export { batchEntityUpdates, batchServiceCalls, callService, sendCommand, toggle, turnOff, turnOn, updateEntity, } from "./commands/service";
20
20
  export type { CommandOptions, CommandResult, EntityUpdateFields, ServiceCallContext, ServiceTarget, } from "./commands/types";
21
21
  export type { GlassHomeState } from "./core/store";
22
- export type { AreaId, AreaView, DeviceId, DeviceView, EntityDomain, EntityId, EntityView, HassEntity, } from "./core/types";
22
+ export type { AreaId, AreaView, ConnectionState, DeviceId, DeviceView, EntityDomain, EntityId, EntityView, HassEntity, } from "./core/types";
23
+ export { extractDomain } from "./core/types";
23
24
  export { resetStore, setState, state } from "./core/store";
24
25
  export type { CallServiceRequest, CoreEventType, Domain, EntityCategory, EntityRegistryEntry, EventStateChangedData, HAEvent, RegistryEntryDisabler, RegistryEntryHider, ServiceCall, ServiceName, WsCommandType, WsResult, } from "@glasshome/ha-types";
25
26
  export { fetchStream, fetchStreamData } from "./camera/fetch";
package/dist/index.js CHANGED
@@ -1,7 +1,6 @@
1
1
  import { resetStore, setState, state } from "./store-B1sbyexX.js";
2
- import { batchEntityUpdates, batchServiceCalls, buildAreaView, buildDeviceView, buildEntityView, callService, getAllEntityIds, getAllEntityViews, getAreaView, getAreaViews, getEntitiesByArea, getEntitiesByDevice, getEntitiesByDomain, getEntitiesByFloor, getEntitiesByLabel, getEntityView, getEntityViews, sendCommand, toggle, turnOff, turnOn, updateEntity } from "./area-views-B-m8vwce.js";
3
- import { extractDomain } from "./types-Csog0nqk.js";
4
- import { createConnection, createLongLivedTokenAuth, getAuth } from "home-assistant-js-websocket";
2
+ import { extractDomain } from "./types-CxMcKkJQ.js";
3
+ import { callService as callService$1, createConnection, createLongLivedTokenAuth, getAuth } from "home-assistant-js-websocket";
5
4
  import { produce } from "solid-js/store";
6
5
 
7
6
  //#region src/connection/adapter.ts
@@ -396,6 +395,96 @@ async function refreshAuth(auth) {
396
395
  return auth;
397
396
  }
398
397
 
398
+ //#endregion
399
+ //#region src/commands/service.ts
400
+ /**
401
+ * Call a Home Assistant service
402
+ */
403
+ async function callService(domain, service, serviceData = {}, target = {}) {
404
+ const connection = state.conn;
405
+ if (!connection) throw new Error("Not connected to Home Assistant. Call initConnection() first.");
406
+ try {
407
+ const mockConnection = connection;
408
+ if (mockConnection && typeof mockConnection.callService === "function") await mockConnection.callService(domain, service, serviceData, target);
409
+ else await callService$1(connection, domain, service, serviceData, target);
410
+ } catch (error) {
411
+ throw new Error(`Service call failed: ${error.message || String(error)}`);
412
+ }
413
+ }
414
+ /**
415
+ * Turn on entity or entities
416
+ */
417
+ async function turnOn(entityId, serviceData = {}) {
418
+ const firstEntityId = Array.isArray(entityId) ? entityId[0] : entityId;
419
+ if (!firstEntityId) throw new Error("Entity ID is required");
420
+ const domain = firstEntityId.split(".")[0];
421
+ if (!domain) throw new Error(`Invalid entity ID: ${firstEntityId}`);
422
+ await callService(domain, "turn_on", serviceData, { entity_id: entityId });
423
+ }
424
+ /**
425
+ * Turn off entity or entities
426
+ */
427
+ async function turnOff(entityId, serviceData = {}) {
428
+ const firstEntityId = Array.isArray(entityId) ? entityId[0] : entityId;
429
+ if (!firstEntityId) throw new Error("Entity ID is required");
430
+ const domain = firstEntityId.split(".")[0];
431
+ if (!domain) throw new Error(`Invalid entity ID: ${firstEntityId}`);
432
+ await callService(domain, "turn_off", serviceData, { entity_id: entityId });
433
+ }
434
+ /**
435
+ * Toggle entity or entities
436
+ */
437
+ async function toggle(entityId, serviceData = {}) {
438
+ const firstEntityId = Array.isArray(entityId) ? entityId[0] : entityId;
439
+ if (!firstEntityId) throw new Error("Entity ID is required");
440
+ const domain = firstEntityId.split(".")[0];
441
+ if (!domain) throw new Error(`Invalid entity ID: ${firstEntityId}`);
442
+ await callService(domain, "toggle", serviceData, { entity_id: entityId });
443
+ }
444
+ /**
445
+ * Update entity registry entry
446
+ */
447
+ async function updateEntity(entityId, updates) {
448
+ const connection = state.conn;
449
+ if (!connection) throw new Error("Not connected to Home Assistant. Call initConnection() first.");
450
+ try {
451
+ const result = await connection.sendMessagePromise({
452
+ type: "config/entity_registry/update",
453
+ entity_id: entityId,
454
+ ...updates
455
+ });
456
+ if (result && typeof result === "object" && "_mockError" in result) throw new Error(`Entity registry entry not found: ${entityId}`);
457
+ return result;
458
+ } catch (error) {
459
+ if (error?.message && error.message.includes("Entity registry entry not found")) throw error;
460
+ throw new Error(`Entity update failed: ${error.message || String(error)}`);
461
+ }
462
+ }
463
+ /**
464
+ * Send a generic WebSocket command
465
+ */
466
+ async function sendCommand(command) {
467
+ const connection = state.conn;
468
+ if (!connection) throw new Error("Not connected");
469
+ try {
470
+ return await connection.sendMessagePromise(command);
471
+ } catch (error) {
472
+ throw new Error(`Command failed: ${error.message || String(error)}`);
473
+ }
474
+ }
475
+ /**
476
+ * Execute multiple service calls in parallel
477
+ */
478
+ async function batchServiceCalls(calls) {
479
+ await Promise.all(calls.map((call) => callService(call.domain, call.service, call.serviceData, call.target)));
480
+ }
481
+ /**
482
+ * Execute multiple entity updates in parallel
483
+ */
484
+ async function batchEntityUpdates(updates) {
485
+ return Promise.all(updates.map((u) => updateEntity(u.entityId, u.updates)));
486
+ }
487
+
399
488
  //#endregion
400
489
  //#region src/history/constants.ts
401
490
  const DOMAINS_USE_LAST_UPDATED = [
@@ -897,6 +986,232 @@ async function loadInitialData(conn) {
897
986
  }
898
987
  }
899
988
 
989
+ //#endregion
990
+ //#region src/entities/index.ts
991
+ /**
992
+ * Get all entity IDs
993
+ */
994
+ function getAllEntityIds() {
995
+ return Object.keys(state.entities);
996
+ }
997
+ /**
998
+ * Get entities by domain
999
+ */
1000
+ function getEntitiesByDomain(domain) {
1001
+ return Object.keys(state.entities).filter((id) => extractDomain(id) === domain);
1002
+ }
1003
+ /**
1004
+ * Get entities via devices in area
1005
+ *
1006
+ * Only includes entities that inherit area from device (entity.area_id is null).
1007
+ */
1008
+ function getEntitiesViaDevices(areaId) {
1009
+ const devicesInArea = Object.values(state.devices).filter((device) => device.area_id === areaId);
1010
+ const entityIds = [];
1011
+ for (const device of devicesInArea) {
1012
+ const deviceEntities = getEntitiesByDevice(device.id);
1013
+ for (const entityId of deviceEntities) {
1014
+ const registry = state.entityRegistry[entityId];
1015
+ if (!registry?.area_id) entityIds.push(entityId);
1016
+ }
1017
+ }
1018
+ return entityIds;
1019
+ }
1020
+ /**
1021
+ * Get entities by area
1022
+ *
1023
+ * Returns entities with direct area_id assignments and entities that inherit their area from devices.
1024
+ */
1025
+ function getEntitiesByArea(areaId) {
1026
+ const directEntityIds = [];
1027
+ for (const [entityId, entry] of Object.entries(state.entityRegistry)) if (entry.area_id === areaId) directEntityIds.push(entityId);
1028
+ const deviceEntityIds = getEntitiesViaDevices(areaId);
1029
+ return Array.from(new Set([...directEntityIds, ...deviceEntityIds]));
1030
+ }
1031
+ /**
1032
+ * Get entities by device
1033
+ */
1034
+ function getEntitiesByDevice(deviceId) {
1035
+ const entityIds = [];
1036
+ for (const [entityId, entry] of Object.entries(state.entityRegistry)) if (entry.device_id === deviceId) entityIds.push(entityId);
1037
+ return entityIds;
1038
+ }
1039
+ /**
1040
+ * Get entities by floor
1041
+ */
1042
+ function getEntitiesByFloor(floorId) {
1043
+ const entityIds = [];
1044
+ const areasOnFloor = Object.values(state.areas).filter((area$1) => area$1.floor_id === floorId);
1045
+ for (const area$1 of areasOnFloor) {
1046
+ const areaEntities = getEntitiesByArea(area$1.id);
1047
+ entityIds.push(...areaEntities);
1048
+ }
1049
+ return entityIds;
1050
+ }
1051
+ /**
1052
+ * Get entities by label
1053
+ */
1054
+ function getEntitiesByLabel(labelId) {
1055
+ const entityIds = [];
1056
+ for (const [entityId, entry] of Object.entries(state.entityRegistry)) if (entry.labels?.includes(labelId)) entityIds.push(entityId);
1057
+ return entityIds;
1058
+ }
1059
+
1060
+ //#endregion
1061
+ //#region src/entities/views.ts
1062
+ /**
1063
+ * Get icon for an entity from registry or attributes
1064
+ */
1065
+ function resolveIcon(entity$1, registry) {
1066
+ if (registry?.icon) return {
1067
+ icon: registry.icon,
1068
+ source: "registry"
1069
+ };
1070
+ if (entity$1.attributes?.icon) return {
1071
+ icon: entity$1.attributes.icon,
1072
+ source: "attribute"
1073
+ };
1074
+ return null;
1075
+ }
1076
+ /**
1077
+ * Build an EntityView from runtime state and registry metadata
1078
+ */
1079
+ function buildEntityView(entity$1, registry) {
1080
+ const domain = extractDomain(entity$1.entity_id);
1081
+ const friendlyName = entity$1.attributes.friendly_name ?? registry?.name ?? entity$1.entity_id;
1082
+ const name = registry?.name ?? friendlyName;
1083
+ const iconResolution = resolveIcon(entity$1, registry);
1084
+ let areaId = registry?.area_id ?? null;
1085
+ if (areaId === null && registry?.device_id) {
1086
+ const device = state.devices[registry.device_id];
1087
+ if (device) areaId = device.area_id ?? null;
1088
+ }
1089
+ return {
1090
+ id: entity$1.entity_id,
1091
+ domain,
1092
+ state: entity$1.state,
1093
+ attributes: entity$1.attributes,
1094
+ lastChanged: new Date(entity$1.last_changed),
1095
+ lastUpdated: new Date(entity$1.last_updated),
1096
+ context: {
1097
+ id: entity$1.context.id,
1098
+ parentId: entity$1.context.parent_id,
1099
+ userId: entity$1.context.user_id
1100
+ },
1101
+ name,
1102
+ friendlyName,
1103
+ areaId,
1104
+ deviceId: registry?.device_id ?? null,
1105
+ platform: registry?.platform ?? "unknown",
1106
+ uniqueId: registry?.unique_id ?? null,
1107
+ isDisabled: registry?.disabled_by !== null && registry?.disabled_by !== void 0,
1108
+ isHidden: registry?.hidden_by !== null && registry?.hidden_by !== void 0,
1109
+ icon: iconResolution?.icon ?? null,
1110
+ iconSource: iconResolution?.source ?? "default",
1111
+ entityCategory: registry?.entity_category ?? null,
1112
+ labels: registry?.labels ?? [],
1113
+ aliases: registry?.aliases ?? [],
1114
+ deviceClass: registry?.device_class ?? entity$1.attributes.device_class ?? null,
1115
+ unitOfMeasurement: registry?.unit_of_measurement ?? entity$1.attributes.unit_of_measurement ?? null,
1116
+ supportedFeatures: registry?.supported_features ?? entity$1.attributes.supported_features
1117
+ };
1118
+ }
1119
+ /**
1120
+ * Get or build an EntityView
1121
+ */
1122
+ function getEntityView(entityId) {
1123
+ const entity$1 = state.entities[entityId];
1124
+ if (!entity$1) return void 0;
1125
+ const registry = state.entityRegistry[entityId];
1126
+ return buildEntityView(entity$1, registry);
1127
+ }
1128
+ /**
1129
+ * Get multiple entity views
1130
+ */
1131
+ function getEntityViews(entityIds) {
1132
+ const views = [];
1133
+ for (const id of entityIds) {
1134
+ const view = getEntityView(id);
1135
+ if (view) views.push(view);
1136
+ }
1137
+ return views;
1138
+ }
1139
+ /**
1140
+ * Get all entity views
1141
+ */
1142
+ function getAllEntityViews() {
1143
+ return getEntityViews(Object.keys(state.entities));
1144
+ }
1145
+
1146
+ //#endregion
1147
+ //#region src/entities/area-views.ts
1148
+ /**
1149
+ * Build a DeviceView from registry data
1150
+ */
1151
+ function buildDeviceView(device) {
1152
+ return {
1153
+ id: device.id,
1154
+ name: device.name ?? "",
1155
+ nameByUser: device.name_by_user ?? null,
1156
+ manufacturer: device.manufacturer ?? null,
1157
+ model: device.model ?? null,
1158
+ swVersion: device.sw_version ?? null,
1159
+ areaId: device.area_id ?? null,
1160
+ configEntries: device.config_entries ?? [],
1161
+ connections: device.connections ?? [],
1162
+ identifiers: device.identifiers ?? [],
1163
+ disabledBy: device.disabled_by ?? null
1164
+ };
1165
+ }
1166
+ /**
1167
+ * Build an AreaView from registry data using query engine
1168
+ */
1169
+ function buildAreaView(areaId) {
1170
+ const area$1 = state.areas[areaId];
1171
+ if (!area$1) throw new Error(`Area ${areaId} not found`);
1172
+ const directEntityIds = [];
1173
+ for (const [entityId, entry] of Object.entries(state.entityRegistry)) if (entry.area_id === areaId) directEntityIds.push(entityId);
1174
+ const deviceEntityIds = getEntitiesViaDevices(areaId);
1175
+ const allEntityIds = Array.from(new Set([...directEntityIds, ...deviceEntityIds]));
1176
+ const areaEntities = allEntityIds.length > 0 ? getEntityViews(allEntityIds) : [];
1177
+ const devicesInArea = Object.values(state.devices).filter((device) => device.area_id === areaId);
1178
+ const devices = devicesInArea.map((device) => buildDeviceView(device));
1179
+ return {
1180
+ id: areaId,
1181
+ name: area$1.name,
1182
+ normalizedName: area$1.normalized_name ?? area$1.name.toLowerCase(),
1183
+ aliases: Array.isArray(area$1.aliases) ? area$1.aliases : [],
1184
+ floorId: area$1.floor_id ?? null,
1185
+ icon: area$1.icon ?? null,
1186
+ picture: area$1.picture ?? null,
1187
+ labels: Array.isArray(area$1.labels) ? area$1.labels : [],
1188
+ temperatureEntityId: area$1.temperature_entity_id ?? null,
1189
+ humidityEntityId: area$1.humidity_entity_id ?? null,
1190
+ createdAt: area$1.created_at ?? (/* @__PURE__ */ new Date()).toISOString(),
1191
+ modifiedAt: area$1.modified_at ?? (/* @__PURE__ */ new Date()).toISOString(),
1192
+ entities: areaEntities,
1193
+ devices,
1194
+ entityIds: allEntityIds,
1195
+ deviceIds: devices.map((d) => d.id)
1196
+ };
1197
+ }
1198
+ /**
1199
+ * Get all area views
1200
+ */
1201
+ function getAreaViews() {
1202
+ return Object.keys(state.areas).map((areaId) => buildAreaView(areaId));
1203
+ }
1204
+ /**
1205
+ * Get area view
1206
+ */
1207
+ function getAreaView(areaId) {
1208
+ try {
1209
+ return buildAreaView(areaId);
1210
+ } catch {
1211
+ return void 0;
1212
+ }
1213
+ }
1214
+
900
1215
  //#endregion
901
1216
  //#region src/query/area-builder.ts
902
1217
  var AreaQueryBuilderImpl = class AreaQueryBuilderImpl {
@@ -1744,4 +2059,4 @@ async function refreshForecasts(entityId, types) {
1744
2059
  const VERSION = "0.1.0";
1745
2060
 
1746
2061
  //#endregion
1747
- export { DOMAINS_USE_LAST_UPDATED, LINE_ATTRIBUTES_TO_KEEP, NEED_ATTRIBUTE_DOMAINS, VERSION, addDebugIncomingMessageListener, addDebugOutgoingMessageListener, appendHistoryPoint, area, areas, authenticateWithOAuth, authenticateWithToken, batchEntityUpdates, batchServiceCalls, buildAreaView, buildDeviceView, buildEntityView, callService, disconnect, entities, entitiesByAreaAndDomain, entitiesWhere, entity, entityIdHistoryNeedsAttributes, fetchEntityHistory, fetchForecast, fetchForecastData, fetchForecasts, fetchHistory, fetchStream, fetchStreamData, getAllEntityViews, getAreaView, getAreaViews, getConnection, getEntityView, getEntityViews, getForecast, getForecasts, getStream, getWebRtcClientConfig, historyStateToTimeline, initConnection, isAuthValid, isConnected, isHistoryTracked, refreshAuth, refreshForecast, refreshForecasts, refreshStream, resetStore, searchEntities, sendCommand, sendWebRtcCandidate, setState, startWebRtcSession, state, toggle, trackEntityHistory, turnOff, turnOn, untrackEntityHistory, updateEntity };
2062
+ export { DOMAINS_USE_LAST_UPDATED, LINE_ATTRIBUTES_TO_KEEP, NEED_ATTRIBUTE_DOMAINS, VERSION, addDebugIncomingMessageListener, addDebugOutgoingMessageListener, appendHistoryPoint, area, areas, authenticateWithOAuth, authenticateWithToken, batchEntityUpdates, batchServiceCalls, buildAreaView, buildDeviceView, buildEntityView, callService, disconnect, entities, entitiesByAreaAndDomain, entitiesWhere, entity, entityIdHistoryNeedsAttributes, extractDomain, fetchEntityHistory, fetchForecast, fetchForecastData, fetchForecasts, fetchHistory, fetchStream, fetchStreamData, getAllEntityViews, getAreaView, getAreaViews, getConnection, getEntityView, getEntityViews, getForecast, getForecasts, getStream, getWebRtcClientConfig, historyStateToTimeline, initConnection, isAuthValid, isConnected, isHistoryTracked, refreshAuth, refreshForecast, refreshForecasts, refreshStream, resetStore, searchEntities, sendCommand, sendWebRtcCandidate, setState, startWebRtcSession, state, toggle, trackEntityHistory, turnOff, turnOn, untrackEntityHistory, updateEntity };
@@ -11,12 +11,8 @@
11
11
  * @packageDocumentation
12
12
  */
13
13
  import type { Accessor } from "solid-js";
14
- import type { CameraStreamData } from "../camera/types";
15
- import { callService, toggle, turnOff, turnOn } from "../commands/service";
16
- import type { GlassHomeState } from "../core/store";
17
- import type { AreaView, ConnectionState, EntityView, HassEntity } from "../core/types";
18
- import type { EntityHistoryData } from "../history/types";
19
- import type { WeatherForecastsData } from "../weather/types";
14
+ import type { AreaView, CameraStreamData, ConnectionState, EntityHistoryData, EntityView, GlassHomeState, HassEntity, WeatherForecastsData } from "@glasshome/sync-layer";
15
+ import { callService, toggle, turnOff, turnOn } from "@glasshome/sync-layer";
20
16
  /**
21
17
  * Get a reactive EntityView for an entity ID.
22
18
  *
@@ -1,7 +1,5 @@
1
- import { state } from "../store-B1sbyexX.js";
2
- import { buildAreaView, callService, getEntityView, toggle, turnOff, turnOn } from "../area-views-B-m8vwce.js";
3
- import { extractDomain } from "../types-Csog0nqk.js";
4
1
  import { createMemo } from "solid-js";
2
+ import { buildAreaView, callService, extractDomain, getEntityView, state, toggle, turnOff, turnOn } from "@glasshome/sync-layer";
5
3
 
6
4
  //#region src/solid/hooks.ts
7
5
  /**
@@ -145,14 +143,26 @@ function groupBy(items, keyFn) {
145
143
  }
146
144
  return result;
147
145
  }
146
+ /**
147
+ * Create a lazily-initialized memo. The actual createMemo runs on first
148
+ * access, inside whichever Solid root is current at that point.
149
+ * This avoids top-level createMemo calls that execute outside any root.
150
+ */
151
+ function lazyMemo(fn) {
152
+ let memo;
153
+ return () => {
154
+ if (!memo) memo = createMemo(fn);
155
+ return memo();
156
+ };
157
+ }
148
158
  /** Entity IDs grouped by domain (e.g., "light" -> ["light.living_room", ...]) */
149
- const byDomain = createMemo(() => groupBy(Object.keys(state.entities), (id) => extractDomain(id)));
159
+ const byDomain = lazyMemo(() => groupBy(Object.keys(state.entities), (id) => extractDomain(id)));
150
160
  /** Entity IDs grouped by area (via entity registry area_id) */
151
- const byArea = createMemo(() => groupBy(Object.keys(state.entityRegistry), (id) => state.entityRegistry[id]?.area_id ?? null));
161
+ const byArea = lazyMemo(() => groupBy(Object.keys(state.entityRegistry), (id) => state.entityRegistry[id]?.area_id ?? null));
152
162
  /** Entity IDs grouped by device (via entity registry device_id) */
153
- const byDevice = createMemo(() => groupBy(Object.keys(state.entityRegistry), (id) => state.entityRegistry[id]?.device_id ?? null));
163
+ const byDevice = lazyMemo(() => groupBy(Object.keys(state.entityRegistry), (id) => state.entityRegistry[id]?.device_id ?? null));
154
164
  /** Entity IDs grouped by floor (device -> area -> floor chain) */
155
- const byFloor = createMemo(() => {
165
+ const byFloor = lazyMemo(() => {
156
166
  const result = {};
157
167
  for (const [entityId, reg] of Object.entries(state.entityRegistry)) if (reg.device_id) {
158
168
  const device = state.devices[reg.device_id];
@@ -164,13 +174,13 @@ const byFloor = createMemo(() => {
164
174
  return result;
165
175
  });
166
176
  /** Entity IDs grouped by label */
167
- const byLabel = createMemo(() => {
177
+ const byLabel = lazyMemo(() => {
168
178
  const result = {};
169
179
  for (const [entityId, reg] of Object.entries(state.entityRegistry)) for (const label of reg.labels ?? []) (result[label] ??= []).push(entityId);
170
180
  return result;
171
181
  });
172
182
  /** All entity IDs currently in the store */
173
- const allEntityIds = createMemo(() => Object.keys(state.entities));
183
+ const allEntityIds = lazyMemo(() => Object.keys(state.entities));
174
184
 
175
185
  //#endregion
176
186
  export { allEntityIds, byArea, byDevice, byDomain, byFloor, byLabel, useArea, useAreas, useCamera, useConnection, useEntities, useEntity, useEntityHistory, useEntityState, useForecast, useService, useStore, useToggle, useTurnOff, useTurnOn };
@@ -1,4 +1,4 @@
1
- /** Reactive derived entity indices via createMemo. */
1
+ /** Reactive derived entity indices — lazily created inside the caller's root. */
2
2
  import type { Accessor } from "solid-js";
3
3
  /** Entity IDs grouped by domain (e.g., "light" -> ["light.living_room", ...]) */
4
4
  export declare const byDomain: Accessor<Record<string, string[]>>;
@@ -1,5 +1,5 @@
1
1
  import { state } from "../store-B1sbyexX.js";
2
- import { createDemoFixtures } from "../demo-data-D8JXWRpw.js";
2
+ import { createDemoFixtures } from "../demo-data-Bir7Px_e.js";
3
3
 
4
4
  //#region src/testing/mock-behaviors/base.ts
5
5
  /**
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@glasshome/sync-layer",
3
- "version": "0.1.0",
3
+ "version": "0.1.2",
4
4
  "description": "Type-safe, reactive state synchronization layer for Home Assistant",
5
5
  "repository": {
6
6
  "type": "git",
@@ -1,321 +0,0 @@
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 };
File without changes