@glasshome/sync-layer 0.3.1 → 0.4.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.
@@ -53,18 +53,18 @@ export interface OAuthOptions {
53
53
  * Authenticate using OAuth flow
54
54
  *
55
55
  * This function uses `getAuth` from `home-assistant-js-websocket` which:
56
- * 1. Opens Home Assistant authorization page in a popup
56
+ * 1. Redirects the whole page to the Home Assistant authorization page
57
57
  * 2. Waits for user authorization
58
- * 3. Exchanges authorization code for tokens
59
- * 4. Returns authenticated Auth object
58
+ * 3. On return, HA redirects back to the app URL with `?auth_callback=1&code=…`
59
+ * 4. Exchanges the authorization code for tokens and returns an Auth object
60
60
  *
61
61
  * Note: For a complete OAuth flow, you typically need to:
62
62
  * - Set up a redirect URI endpoint in your app
63
63
  * - Handle the OAuth callback with the authorization code
64
64
  * - Use `getAuthCode` callback to provide the code
65
65
  *
66
- * Alternatively, `getAuth` can automatically handle the flow in browser environments
67
- * by opening a popup and listening for the callback.
66
+ * In browser environments `getAuth` handles this automatically via a full-page
67
+ * redirect (not a popup); native platforms supply `getAuthCode` instead.
68
68
  *
69
69
  * @param options - OAuth options
70
70
  * @returns Authenticated Auth object
@@ -89,6 +89,12 @@ export interface DemoStatisticBucket {
89
89
  max: number;
90
90
  /** Energy in Wh accumulated over the bucket (W-h integration). */
91
91
  sum: number;
92
+ /**
93
+ * Energy added during this bucket, in kWh, matching HA's `change` field for
94
+ * an energy (kWh) sensor. This is the per-bucket delta (not cumulative), the
95
+ * value daily-total consumers read.
96
+ */
97
+ change: number;
92
98
  }
93
99
  /**
94
100
  * Synthesize statistics buckets for one energy entity across a window.
@@ -160,6 +160,23 @@ function evChargerPower(lt) {
160
160
  const ramp = into < 2 ? into / 2 : 1;
161
161
  return 7200 * ramp;
162
162
  }
163
+ const HEAT_PUMP_PEAK_W = 3200;
164
+ /**
165
+ * A smooth heat-pump load: a slow morning/evening envelope carrying a
166
+ * few-minute shimmer. Its peaks push the home deficit past the battery's power
167
+ * cap, so grid import (and the running cost) visibly rise and fall on live
168
+ * ticks instead of sitting at zero. Gated out of the deep-night window so the
169
+ * quiet 02:00-03:00 baseline is unchanged.
170
+ */
171
+ function heatPumpPower(lt) {
172
+ const h = lt.hourFrac;
173
+ let envelope = 0;
174
+ if (h >= 6 && h <= 9) envelope = Math.sin((h - 6) / 3 * Math.PI);
175
+ else if (h >= 16 && h <= 23) envelope = Math.sin((h - 16) / 7 * Math.PI);
176
+ else return 0;
177
+ const shimmer = .7 + .3 * Math.sin(lt.minuteOfDay * Math.PI * 2);
178
+ return HEAT_PUMP_PEAK_W * envelope * shimmer;
179
+ }
163
180
  /** Smooth occupancy envelope: extra load mornings and evenings. */
164
181
  function occupancyEnvelope(lt) {
165
182
  const h = lt.hourFrac;
@@ -228,7 +245,7 @@ function batterySoc(lt, midnightMs) {
228
245
  return a + (b - a) * frac;
229
246
  }
230
247
  function homePower(lt) {
231
- const base = ALWAYS_ON_W + fridgePower(lt) + kettlePower(lt) + ovenPower(lt) + dishwasherPower(lt) + washerPower(lt) + evChargerPower(lt) + occupancyEnvelope(lt);
248
+ const base = ALWAYS_ON_W + fridgePower(lt) + kettlePower(lt) + ovenPower(lt) + dishwasherPower(lt) + washerPower(lt) + evChargerPower(lt) + heatPumpPower(lt) + occupancyEnvelope(lt);
232
249
  return base * homeNoise(lt);
233
250
  }
234
251
  /**
@@ -416,7 +433,8 @@ function synthesizeEnergyStatistics(entityId, startMs, endMs, period) {
416
433
  mean,
417
434
  min: min === Number.POSITIVE_INFINITY ? mean : min,
418
435
  max: max === Number.NEGATIVE_INFINITY ? mean : max,
419
- sum: cumulativeWh
436
+ sum: cumulativeWh,
437
+ change: energyWh / 1e3
420
438
  });
421
439
  }
422
440
  return buckets;
@@ -851,6 +869,26 @@ function createDemoFixtures() {
851
869
  area_id: "entry",
852
870
  supported_features: 1
853
871
  });
872
+ add(makeEntity("light.studio_rgb", "on", {
873
+ brightness: 205,
874
+ hs_color: [265, 66],
875
+ color_mode: "hs",
876
+ supported_color_modes: ["hs", "color_temp"]
877
+ }), {
878
+ device_id: "studio_lights",
879
+ area_id: "living_room",
880
+ supported_features: 44
881
+ });
882
+ add(makeEntity("light.desk_rgb", "on", {
883
+ brightness: 200,
884
+ hs_color: [172, 79],
885
+ color_mode: "hs",
886
+ supported_color_modes: ["hs", "color_temp"]
887
+ }), {
888
+ device_id: "desk_lights",
889
+ area_id: "bedroom",
890
+ supported_features: 44
891
+ });
854
892
  add(makeEntity("sensor.temperature_living", "22.5", {
855
893
  unit_of_measurement: "°C",
856
894
  device_class: "temperature",
package/dist/index.js CHANGED
@@ -1,4 +1,4 @@
1
- import { ENERGY_ENTITY_IDS, createDemoFixtures, demoEnergyPreferences, energyEntityValue, formatEnergyState, isEnergyEntity, isSunUp, resetStore, setState, simulateEnergy, state, sunEvents, synthesizeEnergyHistory, synthesizeEnergyStatistics } from "./demo-data-DiGSP4EH.js";
1
+ import { ENERGY_ENTITY_IDS, createDemoFixtures, demoEnergyPreferences, energyEntityValue, formatEnergyState, isEnergyEntity, isSunUp, resetStore, setState, simulateEnergy, state, sunEvents, synthesizeEnergyHistory, synthesizeEnergyStatistics } from "./demo-data-CcG2efdQ.js";
2
2
  import { runHaBridgeWorker } from "./worker-main-DTbzEKor.js";
3
3
  import { createConnection, createLongLivedTokenAuth, getAuth } from "home-assistant-js-websocket";
4
4
  import { produce, reconcile } from "solid-js/store";
@@ -247,18 +247,18 @@ function updateStatisticsMetadata(metadata) {
247
247
  * Authenticate using OAuth flow
248
248
  *
249
249
  * This function uses `getAuth` from `home-assistant-js-websocket` which:
250
- * 1. Opens Home Assistant authorization page in a popup
250
+ * 1. Redirects the whole page to the Home Assistant authorization page
251
251
  * 2. Waits for user authorization
252
- * 3. Exchanges authorization code for tokens
253
- * 4. Returns authenticated Auth object
252
+ * 3. On return, HA redirects back to the app URL with `?auth_callback=1&code=…`
253
+ * 4. Exchanges the authorization code for tokens and returns an Auth object
254
254
  *
255
255
  * Note: For a complete OAuth flow, you typically need to:
256
256
  * - Set up a redirect URI endpoint in your app
257
257
  * - Handle the OAuth callback with the authorization code
258
258
  * - Use `getAuthCode` callback to provide the code
259
259
  *
260
- * Alternatively, `getAuth` can automatically handle the flow in browser environments
261
- * by opening a popup and listening for the callback.
260
+ * In browser environments `getAuth` handles this automatically via a full-page
261
+ * redirect (not a popup); native platforms supply `getAuthCode` instead.
262
262
  *
263
263
  * @param options - OAuth options
264
264
  * @returns Authenticated Auth object
@@ -1491,10 +1491,11 @@ function resolveIcon(entity$1, registry) {
1491
1491
  */
1492
1492
  function buildEntityView(entity$1, registry) {
1493
1493
  const domain = extractDomain(entity$1.entity_id);
1494
- const friendlyName = entity$1.attributes.friendly_name ?? registry?.name ?? entity$1.entity_id;
1494
+ const attributes = entity$1.attributes ?? {};
1495
+ const friendlyName = attributes.friendly_name ?? registry?.name ?? entity$1.entity_id;
1495
1496
  const name = registry?.name ?? friendlyName;
1496
1497
  const iconResolution = resolveIcon(entity$1, registry);
1497
- const { device_class: _dc, unit_of_measurement: _uom, friendly_name: _fn, icon: _icon,...restAttributes } = entity$1.attributes;
1498
+ const { device_class: _dc, unit_of_measurement: _uom, friendly_name: _fn, icon: _icon,...restAttributes } = attributes;
1498
1499
  let areaId = registry?.area_id ?? null;
1499
1500
  if (areaId === null && registry?.device_id) {
1500
1501
  const device = state.devices[registry.device_id];
@@ -1508,9 +1509,9 @@ function buildEntityView(entity$1, registry) {
1508
1509
  lastChanged: new Date(entity$1.last_changed),
1509
1510
  lastUpdated: new Date(entity$1.last_updated),
1510
1511
  context: {
1511
- id: entity$1.context.id,
1512
- parentId: entity$1.context.parent_id,
1513
- userId: entity$1.context.user_id
1512
+ id: entity$1.context?.id ?? "",
1513
+ parentId: entity$1.context?.parent_id ?? null,
1514
+ userId: entity$1.context?.user_id ?? null
1514
1515
  },
1515
1516
  name,
1516
1517
  friendlyName,
@@ -1525,9 +1526,9 @@ function buildEntityView(entity$1, registry) {
1525
1526
  entityCategory: registry?.entity_category ?? null,
1526
1527
  labels: registry?.labels ?? [],
1527
1528
  aliases: registry?.aliases ?? [],
1528
- deviceClass: registry?.device_class ?? entity$1.attributes.device_class ?? null,
1529
- unitOfMeasurement: registry?.unit_of_measurement ?? entity$1.attributes.unit_of_measurement ?? null,
1530
- supportedFeatures: registry?.supported_features ?? entity$1.attributes.supported_features
1529
+ deviceClass: registry?.device_class ?? attributes.device_class ?? null,
1530
+ unitOfMeasurement: registry?.unit_of_measurement ?? attributes.unit_of_measurement ?? null,
1531
+ supportedFeatures: registry?.supported_features ?? attributes.supported_features
1531
1532
  };
1532
1533
  }
1533
1534
  /**
@@ -2190,7 +2191,8 @@ const DEMO_HLS_STREAM = "https://test-streams.mux.dev/x36xhzz/x36xhzz.m3u8";
2190
2191
  * Returns a public demo HLS stream when not connected.
2191
2192
  */
2192
2193
  async function fetchStream(entityId, format = "hls") {
2193
- if (!state.conn) return format === "hls" ? DEMO_HLS_STREAM : null;
2194
+ if (isDemoMode()) return format === "hls" ? DEMO_HLS_STREAM : null;
2195
+ if (!state.conn) return null;
2194
2196
  const response = await sendCommand({
2195
2197
  type: "camera/stream",
2196
2198
  entity_id: entityId,
@@ -1,4 +1,4 @@
1
- import { createDemoFixtures, state } from "../demo-data-DiGSP4EH.js";
1
+ import { createDemoFixtures, state } from "../demo-data-CcG2efdQ.js";
2
2
 
3
3
  //#region src/testing/mock-behaviors/base.ts
4
4
  /**
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@glasshome/sync-layer",
3
- "version": "0.3.1",
3
+ "version": "0.4.0",
4
4
  "description": "Type-safe, reactive state synchronization layer for Home Assistant",
5
5
  "repository": {
6
6
  "type": "git",