@camstack/types 1.2.421 → 1.2.422

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.
Files changed (39) hide show
  1. package/dist/addon.js +1 -1
  2. package/dist/addon.mjs +1 -1
  3. package/dist/capabilities/composer.cap.d.ts +129 -57
  4. package/dist/capabilities/core-blocks.cap.d.ts +936 -410
  5. package/dist/capabilities/index.d.ts +3 -1
  6. package/dist/capabilities/meter.cap.d.ts +53 -0
  7. package/dist/client.js +3 -0
  8. package/dist/client.mjs +3 -0
  9. package/dist/composition/composition-report.d.ts +45 -0
  10. package/dist/composition/composition.d.ts +196 -19
  11. package/dist/composition/helpers/compile-history.d.ts +13 -0
  12. package/dist/composition/helpers/compile.d.ts +2 -1
  13. package/dist/composition/helpers/energy.d.ts +8 -0
  14. package/dist/composition/helpers/history-math.d.ts +39 -0
  15. package/dist/composition/helpers/history-window.d.ts +71 -0
  16. package/dist/composition/helpers/index.d.ts +6 -2
  17. package/dist/composition/helpers/source-pick.d.ts +2 -0
  18. package/dist/composition/helpers/spec.d.ts +23 -10
  19. package/dist/{composition-B3Hkb6v7.js → composition-CPRAP41h.js} +792 -8
  20. package/dist/{composition-B2rw5L29.mjs → composition-CpETZ4HL.mjs} +595 -9
  21. package/dist/device/cap-vocabulary/entries/sensors.d.ts +1 -1
  22. package/dist/generated/addon-api.d.ts +14 -0
  23. package/dist/generated/addon-cap-facts.d.ts +1 -1
  24. package/dist/generated/cap-status-types.d.ts +3 -1
  25. package/dist/generated/capability-router-map.d.ts +5 -2
  26. package/dist/generated/device-local-state.d.ts +3 -0
  27. package/dist/generated/device-proxy.d.ts +3 -0
  28. package/dist/generated/device-scoped-caps.d.ts +1 -1
  29. package/dist/generated/history-catalog.d.ts +1 -1
  30. package/dist/generated/method-access-map.d.ts +1 -1
  31. package/dist/generated/method-device-selectors.d.ts +1 -1
  32. package/dist/generated/runtime-state-durability.d.ts +1 -1
  33. package/dist/index.js +747 -446
  34. package/dist/index.mjs +709 -447
  35. package/dist/node.js +1 -1
  36. package/dist/node.mjs +1 -1
  37. package/dist/{sleep-bykKrTgu.mjs → sleep-0sD_LHLI.mjs} +10 -1
  38. package/dist/{sleep-YJ6FVsUu.js → sleep-CEKGW7A5.js} +10 -1
  39. package/package.json +1 -1
@@ -2232,6 +2232,544 @@ function sourcePickForAppliance() {
2232
2232
  };
2233
2233
  }
2234
2234
  //#endregion
2235
+ //#region src/composition/helpers/energy.ts
2236
+ /**
2237
+ * Watts integrated over time, in kWh (D944).
2238
+ *
2239
+ * 1 kWh = 1 000 W × 3 600 000 ms. The history energy series uses the same
2240
+ * factor (`tw_sum / 3.6e9`), so a chart and an integral helper cannot diverge.
2241
+ */
2242
+ var KWH_PER_WATT_MS = 1 / 36e8;
2243
+ function wattMillisecondsToKwh(wattMilliseconds) {
2244
+ return wattMilliseconds * KWH_PER_WATT_MS;
2245
+ }
2246
+ //#endregion
2247
+ //#region src/composition/helpers/history-math.ts
2248
+ /**
2249
+ * Statistics and integrals over a history read (D944).
2250
+ *
2251
+ * Pure: the same points always give the same answer, so a restart recomputes
2252
+ * rather than trusting a buffer. A gauge mean is time-weighted (value × ms),
2253
+ * never the mean of the samples. A hole with no held value is not integrated
2254
+ * as zero. A truncated read is not published.
2255
+ */
2256
+ var STATISTIC_STATS = [
2257
+ "min",
2258
+ "max",
2259
+ "mean",
2260
+ "sum",
2261
+ "count",
2262
+ "change"
2263
+ ];
2264
+ var SERIES_KINDS = [
2265
+ "gauge",
2266
+ "counter",
2267
+ "boolean"
2268
+ ];
2269
+ function finite(value) {
2270
+ return typeof value === "number" && Number.isFinite(value);
2271
+ }
2272
+ function pointValue(point) {
2273
+ if (finite(point.value)) return point.value;
2274
+ if (finite(point.last)) return point.last;
2275
+ return null;
2276
+ }
2277
+ function stepCoverage(read) {
2278
+ const points = read.points.filter((point) => point.t >= read.from && point.t < read.to);
2279
+ const start = finite(read.before?.value) ? read.before.value : null;
2280
+ let held = start;
2281
+ let cursor = read.from;
2282
+ let gaps = 0;
2283
+ const segments = [];
2284
+ if (held === null) {
2285
+ const first = points.find((point) => pointValue(point) !== null);
2286
+ if (first === void 0) return {
2287
+ segments,
2288
+ included: 0,
2289
+ gaps: 1,
2290
+ end: null,
2291
+ start: null
2292
+ };
2293
+ if (first.t > read.from) gaps += 1;
2294
+ cursor = first.t;
2295
+ held = pointValue(first);
2296
+ }
2297
+ for (const point of points) {
2298
+ const next = pointValue(point);
2299
+ if (held !== null && point.t > cursor) segments.push({
2300
+ value: held,
2301
+ ms: point.t - cursor
2302
+ });
2303
+ if (next !== null) held = next;
2304
+ if (point.t > cursor) cursor = point.t;
2305
+ }
2306
+ if (held !== null && read.to > cursor) segments.push({
2307
+ value: held,
2308
+ ms: read.to - cursor
2309
+ });
2310
+ const end = points.reduce((latest, point) => pointValue(point) ?? latest, held);
2311
+ return {
2312
+ segments,
2313
+ included: points.length,
2314
+ gaps,
2315
+ end,
2316
+ start
2317
+ };
2318
+ }
2319
+ function rollupCoverage(read) {
2320
+ const weighted = read.points.filter((point) => finite(point.twMean) && finite(point.twMs) && point.twMs > 0);
2321
+ if (weighted.length === 0) return null;
2322
+ const first = weighted[0];
2323
+ const gaps = read.before === null && first !== void 0 && first.t > read.from ? 1 : 0;
2324
+ const levels = read.points.flatMap((point) => finite(point.last) ? [point.last] : []);
2325
+ const end = levels.length > 0 ? levels[levels.length - 1] ?? null : null;
2326
+ return {
2327
+ segments: weighted.flatMap((point) => finite(point.twMean) && finite(point.twMs) ? [{
2328
+ value: point.twMean,
2329
+ ms: point.twMs
2330
+ }] : []),
2331
+ included: weighted.length,
2332
+ gaps,
2333
+ end,
2334
+ start: finite(read.before?.value) ? read.before.value : null
2335
+ };
2336
+ }
2337
+ function coverageOf(read) {
2338
+ if (read.tier !== "raw") {
2339
+ const rolled = rollupCoverage(read);
2340
+ if (rolled !== null) return rolled;
2341
+ }
2342
+ return stepCoverage(read);
2343
+ }
2344
+ function timeWeighted(segments) {
2345
+ let ms = 0;
2346
+ let sum = 0;
2347
+ for (const segment of segments) {
2348
+ ms += segment.ms;
2349
+ sum += segment.value * segment.ms;
2350
+ }
2351
+ if (ms <= 0) return null;
2352
+ return sum / ms;
2353
+ }
2354
+ function extremes(read, covered) {
2355
+ const lows = [];
2356
+ const highs = [];
2357
+ for (const point of read.points) {
2358
+ if (finite(point.min)) lows.push(point.min);
2359
+ if (finite(point.max)) highs.push(point.max);
2360
+ }
2361
+ if (covered.start !== null) {
2362
+ lows.push(covered.start);
2363
+ highs.push(covered.start);
2364
+ }
2365
+ return {
2366
+ min: lows.length === 0 ? null : Math.min(...lows),
2367
+ max: highs.length === 0 ? null : Math.max(...highs)
2368
+ };
2369
+ }
2370
+ function counterGrowth(read) {
2371
+ const deltas = read.points.flatMap((point) => finite(point.delta) ? [point.delta] : []);
2372
+ if (deltas.length === 0) return null;
2373
+ return deltas.reduce((sum, delta) => sum + delta, 0);
2374
+ }
2375
+ function hold(reason) {
2376
+ return {
2377
+ write: "hold",
2378
+ value: null,
2379
+ included: 0,
2380
+ excluded: 0,
2381
+ reason
2382
+ };
2383
+ }
2384
+ function gapValue(reason, included) {
2385
+ return {
2386
+ write: "null",
2387
+ value: null,
2388
+ included,
2389
+ excluded: 1,
2390
+ reason
2391
+ };
2392
+ }
2393
+ function numberValue(value, included, gaps, reason) {
2394
+ return {
2395
+ write: "number",
2396
+ value,
2397
+ included,
2398
+ excluded: gaps,
2399
+ reason: gaps > 0 ? reason : null
2400
+ };
2401
+ }
2402
+ var GAP_REASON = "history has a gap — that interval was not counted as zero";
2403
+ function evaluateStatistic(stat, read) {
2404
+ if (read.truncated) return hold("the history query was truncated — the last statistic is kept");
2405
+ if (stat === "sum" && read.kind !== "counter") return hold("a gauge has no sum — that is the integral template");
2406
+ if (read.to <= read.from) return gapValue("the range is empty", 0);
2407
+ const covered = coverageOf(read);
2408
+ if (covered.segments.length === 0 && covered.start === null) return gapValue(GAP_REASON, covered.included);
2409
+ if (stat === "count") return numberValue(covered.included, covered.included, covered.gaps, GAP_REASON);
2410
+ if (stat === "sum" || stat === "change" && read.kind === "counter") {
2411
+ const growth = counterGrowth(read);
2412
+ if (growth === null) return gapValue("the counter has no growth in this range", covered.included);
2413
+ return numberValue(growth, covered.included, covered.gaps, GAP_REASON);
2414
+ }
2415
+ if (stat === "change") {
2416
+ if (covered.start === null || covered.end === null) return gapValue("the change has no value at the start of the range", covered.included);
2417
+ return numberValue(covered.end - covered.start, covered.included, covered.gaps, GAP_REASON);
2418
+ }
2419
+ if (stat === "min" || stat === "max") {
2420
+ const ends = extremes(read, covered);
2421
+ const value = stat === "min" ? ends.min : ends.max;
2422
+ if (value === null) return gapValue(GAP_REASON, covered.included);
2423
+ return numberValue(value, covered.included, covered.gaps, GAP_REASON);
2424
+ }
2425
+ const mean = timeWeighted(covered.segments);
2426
+ if (mean === null) return gapValue(GAP_REASON, covered.included);
2427
+ return numberValue(mean, covered.included, covered.gaps, GAP_REASON);
2428
+ }
2429
+ function integrateReading(read, scale) {
2430
+ if (read.truncated) return hold("the history query was truncated — the last total is kept");
2431
+ if (read.kind === "counter") return hold("a counter is already a total — it is not integrated again");
2432
+ if (read.to <= read.from) return gapValue("the range is empty", 0);
2433
+ const covered = coverageOf(read);
2434
+ if (covered.segments.length === 0) return {
2435
+ write: "hold",
2436
+ value: null,
2437
+ included: covered.included,
2438
+ excluded: covered.gaps,
2439
+ reason: GAP_REASON
2440
+ };
2441
+ let unitMs = 0;
2442
+ for (const segment of covered.segments) unitMs += segment.value * segment.ms;
2443
+ return numberValue(scale === "energy-kwh" ? wattMillisecondsToKwh(unitMs) : unitMs / 1e3, covered.included, covered.gaps, GAP_REASON);
2444
+ }
2445
+ //#endregion
2446
+ //#region src/composition/helpers/history-window.ts
2447
+ /**
2448
+ * The range a statistics or integral helper asks history for (D944).
2449
+ *
2450
+ * Calendar edges are the hub's timezone, never UTC midnight: a day is 23, 24
2451
+ * or 25 hours across a DST change. A tier is eligible only when its retention
2452
+ * is strictly longer than the longest that range can be. Forever qualifies.
2453
+ * Equal to a tier's retention is a refusal, not a short answer.
2454
+ */
2455
+ var MINUTE_MS = 6e4;
2456
+ var HOUR_MS = 60 * MINUTE_MS;
2457
+ var DAY_MS = 24 * HOUR_MS;
2458
+ var ROLLING_SPANS = [
2459
+ "1h",
2460
+ "24h",
2461
+ "2d",
2462
+ "7d",
2463
+ "30d",
2464
+ "custom"
2465
+ ];
2466
+ var CALENDAR_PERIODS = [
2467
+ "today",
2468
+ "yesterday",
2469
+ "this-week",
2470
+ "this-month"
2471
+ ];
2472
+ var HISTORY_TIERS = [
2473
+ "raw",
2474
+ "5m",
2475
+ "1h",
2476
+ "1d"
2477
+ ];
2478
+ var RESET_PERIODS = [
2479
+ "never",
2480
+ "daily",
2481
+ "weekly",
2482
+ "monthly"
2483
+ ];
2484
+ var MIN_CUSTOM_MINUTES = 5;
2485
+ /** Faster than this is not a history query. Rolling windows at most this often. */
2486
+ var HISTORY_FAST_MS = 6e4;
2487
+ var HISTORY_SLOW_MS = 5 * MINUTE_MS;
2488
+ var ROLLING_MS = {
2489
+ "1h": HOUR_MS,
2490
+ "24h": 24 * HOUR_MS,
2491
+ "2d": 2 * DAY_MS,
2492
+ "7d": 7 * DAY_MS,
2493
+ "30d": 30 * DAY_MS
2494
+ };
2495
+ /** The device-history defaults: raw 7 d, 5 m 30 d, 1 h 365 d, 1 d forever. */
2496
+ var DEFAULT_HISTORY_RETENTION = {
2497
+ rawMs: 7 * DAY_MS,
2498
+ fiveMinuteMs: 30 * DAY_MS,
2499
+ hourMs: 365 * DAY_MS,
2500
+ dayMs: null
2501
+ };
2502
+ var rollingWindowObject = z.object({
2503
+ kind: z.literal("rolling"),
2504
+ span: z.enum(ROLLING_SPANS),
2505
+ customMinutes: z.number().int().min(5).optional()
2506
+ }).check((payload) => {
2507
+ if (payload.value.span === "custom" && payload.value.customMinutes === void 0) payload.issues.push({
2508
+ code: "custom",
2509
+ message: "a custom range needs a duration in minutes",
2510
+ path: ["customMinutes"],
2511
+ input: payload.value
2512
+ });
2513
+ if (payload.value.span !== "custom" && payload.value.customMinutes !== void 0) payload.issues.push({
2514
+ code: "custom",
2515
+ message: "only a custom range carries a duration",
2516
+ path: ["customMinutes"],
2517
+ input: payload.value
2518
+ });
2519
+ });
2520
+ var RollingWindowSchema = rollingWindowObject;
2521
+ var calendarWindowObject = z.object({
2522
+ kind: z.literal("calendar"),
2523
+ period: z.enum(CALENDAR_PERIODS)
2524
+ });
2525
+ var CalendarWindowSchema = calendarWindowObject;
2526
+ var StatisticWindowSchema = z.discriminatedUnion("kind", [rollingWindowObject, calendarWindowObject]);
2527
+ var WEEKDAY_INDEX = {
2528
+ Sun: 0,
2529
+ Mon: 1,
2530
+ Tue: 2,
2531
+ Wed: 3,
2532
+ Thu: 4,
2533
+ Fri: 5,
2534
+ Sat: 6
2535
+ };
2536
+ var FORMATTERS = /* @__PURE__ */ new Map();
2537
+ function formatterFor(timeZone) {
2538
+ const held = FORMATTERS.get(timeZone);
2539
+ if (held !== void 0) return held;
2540
+ let made;
2541
+ try {
2542
+ made = new Intl.DateTimeFormat("en-US", {
2543
+ timeZone,
2544
+ weekday: "short",
2545
+ year: "numeric",
2546
+ month: "2-digit",
2547
+ day: "2-digit",
2548
+ hour: "2-digit",
2549
+ minute: "2-digit",
2550
+ second: "2-digit",
2551
+ hourCycle: "h23"
2552
+ });
2553
+ } catch {
2554
+ made = null;
2555
+ }
2556
+ FORMATTERS.set(timeZone, made);
2557
+ return made;
2558
+ }
2559
+ function zonedClock(ms, timeZone) {
2560
+ const formatter = formatterFor(timeZone);
2561
+ if (formatter === null) return null;
2562
+ const parts = formatter.formatToParts(new Date(ms));
2563
+ const read = (type) => parts.find((part) => part.type === type)?.value;
2564
+ const year = Number(read("year"));
2565
+ const month = Number(read("month"));
2566
+ const day = Number(read("day"));
2567
+ const hourRaw = Number(read("hour"));
2568
+ const minute = Number(read("minute"));
2569
+ const second = Number(read("second"));
2570
+ const weekday = WEEKDAY_INDEX[read("weekday") ?? ""];
2571
+ if (!Number.isInteger(year) || !Number.isInteger(month) || !Number.isInteger(day) || !Number.isInteger(hourRaw) || !Number.isInteger(minute) || !Number.isInteger(second) || weekday === void 0) return null;
2572
+ return {
2573
+ year,
2574
+ month,
2575
+ day,
2576
+ hour: hourRaw === 24 ? 0 : hourRaw,
2577
+ minute,
2578
+ second,
2579
+ weekday
2580
+ };
2581
+ }
2582
+ function utcOf(clock, timeZone) {
2583
+ const guess = Date.UTC(clock.year, clock.month - 1, clock.day, clock.hour, clock.minute, clock.second);
2584
+ const first = zonedClock(guess, timeZone);
2585
+ if (first === null) return null;
2586
+ const offset = Date.UTC(first.year, first.month - 1, first.day, first.hour, first.minute, first.second) - guess;
2587
+ const shifted = guess - offset;
2588
+ const second = zonedClock(shifted, timeZone);
2589
+ if (second === null) return null;
2590
+ const offset2 = Date.UTC(second.year, second.month - 1, second.day, second.hour, second.minute, second.second) - shifted;
2591
+ return offset2 === offset ? shifted : guess - offset2;
2592
+ }
2593
+ function midnightOf(ms, timeZone) {
2594
+ const clock = zonedClock(ms, timeZone);
2595
+ if (clock === null) return null;
2596
+ return utcOf({
2597
+ ...clock,
2598
+ hour: 0,
2599
+ minute: 0,
2600
+ second: 0
2601
+ }, timeZone);
2602
+ }
2603
+ function monthStart(ms, timeZone) {
2604
+ const clock = zonedClock(ms, timeZone);
2605
+ if (clock === null) return null;
2606
+ return utcOf({
2607
+ ...clock,
2608
+ day: 1,
2609
+ hour: 0,
2610
+ minute: 0,
2611
+ second: 0
2612
+ }, timeZone);
2613
+ }
2614
+ function weekStart(ms, timeZone) {
2615
+ const clock = zonedClock(ms, timeZone);
2616
+ let cursor = midnightOf(ms, timeZone);
2617
+ if (clock === null || cursor === null) return null;
2618
+ const back = clock.weekday === 0 ? 6 : clock.weekday - 1;
2619
+ for (let step = 0; step < back; step++) {
2620
+ const previous = midnightOf(cursor - 1, timeZone);
2621
+ if (previous === null) return null;
2622
+ cursor = previous;
2623
+ }
2624
+ return cursor;
2625
+ }
2626
+ function keptLabel(ms) {
2627
+ if (ms === null) return "forever";
2628
+ if (ms % 864e5 === 0) return `${ms / DAY_MS} d`;
2629
+ if (ms % 36e5 === 0) return `${ms / HOUR_MS} h`;
2630
+ return `${ms / MINUTE_MS} min`;
2631
+ }
2632
+ function retentionSentence(retention) {
2633
+ return `raw ${keptLabel(retention.rawMs)}, 5m ${keptLabel(retention.fiveMinuteMs)}, 1h ${keptLabel(retention.hourMs)}, 1d ${keptLabel(retention.dayMs)}`;
2634
+ }
2635
+ function tierFor(durationMs, retention) {
2636
+ const rows = [
2637
+ {
2638
+ tier: "raw",
2639
+ keptMs: retention.rawMs
2640
+ },
2641
+ {
2642
+ tier: "5m",
2643
+ keptMs: retention.fiveMinuteMs
2644
+ },
2645
+ {
2646
+ tier: "1h",
2647
+ keptMs: retention.hourMs
2648
+ },
2649
+ {
2650
+ tier: "1d",
2651
+ keptMs: retention.dayMs
2652
+ }
2653
+ ];
2654
+ for (const row of rows) if (row.keptMs === null || row.keptMs > durationMs) return {
2655
+ ok: true,
2656
+ tier: row.tier,
2657
+ maxDurationMs: durationMs
2658
+ };
2659
+ return {
2660
+ ok: false,
2661
+ reason: `the range is ${keptLabel(durationMs)} and no history tier is kept strictly longer (${retentionSentence(retention)})`
2662
+ };
2663
+ }
2664
+ function rollingDurationMs(window) {
2665
+ if (window.span === "custom") return window.customMinutes === void 0 ? null : window.customMinutes * MINUTE_MS;
2666
+ return ROLLING_MS[window.span];
2667
+ }
2668
+ /** Longest the window can be. Calendar periods use their full length, not the elapsed part. */
2669
+ function maxDurationMs(window) {
2670
+ if (window.kind === "rolling") return rollingDurationMs(window);
2671
+ switch (window.period) {
2672
+ case "today":
2673
+ case "yesterday": return 25 * HOUR_MS;
2674
+ case "this-week": return 6084e5;
2675
+ case "this-month": return 2682e6;
2676
+ }
2677
+ }
2678
+ function planStatisticTier(window, retention) {
2679
+ const duration = maxDurationMs(window);
2680
+ if (duration === null) return {
2681
+ ok: false,
2682
+ reason: "a custom range needs a duration of at least 5 minutes"
2683
+ };
2684
+ return tierFor(duration, retention);
2685
+ }
2686
+ function planResetTier(reset, retention) {
2687
+ switch (reset) {
2688
+ case "never": return tierFor(Number.POSITIVE_INFINITY, retention);
2689
+ case "daily": return tierFor(25 * HOUR_MS, retention);
2690
+ case "weekly": return tierFor(6084e5, retention);
2691
+ case "monthly": return tierFor(2682e6, retention);
2692
+ }
2693
+ }
2694
+ function openStatistic(window, now, timeZone) {
2695
+ if (window.kind === "rolling") {
2696
+ const duration = rollingDurationMs(window);
2697
+ if (duration === null) return null;
2698
+ return {
2699
+ from: now - duration,
2700
+ to: now
2701
+ };
2702
+ }
2703
+ const today = midnightOf(now, timeZone);
2704
+ if (today === null) return null;
2705
+ switch (window.period) {
2706
+ case "today": return {
2707
+ from: today,
2708
+ to: now
2709
+ };
2710
+ case "yesterday": {
2711
+ const start = midnightOf(today - 1, timeZone);
2712
+ return start === null ? null : {
2713
+ from: start,
2714
+ to: today
2715
+ };
2716
+ }
2717
+ case "this-week": {
2718
+ const start = weekStart(now, timeZone);
2719
+ return start === null ? null : {
2720
+ from: start,
2721
+ to: now
2722
+ };
2723
+ }
2724
+ case "this-month": {
2725
+ const start = monthStart(now, timeZone);
2726
+ return start === null ? null : {
2727
+ from: start,
2728
+ to: now
2729
+ };
2730
+ }
2731
+ }
2732
+ }
2733
+ function openReset(reset, now, timeZone) {
2734
+ if (reset === "never") return {
2735
+ from: 0,
2736
+ to: now
2737
+ };
2738
+ const today = midnightOf(now, timeZone);
2739
+ if (today === null) return null;
2740
+ if (reset === "daily") return {
2741
+ from: today,
2742
+ to: now
2743
+ };
2744
+ if (reset === "weekly") {
2745
+ const start = weekStart(now, timeZone);
2746
+ return start === null ? null : {
2747
+ from: start,
2748
+ to: now
2749
+ };
2750
+ }
2751
+ const start = monthStart(now, timeZone);
2752
+ return start === null ? null : {
2753
+ from: start,
2754
+ to: now
2755
+ };
2756
+ }
2757
+ /** The next hub-local midnight, strictly after `now`. Monday is one of these. */
2758
+ function nextLocalMidnight(now, timeZone) {
2759
+ const today = midnightOf(now, timeZone);
2760
+ if (today === null) return null;
2761
+ return midnightOf(today + 26 * HOUR_MS, timeZone);
2762
+ }
2763
+ function statisticCadenceMs(window) {
2764
+ if (window.kind === "calendar") return window.period === "today" ? HISTORY_FAST_MS : HISTORY_SLOW_MS;
2765
+ const duration = rollingDurationMs(window);
2766
+ if (duration !== null && duration <= 24 * 36e5) return HISTORY_FAST_MS;
2767
+ return HISTORY_SLOW_MS;
2768
+ }
2769
+ function integralCadenceMs(tier) {
2770
+ return tier === "raw" ? HISTORY_FAST_MS : HISTORY_SLOW_MS;
2771
+ }
2772
+ //#endregion
2235
2773
  //#region src/composition/source-ref.ts
2236
2774
  /**
2237
2775
  * A composition source identity: `(addonId, stableId)`, never a numeric device
@@ -2297,10 +2835,7 @@ var HelperMemberSchema = z.object({
2297
2835
  cap: z.string().min(1),
2298
2836
  fieldPath: z.string().min(1)
2299
2837
  });
2300
- /**
2301
- * Phase 1a is groups only. Math, integral and statistics stay off this union
2302
- * until the composer can run them — a control must not save a spec it cannot.
2303
- */
2838
+ /** A group, a statistic, or an integral (D944). The composer runs all three. */
2304
2839
  var GroupHelperSpecSchema = z.object({
2305
2840
  kind: z.literal("group"),
2306
2841
  templateId: GroupTemplateIdSchema,
@@ -2318,10 +2853,35 @@ var ApplianceHelperSpecSchema = z.object({
2318
2853
  stopDwellMs: z.number().int().nonnegative(),
2319
2854
  member: HelperMemberSchema
2320
2855
  });
2321
- var HelperSpecSchema = z.discriminatedUnion("kind", [GroupHelperSpecSchema, ApplianceHelperSpecSchema]);
2322
- /** Every member a helper reads. An appliance has one. */
2856
+ var statisticHelperObject = z.object({
2857
+ kind: z.literal("statistic"),
2858
+ stat: z.enum(STATISTIC_STATS),
2859
+ window: StatisticWindowSchema,
2860
+ source: HelperMemberSchema
2861
+ });
2862
+ var StatisticHelperSpecSchema = statisticHelperObject;
2863
+ var INTEGRAL_OUTPUTS = ["energy", "meter"];
2864
+ var integralHelperObject = z.object({
2865
+ kind: z.literal("integral"),
2866
+ reset: z.enum(RESET_PERIODS),
2867
+ output: z.enum(INTEGRAL_OUTPUTS),
2868
+ source: HelperMemberSchema
2869
+ });
2870
+ var IntegralHelperSpecSchema = integralHelperObject;
2871
+ var HelperSpecSchema = z.discriminatedUnion("kind", [
2872
+ GroupHelperSpecSchema,
2873
+ ApplianceHelperSpecSchema,
2874
+ statisticHelperObject,
2875
+ integralHelperObject
2876
+ ]);
2877
+ /** Every member a helper reads. A group has many; the others have one. */
2323
2878
  function helperMembers(spec) {
2324
- return spec.kind === "group" ? spec.members : [spec.member];
2879
+ switch (spec.kind) {
2880
+ case "group": return spec.members;
2881
+ case "appliance": return [spec.member];
2882
+ case "statistic":
2883
+ case "integral": return [spec.source];
2884
+ }
2325
2885
  }
2326
2886
  //#endregion
2327
2887
  //#region src/composition/composition.ts
@@ -2432,11 +2992,36 @@ var CompositionAggregateSourceSchema = z.object({
2432
2992
  "offline"
2433
2993
  ])
2434
2994
  });
2995
+ /**
2996
+ * `series`: a statistics or integral field. The composer writes it from a
2997
+ * history query. The live pass does not recompute it (D944).
2998
+ */
2999
+ var CompositionSeriesSourceSchema = z.object({
3000
+ kind: z.literal("series"),
3001
+ read: CompositionFieldReadSchema,
3002
+ /** `value` is the statistic or the integral. The others are the gap counts. */
3003
+ publish: z.enum([
3004
+ "value",
3005
+ "included",
3006
+ "excluded",
3007
+ "total"
3008
+ ]),
3009
+ /**
3010
+ * What an unanswered `value` does. Counts ignore this. `offline` leaves the
3011
+ * device unseeded until history answers, so a restart cannot publish 0.
3012
+ */
3013
+ empty: z.enum([
3014
+ "null",
3015
+ "omit",
3016
+ "offline"
3017
+ ])
3018
+ });
2435
3019
  var CompositionFieldSourceSchema = z.discriminatedUnion("kind", [
2436
3020
  CompositionFromSourceSchema,
2437
3021
  CompositionExpressionSourceSchema,
2438
3022
  CompositionCodeSourceSchema,
2439
- CompositionAggregateSourceSchema
3023
+ CompositionAggregateSourceSchema,
3024
+ CompositionSeriesSourceSchema
2440
3025
  ]);
2441
3026
  /** `forward`: RESERVED for slice 3. Argument mapping arrives there as an additive optional `args`. */
2442
3027
  var CompositionForwardCommandSchema = z.object({
@@ -2556,6 +3141,7 @@ function fieldReads(source) {
2556
3141
  fieldPath: binding.fieldPath
2557
3142
  }] : []);
2558
3143
  case "aggregate": return source.reads;
3144
+ case "series": return [source.read];
2559
3145
  case "code": return [];
2560
3146
  }
2561
3147
  }
@@ -2582,4 +3168,4 @@ function featureSources(feature) {
2582
3168
  return featureFieldEntries(feature).map((e) => e.source);
2583
3169
  }
2584
3170
  //#endregion
2585
- export { APPLIANCE_DEFAULTS as $, audioPlanFromEncodeProfile as $t, compositionFeatureEnabled as A, evaluateRose as At, ActiveCriterionSchema as B, evaluateSensorLevelEdge as Bt, CompositionTargetSchema as C, MAX_EXPRESSION_EVAL_STEPS as Ct, MAX_COMPOSITION_ITEMS as D, EXPRESSION_BUILTIN_NAMES as Dt, MAX_COMPOSITION_FIELDS_PER_FEATURE as E, EXPRESSION_BUILTINS as Et, featureFieldEntries as F, SOURCE_CAPS as Ft, GroupHelperSpecSchema as G, chooseFfmpegBinary as Gt, ApplianceHelperSpecSchema as H, sliceActiveValue as Ht, featureSources as I, SOURCE_CAP_ACTIVE_FIELD as It, HelperSpecSchema as J, getFfmpegArtifact as Jt, GroupTemplateIdSchema as K, ffmpegBinaryName as Kt, fieldReads as L, SOURCE_CAP_CHANGED_AT_FIELD as Lt, compositionReadKey as M, ExpressionEvalError as Mt, compositionSliceKey as N, ExpressionParseError as Nt, composedDeviceRef as O, disarmRoseMemory as Ot, compositionSourceKey as P, DEFAULT_FIRST_SIGHTING_FRESHNESS_MS as Pt, CompositionSourceRefSchema as Q, AUDIO_PRESETS as Qt, ACTIVE_CRITERIA as R, SOURCE_DEVICE_TYPES as Rt, CompositionSchema as S, MAX_EXPRESSION_CALL_ARGS as St, MAX_COMPOSITION_FEATURES as T, RESERVED_BINDING_NAMES as Tt, BINARY_AGGREGATES as U, sliceChangedAt as Ut, AggregateReducerSchema as V, isSourceCap as Vt, GROUP_TEMPLATE_IDS as W, FFMPEG_PATH_ENV as Wt, NUMERIC_AGGREGATES as X, canonicalHash as Xt, MAX_HELPER_MEMBERS as Y, getFfmpegDownloadUrl as Yt, helperMembers as Z, Fmp4BoxSplitter as Zt, CompositionFromSourceSchema as _, tokenize as _t, CUSTOMIZATION_BLOCK_NAME_PREFIX as a, isSoftwareDecode as an, EXPRESSION_INJECTED_NOW as at, CompositionNewTargetSchema as b, MAX_EXPRESSION_AST_NODES as bt, CompositionCodeSourceSchema as c, resolveStreamMaps as cn, evaluateExpressionSource as ct, CompositionExpressionSourceSchema as d, validateExpressionSource as dt, buildAudioArgs as en, APPLIANCE_KINDS as et, CompositionFeatureSchema as f, createExpressionScope as ft, CompositionForwardCommandSchema as g, parseExpression as gt, CompositionFieldsFeatureSchema as h, compileExpressionSafe as ht, COMPOSITION_ITEM_KEY_RE as i, invocationFromEncodeProfile as in, sourcePickForAppliance as it, compositionItemFieldPath as j, isStatefulBuiltin as jt, composedDeviceStableId as k, evaluateCount as kt, CompositionCommandTargetSchema as l, evaluateStatefulExpressionCall as lt, CompositionFieldSourceSchema as m, compileExpression as mt, COMPOSER_ADDON_ID as n, buildInputArgs as nn, applianceDeviceType as nt, CompositionAggregateSourceSchema as o, logBannerArgs as on, cloneStatefulExpressionMemory as ot, CompositionFieldReadSchema as p, evaluateAst as pt, HelperMemberSchema as q, FFMPEG_RELEASE as qt, COMPOSITION_ITEM_ARRAY_PATH as r, buildVideoArgs as rn, isApplianceKind as rt, CompositionBindingSchema as s, pickVideoEncoder as sn, createStatefulExpressionMemory as st, COMPOSED_DEVICE_STABLE_ID_PREFIX as t, buildFfmpegArgs as tn, APPLIANCE_TEMPLATE_ID as tt, CompositionExistingTargetSchema as u, toExpressionValue as ut, CompositionItemEntrySchema as v, EXPRESSION_COMPILE_CACHE_CAPACITY as vt, MAX_COMPOSITION_CODE_LENGTH as w, MAX_EXPRESSION_SOURCE_LENGTH as wt, CompositionPassthroughFeatureSchema as x, MAX_EXPRESSION_BINDINGS as xt, CompositionLiteralBindingSchema as y, EXPRESSION_IDENTIFIER_RE as yt, AGGREGATE_REDUCERS as z, evaluateSensorEdge as zt };
3171
+ export { NUMERIC_AGGREGATES as $, MAX_EXPRESSION_AST_NODES as $t, composedDeviceStableId as A, Fmp4BoxSplitter as An, KWH_PER_WATT_MS as At, AGGREGATE_REDUCERS as B, pickVideoEncoder as Bn, createStatefulExpressionMemory as Bt, CompositionSeriesSourceSchema as C, FFMPEG_PATH_ENV as Cn, planStatisticTier as Ct, MAX_COMPOSITION_FIELDS_PER_FEATURE as D, getFfmpegArtifact as Dn, STATISTIC_STATS as Dt, MAX_COMPOSITION_FEATURES as E, FFMPEG_RELEASE as En, SERIES_KINDS as Et, compositionSourceKey as F, buildInputArgs as Fn, applianceDeviceType as Ft, GROUP_TEMPLATE_IDS as G, createExpressionScope as Gt, AggregateReducerSchema as H, evaluateStatefulExpressionCall as Ht, featureFieldEntries as I, buildVideoArgs as In, isApplianceKind as It, HelperMemberSchema as J, compileExpressionSafe as Jt, GroupHelperSpecSchema as K, evaluateAst as Kt, featureSources as L, invocationFromEncodeProfile as Ln, sourcePickForAppliance as Lt, compositionItemFieldPath as M, audioPlanFromEncodeProfile as Mn, APPLIANCE_DEFAULTS as Mt, compositionReadKey as N, buildAudioArgs as Nn, APPLIANCE_KINDS as Nt, MAX_COMPOSITION_ITEMS as O, getFfmpegDownloadUrl as On, evaluateStatistic as Ot, compositionSliceKey as P, buildFfmpegArgs as Pn, APPLIANCE_TEMPLATE_ID as Pt, MAX_HELPER_MEMBERS as Q, EXPRESSION_IDENTIFIER_RE as Qt, fieldReads as R, isSoftwareDecode as Rn, EXPRESSION_INJECTED_NOW as Rt, CompositionSchema as S, sliceChangedAt as Sn, planResetTier as St, MAX_COMPOSITION_CODE_LENGTH as T, ffmpegBinaryName as Tn, statisticCadenceMs as Tt, ApplianceHelperSpecSchema as U, toExpressionValue as Ut, ActiveCriterionSchema as V, resolveStreamMaps as Vn, evaluateExpressionSource as Vt, BINARY_AGGREGATES as W, validateExpressionSource as Wt, INTEGRAL_OUTPUTS as X, tokenize as Xt, HelperSpecSchema as Y, parseExpression as Yt, IntegralHelperSpecSchema as Z, EXPRESSION_COMPILE_CACHE_CAPACITY as Zt, CompositionFromSourceSchema as _, SOURCE_DEVICE_TYPES as _n, integralCadenceMs as _t, CUSTOMIZATION_BLOCK_NAME_PREFIX as a, EXPRESSION_BUILTINS as an, DAY_MS as at, CompositionNewTargetSchema as b, isSourceCap as bn, openReset as bt, CompositionCodeSourceSchema as c, evaluateCount as cn, HISTORY_SLOW_MS as ct, CompositionExpressionSourceSchema as d, ExpressionEvalError as dn, MINUTE_MS as dt, MAX_EXPRESSION_BINDINGS as en, StatisticHelperSpecSchema as et, CompositionFeatureSchema as f, ExpressionParseError as fn, MIN_CUSTOM_MINUTES as ft, CompositionForwardCommandSchema as g, SOURCE_CAP_CHANGED_AT_FIELD as gn, StatisticWindowSchema as gt, CompositionFieldsFeatureSchema as h, SOURCE_CAP_ACTIVE_FIELD as hn, RollingWindowSchema as ht, COMPOSITION_ITEM_KEY_RE as i, RESERVED_BINDING_NAMES as in, CalendarWindowSchema as it, compositionFeatureEnabled as j, AUDIO_PRESETS as jn, wattMillisecondsToKwh as jt, composedDeviceRef as k, canonicalHash as kn, integrateReading as kt, CompositionCommandTargetSchema as l, evaluateRose as ln, HISTORY_TIERS as lt, CompositionFieldSourceSchema as m, SOURCE_CAPS as mn, ROLLING_SPANS as mt, COMPOSER_ADDON_ID as n, MAX_EXPRESSION_EVAL_STEPS as nn, CompositionSourceRefSchema as nt, CompositionAggregateSourceSchema as o, EXPRESSION_BUILTIN_NAMES as on, DEFAULT_HISTORY_RETENTION as ot, CompositionFieldReadSchema as p, DEFAULT_FIRST_SIGHTING_FRESHNESS_MS as pn, RESET_PERIODS as pt, GroupTemplateIdSchema as q, compileExpression as qt, COMPOSITION_ITEM_ARRAY_PATH as r, MAX_EXPRESSION_SOURCE_LENGTH as rn, CALENDAR_PERIODS as rt, CompositionBindingSchema as s, disarmRoseMemory as sn, HISTORY_FAST_MS as st, COMPOSED_DEVICE_STABLE_ID_PREFIX as t, MAX_EXPRESSION_CALL_ARGS as tn, helperMembers as tt, CompositionExistingTargetSchema as u, isStatefulBuiltin as un, HOUR_MS as ut, CompositionItemEntrySchema as v, evaluateSensorEdge as vn, maxDurationMs as vt, CompositionTargetSchema as w, chooseFfmpegBinary as wn, rollingDurationMs as wt, CompositionPassthroughFeatureSchema as x, sliceActiveValue as xn, openStatistic as xt, CompositionLiteralBindingSchema as y, evaluateSensorLevelEdge as yn, nextLocalMidnight as yt, ACTIVE_CRITERIA as z, logBannerArgs as zn, cloneStatefulExpressionMemory as zt };
@@ -1,5 +1,5 @@
1
1
  import type { CapVocabularySlice } from '../types.js';
2
- type SensorCap = 'air-quality-sensor' | 'ambient-light-sensor' | 'binary' | 'carbon-monoxide' | 'connectivity' | 'contact' | 'enum-sensor' | 'flood' | 'gas' | 'group-commands' | 'group-status' | 'humidity-sensor' | 'motion' | 'numeric-sensor' | 'power-meter' | 'presence' | 'pressure-sensor' | 'smoke' | 'tamper' | 'temperature-sensor' | 'vibration' | 'weather' | 'battery' | 'network-link' | 'device-status' | 'doorbell' | 'event-emitter';
2
+ type SensorCap = 'air-quality-sensor' | 'ambient-light-sensor' | 'binary' | 'carbon-monoxide' | 'connectivity' | 'contact' | 'enum-sensor' | 'flood' | 'gas' | 'group-commands' | 'group-status' | 'humidity-sensor' | 'meter' | 'motion' | 'numeric-sensor' | 'power-meter' | 'presence' | 'pressure-sensor' | 'smoke' | 'tamper' | 'temperature-sensor' | 'vibration' | 'weather' | 'battery' | 'network-link' | 'device-status' | 'doorbell' | 'event-emitter';
3
3
  /** The sensor family of the vocabulary. */
4
4
  export declare const SENSOR_CAP_VOCABULARY: CapVocabularySlice<SensorCap>;
5
5
  export {};
@@ -4940,6 +4940,20 @@ export type AppRouter = TrpcCoreRouter<{
4940
4940
  meta: object;
4941
4941
  }>;
4942
4942
  }>>;
4943
+ meter: TRPCBuiltRouter<{
4944
+ ctx: TrpcContext;
4945
+ meta: object;
4946
+ errorShape: AugmentedErrorShape;
4947
+ transformer: true;
4948
+ }, TRPCDecorateCreateRouterOptions<{
4949
+ getStatus: TRPCQueryProcedure<{
4950
+ input: {
4951
+ [x: string]: unknown;
4952
+ } & z.input<typeof DEVICE_STATUS_METHOD.getStatus.input>;
4953
+ output: z.infer<typeof DEVICE_STATUS_METHOD.getStatus.output>;
4954
+ meta: object;
4955
+ }>;
4956
+ }>>;
4943
4957
  metricsProvider: TRPCBuiltRouter<{
4944
4958
  ctx: TrpcContext;
4945
4959
  meta: object;