@camstack/types 1.2.420 → 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 (50) hide show
  1. package/dist/addon.js +2 -2
  2. package/dist/addon.mjs +2 -2
  3. package/dist/capabilities/composer.cap.d.ts +129 -21
  4. package/dist/capabilities/core-blocks.cap.d.ts +929 -151
  5. package/dist/capabilities/index.d.ts +3 -1
  6. package/dist/capabilities/meter.cap.d.ts +53 -0
  7. package/dist/capabilities/power-meter.cap.d.ts +1 -1
  8. package/dist/capabilities/running-state.cap.d.ts +1 -1
  9. package/dist/client.js +12 -1
  10. package/dist/client.mjs +12 -1
  11. package/dist/composition/composition-report.d.ts +45 -0
  12. package/dist/composition/composition.d.ts +196 -7
  13. package/dist/composition/helpers/appliance.d.ts +28 -0
  14. package/dist/composition/helpers/compile-history.d.ts +13 -0
  15. package/dist/composition/helpers/compile.d.ts +3 -2
  16. package/dist/composition/helpers/energy.d.ts +8 -0
  17. package/dist/composition/helpers/history-math.d.ts +39 -0
  18. package/dist/composition/helpers/history-window.d.ts +71 -0
  19. package/dist/composition/helpers/index.d.ts +7 -2
  20. package/dist/composition/helpers/save.d.ts +1 -1
  21. package/dist/composition/helpers/source-pick.d.ts +2 -0
  22. package/dist/composition/helpers/spec.d.ts +39 -6
  23. package/dist/{composition-re1aezX9.js → composition-CPRAP41h.js} +924 -7
  24. package/dist/{composition-OhpuespA.mjs → composition-CpETZ4HL.mjs} +679 -8
  25. package/dist/device/appliance-export.d.ts +25 -0
  26. package/dist/device/cap-vocabulary/entries/sensors.d.ts +1 -1
  27. package/dist/device/cap-vocabulary/types.d.ts +10 -0
  28. package/dist/device/device-type.d.ts +8 -2
  29. package/dist/{err-msg-BXKzRBwg.mjs → err-msg-DIorGYeC.mjs} +7 -1
  30. package/dist/{err-msg-BhVbr0NX.js → err-msg-U7lLUMNz.js} +7 -1
  31. package/dist/generated/addon-api.d.ts +14 -0
  32. package/dist/generated/addon-cap-facts.d.ts +1 -1
  33. package/dist/generated/cap-status-types.d.ts +3 -1
  34. package/dist/generated/capability-router-map.d.ts +5 -2
  35. package/dist/generated/customize-catalog.d.ts +2 -2
  36. package/dist/generated/device-local-state.d.ts +3 -0
  37. package/dist/generated/device-proxy.d.ts +3 -0
  38. package/dist/generated/device-scoped-caps.d.ts +1 -1
  39. package/dist/generated/history-catalog.d.ts +1 -1
  40. package/dist/generated/method-access-map.d.ts +1 -1
  41. package/dist/generated/method-device-selectors.d.ts +1 -1
  42. package/dist/generated/runtime-state-durability.d.ts +1 -1
  43. package/dist/index.d.ts +1 -0
  44. package/dist/index.js +1063 -562
  45. package/dist/index.mjs +1013 -563
  46. package/dist/node.js +2 -2
  47. package/dist/node.mjs +2 -2
  48. package/dist/{sleep-bykKrTgu.mjs → sleep-0sD_LHLI.mjs} +10 -1
  49. package/dist/{sleep-YJ6FVsUu.js → sleep-CEKGW7A5.js} +10 -1
  50. package/package.json +1 -1
@@ -1,4 +1,4 @@
1
- import { a as DeviceType, i as DeviceRole } from "./err-msg-BXKzRBwg.mjs";
1
+ import { a as DeviceType, i as DeviceRole } from "./err-msg-DIorGYeC.mjs";
2
2
  import { z } from "zod";
3
3
  import { createHash } from "node:crypto";
4
4
  //#region src/ffmpeg/invocation.ts
@@ -2160,6 +2160,616 @@ function evaluateExpressionSource(expr, bindingValues, now, opts) {
2160
2160
  }
2161
2161
  }
2162
2162
  //#endregion
2163
+ //#region src/composition/helpers/appliance.ts
2164
+ /**
2165
+ * One power reading becomes a washer, dryer or dishwasher (D944 helper).
2166
+ *
2167
+ * Standby of the control board is under 5 W; a motor or a heater is far
2168
+ * above that, so 5 W is the line. A door lock or a short pump spike is not
2169
+ * a cycle, so running starts only after 30 s above the line.
2170
+ *
2171
+ * A wash, dry or dishwasher pause (soak, heat-wait, heater off between
2172
+ * bursts) lasts several minutes. Ending the cycle on the first dip would
2173
+ * report "finished" in the middle of the program, so the stop dwell is
2174
+ * 10 minutes on every type. The number is stored per type so one machine
2175
+ * can move later without moving the others.
2176
+ */
2177
+ var APPLIANCE_TEMPLATE_ID = "appliance";
2178
+ var APPLIANCE_KINDS = [
2179
+ "washer",
2180
+ "dryer",
2181
+ "dishwasher"
2182
+ ];
2183
+ var START_DWELL_MS = 3e4;
2184
+ var STOP_DWELL_MS = 6e5;
2185
+ var THRESHOLD_WATTS = 5;
2186
+ var APPLIANCE_DEFAULTS = {
2187
+ washer: {
2188
+ thresholdWatts: THRESHOLD_WATTS,
2189
+ startDwellMs: START_DWELL_MS,
2190
+ stopDwellMs: STOP_DWELL_MS
2191
+ },
2192
+ dryer: {
2193
+ thresholdWatts: THRESHOLD_WATTS,
2194
+ startDwellMs: START_DWELL_MS,
2195
+ stopDwellMs: STOP_DWELL_MS
2196
+ },
2197
+ dishwasher: {
2198
+ thresholdWatts: THRESHOLD_WATTS,
2199
+ startDwellMs: START_DWELL_MS,
2200
+ stopDwellMs: STOP_DWELL_MS
2201
+ }
2202
+ };
2203
+ var DEVICE_TYPE = {
2204
+ washer: DeviceType.Washer,
2205
+ dryer: DeviceType.Dryer,
2206
+ dishwasher: DeviceType.Dishwasher
2207
+ };
2208
+ function applianceDeviceType(kind) {
2209
+ return DEVICE_TYPE[kind];
2210
+ }
2211
+ function isApplianceKind(value) {
2212
+ return APPLIANCE_KINDS.some((kind) => kind === value);
2213
+ }
2214
+ /** The member step: one watts reading, from a sensor or from the plug itself. */
2215
+ function sourcePickForAppliance() {
2216
+ return {
2217
+ purpose: "reading",
2218
+ multiple: false,
2219
+ max: 1,
2220
+ deviceTypes: [DeviceType.Sensor, DeviceType.Switch],
2221
+ accept: [{
2222
+ cap: "power-meter",
2223
+ fieldPath: "watts",
2224
+ valueKind: "number"
2225
+ }],
2226
+ history: {
2227
+ required: false,
2228
+ minRetentionMs: null
2229
+ },
2230
+ mustBindCaps: [],
2231
+ exclude: []
2232
+ };
2233
+ }
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
2163
2773
  //#region src/composition/source-ref.ts
2164
2774
  /**
2165
2775
  * A composition source identity: `(addonId, stableId)`, never a numeric device
@@ -2225,10 +2835,7 @@ var HelperMemberSchema = z.object({
2225
2835
  cap: z.string().min(1),
2226
2836
  fieldPath: z.string().min(1)
2227
2837
  });
2228
- /**
2229
- * Phase 1a is groups only. Math, integral and statistics stay off this union
2230
- * until the composer can run them — a control must not save a spec it cannot.
2231
- */
2838
+ /** A group, a statistic, or an integral (D944). The composer runs all three. */
2232
2839
  var GroupHelperSpecSchema = z.object({
2233
2840
  kind: z.literal("group"),
2234
2841
  templateId: GroupTemplateIdSchema,
@@ -2237,7 +2844,45 @@ var GroupHelperSpecSchema = z.object({
2237
2844
  /** Actuator groups only. Readings leave this unset. */
2238
2845
  active: ActiveCriterionSchema.optional()
2239
2846
  });
2240
- var HelperSpecSchema = z.discriminatedUnion("kind", [GroupHelperSpecSchema]);
2847
+ var ApplianceHelperSpecSchema = z.object({
2848
+ kind: z.literal("appliance"),
2849
+ templateId: z.literal(APPLIANCE_TEMPLATE_ID),
2850
+ appliance: z.enum(APPLIANCE_KINDS),
2851
+ thresholdWatts: z.number().positive(),
2852
+ startDwellMs: z.number().int().nonnegative(),
2853
+ stopDwellMs: z.number().int().nonnegative(),
2854
+ member: HelperMemberSchema
2855
+ });
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. */
2878
+ function helperMembers(spec) {
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
+ }
2885
+ }
2241
2886
  //#endregion
2242
2887
  //#region src/composition/composition.ts
2243
2888
  /**
@@ -2347,11 +2992,36 @@ var CompositionAggregateSourceSchema = z.object({
2347
2992
  "offline"
2348
2993
  ])
2349
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
+ });
2350
3019
  var CompositionFieldSourceSchema = z.discriminatedUnion("kind", [
2351
3020
  CompositionFromSourceSchema,
2352
3021
  CompositionExpressionSourceSchema,
2353
3022
  CompositionCodeSourceSchema,
2354
- CompositionAggregateSourceSchema
3023
+ CompositionAggregateSourceSchema,
3024
+ CompositionSeriesSourceSchema
2355
3025
  ]);
2356
3026
  /** `forward`: RESERVED for slice 3. Argument mapping arrives there as an additive optional `args`. */
2357
3027
  var CompositionForwardCommandSchema = z.object({
@@ -2471,6 +3141,7 @@ function fieldReads(source) {
2471
3141
  fieldPath: binding.fieldPath
2472
3142
  }] : []);
2473
3143
  case "aggregate": return source.reads;
3144
+ case "series": return [source.read];
2474
3145
  case "code": return [];
2475
3146
  }
2476
3147
  }
@@ -2497,4 +3168,4 @@ function featureSources(feature) {
2497
3168
  return featureFieldEntries(feature).map((e) => e.source);
2498
3169
  }
2499
3170
  //#endregion
2500
- export { createStatefulExpressionMemory as $, pickVideoEncoder as $t, compositionFeatureEnabled as A, SOURCE_DEVICE_TYPES as At, ActiveCriterionSchema as B, getFfmpegArtifact as Bt, CompositionTargetSchema as C, isStatefulBuiltin as Ct, MAX_COMPOSITION_ITEMS as D, SOURCE_CAPS as Dt, MAX_COMPOSITION_FIELDS_PER_FEATURE as E, DEFAULT_FIRST_SIGHTING_FRESHNESS_MS as Et, featureFieldEntries as F, sliceChangedAt as Ft, GroupTemplateIdSchema as G, audioPlanFromEncodeProfile as Gt, BINARY_AGGREGATES as H, canonicalHash as Ht, featureSources as I, FFMPEG_PATH_ENV as It, MAX_HELPER_MEMBERS as J, buildInputArgs as Jt, HelperMemberSchema as K, buildAudioArgs as Kt, fieldReads as L, chooseFfmpegBinary as Lt, compositionReadKey as M, evaluateSensorLevelEdge as Mt, compositionSliceKey as N, isSourceCap as Nt, composedDeviceRef as O, SOURCE_CAP_ACTIVE_FIELD as Ot, compositionSourceKey as P, sliceActiveValue as Pt, cloneStatefulExpressionMemory as Q, logBannerArgs as Qt, ACTIVE_CRITERIA as R, ffmpegBinaryName as Rt, CompositionSchema as S, evaluateRose as St, MAX_COMPOSITION_FEATURES as T, ExpressionParseError as Tt, GROUP_TEMPLATE_IDS as U, Fmp4BoxSplitter as Ut, AggregateReducerSchema as V, getFfmpegDownloadUrl as Vt, GroupHelperSpecSchema as W, AUDIO_PRESETS as Wt, CompositionSourceRefSchema as X, invocationFromEncodeProfile as Xt, NUMERIC_AGGREGATES as Y, buildVideoArgs as Yt, EXPRESSION_INJECTED_NOW as Z, isSoftwareDecode as Zt, CompositionFromSourceSchema as _, RESERVED_BINDING_NAMES as _t, CUSTOMIZATION_BLOCK_NAME_PREFIX as a, evaluateAst as at, CompositionNewTargetSchema as b, disarmRoseMemory as bt, CompositionCodeSourceSchema as c, parseExpression as ct, CompositionExpressionSourceSchema as d, EXPRESSION_IDENTIFIER_RE as dt, resolveStreamMaps as en, evaluateExpressionSource as et, CompositionFeatureSchema as f, MAX_EXPRESSION_AST_NODES as ft, CompositionForwardCommandSchema as g, MAX_EXPRESSION_SOURCE_LENGTH as gt, CompositionFieldsFeatureSchema as h, MAX_EXPRESSION_EVAL_STEPS as ht, COMPOSITION_ITEM_KEY_RE as i, createExpressionScope as it, compositionItemFieldPath as j, evaluateSensorEdge as jt, composedDeviceStableId as k, SOURCE_CAP_CHANGED_AT_FIELD as kt, CompositionCommandTargetSchema as l, tokenize as lt, CompositionFieldSourceSchema as m, MAX_EXPRESSION_CALL_ARGS as mt, COMPOSER_ADDON_ID as n, toExpressionValue as nt, CompositionAggregateSourceSchema as o, compileExpression as ot, CompositionFieldReadSchema as p, MAX_EXPRESSION_BINDINGS as pt, HelperSpecSchema as q, buildFfmpegArgs as qt, COMPOSITION_ITEM_ARRAY_PATH as r, validateExpressionSource as rt, CompositionBindingSchema as s, compileExpressionSafe as st, COMPOSED_DEVICE_STABLE_ID_PREFIX as t, evaluateStatefulExpressionCall as tt, CompositionExistingTargetSchema as u, EXPRESSION_COMPILE_CACHE_CAPACITY as ut, CompositionItemEntrySchema as v, EXPRESSION_BUILTINS as vt, MAX_COMPOSITION_CODE_LENGTH as w, ExpressionEvalError as wt, CompositionPassthroughFeatureSchema as x, evaluateCount as xt, CompositionLiteralBindingSchema as y, EXPRESSION_BUILTIN_NAMES as yt, AGGREGATE_REDUCERS as z, FFMPEG_RELEASE 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 };