@glasshome/sync-layer 0.1.3 → 0.1.4

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.
@@ -6,7 +6,8 @@
6
6
  import type { EntityId } from "../core/types";
7
7
  import type { CameraStreamData, StreamFormat, StreamQueryOptions } from "./types";
8
8
  /**
9
- * Fetch stream for a camera entity
9
+ * Fetch stream for a camera entity.
10
+ * Returns a public demo HLS stream when not connected.
10
11
  */
11
12
  export declare function fetchStream(entityId: EntityId, format?: StreamFormat): Promise<string | null>;
12
13
  /**
@@ -1,103 +1,4 @@
1
- import { setState, state } from "../store-B1sbyexX.js";
2
- import { extractDomain } from "../types-CxMcKkJQ.js";
3
- import { createDemoFixtures } from "../demo-data-Bir7Px_e.js";
4
- import { produce, reconcile } from "solid-js/store";
1
+ import { createDemoFixtures } from "../demo-data-gELwUOS_.js";
2
+ import { applyDemoServiceCall, isDemoMode, loadDemoData, unloadDemoData } from "../demo-provider-I4BC9Woh.js";
5
3
 
6
- //#region src/demo/demo-provider.ts
7
- let _isDemoMode = false;
8
- /**
9
- * Whether demo mode is currently active.
10
- */
11
- function isDemoMode() {
12
- return _isDemoMode;
13
- }
14
- /**
15
- * Load demo fixtures into the SolidJS store.
16
- *
17
- * Populates entities, entity registry, areas, and devices.
18
- * Sets connection state to "connected" (pretend connected).
19
- */
20
- async function loadDemoData() {
21
- const fixtures = createDemoFixtures();
22
- setState("entities", reconcile(fixtures.entities));
23
- setState("entityRegistry", reconcile(fixtures.entityRegistry));
24
- setState("areas", reconcile(fixtures.areas));
25
- setState("devices", reconcile(fixtures.devices));
26
- setState("connectionState", "connected");
27
- _isDemoMode = true;
28
- }
29
- /**
30
- * Unload demo data and reset to disconnected state.
31
- */
32
- function unloadDemoData() {
33
- setState("entities", reconcile({}));
34
- setState("entityRegistry", reconcile({}));
35
- setState("areas", reconcile({}));
36
- setState("devices", reconcile({}));
37
- setState("connectionState", "disconnected");
38
- _isDemoMode = false;
39
- }
40
- /**
41
- * Apply an optimistic state update for demo mode.
42
- *
43
- * When a service call is made in demo mode, this updates the entity
44
- * state locally (no WebSocket message sent). State resets on page refresh.
45
- */
46
- function applyDemoServiceCall(domain, service, _serviceData = {}, target = {}) {
47
- if (!_isDemoMode) return;
48
- const entityIds = Array.isArray(target.entity_id) ? target.entity_id : target.entity_id ? [target.entity_id] : [];
49
- for (const entityId of entityIds) {
50
- const entity = state.entities[entityId];
51
- if (!entity) continue;
52
- const entityDomain = extractDomain(entityId);
53
- setState(produce((s) => {
54
- const e = s.entities[entityId];
55
- if (!e) return;
56
- const now = (/* @__PURE__ */ new Date()).toISOString();
57
- e.last_changed = now;
58
- e.last_updated = now;
59
- if (service === "turn_on") applyTurnOn(e, entityDomain, _serviceData);
60
- else if (service === "turn_off") applyTurnOff(e, entityDomain);
61
- else if (service === "toggle") if (e.state === "on" || e.state === "playing" || e.state === "open") applyTurnOff(e, entityDomain);
62
- else applyTurnOn(e, entityDomain, _serviceData);
63
- else if (service === "lock") e.state = "locked";
64
- else if (service === "unlock") e.state = "unlocked";
65
- else if (service === "open_cover") {
66
- e.state = "open";
67
- e.attributes.current_position = 100;
68
- } else if (service === "close_cover") {
69
- e.state = "closed";
70
- e.attributes.current_position = 0;
71
- } else if (service === "set_cover_position" && _serviceData.position != null) {
72
- e.attributes.current_position = _serviceData.position;
73
- e.state = _serviceData.position > 0 ? "open" : "closed";
74
- } else if (service === "set_temperature" && _serviceData.temperature != null) e.attributes.temperature = _serviceData.temperature;
75
- else if (service === "set_hvac_mode" && _serviceData.hvac_mode != null) e.state = _serviceData.hvac_mode;
76
- else if (service === "volume_set" && _serviceData.volume_level != null) e.attributes.volume_level = _serviceData.volume_level;
77
- }));
78
- }
79
- }
80
- function applyTurnOn(e, domain, serviceData = {}) {
81
- if (domain === "light") {
82
- e.state = "on";
83
- e.attributes.brightness = serviceData.brightness ?? e.attributes.brightness ?? 255;
84
- if (serviceData.color_temp_kelvin) e.attributes.color_temp_kelvin = serviceData.color_temp_kelvin;
85
- } else if (domain === "media_player") e.state = "playing";
86
- else if (domain === "cover") {
87
- e.state = "open";
88
- e.attributes.current_position = 100;
89
- } else e.state = "on";
90
- }
91
- function applyTurnOff(e, domain) {
92
- if (domain === "light") {
93
- e.state = "off";
94
- delete e.attributes.brightness;
95
- } else if (domain === "media_player") e.state = "paused";
96
- else if (domain === "cover") {
97
- e.state = "closed";
98
- e.attributes.current_position = 0;
99
- } else e.state = "off";
100
- }
101
-
102
- //#endregion
103
4
  export { applyDemoServiceCall, createDemoFixtures, isDemoMode, loadDemoData, unloadDemoData };
@@ -1,3 +1,56 @@
1
+ import { createStore, reconcile } from "solid-js/store";
2
+
3
+ //#region src/core/store.ts
4
+ const initialState = {
5
+ conn: null,
6
+ connectionState: "disconnected",
7
+ connectionError: null,
8
+ hassUrl: null,
9
+ entities: {},
10
+ entityRegistry: {},
11
+ devices: {},
12
+ areas: {},
13
+ floors: {},
14
+ labels: {},
15
+ statistics: {},
16
+ statisticsMetadata: {},
17
+ history: {},
18
+ forecasts: {},
19
+ streams: {}
20
+ };
21
+ /**
22
+ * Module-level SolidJS createStore singleton
23
+ *
24
+ * `state` is a read-only reactive proxy.
25
+ * `setState` is used for mutations via produce() or direct path setting.
26
+ *
27
+ * @example
28
+ * ```typescript
29
+ * import { state, setState } from '@glasshome/sync-layer/core/store';
30
+ *
31
+ * // Read state (auto-tracked in SolidJS reactive contexts)
32
+ * const entities = state.entities;
33
+ *
34
+ * // Mutate via produce
35
+ * setState(produce((s) => {
36
+ * s.entities['light.living_room'] = newEntity;
37
+ * }));
38
+ *
39
+ * // Bulk replace
40
+ * setState("entities", reconcile(newEntities));
41
+ * ```
42
+ */
43
+ const storeTuple = createStore(initialState);
44
+ const state = storeTuple[0];
45
+ const setState = storeTuple[1];
46
+ /**
47
+ * Reset store to initial state
48
+ */
49
+ function resetStore() {
50
+ setState(reconcile(initialState));
51
+ }
52
+
53
+ //#endregion
1
54
  //#region src/demo/demo-data.ts
2
55
  const now = (/* @__PURE__ */ new Date()).toISOString();
3
56
  let contextCounter = 0;
@@ -8,10 +61,10 @@ function ctx() {
8
61
  user_id: null
9
62
  };
10
63
  }
11
- function makeEntity(entity_id, state, attributes = {}) {
64
+ function makeEntity(entity_id, state$1, attributes = {}) {
12
65
  return {
13
66
  entity_id,
14
- state,
67
+ state: state$1,
15
68
  attributes: {
16
69
  friendly_name: friendlyName(entity_id),
17
70
  ...attributes
@@ -363,7 +416,8 @@ function createDemoFixtures() {
363
416
  }), { platform: "met" });
364
417
  add(makeEntity("camera.front_door_camera", "streaming", {
365
418
  entity_picture: "/api/camera_proxy/camera.front_door_camera",
366
- frontend_stream_type: "hls"
419
+ frontend_stream_type: "hls",
420
+ access_token: "demo-token"
367
421
  }), {
368
422
  device_id: "camera_front",
369
423
  area_id: "entry",
@@ -416,4 +470,4 @@ function createDemoFixtures() {
416
470
  }
417
471
 
418
472
  //#endregion
419
- export { createDemoFixtures };
473
+ export { createDemoFixtures, resetStore, setState, state };
@@ -0,0 +1,112 @@
1
+ import { createDemoFixtures, setState, state } from "./demo-data-gELwUOS_.js";
2
+ import { produce, reconcile } from "solid-js/store";
3
+
4
+ //#region src/core/types.ts
5
+ /**
6
+ * Extract domain from entity ID
7
+ */
8
+ function extractDomain(entityId) {
9
+ return entityId.split(".")[0] ?? "";
10
+ }
11
+
12
+ //#endregion
13
+ //#region src/demo/demo-provider.ts
14
+ let _isDemoMode = false;
15
+ /**
16
+ * Whether demo mode is currently active.
17
+ */
18
+ function isDemoMode() {
19
+ return _isDemoMode;
20
+ }
21
+ /**
22
+ * Load demo fixtures into the SolidJS store.
23
+ *
24
+ * Populates entities, entity registry, areas, and devices.
25
+ * Sets connection state to "connected" (pretend connected).
26
+ */
27
+ async function loadDemoData() {
28
+ const fixtures = createDemoFixtures();
29
+ setState("entities", reconcile(fixtures.entities));
30
+ setState("entityRegistry", reconcile(fixtures.entityRegistry));
31
+ setState("areas", reconcile(fixtures.areas));
32
+ setState("devices", reconcile(fixtures.devices));
33
+ setState("hassUrl", "https://demo.home-assistant.local");
34
+ setState("connectionState", "connected");
35
+ _isDemoMode = true;
36
+ }
37
+ /**
38
+ * Unload demo data and reset to disconnected state.
39
+ */
40
+ function unloadDemoData() {
41
+ setState("entities", reconcile({}));
42
+ setState("entityRegistry", reconcile({}));
43
+ setState("areas", reconcile({}));
44
+ setState("devices", reconcile({}));
45
+ setState("hassUrl", null);
46
+ setState("connectionState", "disconnected");
47
+ _isDemoMode = false;
48
+ }
49
+ /**
50
+ * Apply an optimistic state update for demo mode.
51
+ *
52
+ * When a service call is made in demo mode, this updates the entity
53
+ * state locally (no WebSocket message sent). State resets on page refresh.
54
+ */
55
+ function applyDemoServiceCall(domain, service, _serviceData = {}, target = {}) {
56
+ if (!_isDemoMode) return;
57
+ const entityIds = Array.isArray(target.entity_id) ? target.entity_id : target.entity_id ? [target.entity_id] : [];
58
+ for (const entityId of entityIds) {
59
+ const entity = state.entities[entityId];
60
+ if (!entity) continue;
61
+ const entityDomain = extractDomain(entityId);
62
+ setState(produce((s) => {
63
+ const e = s.entities[entityId];
64
+ if (!e) return;
65
+ const now = (/* @__PURE__ */ new Date()).toISOString();
66
+ e.last_changed = now;
67
+ e.last_updated = now;
68
+ if (service === "turn_on") applyTurnOn(e, entityDomain, _serviceData);
69
+ else if (service === "turn_off") applyTurnOff(e, entityDomain);
70
+ else if (service === "toggle") if (e.state === "on" || e.state === "playing" || e.state === "open") applyTurnOff(e, entityDomain);
71
+ else applyTurnOn(e, entityDomain, _serviceData);
72
+ else if (service === "lock") e.state = "locked";
73
+ else if (service === "unlock") e.state = "unlocked";
74
+ else if (service === "open_cover") {
75
+ e.state = "open";
76
+ e.attributes.current_position = 100;
77
+ } else if (service === "close_cover") {
78
+ e.state = "closed";
79
+ e.attributes.current_position = 0;
80
+ } else if (service === "set_cover_position" && _serviceData.position != null) {
81
+ e.attributes.current_position = _serviceData.position;
82
+ e.state = _serviceData.position > 0 ? "open" : "closed";
83
+ } else if (service === "set_temperature" && _serviceData.temperature != null) e.attributes.temperature = _serviceData.temperature;
84
+ else if (service === "set_hvac_mode" && _serviceData.hvac_mode != null) e.state = _serviceData.hvac_mode;
85
+ else if (service === "volume_set" && _serviceData.volume_level != null) e.attributes.volume_level = _serviceData.volume_level;
86
+ }));
87
+ }
88
+ }
89
+ function applyTurnOn(e, domain, serviceData = {}) {
90
+ if (domain === "light") {
91
+ e.state = "on";
92
+ e.attributes.brightness = serviceData.brightness ?? e.attributes.brightness ?? 255;
93
+ if (serviceData.color_temp_kelvin) e.attributes.color_temp_kelvin = serviceData.color_temp_kelvin;
94
+ } else if (domain === "media_player") e.state = "playing";
95
+ else if (domain === "cover") {
96
+ e.state = "open";
97
+ e.attributes.current_position = 100;
98
+ } else e.state = "on";
99
+ }
100
+ function applyTurnOff(e, domain) {
101
+ if (domain === "light") {
102
+ e.state = "off";
103
+ delete e.attributes.brightness;
104
+ } else if (domain === "media_player") e.state = "paused";
105
+ else if (domain === "cover") {
106
+ e.state = "closed";
107
+ e.attributes.current_position = 0;
108
+ } else e.state = "off";
109
+ }
110
+
111
+ //#endregion
112
+ export { applyDemoServiceCall, extractDomain, isDemoMode, loadDemoData, unloadDemoData };
package/dist/index.js CHANGED
@@ -1,5 +1,5 @@
1
- import { resetStore, setState, state } from "./store-B1sbyexX.js";
2
- import { extractDomain } from "./types-CxMcKkJQ.js";
1
+ import { resetStore, setState, state } from "./demo-data-gELwUOS_.js";
2
+ import { applyDemoServiceCall, extractDomain } from "./demo-provider-I4BC9Woh.js";
3
3
  import { callService as callService$1, createConnection, createLongLivedTokenAuth, getAuth } from "home-assistant-js-websocket";
4
4
  import { produce } from "solid-js/store";
5
5
 
@@ -402,7 +402,10 @@ async function refreshAuth(auth) {
402
402
  */
403
403
  async function callService(domain, service, serviceData = {}, target = {}) {
404
404
  const connection = state.conn;
405
- if (!connection) throw new Error("Not connected to Home Assistant. Call initConnection() first.");
405
+ if (!connection) {
406
+ applyDemoServiceCall(domain, service, serviceData, target);
407
+ return;
408
+ }
406
409
  try {
407
410
  const mockConnection = connection;
408
411
  if (mockConnection && typeof mockConnection.callService === "function") await mockConnection.callService(domain, service, serviceData, target);
@@ -882,6 +885,7 @@ async function initConnection(options) {
882
885
  const wrappedConn = wrapHAConnection(conn);
883
886
  setState(produce((s) => {
884
887
  s.conn = conn;
888
+ s.hassUrl = options.url.replace(/\/$/, "");
885
889
  s.connectionState = "connected";
886
890
  }));
887
891
  setupEventHandlers(conn, options);
@@ -1638,10 +1642,14 @@ function entitiesWhere(predicate) {
1638
1642
 
1639
1643
  //#endregion
1640
1644
  //#region src/camera/fetch.ts
1645
+ /** Public HLS test stream for demo/disconnected mode */
1646
+ const DEMO_HLS_STREAM = "https://test-streams.mux.dev/x36xhzz/x36xhzz.m3u8";
1641
1647
  /**
1642
- * Fetch stream for a camera entity
1648
+ * Fetch stream for a camera entity.
1649
+ * Returns a public demo HLS stream when not connected.
1643
1650
  */
1644
1651
  async function fetchStream(entityId, format = "hls") {
1652
+ if (!state.conn) return format === "hls" ? DEMO_HLS_STREAM : null;
1645
1653
  const response = await sendCommand({
1646
1654
  type: "camera/stream",
1647
1655
  entity_id: entityId,
@@ -1761,7 +1769,7 @@ async function refreshStream(entityId, format = "hls") {
1761
1769
  */
1762
1770
  async function getWebRtcClientConfig(entityId) {
1763
1771
  const conn = state.conn;
1764
- if (!conn) throw new Error("Not connected");
1772
+ if (!conn) throw new Error("No connection available");
1765
1773
  return conn.sendMessagePromise({
1766
1774
  type: "camera/webrtc/get_client_config",
1767
1775
  entity_id: entityId
@@ -1,5 +1,4 @@
1
- import { state } from "../store-B1sbyexX.js";
2
- import { createDemoFixtures } from "../demo-data-Bir7Px_e.js";
1
+ import { createDemoFixtures, state } from "../demo-data-gELwUOS_.js";
3
2
 
4
3
  //#region src/testing/mock-behaviors/base.ts
5
4
  /**
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@glasshome/sync-layer",
3
- "version": "0.1.3",
3
+ "version": "0.1.4",
4
4
  "description": "Type-safe, reactive state synchronization layer for Home Assistant",
5
5
  "repository": {
6
6
  "type": "git",
@@ -34,7 +34,7 @@
34
34
  "./package.json": "./package.json"
35
35
  },
36
36
  "peerDependencies": {
37
- "@glasshome/ha-types": "^0.1.0",
37
+ "@glasshome/ha-types": "0.1.0",
38
38
  "solid-js": "^1.9.0"
39
39
  },
40
40
  "peerDependenciesMeta": {
@@ -1,54 +0,0 @@
1
- import { createStore, reconcile } from "solid-js/store";
2
-
3
- //#region src/core/store.ts
4
- const initialState = {
5
- conn: null,
6
- connectionState: "disconnected",
7
- connectionError: null,
8
- hassUrl: null,
9
- entities: {},
10
- entityRegistry: {},
11
- devices: {},
12
- areas: {},
13
- floors: {},
14
- labels: {},
15
- statistics: {},
16
- statisticsMetadata: {},
17
- history: {},
18
- forecasts: {},
19
- streams: {}
20
- };
21
- /**
22
- * Module-level SolidJS createStore singleton
23
- *
24
- * `state` is a read-only reactive proxy.
25
- * `setState` is used for mutations via produce() or direct path setting.
26
- *
27
- * @example
28
- * ```typescript
29
- * import { state, setState } from '@glasshome/sync-layer/core/store';
30
- *
31
- * // Read state (auto-tracked in SolidJS reactive contexts)
32
- * const entities = state.entities;
33
- *
34
- * // Mutate via produce
35
- * setState(produce((s) => {
36
- * s.entities['light.living_room'] = newEntity;
37
- * }));
38
- *
39
- * // Bulk replace
40
- * setState("entities", reconcile(newEntities));
41
- * ```
42
- */
43
- const storeTuple = createStore(initialState);
44
- const state = storeTuple[0];
45
- const setState = storeTuple[1];
46
- /**
47
- * Reset store to initial state
48
- */
49
- function resetStore() {
50
- setState(reconcile(initialState));
51
- }
52
-
53
- //#endregion
54
- export { resetStore, setState, state };
@@ -1,10 +0,0 @@
1
- //#region src/core/types.ts
2
- /**
3
- * Extract domain from entity ID
4
- */
5
- function extractDomain(entityId) {
6
- return entityId.split(".")[0] ?? "";
7
- }
8
-
9
- //#endregion
10
- export { extractDomain };