@glasshome/sync-layer 0.3.2 → 0.5.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/dist/connection/auth.d.ts +5 -5
- package/dist/connection/types.d.ts +0 -10
- package/dist/index.js +1353 -29
- package/dist/worker/worker-main.js +1 -1
- package/package.json +1 -6
- package/dist/demo-data-3LO-p1RV.js +0 -1205
- package/dist/testing/fixtures.d.ts +0 -161
- package/dist/testing/helpers.d.ts +0 -160
- package/dist/testing/index.d.ts +0 -13
- package/dist/testing/index.js +0 -2067
- package/dist/testing/mock-behaviors/base.d.ts +0 -73
- package/dist/testing/mock-behaviors/climate.d.ts +0 -22
- package/dist/testing/mock-behaviors/cover.d.ts +0 -22
- package/dist/testing/mock-behaviors/index.d.ts +0 -11
- package/dist/testing/mock-behaviors/light.d.ts +0 -20
- package/dist/testing/mock-behaviors/lock.d.ts +0 -20
- package/dist/testing/mock-behaviors/media_player.d.ts +0 -42
- package/dist/testing/mock-behaviors/registry.d.ts +0 -55
- package/dist/testing/mock-behaviors/switch.d.ts +0 -17
- package/dist/testing/mock.d.ts +0 -196
- package/dist/testing/types.d.ts +0 -87
- /package/dist/{worker-main-DTbzEKor.js → worker-main-Cf-k4f2N.js} +0 -0
package/dist/index.js
CHANGED
|
@@ -1,7 +1,6 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { runHaBridgeWorker } from "./worker-main-DTbzEKor.js";
|
|
1
|
+
import { runHaBridgeWorker } from "./worker-main-Cf-k4f2N.js";
|
|
3
2
|
import { createConnection, createLongLivedTokenAuth, getAuth } from "home-assistant-js-websocket";
|
|
4
|
-
import { produce, reconcile } from "solid-js/store";
|
|
3
|
+
import { createStore, produce, reconcile } from "solid-js/store";
|
|
5
4
|
|
|
6
5
|
//#region src/connection/adapter.ts
|
|
7
6
|
/**
|
|
@@ -106,6 +105,39 @@ function wrapHAConnection(conn$1) {
|
|
|
106
105
|
};
|
|
107
106
|
}
|
|
108
107
|
|
|
108
|
+
//#endregion
|
|
109
|
+
//#region src/core/store.ts
|
|
110
|
+
const initialState = {
|
|
111
|
+
conn: null,
|
|
112
|
+
connectionState: "disconnected",
|
|
113
|
+
connectionError: null,
|
|
114
|
+
hassUrl: null,
|
|
115
|
+
mediaProxyBase: null,
|
|
116
|
+
config: null,
|
|
117
|
+
entities: {},
|
|
118
|
+
entityRegistry: {},
|
|
119
|
+
devices: {},
|
|
120
|
+
areas: {},
|
|
121
|
+
floors: {},
|
|
122
|
+
labels: {},
|
|
123
|
+
statistics: {},
|
|
124
|
+
statisticsMetadata: {},
|
|
125
|
+
history: {},
|
|
126
|
+
forecasts: {},
|
|
127
|
+
streams: {}
|
|
128
|
+
};
|
|
129
|
+
if (globalThis.__GH_SYNC_LAYER_STORE__) console.error("[@glasshome/sync-layer] DUPLICATE store module instantiated. This means @glasshome/sync-layer was bundled into >1 chunk. Reads and writes will hit different singletons. Check vendor build externals and relative-vs-bare imports across entry points.");
|
|
130
|
+
globalThis.__GH_SYNC_LAYER_STORE__ = true;
|
|
131
|
+
const storeTuple = createStore(initialState);
|
|
132
|
+
const state = storeTuple[0];
|
|
133
|
+
const setState = storeTuple[1];
|
|
134
|
+
/**
|
|
135
|
+
* Reset store to initial state
|
|
136
|
+
*/
|
|
137
|
+
function resetStore() {
|
|
138
|
+
setState(reconcile(initialState));
|
|
139
|
+
}
|
|
140
|
+
|
|
109
141
|
//#endregion
|
|
110
142
|
//#region src/core/reducers.ts
|
|
111
143
|
/**
|
|
@@ -247,18 +279,18 @@ function updateStatisticsMetadata(metadata) {
|
|
|
247
279
|
* Authenticate using OAuth flow
|
|
248
280
|
*
|
|
249
281
|
* This function uses `getAuth` from `home-assistant-js-websocket` which:
|
|
250
|
-
* 1.
|
|
282
|
+
* 1. Redirects the whole page to the Home Assistant authorization page
|
|
251
283
|
* 2. Waits for user authorization
|
|
252
|
-
* 3.
|
|
253
|
-
* 4.
|
|
284
|
+
* 3. On return, HA redirects back to the app URL with `?auth_callback=1&code=…`
|
|
285
|
+
* 4. Exchanges the authorization code for tokens and returns an Auth object
|
|
254
286
|
*
|
|
255
287
|
* Note: For a complete OAuth flow, you typically need to:
|
|
256
288
|
* - Set up a redirect URI endpoint in your app
|
|
257
289
|
* - Handle the OAuth callback with the authorization code
|
|
258
290
|
* - Use `getAuthCode` callback to provide the code
|
|
259
291
|
*
|
|
260
|
-
*
|
|
261
|
-
*
|
|
292
|
+
* In browser environments `getAuth` handles this automatically via a full-page
|
|
293
|
+
* redirect (not a popup); native platforms supply `getAuthCode` instead.
|
|
262
294
|
*
|
|
263
295
|
* @param options - OAuth options
|
|
264
296
|
* @returns Authenticated Auth object
|
|
@@ -501,6 +533,1250 @@ function extractDomain(entityId) {
|
|
|
501
533
|
return entityId.split(".")[0] ?? "";
|
|
502
534
|
}
|
|
503
535
|
|
|
536
|
+
//#endregion
|
|
537
|
+
//#region src/demo/energy-sim.ts
|
|
538
|
+
const SOLAR_PEAK_W = 5200;
|
|
539
|
+
const SUNRISE_HOUR = 6.5;
|
|
540
|
+
const SUNSET_HOUR = 20.5;
|
|
541
|
+
const ALWAYS_ON_W = 285;
|
|
542
|
+
const FRIDGE_W = 95;
|
|
543
|
+
const FRIDGE_CYCLE_MIN = 45;
|
|
544
|
+
const FRIDGE_ON_MIN = 18;
|
|
545
|
+
const BATTERY_CAPACITY_WH = 1e4;
|
|
546
|
+
const BATTERY_MIN_SOC = 10;
|
|
547
|
+
const BATTERY_MAX_SOC = 100;
|
|
548
|
+
const BATTERY_POWER_CAP_W = 3e3;
|
|
549
|
+
const MIDNIGHT_SOC = 55;
|
|
550
|
+
const SOC_STEP_MIN = 5;
|
|
551
|
+
/** Integer hash -> uint32, used to seed mulberry32 deterministically. */
|
|
552
|
+
function hashInt(n) {
|
|
553
|
+
let h = n | 0;
|
|
554
|
+
h = Math.imul(h ^ h >>> 16, 73244475);
|
|
555
|
+
h = Math.imul(h ^ h >>> 16, 73244475);
|
|
556
|
+
h = (h ^ h >>> 16) >>> 0;
|
|
557
|
+
return h;
|
|
558
|
+
}
|
|
559
|
+
/** Deterministic [0,1) generator seeded by an integer. */
|
|
560
|
+
function mulberry32(seed) {
|
|
561
|
+
let a = seed >>> 0;
|
|
562
|
+
return () => {
|
|
563
|
+
a |= 0;
|
|
564
|
+
a = a + 1831565813 | 0;
|
|
565
|
+
let t = Math.imul(a ^ a >>> 15, 1 | a);
|
|
566
|
+
t = t + Math.imul(t ^ t >>> 7, 61 | t) ^ t;
|
|
567
|
+
return ((t ^ t >>> 14) >>> 0) / 4294967296;
|
|
568
|
+
};
|
|
569
|
+
}
|
|
570
|
+
function localTime(timestampMs) {
|
|
571
|
+
const d = new Date(timestampMs);
|
|
572
|
+
const hours = d.getHours();
|
|
573
|
+
const minutes = d.getMinutes();
|
|
574
|
+
const seconds = d.getSeconds();
|
|
575
|
+
const hourFrac = hours + minutes / 60 + seconds / 3600;
|
|
576
|
+
const minuteOfDay = hours * 60 + minutes + seconds / 60;
|
|
577
|
+
const offsetMs = d.getTimezoneOffset() * 6e4;
|
|
578
|
+
const dayKey = Math.floor((timestampMs - offsetMs) / 864e5);
|
|
579
|
+
return {
|
|
580
|
+
hourFrac,
|
|
581
|
+
minuteOfDay,
|
|
582
|
+
dayOfWeek: d.getDay(),
|
|
583
|
+
dayKey
|
|
584
|
+
};
|
|
585
|
+
}
|
|
586
|
+
function localMidnightMs(timestampMs, lt) {
|
|
587
|
+
return timestampMs - lt.minuteOfDay * 6e4;
|
|
588
|
+
}
|
|
589
|
+
/** Smooth multiplicative cloud factor in [0.55, 1.0], seeded per day. */
|
|
590
|
+
function cloudFactor(lt) {
|
|
591
|
+
const rand = mulberry32(hashInt(lt.dayKey * 2654435761));
|
|
592
|
+
const phase1 = rand() * Math.PI * 2;
|
|
593
|
+
const phase2 = rand() * Math.PI * 2;
|
|
594
|
+
const phase3 = rand() * Math.PI * 2;
|
|
595
|
+
const m = lt.minuteOfDay;
|
|
596
|
+
const wave = .16 * Math.sin(m / 37 * Math.PI * 2 + phase1) + .14 * Math.sin(m / 73 * Math.PI * 2 + phase2) + .05 * Math.sin(m / 11 * Math.PI * 2 + phase3);
|
|
597
|
+
const f = .85 + wave;
|
|
598
|
+
return Math.max(.55, Math.min(1, f));
|
|
599
|
+
}
|
|
600
|
+
function solarPower(lt) {
|
|
601
|
+
if (lt.hourFrac <= SUNRISE_HOUR || lt.hourFrac >= SUNSET_HOUR) return 0;
|
|
602
|
+
const frac = (lt.hourFrac - SUNRISE_HOUR) / (SUNSET_HOUR - SUNRISE_HOUR);
|
|
603
|
+
const shape = Math.sin(frac * Math.PI) ** 1.3;
|
|
604
|
+
return SOLAR_PEAK_W * shape * cloudFactor(lt);
|
|
605
|
+
}
|
|
606
|
+
/** Coarse solar elevation in degrees for sun.sun attributes. */
|
|
607
|
+
function sunElevation(lt) {
|
|
608
|
+
if (lt.hourFrac <= SUNRISE_HOUR || lt.hourFrac >= SUNSET_HOUR) return -12;
|
|
609
|
+
const frac = (lt.hourFrac - SUNRISE_HOUR) / (SUNSET_HOUR - SUNRISE_HOUR);
|
|
610
|
+
return 60 * Math.sin(frac * Math.PI);
|
|
611
|
+
}
|
|
612
|
+
function fridgePower(lt) {
|
|
613
|
+
const rand = mulberry32(hashInt(lt.dayKey * 40503 + 7));
|
|
614
|
+
const phase = rand() * FRIDGE_CYCLE_MIN;
|
|
615
|
+
const pos = (lt.minuteOfDay + phase) % FRIDGE_CYCLE_MIN;
|
|
616
|
+
return pos < FRIDGE_ON_MIN ? FRIDGE_W : 0;
|
|
617
|
+
}
|
|
618
|
+
/** A square wave: `power` while inside [start, start+durationMin). */
|
|
619
|
+
function windowPower(minuteOfDay, startMin, durationMin, power) {
|
|
620
|
+
return minuteOfDay >= startMin && minuteOfDay < startMin + durationMin ? power : 0;
|
|
621
|
+
}
|
|
622
|
+
function kettlePower(lt) {
|
|
623
|
+
return windowPower(lt.minuteOfDay, 430, 4, 2e3) + windowPower(lt.minuteOfDay, 1e3, 4, 2e3);
|
|
624
|
+
}
|
|
625
|
+
function ovenPower(lt) {
|
|
626
|
+
const start = 1125;
|
|
627
|
+
if (lt.minuteOfDay < start || lt.minuteOfDay >= start + 40) return 0;
|
|
628
|
+
const into = lt.minuteOfDay - start;
|
|
629
|
+
const cyclePos = into % 6.5;
|
|
630
|
+
return cyclePos < 4 ? 2200 : 0;
|
|
631
|
+
}
|
|
632
|
+
function dishwasherPower(lt) {
|
|
633
|
+
const start = 1270;
|
|
634
|
+
if (lt.minuteOfDay < start || lt.minuteOfDay >= start + 65) return 0;
|
|
635
|
+
const into = lt.minuteOfDay - start;
|
|
636
|
+
if (into < 12) return 1900;
|
|
637
|
+
if (into >= 57) return 1900;
|
|
638
|
+
return 120;
|
|
639
|
+
}
|
|
640
|
+
function washerPower(lt) {
|
|
641
|
+
if (![
|
|
642
|
+
1,
|
|
643
|
+
4,
|
|
644
|
+
6
|
|
645
|
+
].includes(lt.dayOfWeek)) return 0;
|
|
646
|
+
const start = 630;
|
|
647
|
+
if (lt.minuteOfDay < start || lt.minuteOfDay >= start + 50) return 0;
|
|
648
|
+
const into = lt.minuteOfDay - start;
|
|
649
|
+
return into < 14 ? 2100 : 250;
|
|
650
|
+
}
|
|
651
|
+
function evChargerPower(lt) {
|
|
652
|
+
if (![
|
|
653
|
+
2,
|
|
654
|
+
4,
|
|
655
|
+
0
|
|
656
|
+
].includes(lt.dayOfWeek)) return 0;
|
|
657
|
+
const start = 1100;
|
|
658
|
+
const stop = 1300;
|
|
659
|
+
if (lt.minuteOfDay < start || lt.minuteOfDay >= stop) return 0;
|
|
660
|
+
const into = lt.minuteOfDay - start;
|
|
661
|
+
const ramp = into < 2 ? into / 2 : 1;
|
|
662
|
+
return 7200 * ramp;
|
|
663
|
+
}
|
|
664
|
+
const HEAT_PUMP_PEAK_W = 3200;
|
|
665
|
+
/**
|
|
666
|
+
* A smooth heat-pump load: a slow morning/evening envelope carrying a
|
|
667
|
+
* few-minute shimmer. Its peaks push the home deficit past the battery's power
|
|
668
|
+
* cap, so grid import (and the running cost) visibly rise and fall on live
|
|
669
|
+
* ticks instead of sitting at zero. Gated out of the deep-night window so the
|
|
670
|
+
* quiet 02:00-03:00 baseline is unchanged.
|
|
671
|
+
*/
|
|
672
|
+
function heatPumpPower(lt) {
|
|
673
|
+
const h = lt.hourFrac;
|
|
674
|
+
let envelope = 0;
|
|
675
|
+
if (h >= 6 && h <= 9) envelope = Math.sin((h - 6) / 3 * Math.PI);
|
|
676
|
+
else if (h >= 16 && h <= 23) envelope = Math.sin((h - 16) / 7 * Math.PI);
|
|
677
|
+
else return 0;
|
|
678
|
+
const shimmer = .7 + .3 * Math.sin(lt.minuteOfDay * Math.PI * 2);
|
|
679
|
+
return HEAT_PUMP_PEAK_W * envelope * shimmer;
|
|
680
|
+
}
|
|
681
|
+
/** Smooth occupancy envelope: extra load mornings and evenings. */
|
|
682
|
+
function occupancyEnvelope(lt) {
|
|
683
|
+
const h = lt.hourFrac;
|
|
684
|
+
let e = 0;
|
|
685
|
+
if (h >= 7 && h <= 9) e += 80 + 270 * Math.sin((h - 7) / 2 * Math.PI);
|
|
686
|
+
if (h >= 17.5 && h <= 23) e += 80 + 270 * Math.sin((h - 17.5) / 5.5 * Math.PI);
|
|
687
|
+
return e;
|
|
688
|
+
}
|
|
689
|
+
/** Low-amplitude smooth noise multiplier, ±4%, seeded per day. */
|
|
690
|
+
function homeNoise(lt) {
|
|
691
|
+
const rand = mulberry32(hashInt(lt.dayKey * 22699 + 13));
|
|
692
|
+
const phase = rand() * Math.PI * 2;
|
|
693
|
+
return 1 + .04 * Math.sin(lt.minuteOfDay / 23 * Math.PI * 2 + phase);
|
|
694
|
+
}
|
|
695
|
+
const socCache = /* @__PURE__ */ new Map();
|
|
696
|
+
/** Net surplus (solar - home) at a given local-time sample, in watts. */
|
|
697
|
+
function netSurplusAt(timestampMs) {
|
|
698
|
+
const lt = localTime(timestampMs);
|
|
699
|
+
const solar = solarPower(lt);
|
|
700
|
+
const home = homePower(lt);
|
|
701
|
+
return solar - home;
|
|
702
|
+
}
|
|
703
|
+
/**
|
|
704
|
+
* Battery SOC (%) at every {@link SOC_STEP_MIN}-minute step from local
|
|
705
|
+
* midnight. Returns step `i` = SOC after integrating `i` steps. Index 0 is
|
|
706
|
+
* the midnight SOC. Memoized per local day (pure computation).
|
|
707
|
+
*/
|
|
708
|
+
function socPrefix(dayKey, midnightMs) {
|
|
709
|
+
const cached = socCache.get(dayKey);
|
|
710
|
+
if (cached) return cached;
|
|
711
|
+
const steps = Math.ceil(24 * 60 / SOC_STEP_MIN);
|
|
712
|
+
const arr = new Float64Array(steps + 1);
|
|
713
|
+
arr[0] = MIDNIGHT_SOC;
|
|
714
|
+
const stepMs = SOC_STEP_MIN * 6e4;
|
|
715
|
+
const stepHours = SOC_STEP_MIN / 60;
|
|
716
|
+
for (let i = 0; i < steps; i++) {
|
|
717
|
+
const soc = arr[i] ?? MIDNIGHT_SOC;
|
|
718
|
+
const sampleMs = midnightMs + i * stepMs;
|
|
719
|
+
const surplus = netSurplusAt(sampleMs);
|
|
720
|
+
let chargeW = 0;
|
|
721
|
+
let dischargeW = 0;
|
|
722
|
+
if (surplus > 0) {
|
|
723
|
+
const headroomWh = (BATTERY_MAX_SOC - soc) / 100 * BATTERY_CAPACITY_WH;
|
|
724
|
+
const headroomW = headroomWh / stepHours;
|
|
725
|
+
chargeW = Math.min(surplus, BATTERY_POWER_CAP_W, Math.max(0, headroomW));
|
|
726
|
+
} else if (surplus < 0) {
|
|
727
|
+
const availWh = (soc - BATTERY_MIN_SOC) / 100 * BATTERY_CAPACITY_WH;
|
|
728
|
+
const availW = availWh / stepHours;
|
|
729
|
+
dischargeW = Math.min(-surplus, BATTERY_POWER_CAP_W, Math.max(0, availW));
|
|
730
|
+
}
|
|
731
|
+
const deltaWh = (chargeW - dischargeW) * stepHours;
|
|
732
|
+
const nextSoc = soc + deltaWh / BATTERY_CAPACITY_WH * 100;
|
|
733
|
+
arr[i + 1] = Math.max(BATTERY_MIN_SOC, Math.min(BATTERY_MAX_SOC, nextSoc));
|
|
734
|
+
}
|
|
735
|
+
socCache.set(dayKey, arr);
|
|
736
|
+
return arr;
|
|
737
|
+
}
|
|
738
|
+
/** Battery SOC (%) at an exact timestamp, linearly interpolated between steps. */
|
|
739
|
+
function batterySoc(lt, midnightMs) {
|
|
740
|
+
const prefix = socPrefix(lt.dayKey, midnightMs);
|
|
741
|
+
const stepFloat = lt.minuteOfDay / SOC_STEP_MIN;
|
|
742
|
+
const i = Math.floor(stepFloat);
|
|
743
|
+
const frac = stepFloat - i;
|
|
744
|
+
const a = prefix[i] ?? MIDNIGHT_SOC;
|
|
745
|
+
const b = prefix[i + 1] ?? a;
|
|
746
|
+
return a + (b - a) * frac;
|
|
747
|
+
}
|
|
748
|
+
function homePower(lt) {
|
|
749
|
+
const base = ALWAYS_ON_W + fridgePower(lt) + kettlePower(lt) + ovenPower(lt) + dishwasherPower(lt) + washerPower(lt) + evChargerPower(lt) + heatPumpPower(lt) + occupancyEnvelope(lt);
|
|
750
|
+
return base * homeNoise(lt);
|
|
751
|
+
}
|
|
752
|
+
/**
|
|
753
|
+
* Compute every instantaneous energy value at a timestamp.
|
|
754
|
+
*
|
|
755
|
+
* Deterministic: equal timestamps always yield deep-equal samples. Obeys the
|
|
756
|
+
* conservation invariant
|
|
757
|
+
* `solar + gridImport + batteryDischarge === home + gridExport + batteryCharge`
|
|
758
|
+
* to within rounding.
|
|
759
|
+
*/
|
|
760
|
+
function simulateEnergy(timestampMs) {
|
|
761
|
+
const lt = localTime(timestampMs);
|
|
762
|
+
const midnightMs = localMidnightMs(timestampMs, lt);
|
|
763
|
+
const solarW = solarPower(lt);
|
|
764
|
+
const fridgeW = fridgePower(lt);
|
|
765
|
+
const ovenW = ovenPower(lt);
|
|
766
|
+
const dishwasherW = dishwasherPower(lt);
|
|
767
|
+
const washerW = washerPower(lt);
|
|
768
|
+
const evChargerW = evChargerPower(lt);
|
|
769
|
+
const alwaysOnW = ALWAYS_ON_W;
|
|
770
|
+
const homeW = homePower(lt);
|
|
771
|
+
const soc = batterySoc(lt, midnightMs);
|
|
772
|
+
const surplus = solarW - homeW;
|
|
773
|
+
let batteryChargeW = 0;
|
|
774
|
+
let batteryDischargeW = 0;
|
|
775
|
+
if (surplus > 0) {
|
|
776
|
+
const headroom = soc < BATTERY_MAX_SOC ? BATTERY_POWER_CAP_W : 0;
|
|
777
|
+
batteryChargeW = Math.min(surplus, headroom);
|
|
778
|
+
} else if (surplus < 0) {
|
|
779
|
+
const avail = soc > BATTERY_MIN_SOC ? BATTERY_POWER_CAP_W : 0;
|
|
780
|
+
batteryDischargeW = Math.min(-surplus, avail);
|
|
781
|
+
}
|
|
782
|
+
const remainingDeficit = homeW - solarW - batteryDischargeW + batteryChargeW;
|
|
783
|
+
const gridImportW = Math.max(0, remainingDeficit);
|
|
784
|
+
const gridExportW = Math.max(0, -remainingDeficit);
|
|
785
|
+
return {
|
|
786
|
+
solarW,
|
|
787
|
+
homeW,
|
|
788
|
+
batteryChargeW,
|
|
789
|
+
batteryDischargeW,
|
|
790
|
+
batterySocPct: soc,
|
|
791
|
+
gridImportW,
|
|
792
|
+
gridExportW,
|
|
793
|
+
fridgeW,
|
|
794
|
+
dishwasherW,
|
|
795
|
+
washerW,
|
|
796
|
+
ovenW,
|
|
797
|
+
evChargerW,
|
|
798
|
+
alwaysOnW
|
|
799
|
+
};
|
|
800
|
+
}
|
|
801
|
+
/** Whether the sun is above the horizon at a timestamp. */
|
|
802
|
+
function isSunUp(timestampMs) {
|
|
803
|
+
const lt = localTime(timestampMs);
|
|
804
|
+
return lt.hourFrac > SUNRISE_HOUR && lt.hourFrac < SUNSET_HOUR;
|
|
805
|
+
}
|
|
806
|
+
/** Next sunrise/sunset ISO strings relative to a timestamp. */
|
|
807
|
+
function sunEvents(timestampMs) {
|
|
808
|
+
const lt = localTime(timestampMs);
|
|
809
|
+
const midnightMs = localMidnightMs(timestampMs, lt);
|
|
810
|
+
const sunriseMs = midnightMs + SUNRISE_HOUR * 36e5;
|
|
811
|
+
const sunsetMs = midnightMs + SUNSET_HOUR * 36e5;
|
|
812
|
+
const nextRising = timestampMs < sunriseMs ? sunriseMs : sunriseMs + 864e5;
|
|
813
|
+
const nextSetting = timestampMs < sunsetMs ? sunsetMs : sunsetMs + 864e5;
|
|
814
|
+
return {
|
|
815
|
+
nextRising: new Date(nextRising).toISOString(),
|
|
816
|
+
nextSetting: new Date(nextSetting).toISOString(),
|
|
817
|
+
elevation: Math.round(sunElevation(lt) * 100) / 100
|
|
818
|
+
};
|
|
819
|
+
}
|
|
820
|
+
/** The energy sensor entity ids the simulation drives. */
|
|
821
|
+
const ENERGY_ENTITY_IDS = [
|
|
822
|
+
"sensor.solar_power",
|
|
823
|
+
"sensor.grid_import_power",
|
|
824
|
+
"sensor.grid_export_power",
|
|
825
|
+
"sensor.battery_charge_power",
|
|
826
|
+
"sensor.battery_discharge_power",
|
|
827
|
+
"sensor.battery_soc",
|
|
828
|
+
"sensor.home_power",
|
|
829
|
+
"sensor.fridge_power",
|
|
830
|
+
"sensor.dishwasher_power",
|
|
831
|
+
"sensor.washing_machine_power",
|
|
832
|
+
"sensor.oven_power",
|
|
833
|
+
"sensor.ev_charger_power",
|
|
834
|
+
"sensor.always_on_power"
|
|
835
|
+
];
|
|
836
|
+
/** Map an energy entity id to its numeric value from a sample (W, or % for SOC). */
|
|
837
|
+
function energyEntityValue(entityId, sample) {
|
|
838
|
+
switch (entityId) {
|
|
839
|
+
case "sensor.solar_power": return sample.solarW;
|
|
840
|
+
case "sensor.grid_import_power": return sample.gridImportW;
|
|
841
|
+
case "sensor.grid_export_power": return sample.gridExportW;
|
|
842
|
+
case "sensor.battery_charge_power": return sample.batteryChargeW;
|
|
843
|
+
case "sensor.battery_discharge_power": return sample.batteryDischargeW;
|
|
844
|
+
case "sensor.battery_soc": return sample.batterySocPct;
|
|
845
|
+
case "sensor.home_power": return sample.homeW;
|
|
846
|
+
case "sensor.fridge_power": return sample.fridgeW;
|
|
847
|
+
case "sensor.dishwasher_power": return sample.dishwasherW;
|
|
848
|
+
case "sensor.washing_machine_power": return sample.washerW;
|
|
849
|
+
case "sensor.oven_power": return sample.ovenW;
|
|
850
|
+
case "sensor.ev_charger_power": return sample.evChargerW;
|
|
851
|
+
case "sensor.always_on_power": return sample.alwaysOnW;
|
|
852
|
+
default: return void 0;
|
|
853
|
+
}
|
|
854
|
+
}
|
|
855
|
+
/** Whether an entity id is one of the simulated energy sensors. */
|
|
856
|
+
function isEnergyEntity(entityId) {
|
|
857
|
+
return energyEntityValue(entityId, ZERO_SAMPLE) !== void 0;
|
|
858
|
+
}
|
|
859
|
+
const ZERO_SAMPLE = {
|
|
860
|
+
solarW: 0,
|
|
861
|
+
homeW: 0,
|
|
862
|
+
batteryChargeW: 0,
|
|
863
|
+
batteryDischargeW: 0,
|
|
864
|
+
batterySocPct: 0,
|
|
865
|
+
gridImportW: 0,
|
|
866
|
+
gridExportW: 0,
|
|
867
|
+
fridgeW: 0,
|
|
868
|
+
dishwasherW: 0,
|
|
869
|
+
washerW: 0,
|
|
870
|
+
ovenW: 0,
|
|
871
|
+
evChargerW: 0,
|
|
872
|
+
alwaysOnW: 0
|
|
873
|
+
};
|
|
874
|
+
/** Round to 1 decimal for display-friendly state strings (whole % for SOC). */
|
|
875
|
+
function formatEnergyState(entityId, value) {
|
|
876
|
+
if (entityId === "sensor.battery_soc") return String(Math.round(value));
|
|
877
|
+
return String(Math.round(value));
|
|
878
|
+
}
|
|
879
|
+
/**
|
|
880
|
+
* Synthesize history points for one energy entity by sampling the model across
|
|
881
|
+
* the window at a fixed cadence (default 1-minute). Returns HA-compressed
|
|
882
|
+
* `{ lu, s }` points with `lu` in Unix seconds.
|
|
883
|
+
*/
|
|
884
|
+
function synthesizeEnergyHistory(entityId, startMs, endMs, stepMs = 6e4) {
|
|
885
|
+
const points = [];
|
|
886
|
+
for (let t = startMs; t <= endMs; t += stepMs) {
|
|
887
|
+
const value = energyEntityValue(entityId, simulateEnergy(t));
|
|
888
|
+
if (value === void 0) continue;
|
|
889
|
+
points.push({
|
|
890
|
+
lu: Math.round(t / 1e3),
|
|
891
|
+
s: formatEnergyState(entityId, value)
|
|
892
|
+
});
|
|
893
|
+
}
|
|
894
|
+
return points;
|
|
895
|
+
}
|
|
896
|
+
/** Bucket length in ms for a statistics period. */
|
|
897
|
+
function bucketMs(period) {
|
|
898
|
+
return period === "hour" ? 36e5 : 864e5;
|
|
899
|
+
}
|
|
900
|
+
/**
|
|
901
|
+
* Synthesize statistics buckets for one energy entity across a window.
|
|
902
|
+
*
|
|
903
|
+
* `mean`/`min`/`max` come from 5-minute power samples in the bucket; `sum` is
|
|
904
|
+
* the energy in Wh (mean W times bucket hours). `sum` is cumulative across the
|
|
905
|
+
* window, matching HA's monotonically increasing meter semantics.
|
|
906
|
+
*/
|
|
907
|
+
function synthesizeEnergyStatistics(entityId, startMs, endMs, period) {
|
|
908
|
+
const span = bucketMs(period);
|
|
909
|
+
const sampleMs = 5 * 6e4;
|
|
910
|
+
const buckets = [];
|
|
911
|
+
let cumulativeWh = 0;
|
|
912
|
+
for (let bStart = startMs; bStart < endMs; bStart += span) {
|
|
913
|
+
const bEnd = bStart + span;
|
|
914
|
+
let sum = 0;
|
|
915
|
+
let count = 0;
|
|
916
|
+
let min = Number.POSITIVE_INFINITY;
|
|
917
|
+
let max = Number.NEGATIVE_INFINITY;
|
|
918
|
+
for (let t = bStart; t < bEnd; t += sampleMs) {
|
|
919
|
+
const value = energyEntityValue(entityId, simulateEnergy(t));
|
|
920
|
+
if (value === void 0) continue;
|
|
921
|
+
sum += value;
|
|
922
|
+
count++;
|
|
923
|
+
if (value < min) min = value;
|
|
924
|
+
if (value > max) max = value;
|
|
925
|
+
}
|
|
926
|
+
if (count === 0) continue;
|
|
927
|
+
const mean = sum / count;
|
|
928
|
+
const bucketHours = span / 36e5;
|
|
929
|
+
const energyWh = entityId === "sensor.battery_soc" ? 0 : mean * bucketHours;
|
|
930
|
+
cumulativeWh += energyWh;
|
|
931
|
+
buckets.push({
|
|
932
|
+
start: bStart,
|
|
933
|
+
end: bEnd,
|
|
934
|
+
mean,
|
|
935
|
+
min: min === Number.POSITIVE_INFINITY ? mean : min,
|
|
936
|
+
max: max === Number.NEGATIVE_INFINITY ? mean : max,
|
|
937
|
+
sum: cumulativeWh,
|
|
938
|
+
change: energyWh / 1e3
|
|
939
|
+
});
|
|
940
|
+
}
|
|
941
|
+
return buckets;
|
|
942
|
+
}
|
|
943
|
+
/** The demo energy dashboard preferences wiring the simulated sensors. */
|
|
944
|
+
function demoEnergyPreferences() {
|
|
945
|
+
return {
|
|
946
|
+
energy_sources: [
|
|
947
|
+
{
|
|
948
|
+
type: "solar",
|
|
949
|
+
stat_energy_from: "sensor.solar_power"
|
|
950
|
+
},
|
|
951
|
+
{
|
|
952
|
+
type: "grid",
|
|
953
|
+
flow_from: [{ stat_energy_from: "sensor.grid_import_power" }],
|
|
954
|
+
flow_to: [{ stat_energy_to: "sensor.grid_export_power" }]
|
|
955
|
+
},
|
|
956
|
+
{
|
|
957
|
+
type: "battery",
|
|
958
|
+
stat_energy_from: "sensor.battery_discharge_power",
|
|
959
|
+
stat_energy_to: "sensor.battery_charge_power"
|
|
960
|
+
}
|
|
961
|
+
],
|
|
962
|
+
device_consumption: [
|
|
963
|
+
{
|
|
964
|
+
stat_consumption: "sensor.fridge_power",
|
|
965
|
+
name: "Fridge"
|
|
966
|
+
},
|
|
967
|
+
{
|
|
968
|
+
stat_consumption: "sensor.dishwasher_power",
|
|
969
|
+
name: "Dishwasher"
|
|
970
|
+
},
|
|
971
|
+
{
|
|
972
|
+
stat_consumption: "sensor.washing_machine_power",
|
|
973
|
+
name: "Washing Machine"
|
|
974
|
+
},
|
|
975
|
+
{
|
|
976
|
+
stat_consumption: "sensor.oven_power",
|
|
977
|
+
name: "Oven"
|
|
978
|
+
},
|
|
979
|
+
{
|
|
980
|
+
stat_consumption: "sensor.ev_charger_power",
|
|
981
|
+
name: "EV Charger"
|
|
982
|
+
},
|
|
983
|
+
{
|
|
984
|
+
stat_consumption: "sensor.always_on_power",
|
|
985
|
+
name: "Always On"
|
|
986
|
+
}
|
|
987
|
+
]
|
|
988
|
+
};
|
|
989
|
+
}
|
|
990
|
+
|
|
991
|
+
//#endregion
|
|
992
|
+
//#region src/demo/demo-data.ts
|
|
993
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
994
|
+
let contextCounter = 0;
|
|
995
|
+
function ctx() {
|
|
996
|
+
return {
|
|
997
|
+
id: `demo-${++contextCounter}`,
|
|
998
|
+
parent_id: null,
|
|
999
|
+
user_id: null
|
|
1000
|
+
};
|
|
1001
|
+
}
|
|
1002
|
+
function makeEntity(entity_id, state$1, attributes = {}) {
|
|
1003
|
+
return {
|
|
1004
|
+
entity_id,
|
|
1005
|
+
state: state$1,
|
|
1006
|
+
attributes: {
|
|
1007
|
+
friendly_name: friendlyName(entity_id),
|
|
1008
|
+
...attributes
|
|
1009
|
+
},
|
|
1010
|
+
last_changed: now,
|
|
1011
|
+
last_updated: now,
|
|
1012
|
+
context: ctx()
|
|
1013
|
+
};
|
|
1014
|
+
}
|
|
1015
|
+
/**
|
|
1016
|
+
* One entity per weather scene the widget can render. Entity IDs use the
|
|
1017
|
+
* `weather.demo_<slug>` form so a demo dashboard can reference them directly.
|
|
1018
|
+
*/
|
|
1019
|
+
const WEATHER_FIXTURES = [
|
|
1020
|
+
{
|
|
1021
|
+
slug: "sunny",
|
|
1022
|
+
state: "sunny",
|
|
1023
|
+
temp: 28,
|
|
1024
|
+
apparent: 31,
|
|
1025
|
+
humidity: 35,
|
|
1026
|
+
pressure: 1018,
|
|
1027
|
+
wind: 8,
|
|
1028
|
+
low: 18
|
|
1029
|
+
},
|
|
1030
|
+
{
|
|
1031
|
+
slug: "clear_night",
|
|
1032
|
+
state: "clear-night",
|
|
1033
|
+
temp: 14,
|
|
1034
|
+
apparent: 12,
|
|
1035
|
+
humidity: 55,
|
|
1036
|
+
pressure: 1016,
|
|
1037
|
+
wind: 5,
|
|
1038
|
+
low: 9
|
|
1039
|
+
},
|
|
1040
|
+
{
|
|
1041
|
+
slug: "cloudy",
|
|
1042
|
+
state: "cloudy",
|
|
1043
|
+
temp: 17,
|
|
1044
|
+
apparent: 16,
|
|
1045
|
+
humidity: 72,
|
|
1046
|
+
pressure: 1010,
|
|
1047
|
+
wind: 14,
|
|
1048
|
+
low: 11
|
|
1049
|
+
},
|
|
1050
|
+
{
|
|
1051
|
+
slug: "partly_cloudy",
|
|
1052
|
+
state: "partlycloudy",
|
|
1053
|
+
temp: 23,
|
|
1054
|
+
apparent: 24,
|
|
1055
|
+
humidity: 50,
|
|
1056
|
+
pressure: 1014,
|
|
1057
|
+
wind: 12,
|
|
1058
|
+
low: 15
|
|
1059
|
+
},
|
|
1060
|
+
{
|
|
1061
|
+
slug: "rainy",
|
|
1062
|
+
state: "rainy",
|
|
1063
|
+
temp: 12,
|
|
1064
|
+
apparent: 10,
|
|
1065
|
+
humidity: 88,
|
|
1066
|
+
pressure: 1004,
|
|
1067
|
+
wind: 18,
|
|
1068
|
+
low: 8
|
|
1069
|
+
},
|
|
1070
|
+
{
|
|
1071
|
+
slug: "pouring",
|
|
1072
|
+
state: "pouring",
|
|
1073
|
+
temp: 11,
|
|
1074
|
+
apparent: 8,
|
|
1075
|
+
humidity: 95,
|
|
1076
|
+
pressure: 998,
|
|
1077
|
+
wind: 26,
|
|
1078
|
+
low: 7
|
|
1079
|
+
},
|
|
1080
|
+
{
|
|
1081
|
+
slug: "snowy",
|
|
1082
|
+
state: "snowy",
|
|
1083
|
+
temp: -2,
|
|
1084
|
+
apparent: -6,
|
|
1085
|
+
humidity: 80,
|
|
1086
|
+
pressure: 1020,
|
|
1087
|
+
wind: 10,
|
|
1088
|
+
low: -7
|
|
1089
|
+
},
|
|
1090
|
+
{
|
|
1091
|
+
slug: "snowy_rainy",
|
|
1092
|
+
state: "snowy-rainy",
|
|
1093
|
+
temp: 1,
|
|
1094
|
+
apparent: -2,
|
|
1095
|
+
humidity: 92,
|
|
1096
|
+
pressure: 1006,
|
|
1097
|
+
wind: 16,
|
|
1098
|
+
low: -2
|
|
1099
|
+
},
|
|
1100
|
+
{
|
|
1101
|
+
slug: "lightning",
|
|
1102
|
+
state: "lightning",
|
|
1103
|
+
temp: 22,
|
|
1104
|
+
apparent: 24,
|
|
1105
|
+
humidity: 78,
|
|
1106
|
+
pressure: 1001,
|
|
1107
|
+
wind: 22,
|
|
1108
|
+
low: 17
|
|
1109
|
+
},
|
|
1110
|
+
{
|
|
1111
|
+
slug: "lightning_rainy",
|
|
1112
|
+
state: "lightning-rainy",
|
|
1113
|
+
temp: 19,
|
|
1114
|
+
apparent: 18,
|
|
1115
|
+
humidity: 90,
|
|
1116
|
+
pressure: 996,
|
|
1117
|
+
wind: 28,
|
|
1118
|
+
low: 14
|
|
1119
|
+
},
|
|
1120
|
+
{
|
|
1121
|
+
slug: "fog",
|
|
1122
|
+
state: "fog",
|
|
1123
|
+
temp: 8,
|
|
1124
|
+
apparent: 6,
|
|
1125
|
+
humidity: 98,
|
|
1126
|
+
pressure: 1015,
|
|
1127
|
+
wind: 4,
|
|
1128
|
+
low: 6
|
|
1129
|
+
},
|
|
1130
|
+
{
|
|
1131
|
+
slug: "hail",
|
|
1132
|
+
state: "hail",
|
|
1133
|
+
temp: 6,
|
|
1134
|
+
apparent: 3,
|
|
1135
|
+
humidity: 84,
|
|
1136
|
+
pressure: 1002,
|
|
1137
|
+
wind: 20,
|
|
1138
|
+
low: 1
|
|
1139
|
+
},
|
|
1140
|
+
{
|
|
1141
|
+
slug: "windy",
|
|
1142
|
+
state: "windy",
|
|
1143
|
+
temp: 18,
|
|
1144
|
+
apparent: 15,
|
|
1145
|
+
humidity: 60,
|
|
1146
|
+
pressure: 1009,
|
|
1147
|
+
wind: 42,
|
|
1148
|
+
low: 12
|
|
1149
|
+
},
|
|
1150
|
+
{
|
|
1151
|
+
slug: "exceptional",
|
|
1152
|
+
state: "exceptional",
|
|
1153
|
+
temp: 38,
|
|
1154
|
+
apparent: 44,
|
|
1155
|
+
humidity: 22,
|
|
1156
|
+
pressure: 1005,
|
|
1157
|
+
wind: 30,
|
|
1158
|
+
low: 28
|
|
1159
|
+
}
|
|
1160
|
+
];
|
|
1161
|
+
function makeWeatherEntity(w) {
|
|
1162
|
+
const today = /* @__PURE__ */ new Date();
|
|
1163
|
+
const day = (n) => new Date(today.getTime() + n * 864e5).toISOString();
|
|
1164
|
+
return makeEntity(`weather.demo_${w.slug}`, w.state, {
|
|
1165
|
+
temperature: w.temp,
|
|
1166
|
+
temperature_unit: "°C",
|
|
1167
|
+
apparent_temperature: w.apparent ?? w.temp,
|
|
1168
|
+
humidity: w.humidity,
|
|
1169
|
+
pressure: w.pressure,
|
|
1170
|
+
pressure_unit: "hPa",
|
|
1171
|
+
wind_speed: w.wind,
|
|
1172
|
+
wind_speed_unit: "km/h",
|
|
1173
|
+
wind_bearing: w.bearing ?? 220,
|
|
1174
|
+
visibility: 10,
|
|
1175
|
+
visibility_unit: "km",
|
|
1176
|
+
uv_index: w.state === "sunny" ? 8 : 2,
|
|
1177
|
+
forecast: [
|
|
1178
|
+
{
|
|
1179
|
+
datetime: day(0),
|
|
1180
|
+
condition: w.state,
|
|
1181
|
+
temperature: w.temp,
|
|
1182
|
+
templow: w.low
|
|
1183
|
+
},
|
|
1184
|
+
{
|
|
1185
|
+
datetime: day(1),
|
|
1186
|
+
condition: "partlycloudy",
|
|
1187
|
+
temperature: w.temp + 2,
|
|
1188
|
+
templow: w.low
|
|
1189
|
+
},
|
|
1190
|
+
{
|
|
1191
|
+
datetime: day(2),
|
|
1192
|
+
condition: "cloudy",
|
|
1193
|
+
temperature: w.temp + 1,
|
|
1194
|
+
templow: w.low - 1
|
|
1195
|
+
},
|
|
1196
|
+
{
|
|
1197
|
+
datetime: day(3),
|
|
1198
|
+
condition: "sunny",
|
|
1199
|
+
temperature: w.temp + 4,
|
|
1200
|
+
templow: w.low + 1
|
|
1201
|
+
},
|
|
1202
|
+
{
|
|
1203
|
+
datetime: day(4),
|
|
1204
|
+
condition: "rainy",
|
|
1205
|
+
temperature: w.temp - 2,
|
|
1206
|
+
templow: w.low - 2
|
|
1207
|
+
}
|
|
1208
|
+
]
|
|
1209
|
+
});
|
|
1210
|
+
}
|
|
1211
|
+
function friendlyName(entityId) {
|
|
1212
|
+
const objectId = entityId.split(".")[1] ?? entityId;
|
|
1213
|
+
return objectId.split("_").map((w) => w.charAt(0).toUpperCase() + w.slice(1)).join(" ");
|
|
1214
|
+
}
|
|
1215
|
+
function makeRegistryEntry(entity_id, overrides = {}) {
|
|
1216
|
+
const domain = entity_id.split(".")[0] ?? "";
|
|
1217
|
+
return {
|
|
1218
|
+
aliases: [],
|
|
1219
|
+
area_id: null,
|
|
1220
|
+
capabilities: null,
|
|
1221
|
+
categories: {},
|
|
1222
|
+
config_entry_id: "demo_config",
|
|
1223
|
+
config_subentry_id: null,
|
|
1224
|
+
created_at: now,
|
|
1225
|
+
device_class: null,
|
|
1226
|
+
device_id: null,
|
|
1227
|
+
disabled_by: null,
|
|
1228
|
+
entity_category: null,
|
|
1229
|
+
entity_id,
|
|
1230
|
+
has_entity_name: false,
|
|
1231
|
+
hidden_by: null,
|
|
1232
|
+
icon: null,
|
|
1233
|
+
id: `reg-${entity_id.replace(".", "-")}`,
|
|
1234
|
+
labels: [],
|
|
1235
|
+
modified_at: now,
|
|
1236
|
+
name: null,
|
|
1237
|
+
options: {},
|
|
1238
|
+
original_device_class: null,
|
|
1239
|
+
original_icon: null,
|
|
1240
|
+
original_name: null,
|
|
1241
|
+
platform: domain === "weather" ? "met" : "demo",
|
|
1242
|
+
previous_unique_id: null,
|
|
1243
|
+
suggested_object_id: null,
|
|
1244
|
+
supported_features: 0,
|
|
1245
|
+
translation_key: null,
|
|
1246
|
+
unique_id: `demo_${entity_id.replace(".", "_")}`,
|
|
1247
|
+
unit_of_measurement: null,
|
|
1248
|
+
...overrides
|
|
1249
|
+
};
|
|
1250
|
+
}
|
|
1251
|
+
function makeDevice(id, name, areaId, manufacturer = "Demo", model = "Virtual") {
|
|
1252
|
+
return {
|
|
1253
|
+
id,
|
|
1254
|
+
name,
|
|
1255
|
+
name_by_user: null,
|
|
1256
|
+
manufacturer,
|
|
1257
|
+
model,
|
|
1258
|
+
model_id: null,
|
|
1259
|
+
sw_version: "1.0.0",
|
|
1260
|
+
hw_version: null,
|
|
1261
|
+
area_id: areaId,
|
|
1262
|
+
config_entries: ["demo_config"],
|
|
1263
|
+
config_entries_subentries: {},
|
|
1264
|
+
configuration_url: null,
|
|
1265
|
+
connections: [],
|
|
1266
|
+
created_at: now,
|
|
1267
|
+
disabled_by: null,
|
|
1268
|
+
entry_type: null,
|
|
1269
|
+
identifiers: [["demo", id]],
|
|
1270
|
+
labels: [],
|
|
1271
|
+
modified_at: now,
|
|
1272
|
+
primary_config_entry: "demo_config",
|
|
1273
|
+
serial_number: null,
|
|
1274
|
+
via_device_id: null
|
|
1275
|
+
};
|
|
1276
|
+
}
|
|
1277
|
+
function makeArea(id, name, floorId = "ground_floor") {
|
|
1278
|
+
return {
|
|
1279
|
+
id,
|
|
1280
|
+
name,
|
|
1281
|
+
normalized_name: name.toLowerCase().replace(/\s+/g, "_"),
|
|
1282
|
+
created_at: now,
|
|
1283
|
+
modified_at: now,
|
|
1284
|
+
aliases: [],
|
|
1285
|
+
floor_id: floorId,
|
|
1286
|
+
humidity_entity_id: null,
|
|
1287
|
+
icon: null,
|
|
1288
|
+
labels: [],
|
|
1289
|
+
picture: null,
|
|
1290
|
+
temperature_entity_id: null
|
|
1291
|
+
};
|
|
1292
|
+
}
|
|
1293
|
+
/**
|
|
1294
|
+
* Create ~30 curated apartment entity fixtures.
|
|
1295
|
+
*
|
|
1296
|
+
* Entities span lights, sensors, binary sensors, climate, covers,
|
|
1297
|
+
* locks, switches, media player, weather, camera, scenes, and buttons
|
|
1298
|
+
* across 6 areas: living_room, bedroom, kitchen, entry, utility, garage.
|
|
1299
|
+
*/
|
|
1300
|
+
function createDemoFixtures() {
|
|
1301
|
+
contextCounter = 0;
|
|
1302
|
+
const areas$1 = {
|
|
1303
|
+
living_room: makeArea("living_room", "Living Room"),
|
|
1304
|
+
bedroom: makeArea("bedroom", "Bedroom"),
|
|
1305
|
+
kitchen: makeArea("kitchen", "Kitchen"),
|
|
1306
|
+
entry: makeArea("entry", "Entry"),
|
|
1307
|
+
utility: makeArea("utility", "Utility"),
|
|
1308
|
+
garage: makeArea("garage", "Garage")
|
|
1309
|
+
};
|
|
1310
|
+
areas$1.living_room.temperature_entity_id = "sensor.temperature_living";
|
|
1311
|
+
areas$1.living_room.humidity_entity_id = "sensor.humidity_living";
|
|
1312
|
+
const devices = {
|
|
1313
|
+
living_room_lights: makeDevice("living_room_lights", "Living Room Lights", "living_room", "Philips", "Hue"),
|
|
1314
|
+
kitchen_lights: makeDevice("kitchen_lights", "Kitchen Lights", "kitchen", "IKEA", "Tradfri"),
|
|
1315
|
+
bedroom_lights: makeDevice("bedroom_lights", "Bedroom Lights", "bedroom", "Philips", "Hue"),
|
|
1316
|
+
hallway_lights: makeDevice("hallway_lights", "Hallway Lights", "entry", "IKEA", "Tradfri"),
|
|
1317
|
+
bathroom_lights: makeDevice("bathroom_lights", "Bathroom Lights", null),
|
|
1318
|
+
thermostat_lr: makeDevice("thermostat_lr", "Living Room Thermostat", "living_room", "Nest", "Learning Thermostat"),
|
|
1319
|
+
thermostat_br: makeDevice("thermostat_br", "Bedroom AC", "bedroom", "Daikin", "Split AC"),
|
|
1320
|
+
blinds_lr: makeDevice("blinds_lr", "Living Room Blinds", "living_room", "Somfy", "Roller Shade"),
|
|
1321
|
+
curtains_br: makeDevice("curtains_br", "Bedroom Curtains", "bedroom", "IKEA", "Fyrtur"),
|
|
1322
|
+
garage_door_opener: makeDevice("garage_door_opener", "Garage Door Opener", "garage", "Chamberlain", "MyQ"),
|
|
1323
|
+
shutters_lr: makeDevice("shutters_lr", "Living Room Shutters", "living_room", "Somfy", "Venetian Shutter"),
|
|
1324
|
+
blinds_kitchen: makeDevice("blinds_kitchen", "Kitchen Blinds", "kitchen", "IKEA", "Praktlysing"),
|
|
1325
|
+
lock_front: makeDevice("lock_front", "Front Door Lock", "entry", "Yale", "Assure Lock 2"),
|
|
1326
|
+
lock_back: makeDevice("lock_back", "Back Door Lock", "entry", "August", "Smart Lock Pro"),
|
|
1327
|
+
speaker_lr: makeDevice("speaker_lr", "Living Room Speaker", "living_room", "Sonos", "One"),
|
|
1328
|
+
camera_front: makeDevice("camera_front", "Front Door Camera", "entry", "Ring", "Video Doorbell"),
|
|
1329
|
+
door_sensor: makeDevice("door_sensor", "Front Door Sensor", "entry", "Aqara", "Door Sensor"),
|
|
1330
|
+
motion_sensor: makeDevice("motion_sensor", "Hallway Motion Sensor", "entry", "Aqara", "Motion Sensor")
|
|
1331
|
+
};
|
|
1332
|
+
const entities$1 = {};
|
|
1333
|
+
const entityRegistry = {};
|
|
1334
|
+
function add(e, regOverrides = {}) {
|
|
1335
|
+
entities$1[e.entity_id] = e;
|
|
1336
|
+
entityRegistry[e.entity_id] = makeRegistryEntry(e.entity_id, regOverrides);
|
|
1337
|
+
}
|
|
1338
|
+
add(makeEntity("light.living_room_main", "on", {
|
|
1339
|
+
brightness: 179,
|
|
1340
|
+
color_temp_kelvin: 3e3,
|
|
1341
|
+
color_mode: "color_temp",
|
|
1342
|
+
supported_color_modes: ["color_temp", "xy"]
|
|
1343
|
+
}), {
|
|
1344
|
+
device_id: "living_room_lights",
|
|
1345
|
+
area_id: "living_room",
|
|
1346
|
+
supported_features: 44
|
|
1347
|
+
});
|
|
1348
|
+
add(makeEntity("light.kitchen_counter", "off", { supported_color_modes: ["brightness"] }), {
|
|
1349
|
+
device_id: "kitchen_lights",
|
|
1350
|
+
area_id: "kitchen",
|
|
1351
|
+
supported_features: 1
|
|
1352
|
+
});
|
|
1353
|
+
add(makeEntity("light.bedroom_ceiling", "on", {
|
|
1354
|
+
brightness: 77,
|
|
1355
|
+
color_temp_kelvin: 2700,
|
|
1356
|
+
color_mode: "color_temp",
|
|
1357
|
+
supported_color_modes: ["color_temp"]
|
|
1358
|
+
}), {
|
|
1359
|
+
device_id: "bedroom_lights",
|
|
1360
|
+
area_id: "bedroom",
|
|
1361
|
+
supported_features: 44
|
|
1362
|
+
});
|
|
1363
|
+
add(makeEntity("light.bathroom", "off", { supported_color_modes: ["brightness"] }), { device_id: "bathroom_lights" });
|
|
1364
|
+
add(makeEntity("light.hallway", "on", {
|
|
1365
|
+
brightness: 128,
|
|
1366
|
+
color_mode: "brightness",
|
|
1367
|
+
supported_color_modes: ["brightness"]
|
|
1368
|
+
}), {
|
|
1369
|
+
device_id: "hallway_lights",
|
|
1370
|
+
area_id: "entry",
|
|
1371
|
+
supported_features: 1
|
|
1372
|
+
});
|
|
1373
|
+
add(makeEntity("light.studio_rgb", "on", {
|
|
1374
|
+
brightness: 205,
|
|
1375
|
+
hs_color: [265, 66],
|
|
1376
|
+
color_mode: "hs",
|
|
1377
|
+
supported_color_modes: ["hs", "color_temp"]
|
|
1378
|
+
}), {
|
|
1379
|
+
device_id: "studio_lights",
|
|
1380
|
+
area_id: "living_room",
|
|
1381
|
+
supported_features: 44
|
|
1382
|
+
});
|
|
1383
|
+
add(makeEntity("light.desk_rgb", "on", {
|
|
1384
|
+
brightness: 200,
|
|
1385
|
+
hs_color: [172, 79],
|
|
1386
|
+
color_mode: "hs",
|
|
1387
|
+
supported_color_modes: ["hs", "color_temp"]
|
|
1388
|
+
}), {
|
|
1389
|
+
device_id: "desk_lights",
|
|
1390
|
+
area_id: "bedroom",
|
|
1391
|
+
supported_features: 44
|
|
1392
|
+
});
|
|
1393
|
+
add(makeEntity("sensor.temperature_living", "22.5", {
|
|
1394
|
+
unit_of_measurement: "°C",
|
|
1395
|
+
device_class: "temperature",
|
|
1396
|
+
state_class: "measurement"
|
|
1397
|
+
}), {
|
|
1398
|
+
area_id: "living_room",
|
|
1399
|
+
device_class: "temperature",
|
|
1400
|
+
unit_of_measurement: "°C"
|
|
1401
|
+
});
|
|
1402
|
+
add(makeEntity("sensor.humidity_living", "45", {
|
|
1403
|
+
unit_of_measurement: "%",
|
|
1404
|
+
device_class: "humidity",
|
|
1405
|
+
state_class: "measurement"
|
|
1406
|
+
}), {
|
|
1407
|
+
area_id: "living_room",
|
|
1408
|
+
device_class: "humidity",
|
|
1409
|
+
unit_of_measurement: "%"
|
|
1410
|
+
});
|
|
1411
|
+
add(makeEntity("sensor.temperature_outdoor", "-2", {
|
|
1412
|
+
unit_of_measurement: "°C",
|
|
1413
|
+
device_class: "temperature",
|
|
1414
|
+
state_class: "measurement"
|
|
1415
|
+
}), {
|
|
1416
|
+
device_class: "temperature",
|
|
1417
|
+
unit_of_measurement: "°C"
|
|
1418
|
+
});
|
|
1419
|
+
add(makeEntity("sensor.power_consumption", "1.2", {
|
|
1420
|
+
unit_of_measurement: "kW",
|
|
1421
|
+
device_class: "power",
|
|
1422
|
+
state_class: "measurement"
|
|
1423
|
+
}), {
|
|
1424
|
+
device_class: "power",
|
|
1425
|
+
unit_of_measurement: "kW"
|
|
1426
|
+
});
|
|
1427
|
+
add(makeEntity("sensor.battery_door_sensor", "87", {
|
|
1428
|
+
unit_of_measurement: "%",
|
|
1429
|
+
device_class: "battery",
|
|
1430
|
+
state_class: "measurement"
|
|
1431
|
+
}), {
|
|
1432
|
+
device_id: "door_sensor",
|
|
1433
|
+
area_id: "entry",
|
|
1434
|
+
device_class: "battery",
|
|
1435
|
+
unit_of_measurement: "%"
|
|
1436
|
+
});
|
|
1437
|
+
add(makeEntity("binary_sensor.front_door", "off", { device_class: "door" }), {
|
|
1438
|
+
device_id: "door_sensor",
|
|
1439
|
+
area_id: "entry",
|
|
1440
|
+
device_class: "door"
|
|
1441
|
+
});
|
|
1442
|
+
add(makeEntity("binary_sensor.motion_hallway", "off", { device_class: "motion" }), {
|
|
1443
|
+
device_id: "motion_sensor",
|
|
1444
|
+
area_id: "entry",
|
|
1445
|
+
device_class: "motion"
|
|
1446
|
+
});
|
|
1447
|
+
add(makeEntity("binary_sensor.window_bedroom", "off", { device_class: "window" }), {
|
|
1448
|
+
area_id: "bedroom",
|
|
1449
|
+
device_class: "window"
|
|
1450
|
+
});
|
|
1451
|
+
add(makeEntity("climate.living_room_thermostat", "heat", {
|
|
1452
|
+
temperature: 22,
|
|
1453
|
+
current_temperature: 21.8,
|
|
1454
|
+
hvac_modes: [
|
|
1455
|
+
"off",
|
|
1456
|
+
"heat",
|
|
1457
|
+
"cool",
|
|
1458
|
+
"auto"
|
|
1459
|
+
],
|
|
1460
|
+
hvac_action: "heating",
|
|
1461
|
+
min_temp: 7,
|
|
1462
|
+
max_temp: 35
|
|
1463
|
+
}), {
|
|
1464
|
+
device_id: "thermostat_lr",
|
|
1465
|
+
area_id: "living_room",
|
|
1466
|
+
supported_features: 385
|
|
1467
|
+
});
|
|
1468
|
+
add(makeEntity("climate.bedroom_ac", "off", {
|
|
1469
|
+
temperature: 20,
|
|
1470
|
+
current_temperature: 22.1,
|
|
1471
|
+
hvac_modes: [
|
|
1472
|
+
"off",
|
|
1473
|
+
"cool",
|
|
1474
|
+
"heat",
|
|
1475
|
+
"dry",
|
|
1476
|
+
"fan_only"
|
|
1477
|
+
],
|
|
1478
|
+
hvac_action: "idle",
|
|
1479
|
+
min_temp: 16,
|
|
1480
|
+
max_temp: 30
|
|
1481
|
+
}), {
|
|
1482
|
+
device_id: "thermostat_br",
|
|
1483
|
+
area_id: "bedroom",
|
|
1484
|
+
supported_features: 385
|
|
1485
|
+
});
|
|
1486
|
+
add(makeEntity("water_heater.boiler", "eco", {
|
|
1487
|
+
temperature: 50,
|
|
1488
|
+
current_temperature: 47.5,
|
|
1489
|
+
operation_list: [
|
|
1490
|
+
"off",
|
|
1491
|
+
"eco",
|
|
1492
|
+
"performance"
|
|
1493
|
+
],
|
|
1494
|
+
min_temp: 43,
|
|
1495
|
+
max_temp: 60,
|
|
1496
|
+
away_mode: "off"
|
|
1497
|
+
}), {
|
|
1498
|
+
device_id: "boiler",
|
|
1499
|
+
area_id: "utility",
|
|
1500
|
+
supported_features: 7
|
|
1501
|
+
});
|
|
1502
|
+
add(makeEntity("water_heater.heat_pump_tank", "heat_pump", {
|
|
1503
|
+
temperature: 55,
|
|
1504
|
+
current_temperature: 52.3,
|
|
1505
|
+
operation_list: [
|
|
1506
|
+
"off",
|
|
1507
|
+
"eco",
|
|
1508
|
+
"heat_pump",
|
|
1509
|
+
"high_demand"
|
|
1510
|
+
],
|
|
1511
|
+
min_temp: 43,
|
|
1512
|
+
max_temp: 60
|
|
1513
|
+
}), {
|
|
1514
|
+
device_id: "hp_tank",
|
|
1515
|
+
area_id: "garage",
|
|
1516
|
+
supported_features: 3
|
|
1517
|
+
});
|
|
1518
|
+
add(makeEntity("fan.bedroom_ceiling", "on", {
|
|
1519
|
+
percentage: 200 / 3,
|
|
1520
|
+
percentage_step: 100 / 3,
|
|
1521
|
+
current_direction: "forward"
|
|
1522
|
+
}), {
|
|
1523
|
+
device_id: "ceiling_fan",
|
|
1524
|
+
area_id: "bedroom",
|
|
1525
|
+
supported_features: 5
|
|
1526
|
+
});
|
|
1527
|
+
add(makeEntity("fan.air_purifier", "on", {
|
|
1528
|
+
percentage: 40,
|
|
1529
|
+
preset_modes: [
|
|
1530
|
+
"auto",
|
|
1531
|
+
"sleep",
|
|
1532
|
+
"turbo"
|
|
1533
|
+
],
|
|
1534
|
+
preset_mode: "auto",
|
|
1535
|
+
oscillating: false
|
|
1536
|
+
}), {
|
|
1537
|
+
device_id: "purifier",
|
|
1538
|
+
area_id: "living_room",
|
|
1539
|
+
supported_features: 11
|
|
1540
|
+
});
|
|
1541
|
+
add(makeEntity("cover.living_room_blinds", "open", {
|
|
1542
|
+
current_position: 80,
|
|
1543
|
+
device_class: "shade"
|
|
1544
|
+
}), {
|
|
1545
|
+
device_id: "blinds_lr",
|
|
1546
|
+
area_id: "living_room",
|
|
1547
|
+
device_class: "shade",
|
|
1548
|
+
supported_features: 15
|
|
1549
|
+
});
|
|
1550
|
+
add(makeEntity("cover.bedroom_curtains", "closed", {
|
|
1551
|
+
current_position: 0,
|
|
1552
|
+
device_class: "curtain"
|
|
1553
|
+
}), {
|
|
1554
|
+
device_id: "curtains_br",
|
|
1555
|
+
area_id: "bedroom",
|
|
1556
|
+
device_class: "curtain",
|
|
1557
|
+
supported_features: 15
|
|
1558
|
+
});
|
|
1559
|
+
add(makeEntity("cover.garage_door", "closed", { device_class: "garage" }), {
|
|
1560
|
+
device_id: "garage_door_opener",
|
|
1561
|
+
area_id: "garage",
|
|
1562
|
+
device_class: "garage",
|
|
1563
|
+
supported_features: 11
|
|
1564
|
+
});
|
|
1565
|
+
add(makeEntity("cover.driveway_gate", "open", {
|
|
1566
|
+
device_class: "gate",
|
|
1567
|
+
assumed_state: true
|
|
1568
|
+
}), {
|
|
1569
|
+
area_id: "garage",
|
|
1570
|
+
device_class: "gate",
|
|
1571
|
+
supported_features: 3
|
|
1572
|
+
});
|
|
1573
|
+
add(makeEntity("cover.living_room_shutters", "open", {
|
|
1574
|
+
current_position: 45,
|
|
1575
|
+
current_tilt_position: 30,
|
|
1576
|
+
device_class: "shutter"
|
|
1577
|
+
}), {
|
|
1578
|
+
device_id: "shutters_lr",
|
|
1579
|
+
area_id: "living_room",
|
|
1580
|
+
device_class: "shutter",
|
|
1581
|
+
supported_features: 255
|
|
1582
|
+
});
|
|
1583
|
+
add(makeEntity("cover.kitchen_blinds", "open", {
|
|
1584
|
+
current_position: 100,
|
|
1585
|
+
device_class: "blind"
|
|
1586
|
+
}), {
|
|
1587
|
+
device_id: "blinds_kitchen",
|
|
1588
|
+
area_id: "kitchen",
|
|
1589
|
+
device_class: "blind",
|
|
1590
|
+
supported_features: 127
|
|
1591
|
+
});
|
|
1592
|
+
add(makeEntity("lock.front_door_lock", "locked", {}), {
|
|
1593
|
+
device_id: "lock_front",
|
|
1594
|
+
area_id: "entry",
|
|
1595
|
+
supported_features: 1
|
|
1596
|
+
});
|
|
1597
|
+
add(makeEntity("lock.back_door_lock", "locked", {}), {
|
|
1598
|
+
device_id: "lock_back",
|
|
1599
|
+
area_id: "entry",
|
|
1600
|
+
supported_features: 1
|
|
1601
|
+
});
|
|
1602
|
+
add(makeEntity("switch.coffee_machine", "off", { device_class: "outlet" }), {
|
|
1603
|
+
area_id: "kitchen",
|
|
1604
|
+
device_class: "outlet"
|
|
1605
|
+
});
|
|
1606
|
+
add(makeEntity("switch.fan_living_room", "on", { device_class: "switch" }), {
|
|
1607
|
+
area_id: "living_room",
|
|
1608
|
+
device_class: "switch"
|
|
1609
|
+
});
|
|
1610
|
+
add(makeEntity("media_player.living_room_speaker", "playing", {
|
|
1611
|
+
media_title: "Lo-fi Beats",
|
|
1612
|
+
media_artist: "Chill Station",
|
|
1613
|
+
media_content_type: "music",
|
|
1614
|
+
volume_level: .4,
|
|
1615
|
+
is_volume_muted: false,
|
|
1616
|
+
supported_features: 152461
|
|
1617
|
+
}), {
|
|
1618
|
+
device_id: "speaker_lr",
|
|
1619
|
+
area_id: "living_room",
|
|
1620
|
+
supported_features: 152461
|
|
1621
|
+
});
|
|
1622
|
+
for (const w of WEATHER_FIXTURES) add(makeWeatherEntity(w), { platform: "met" });
|
|
1623
|
+
add(makeEntity("camera.front_door_camera", "streaming", {
|
|
1624
|
+
entity_picture: "/api/camera_proxy/camera.front_door_camera",
|
|
1625
|
+
frontend_stream_type: "hls",
|
|
1626
|
+
access_token: "demo-token"
|
|
1627
|
+
}), {
|
|
1628
|
+
device_id: "camera_front",
|
|
1629
|
+
area_id: "entry",
|
|
1630
|
+
supported_features: 2
|
|
1631
|
+
});
|
|
1632
|
+
add(makeEntity("scene.movie_night", "scening", { entity_id: [
|
|
1633
|
+
"light.living_room_main",
|
|
1634
|
+
"cover.living_room_blinds",
|
|
1635
|
+
"media_player.living_room_speaker"
|
|
1636
|
+
] }), { area_id: "living_room" });
|
|
1637
|
+
add(makeEntity("scene.good_morning", "scening", { entity_id: [
|
|
1638
|
+
"light.bedroom_ceiling",
|
|
1639
|
+
"cover.bedroom_curtains",
|
|
1640
|
+
"switch.coffee_machine"
|
|
1641
|
+
] }), {});
|
|
1642
|
+
add(makeEntity("button.restart_home_assistant", "unknown", { device_class: "restart" }), {
|
|
1643
|
+
entity_category: "config",
|
|
1644
|
+
device_class: "restart"
|
|
1645
|
+
});
|
|
1646
|
+
add(makeEntity("button.update_firmware", "unknown", { device_class: "update" }), {
|
|
1647
|
+
entity_category: "config",
|
|
1648
|
+
device_class: "update"
|
|
1649
|
+
});
|
|
1650
|
+
add(makeEntity("sensor.door_lock_battery", "92", {
|
|
1651
|
+
unit_of_measurement: "%",
|
|
1652
|
+
device_class: "battery",
|
|
1653
|
+
state_class: "measurement"
|
|
1654
|
+
}), {
|
|
1655
|
+
device_id: "lock_front",
|
|
1656
|
+
area_id: "entry",
|
|
1657
|
+
device_class: "battery",
|
|
1658
|
+
unit_of_measurement: "%"
|
|
1659
|
+
});
|
|
1660
|
+
add(makeEntity("sensor.motion_sensor_battery", "65", {
|
|
1661
|
+
unit_of_measurement: "%",
|
|
1662
|
+
device_class: "battery",
|
|
1663
|
+
state_class: "measurement"
|
|
1664
|
+
}), {
|
|
1665
|
+
device_id: "motion_sensor",
|
|
1666
|
+
area_id: "entry",
|
|
1667
|
+
device_class: "battery",
|
|
1668
|
+
unit_of_measurement: "%"
|
|
1669
|
+
});
|
|
1670
|
+
const nowMs = Date.now();
|
|
1671
|
+
const sample = simulateEnergy(nowMs);
|
|
1672
|
+
const sun = sunEvents(nowMs);
|
|
1673
|
+
add(makeEntity("sun.sun", isSunUp(nowMs) ? "above_horizon" : "below_horizon", {
|
|
1674
|
+
friendly_name: "Sun",
|
|
1675
|
+
elevation: sun.elevation,
|
|
1676
|
+
next_rising: sun.nextRising,
|
|
1677
|
+
next_setting: sun.nextSetting
|
|
1678
|
+
}), {});
|
|
1679
|
+
const powerSensors = [
|
|
1680
|
+
{
|
|
1681
|
+
id: "sensor.solar_power",
|
|
1682
|
+
name: "Solar Production",
|
|
1683
|
+
deviceClass: "power",
|
|
1684
|
+
unit: "W"
|
|
1685
|
+
},
|
|
1686
|
+
{
|
|
1687
|
+
id: "sensor.grid_import_power",
|
|
1688
|
+
name: "Grid Import",
|
|
1689
|
+
deviceClass: "power",
|
|
1690
|
+
unit: "W"
|
|
1691
|
+
},
|
|
1692
|
+
{
|
|
1693
|
+
id: "sensor.grid_export_power",
|
|
1694
|
+
name: "Grid Export",
|
|
1695
|
+
deviceClass: "power",
|
|
1696
|
+
unit: "W"
|
|
1697
|
+
},
|
|
1698
|
+
{
|
|
1699
|
+
id: "sensor.battery_charge_power",
|
|
1700
|
+
name: "Battery Charge",
|
|
1701
|
+
deviceClass: "power",
|
|
1702
|
+
unit: "W"
|
|
1703
|
+
},
|
|
1704
|
+
{
|
|
1705
|
+
id: "sensor.battery_discharge_power",
|
|
1706
|
+
name: "Battery Discharge",
|
|
1707
|
+
deviceClass: "power",
|
|
1708
|
+
unit: "W"
|
|
1709
|
+
},
|
|
1710
|
+
{
|
|
1711
|
+
id: "sensor.battery_soc",
|
|
1712
|
+
name: "Battery",
|
|
1713
|
+
deviceClass: "battery",
|
|
1714
|
+
unit: "%"
|
|
1715
|
+
},
|
|
1716
|
+
{
|
|
1717
|
+
id: "sensor.home_power",
|
|
1718
|
+
name: "Home Consumption",
|
|
1719
|
+
deviceClass: "power",
|
|
1720
|
+
unit: "W"
|
|
1721
|
+
},
|
|
1722
|
+
{
|
|
1723
|
+
id: "sensor.fridge_power",
|
|
1724
|
+
name: "Fridge",
|
|
1725
|
+
deviceClass: "power",
|
|
1726
|
+
unit: "W"
|
|
1727
|
+
},
|
|
1728
|
+
{
|
|
1729
|
+
id: "sensor.dishwasher_power",
|
|
1730
|
+
name: "Dishwasher",
|
|
1731
|
+
deviceClass: "power",
|
|
1732
|
+
unit: "W"
|
|
1733
|
+
},
|
|
1734
|
+
{
|
|
1735
|
+
id: "sensor.washing_machine_power",
|
|
1736
|
+
name: "Washing Machine",
|
|
1737
|
+
deviceClass: "power",
|
|
1738
|
+
unit: "W"
|
|
1739
|
+
},
|
|
1740
|
+
{
|
|
1741
|
+
id: "sensor.oven_power",
|
|
1742
|
+
name: "Oven",
|
|
1743
|
+
deviceClass: "power",
|
|
1744
|
+
unit: "W"
|
|
1745
|
+
},
|
|
1746
|
+
{
|
|
1747
|
+
id: "sensor.ev_charger_power",
|
|
1748
|
+
name: "EV Charger",
|
|
1749
|
+
deviceClass: "power",
|
|
1750
|
+
unit: "W"
|
|
1751
|
+
},
|
|
1752
|
+
{
|
|
1753
|
+
id: "sensor.always_on_power",
|
|
1754
|
+
name: "Always On",
|
|
1755
|
+
deviceClass: "power",
|
|
1756
|
+
unit: "W"
|
|
1757
|
+
}
|
|
1758
|
+
];
|
|
1759
|
+
for (const s of powerSensors) {
|
|
1760
|
+
const value = energyEntityValue(s.id, sample) ?? 0;
|
|
1761
|
+
add(makeEntity(s.id, formatEnergyState(s.id, value), {
|
|
1762
|
+
friendly_name: s.name,
|
|
1763
|
+
unit_of_measurement: s.unit,
|
|
1764
|
+
device_class: s.deviceClass,
|
|
1765
|
+
state_class: "measurement"
|
|
1766
|
+
}), {
|
|
1767
|
+
area_id: "utility",
|
|
1768
|
+
device_class: s.deviceClass,
|
|
1769
|
+
unit_of_measurement: s.unit
|
|
1770
|
+
});
|
|
1771
|
+
}
|
|
1772
|
+
return {
|
|
1773
|
+
entities: entities$1,
|
|
1774
|
+
entityRegistry,
|
|
1775
|
+
areas: areas$1,
|
|
1776
|
+
devices
|
|
1777
|
+
};
|
|
1778
|
+
}
|
|
1779
|
+
|
|
504
1780
|
//#endregion
|
|
505
1781
|
//#region src/demo/demo-provider.ts
|
|
506
1782
|
let _isDemoMode = false;
|
|
@@ -604,10 +1880,11 @@ function applyDemoServiceCall(_domain, service, _serviceData = {}, target = {})
|
|
|
604
1880
|
setState(produce((s) => {
|
|
605
1881
|
const e = s.entities[entityId];
|
|
606
1882
|
if (!e) return;
|
|
607
|
-
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
608
|
-
e.last_changed = now;
|
|
609
|
-
e.last_updated = now;
|
|
1883
|
+
const now$1 = (/* @__PURE__ */ new Date()).toISOString();
|
|
1884
|
+
e.last_changed = now$1;
|
|
1885
|
+
e.last_updated = now$1;
|
|
610
1886
|
if (service === "turn_on") applyTurnOn(e, entityDomain, _serviceData);
|
|
1887
|
+
else if (service === "press" && entityDomain === "button") e.state = now$1;
|
|
611
1888
|
else if (service === "turn_off") applyTurnOff(e, entityDomain);
|
|
612
1889
|
else if (service === "toggle") if (e.state === "on" || e.state === "playing" || e.state === "open") applyTurnOff(e, entityDomain);
|
|
613
1890
|
else applyTurnOn(e, entityDomain, _serviceData);
|
|
@@ -622,23 +1899,69 @@ function applyDemoServiceCall(_domain, service, _serviceData = {}, target = {})
|
|
|
622
1899
|
} else if (service === "set_cover_position" && _serviceData.position != null) {
|
|
623
1900
|
e.attributes.current_position = _serviceData.position;
|
|
624
1901
|
e.state = _serviceData.position > 0 ? "open" : "closed";
|
|
625
|
-
} else if (service === "
|
|
626
|
-
else if (service === "
|
|
1902
|
+
} else if (service === "open_cover_tilt") e.attributes.current_tilt_position = 100;
|
|
1903
|
+
else if (service === "close_cover_tilt") e.attributes.current_tilt_position = 0;
|
|
1904
|
+
else if (service === "set_cover_tilt_position" && _serviceData.tilt_position != null) e.attributes.current_tilt_position = _serviceData.tilt_position;
|
|
1905
|
+
else if (service === "set_temperature") {
|
|
1906
|
+
if (_serviceData.temperature != null) e.attributes.temperature = _serviceData.temperature;
|
|
1907
|
+
if (_serviceData.target_temp_low != null) e.attributes.target_temp_low = _serviceData.target_temp_low;
|
|
1908
|
+
if (_serviceData.target_temp_high != null) e.attributes.target_temp_high = _serviceData.target_temp_high;
|
|
1909
|
+
} else if (service === "set_hvac_mode" && _serviceData.hvac_mode != null) e.state = _serviceData.hvac_mode;
|
|
1910
|
+
else if (service === "set_fan_mode" && _serviceData.fan_mode != null) e.attributes.fan_mode = _serviceData.fan_mode;
|
|
1911
|
+
else if (service === "set_preset_mode" && _serviceData.preset_mode != null) {
|
|
1912
|
+
e.attributes.preset_mode = _serviceData.preset_mode;
|
|
1913
|
+
if (entityDomain === "fan") e.state = "on";
|
|
1914
|
+
} else if (service === "set_percentage" && _serviceData.percentage != null) {
|
|
1915
|
+
e.attributes.percentage = _serviceData.percentage;
|
|
1916
|
+
e.state = _serviceData.percentage > 0 ? "on" : "off";
|
|
1917
|
+
} else if (service === "oscillate" && _serviceData.oscillating != null) e.attributes.oscillating = _serviceData.oscillating;
|
|
1918
|
+
else if (service === "set_direction" && _serviceData.direction != null) e.attributes.current_direction = _serviceData.direction;
|
|
1919
|
+
else if (service === "set_operation_mode" && _serviceData.operation_mode != null) e.state = _serviceData.operation_mode;
|
|
1920
|
+
else if (service === "set_away_mode" && _serviceData.away_mode != null) e.attributes.away_mode = _serviceData.away_mode ? "on" : "off";
|
|
627
1921
|
else if (service === "volume_set" && _serviceData.volume_level != null) e.attributes.volume_level = _serviceData.volume_level;
|
|
1922
|
+
else if (service === "media_play_pause") e.state = e.state === "playing" ? "paused" : "playing";
|
|
1923
|
+
else if (service === "media_next_track" || service === "media_previous_track") applyTrackSkip(e, service === "media_next_track" ? 1 : -1);
|
|
1924
|
+
else if (service === "select_source" && _serviceData.source != null) e.attributes.source = _serviceData.source;
|
|
628
1925
|
}));
|
|
629
1926
|
}
|
|
630
1927
|
}
|
|
631
1928
|
function applyTurnOn(e, domain, serviceData = {}) {
|
|
632
1929
|
if (domain === "light") {
|
|
633
1930
|
e.state = "on";
|
|
634
|
-
|
|
1931
|
+
if (serviceData.brightness_pct !== void 0) {
|
|
1932
|
+
const pct = Math.max(0, Math.min(100, serviceData.brightness_pct));
|
|
1933
|
+
e.attributes.brightness = Math.round(pct / 100 * 255);
|
|
1934
|
+
} else e.attributes.brightness = serviceData.brightness ?? e.attributes.brightness ?? 255;
|
|
635
1935
|
if (serviceData.color_temp_kelvin) e.attributes.color_temp_kelvin = serviceData.color_temp_kelvin;
|
|
636
|
-
|
|
1936
|
+
if (serviceData.hs_color) e.attributes.hs_color = serviceData.hs_color;
|
|
1937
|
+
} else if (domain === "scene") e.state = (/* @__PURE__ */ new Date()).toISOString();
|
|
1938
|
+
else if (domain === "media_player") e.state = "playing";
|
|
637
1939
|
else if (domain === "cover") {
|
|
638
1940
|
e.state = "open";
|
|
639
1941
|
if ("current_position" in e.attributes) e.attributes.current_position = 100;
|
|
640
1942
|
} else e.state = "on";
|
|
641
1943
|
}
|
|
1944
|
+
const DEMO_TRACKS = [
|
|
1945
|
+
{
|
|
1946
|
+
title: "Lo-fi Beats",
|
|
1947
|
+
artist: "Chill Station"
|
|
1948
|
+
},
|
|
1949
|
+
{
|
|
1950
|
+
title: "Golden Hour",
|
|
1951
|
+
artist: "Analog Sunset"
|
|
1952
|
+
},
|
|
1953
|
+
{
|
|
1954
|
+
title: "Night Drive",
|
|
1955
|
+
artist: "Neon Coast"
|
|
1956
|
+
}
|
|
1957
|
+
];
|
|
1958
|
+
function applyTrackSkip(e, delta) {
|
|
1959
|
+
const current = DEMO_TRACKS.findIndex((t) => t.title === e.attributes.media_title);
|
|
1960
|
+
const next = DEMO_TRACKS[(current + delta + DEMO_TRACKS.length) % DEMO_TRACKS.length];
|
|
1961
|
+
if (!next) return;
|
|
1962
|
+
e.attributes.media_title = next.title;
|
|
1963
|
+
e.attributes.media_artist = next.artist;
|
|
1964
|
+
}
|
|
642
1965
|
function applyTurnOff(e, domain) {
|
|
643
1966
|
if (domain === "light") {
|
|
644
1967
|
e.state = "off";
|
|
@@ -1491,10 +2814,11 @@ function resolveIcon(entity$1, registry) {
|
|
|
1491
2814
|
*/
|
|
1492
2815
|
function buildEntityView(entity$1, registry) {
|
|
1493
2816
|
const domain = extractDomain(entity$1.entity_id);
|
|
1494
|
-
const
|
|
1495
|
-
const
|
|
2817
|
+
const attributes = entity$1.attributes ?? {};
|
|
2818
|
+
const friendlyName$1 = attributes.friendly_name ?? registry?.name ?? entity$1.entity_id;
|
|
2819
|
+
const name = registry?.name ?? friendlyName$1;
|
|
1496
2820
|
const iconResolution = resolveIcon(entity$1, registry);
|
|
1497
|
-
const { device_class: _dc, unit_of_measurement: _uom, friendly_name: _fn, icon: _icon,...restAttributes } =
|
|
2821
|
+
const { device_class: _dc, unit_of_measurement: _uom, friendly_name: _fn, icon: _icon,...restAttributes } = attributes;
|
|
1498
2822
|
let areaId = registry?.area_id ?? null;
|
|
1499
2823
|
if (areaId === null && registry?.device_id) {
|
|
1500
2824
|
const device = state.devices[registry.device_id];
|
|
@@ -1508,12 +2832,12 @@ function buildEntityView(entity$1, registry) {
|
|
|
1508
2832
|
lastChanged: new Date(entity$1.last_changed),
|
|
1509
2833
|
lastUpdated: new Date(entity$1.last_updated),
|
|
1510
2834
|
context: {
|
|
1511
|
-
id: entity$1.context
|
|
1512
|
-
parentId: entity$1.context
|
|
1513
|
-
userId: entity$1.context
|
|
2835
|
+
id: entity$1.context?.id ?? "",
|
|
2836
|
+
parentId: entity$1.context?.parent_id ?? null,
|
|
2837
|
+
userId: entity$1.context?.user_id ?? null
|
|
1514
2838
|
},
|
|
1515
2839
|
name,
|
|
1516
|
-
friendlyName,
|
|
2840
|
+
friendlyName: friendlyName$1,
|
|
1517
2841
|
areaId,
|
|
1518
2842
|
deviceId: registry?.device_id ?? null,
|
|
1519
2843
|
platform: registry?.platform ?? "unknown",
|
|
@@ -1525,9 +2849,9 @@ function buildEntityView(entity$1, registry) {
|
|
|
1525
2849
|
entityCategory: registry?.entity_category ?? null,
|
|
1526
2850
|
labels: registry?.labels ?? [],
|
|
1527
2851
|
aliases: registry?.aliases ?? [],
|
|
1528
|
-
deviceClass: registry?.device_class ??
|
|
1529
|
-
unitOfMeasurement: registry?.unit_of_measurement ??
|
|
1530
|
-
supportedFeatures: registry?.supported_features ??
|
|
2852
|
+
deviceClass: registry?.device_class ?? attributes.device_class ?? null,
|
|
2853
|
+
unitOfMeasurement: registry?.unit_of_measurement ?? attributes.unit_of_measurement ?? null,
|
|
2854
|
+
supportedFeatures: registry?.supported_features ?? attributes.supported_features
|
|
1531
2855
|
};
|
|
1532
2856
|
}
|
|
1533
2857
|
/**
|
|
@@ -1690,8 +3014,8 @@ function getAreaView(areaId) {
|
|
|
1690
3014
|
//#region src/query/area-builder.ts
|
|
1691
3015
|
var AreaQueryBuilderImpl = class AreaQueryBuilderImpl {
|
|
1692
3016
|
queryState;
|
|
1693
|
-
constructor(initialState) {
|
|
1694
|
-
this.queryState = initialState ?? {
|
|
3017
|
+
constructor(initialState$1) {
|
|
3018
|
+
this.queryState = initialState$1 ?? {
|
|
1695
3019
|
filters: [],
|
|
1696
3020
|
sorts: []
|
|
1697
3021
|
};
|
|
@@ -1945,8 +3269,8 @@ function getNestedValue(obj, path) {
|
|
|
1945
3269
|
//#region src/query/builder.ts
|
|
1946
3270
|
var EntitiesQueryBuilderImpl = class EntitiesQueryBuilderImpl {
|
|
1947
3271
|
queryState;
|
|
1948
|
-
constructor(initialState) {
|
|
1949
|
-
this.queryState = initialState ?? {
|
|
3272
|
+
constructor(initialState$1) {
|
|
3273
|
+
this.queryState = initialState$1 ?? {
|
|
1950
3274
|
indexFilters: [],
|
|
1951
3275
|
predicateFilters: [],
|
|
1952
3276
|
sorts: [],
|