@camstack/addon-provider-dreame 0.2.159 → 0.2.161

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/addon.js CHANGED
@@ -52902,7 +52902,7 @@ var DreameCameraStream = class extends TypedEmitter {
52902
52902
  }
52903
52903
  };
52904
52904
  //#endregion
52905
- //#region ../types/dist/event-category-CT-hz5Rb.mjs
52905
+ //#region ../types/dist/event-category-DmZDPDf_.mjs
52906
52906
  var EventCategory = /* @__PURE__ */ function(EventCategory) {
52907
52907
  EventCategory["SystemBoot"] = "system.boot";
52908
52908
  EventCategory["SystemAddonsReady"] = "system.addons-ready";
@@ -53529,7 +53529,7 @@ var EventCategory = /* @__PURE__ */ function(EventCategory) {
53529
53529
  * (a track was promoted to a stationary-registry entry) or departs (the
53530
53530
  * object moved / was removed). Telemetry (D8): lossy, drives a UI refresh of
53531
53531
  * the dedicated "Stationary" section — never the live event feed. Payload:
53532
- * `{ deviceId, entryId, className, phase:'appeared'|'departed', timestamp }`.
53532
+ * `{ deviceId, entryId, className, phase:'appeared'|'departed', timestamp, observedAt, zones }`.
53533
53533
  */
53534
53534
  EventCategory["PipelineAnalyticsStationaryChanged"] = "pipeline-analytics.stationary-changed";
53535
53535
  /**
@@ -53667,6 +53667,11 @@ var EventCategory = /* @__PURE__ */ function(EventCategory) {
53667
53667
  */
53668
53668
  EventCategory["AccessoriesChanged"] = "accessories.onAccessoriesChanged";
53669
53669
  /**
53670
+ * An action run started or ended. Emitted by the `actions` surface.
53671
+ * Payload: `ActionRunChanged` (`{ actionId, runId, isRunning, outcome?, lastRunAt, lastError }`).
53672
+ */
53673
+ EventCategory["ActionsRunChanged"] = "actions.onRunChanged";
53674
+ /**
53670
53675
  * Progress update from a running model conversion job.
53671
53676
  * Payload: `{ kind: 'model-convert', phase, sessionId?, pct?, detail? }`.
53672
53677
  * Emitted by `addon-model-studio` on the converting node.
@@ -58345,7 +58350,7 @@ var ZodIssueCode = {
58345
58350
  var ZodFirstPartyTypeKind;
58346
58351
  ZodFirstPartyTypeKind || (ZodFirstPartyTypeKind = {});
58347
58352
  //#endregion
58348
- //#region ../types/dist/sleep-BE7IAR2p.mjs
58353
+ //#region ../types/dist/sleep-Crlmu_tO.mjs
58349
58354
  /**
58350
58355
  * The audio chunk plane's byte format, and the ONE expansion from a coded
58351
58356
  * window to float samples (D455).
@@ -61465,6 +61470,8 @@ Object.freeze({ "doorbell": "lastPressedAt" });
61465
61470
  /** Every capability name, so addon code can check a name without the schema graph. */
61466
61471
  var CAP_NAMES = Object.freeze([
61467
61472
  "accessories",
61473
+ "action-source",
61474
+ "actions",
61468
61475
  "addon-pages",
61469
61476
  "addon-pages-source",
61470
61477
  "addon-routes",
@@ -61653,11 +61660,11 @@ var CAP_NAMES = Object.freeze([
61653
61660
  Object.freeze({
61654
61661
  "air-quality-sensor": [
61655
61662
  {
61656
- "path": "co2Ppm",
61663
+ "path": "co2",
61657
61664
  "valueType": "number"
61658
61665
  },
61659
61666
  {
61660
- "path": "vocPpb",
61667
+ "path": "voc",
61661
61668
  "valueType": "number"
61662
61669
  },
61663
61670
  {
@@ -61707,7 +61714,7 @@ Object.freeze({
61707
61714
  }
61708
61715
  ],
61709
61716
  "ambient-light-sensor": [{
61710
- "path": "lux",
61717
+ "path": "illuminance",
61711
61718
  "valueType": "number"
61712
61719
  }],
61713
61720
  "audio-metrics": [
@@ -61768,7 +61775,7 @@ Object.freeze({
61768
61775
  ],
61769
61776
  "battery": [
61770
61777
  {
61771
- "path": "percentage",
61778
+ "path": "level",
61772
61779
  "valueType": "number"
61773
61780
  },
61774
61781
  {
@@ -61790,7 +61797,7 @@ Object.freeze({
61790
61797
  "valueType": "number"
61791
61798
  },
61792
61799
  {
61793
- "path": "volts",
61800
+ "path": "voltage",
61794
61801
  "valueType": "number"
61795
61802
  },
61796
61803
  {
@@ -61811,7 +61818,7 @@ Object.freeze({
61811
61818
  "valueType": "boolean"
61812
61819
  }],
61813
61820
  "brightness": [{
61814
- "path": "percentage",
61821
+ "path": "level",
61815
61822
  "valueType": "number"
61816
61823
  }],
61817
61824
  "broker": [{
@@ -62012,7 +62019,7 @@ Object.freeze({
62012
62019
  "valueType": "number"
62013
62020
  },
62014
62021
  {
62015
- "path": "mireds",
62022
+ "path": "colorTemperature",
62016
62023
  "valueType": "number"
62017
62024
  }
62018
62025
  ],
@@ -62191,15 +62198,15 @@ Object.freeze({
62191
62198
  "valueType": "string"
62192
62199
  },
62193
62200
  {
62194
- "path": "remainingSeconds",
62201
+ "path": "remaining",
62195
62202
  "valueType": "number"
62196
62203
  },
62197
62204
  {
62198
- "path": "delayedStartSeconds",
62205
+ "path": "delayedStart",
62199
62206
  "valueType": "number"
62200
62207
  },
62201
62208
  {
62202
- "path": "progressPercent",
62209
+ "path": "progress",
62203
62210
  "valueType": "number"
62204
62211
  },
62205
62212
  {
@@ -62313,7 +62320,7 @@ Object.freeze({
62313
62320
  ],
62314
62321
  "fan-control": [
62315
62322
  {
62316
- "path": "percentage",
62323
+ "path": "speed",
62317
62324
  "valueType": "number"
62318
62325
  },
62319
62326
  {
@@ -62413,7 +62420,7 @@ Object.freeze({
62413
62420
  }
62414
62421
  ],
62415
62422
  "humidity-sensor": [{
62416
- "path": "percent",
62423
+ "path": "humidity",
62417
62424
  "valueType": "number"
62418
62425
  }],
62419
62426
  "image": [{
@@ -62523,7 +62530,7 @@ Object.freeze({
62523
62530
  "valueType": "number"
62524
62531
  },
62525
62532
  {
62526
- "path": "progressPercent",
62533
+ "path": "progress",
62527
62534
  "valueType": "number"
62528
62535
  },
62529
62536
  {
@@ -62602,11 +62609,11 @@ Object.freeze({
62602
62609
  "valueType": "string"
62603
62610
  },
62604
62611
  {
62605
- "path": "positionMs",
62612
+ "path": "position",
62606
62613
  "valueType": "number"
62607
62614
  },
62608
62615
  {
62609
- "path": "durationMs",
62616
+ "path": "duration",
62610
62617
  "valueType": "number"
62611
62618
  },
62612
62619
  {
@@ -62708,11 +62715,11 @@ Object.freeze({
62708
62715
  ]
62709
62716
  },
62710
62717
  {
62711
- "path": "signalPercent",
62718
+ "path": "signal",
62712
62719
  "valueType": "number"
62713
62720
  },
62714
62721
  {
62715
- "path": "rssiDbm",
62722
+ "path": "rssi",
62716
62723
  "valueType": "number"
62717
62724
  },
62718
62725
  {
@@ -62843,23 +62850,23 @@ Object.freeze({
62843
62850
  ],
62844
62851
  "power-meter": [
62845
62852
  {
62846
- "path": "watts",
62853
+ "path": "power",
62847
62854
  "valueType": "number"
62848
62855
  },
62849
62856
  {
62850
- "path": "kwhTotal",
62857
+ "path": "energy",
62851
62858
  "valueType": "number"
62852
62859
  },
62853
62860
  {
62854
- "path": "volts",
62861
+ "path": "voltage",
62855
62862
  "valueType": "number"
62856
62863
  },
62857
62864
  {
62858
- "path": "amps",
62865
+ "path": "current",
62859
62866
  "valueType": "number"
62860
62867
  },
62861
62868
  {
62862
- "path": "va",
62869
+ "path": "apparentPower",
62863
62870
  "valueType": "number"
62864
62871
  },
62865
62872
  {
@@ -62887,14 +62894,10 @@ Object.freeze({
62887
62894
  {
62888
62895
  "path": "gps.accuracyMeters",
62889
62896
  "valueType": "number"
62890
- },
62891
- {
62892
- "path": "batteryPercent",
62893
- "valueType": "number"
62894
62897
  }
62895
62898
  ],
62896
62899
  "pressure-sensor": [{
62897
- "path": "hpa",
62900
+ "path": "pressure",
62898
62901
  "valueType": "number"
62899
62902
  }],
62900
62903
  "privacy-mask": [{
@@ -63208,7 +63211,7 @@ Object.freeze({
63208
63211
  "valueType": "boolean"
63209
63212
  }],
63210
63213
  "temperature-sensor": [{
63211
- "path": "celsius",
63214
+ "path": "temperature",
63212
63215
  "valueType": "number"
63213
63216
  }],
63214
63217
  "update": [
@@ -63256,7 +63259,7 @@ Object.freeze({
63256
63259
  "valueType": "string"
63257
63260
  },
63258
63261
  {
63259
- "path": "progressPercent",
63262
+ "path": "progress",
63260
63263
  "valueType": "number"
63261
63264
  },
63262
63265
  {
@@ -63755,7 +63758,7 @@ function errMsg(err) {
63755
63758
  return String(err);
63756
63759
  }
63757
63760
  //#endregion
63758
- //#region ../types/dist/composition-Bb8iwHEJ.mjs
63761
+ //#region ../types/dist/composition-CZImLpOI.mjs
63759
63762
  /**
63760
63763
  * The ffmpeg build every CamStack node downloads, per platform and arch.
63761
63764
  *
@@ -68519,6 +68522,14 @@ var AutomationConditionLeafSchema = object({
68519
68522
  array(union([string(), number()]))
68520
68523
  ])
68521
68524
  });
68525
+ var AutomationConditionStationaryPresentSchema = object({
68526
+ kind: literal("stationary-present"),
68527
+ deviceId: number().int().nonnegative(),
68528
+ /** Class names; a macro class matches every raw class that maps to it. */
68529
+ classes: array(string().min(1)).min(1).optional(),
68530
+ /** The parked object must be in at least one of these zones. */
68531
+ zones: array(string().min(1)).min(1).optional()
68532
+ });
68522
68533
  /**
68523
68534
  * The expression leaf, declared as a plain object rather than an intersection
68524
68535
  * with {@link ExpressionSourceSchema}: a discriminated union has to be able to
@@ -68552,7 +68563,8 @@ var AutomationConditionSchema = lazy(() => discriminatedUnion("kind", [
68552
68563
  child: AutomationConditionSchema
68553
68564
  }),
68554
68565
  AutomationConditionLeafSchema,
68555
- AutomationConditionExpressionSchema
68566
+ AutomationConditionExpressionSchema,
68567
+ AutomationConditionStationaryPresentSchema
68556
68568
  ]));
68557
68569
  /**
68558
68570
  * What starts a run.
@@ -68593,7 +68605,34 @@ var AutomationTriggerSchema = discriminatedUnion("kind", [
68593
68605
  kind: literal("schedule"),
68594
68606
  cron: string().min(1).max(120)
68595
68607
  }),
68596
- object({ kind: literal("manual") })
68608
+ object({ kind: literal("manual") }),
68609
+ object({
68610
+ kind: literal("detection"),
68611
+ /** Class names. A macro class (`animal`, `vehicle`, `person`, `package`)
68612
+ * matches every raw class that maps to it; any other name matches
68613
+ * exactly. */
68614
+ classes: array(string().min(1)).min(1),
68615
+ deviceIds: array(number().int().nonnegative()).min(1),
68616
+ /** The track must have visited at least one of these zones. */
68617
+ zones: array(string().min(1)).min(1).optional(),
68618
+ /** Best-so-far confidence the track must reach. */
68619
+ minScore: number().min(0).max(1).optional()
68620
+ }),
68621
+ object({
68622
+ kind: literal("stationary"),
68623
+ deviceIds: array(number().int().nonnegative()).min(1),
68624
+ /** Class names; a macro class matches like the detection trigger. */
68625
+ classes: array(string().min(1)).min(1).optional(),
68626
+ /** The parked object must be in at least one of these zones. */
68627
+ zones: array(string().min(1)).min(1).optional(),
68628
+ /** `appeared`: it became parked. `departed`: the registry retired it.
68629
+ * The vocabulary of `PipelineAnalyticsStationaryChanged`. */
68630
+ phase: _enum(["appeared", "departed"])
68631
+ }),
68632
+ object({
68633
+ kind: literal("interval"),
68634
+ minutes: number().int().min(1).max(1440)
68635
+ })
68597
68636
  ]);
68598
68637
  /**
68599
68638
  * One action step.
@@ -68613,28 +68652,47 @@ var AutomationTriggerSchema = discriminatedUnion("kind", [
68613
68652
  * is called. The runner still receives a number and is untouched; the
68614
68653
  * resolution is the recipe's job, not the runner's.
68615
68654
  */
68655
+ var AutomationWaitStepSchema = object({
68656
+ kind: literal("wait"),
68657
+ seconds: number().min(0).max(300)
68658
+ });
68659
+ var AutomationCapStepSchema = object({
68660
+ kind: literal("cap"),
68661
+ /**
68662
+ * One device, several same-family devices (locks / climate family / …),
68663
+ * or `{ $var }` for the device that triggered the run. An array is
68664
+ * fan-out at codegen — each id gets the same cap.method call.
68665
+ */
68666
+ deviceId: union([
68667
+ number().int(),
68668
+ array(number().int()).min(1),
68669
+ object({ $var: string().min(1) })
68670
+ ]),
68671
+ cap: string().min(1),
68672
+ method: string().min(1),
68673
+ /** Values may carry `{{vars.x}}` slots, or be exactly `{ $var: 'name' }`, which
68674
+ * SUBSTITUTE and do not evaluate (§3.2.3). Anything beyond substitution is
68675
+ * the expression leaf. */
68676
+ args: record(string(), unknown()).optional()
68677
+ });
68678
+ /**
68679
+ * The steps an internal Action may contain: cap and wait, and nothing that can
68680
+ * call back into an Action — so an Action can never reach itself (no cycles).
68681
+ */
68682
+ var ActionStepSchema = discriminatedUnion("kind", [AutomationWaitStepSchema, AutomationCapStepSchema]);
68616
68683
  var AutomationActionSchema = discriminatedUnion("kind", [
68684
+ AutomationWaitStepSchema,
68685
+ AutomationCapStepSchema,
68617
68686
  object({
68618
- kind: literal("wait"),
68619
- seconds: number().min(0).max(300)
68620
- }),
68621
- object({
68622
- kind: literal("cap"),
68623
- /**
68624
- * One device, several same-family devices (locks / climate family / …),
68625
- * or `{ $var }` for the device that triggered the run. An array is
68626
- * fan-out at codegen — each id gets the same cap.method call.
68627
- */
68628
- deviceId: union([
68629
- number().int(),
68630
- array(number().int()).min(1),
68631
- object({ $var: string().min(1) })
68632
- ]),
68633
- cap: string().min(1),
68634
- method: string().min(1),
68635
- /** Values may carry `{{vars.x}}` slots, which SUBSTITUTE and do not
68636
- * evaluate (§3.2.3). Anything beyond substitution is the expression leaf. */
68637
- args: record(string(), unknown()).optional()
68687
+ kind: literal("action"),
68688
+ /** An entry of the `actions` surface (`internal:<uuid>` or `<addonId>:<localId>`). */
68689
+ actionId: string().min(1),
68690
+ /** Values for the action's declared variables; the others take their default. */
68691
+ variables: record(string(), union([
68692
+ string(),
68693
+ number(),
68694
+ boolean()
68695
+ ])).optional()
68638
68696
  }),
68639
68697
  object({
68640
68698
  kind: literal("code"),
@@ -68643,10 +68701,357 @@ var AutomationActionSchema = discriminatedUnion("kind", [
68643
68701
  code: string().min(1).max(2e4)
68644
68702
  })
68645
68703
  ]);
68704
+ /**
68705
+ * How a trigger that arrives while the automation is busy (or cooling down) is
68706
+ * handled. One of the automation's execution modes, never a property of its
68707
+ * actions. Absent means `single`.
68708
+ *
68709
+ * - `single` — drop the trigger while a run is in progress (traced + logged).
68710
+ * - `restart` — abort the run in progress (at its next step boundary), start fresh.
68711
+ * - `queued` — queue up to `max` triggers, drop the rest (traced).
68712
+ * - `parallel` — up to `max` runs at once, drop the rest (traced).
68713
+ * - `cooldown` — `single`, and no new run until `minutes` after the last run
68714
+ * STARTED; persisted across a restart. A manual trigger bypasses only this
68715
+ * wait, never the single/queued limits.
68716
+ */
68717
+ var AutomationRunModeSchema = discriminatedUnion("mode", [
68718
+ object({ mode: literal("single") }),
68719
+ object({ mode: literal("restart") }),
68720
+ object({
68721
+ mode: literal("queued"),
68722
+ max: number().int().min(1).max(10)
68723
+ }),
68724
+ object({
68725
+ mode: literal("parallel"),
68726
+ max: number().int().min(1).max(10)
68727
+ }),
68728
+ object({
68729
+ mode: literal("cooldown"),
68730
+ minutes: number().int().min(1).max(1440)
68731
+ })
68732
+ ]);
68646
68733
  var AutomationRecipeSchema = object({
68647
68734
  triggers: array(AutomationTriggerSchema),
68648
68735
  conditions: AutomationConditionSchema.optional(),
68649
- actions: array(AutomationActionSchema)
68736
+ actions: array(AutomationActionSchema),
68737
+ runMode: AutomationRunModeSchema.optional(),
68738
+ /** How many run traces the automation keeps (default {@link DEFAULT_TRACE_KEEP}). */
68739
+ keepTraces: number().int().min(1).max(100).optional()
68740
+ });
68741
+ new Set([
68742
+ {
68743
+ id: "person",
68744
+ name: "Person"
68745
+ },
68746
+ {
68747
+ id: "vehicle",
68748
+ name: "Vehicle"
68749
+ },
68750
+ {
68751
+ id: "animal",
68752
+ name: "Animal"
68753
+ },
68754
+ {
68755
+ id: "package",
68756
+ name: "Package"
68757
+ }
68758
+ ].map((l) => l.id));
68759
+ var COCO_TO_MACRO = {
68760
+ mapping: {
68761
+ person: "person",
68762
+ bicycle: "vehicle",
68763
+ car: "vehicle",
68764
+ motorcycle: "vehicle",
68765
+ airplane: "vehicle",
68766
+ bus: "vehicle",
68767
+ train: "vehicle",
68768
+ truck: "vehicle",
68769
+ boat: "vehicle",
68770
+ bird: "animal",
68771
+ cat: "animal",
68772
+ dog: "animal",
68773
+ horse: "animal",
68774
+ sheep: "animal",
68775
+ cow: "animal",
68776
+ elephant: "animal",
68777
+ bear: "animal",
68778
+ zebra: "animal",
68779
+ giraffe: "animal",
68780
+ suitcase: "package",
68781
+ backpack: "package",
68782
+ handbag: "package"
68783
+ },
68784
+ preserveOriginal: false
68785
+ };
68786
+ new Set(Object.values(COCO_TO_MACRO.mapping));
68787
+ /**
68788
+ * Run traces — what an automation (or, later, an Action run) DID, node by node,
68789
+ * so the editor can paint every block green or red with its reason.
68790
+ *
68791
+ * A trace is data, not a log: it covers the WHOLE tree — the trigger stage,
68792
+ * every condition node (containers and leaves, including the ones a
68793
+ * short-circuit never reached), the run-mode verdict and every action step —
68794
+ * each addressed by a stable path (`conditions.0.1`, `actions.2`, or `steps.N`
68795
+ * for an Action run). Skipped
68796
+ * evaluations are traced too, and identical consecutive skips collapse into a
68797
+ * repeat count so a trigger that fails every 5 minutes cannot flush real runs.
68798
+ *
68799
+ * {@link AutomationStepTraceSchema} is deliberately free of automation
68800
+ * vocabulary so an Action run records the same step shape.
68801
+ */
68802
+ /** A value a trace may show: JSON primitives and short lists, nothing deeper. */
68803
+ var TraceValueSchema = union([
68804
+ string(),
68805
+ number(),
68806
+ boolean(),
68807
+ _null(),
68808
+ array(union([string(), number()]))
68809
+ ]);
68810
+ var AutomationRunOutcomeSchema = _enum([
68811
+ "ran",
68812
+ "skipped-trigger",
68813
+ "skipped-conditions",
68814
+ "skipped-cooldown",
68815
+ "skipped-overlap",
68816
+ "failed",
68817
+ "aborted"
68818
+ ]);
68819
+ /** `not-evaluated` = a short-circuit never reached the node. */
68820
+ var TraceNodeOutcomeSchema = _enum([
68821
+ "pass",
68822
+ "fail",
68823
+ "not-evaluated"
68824
+ ]);
68825
+ var AutomationTriggerTraceSchema = object({
68826
+ kind: string().min(1),
68827
+ outcome: _enum(["pass", "fail"]),
68828
+ /** Why an event was rejected ("class person not in [dog]") or what fired. */
68829
+ reason: string(),
68830
+ /** deviceId, className, trackId, score … — only what the trigger exposes. */
68831
+ vars: record(string(), TraceValueSchema)
68832
+ });
68833
+ var AutomationConditionTraceNodeSchema = object({
68834
+ /** `conditions` for the root, `conditions.<i>` for its i-th child, … */
68835
+ path: string().min(1),
68836
+ kind: _enum([
68837
+ "all",
68838
+ "any",
68839
+ "not",
68840
+ "condition",
68841
+ "expression",
68842
+ "stationary-present"
68843
+ ]),
68844
+ outcome: TraceNodeOutcomeSchema,
68845
+ reason: string(),
68846
+ deviceId: number().int().optional(),
68847
+ cap: string().optional(),
68848
+ fieldPath: string().optional(),
68849
+ operator: string().optional(),
68850
+ expected: TraceValueSchema.optional(),
68851
+ /** What the mirror held; `null` when it held nothing (unknown). */
68852
+ observed: TraceValueSchema.optional()
68853
+ });
68854
+ var AutomationRunVerdictSchema = object({
68855
+ mode: AutomationRunModeSchema,
68856
+ decision: _enum([
68857
+ "run",
68858
+ "queued",
68859
+ "restart",
68860
+ "drop",
68861
+ "cooldown",
68862
+ "bypass"
68863
+ ]),
68864
+ reason: string()
68865
+ });
68866
+ /** One executed (or not-run) step — shared by automations and Action runs. */
68867
+ var AutomationStepTraceSchema = object({
68868
+ /** `actions.<i>` */
68869
+ path: string().min(1),
68870
+ kind: string().min(1),
68871
+ cap: string().optional(),
68872
+ method: string().optional(),
68873
+ deviceId: number().int().optional(),
68874
+ startedAt: number().optional(),
68875
+ endedAt: number().optional(),
68876
+ outcome: _enum([
68877
+ "ok",
68878
+ "error",
68879
+ "not-run"
68880
+ ]),
68881
+ message: string().optional()
68882
+ });
68883
+ var AutomationRunTraceSchema = object({
68884
+ runId: string().min(1),
68885
+ startedAt: number(),
68886
+ endedAt: number(),
68887
+ trigger: AutomationTriggerTraceSchema,
68888
+ conditions: array(AutomationConditionTraceNodeSchema),
68889
+ verdict: AutomationRunVerdictSchema.optional(),
68890
+ steps: array(AutomationStepTraceSchema),
68891
+ outcome: AutomationRunOutcomeSchema,
68892
+ /** How many identical consecutive skips this trace stands for (>= 1). */
68893
+ repeat: number().int().min(1),
68894
+ /** When the latest of those repeats happened. */
68895
+ lastAt: number()
68896
+ });
68897
+ /**
68898
+ * What the hub's `runs` ledger holds for one owner (`automation:<blockId>` or
68899
+ * `action:<actionId>`): the cooldown stamp and the trace ring.
68900
+ */
68901
+ var AutomationStateDocSchema = object({
68902
+ version: literal(1),
68903
+ lastRunStartedAt: number().nullable(),
68904
+ traces: array(AutomationRunTraceSchema)
68905
+ });
68906
+ /**
68907
+ * Actions — named, runnable things an operator (or an automation) can start,
68908
+ * declared as LIST ENTRIES, never as devices.
68909
+ *
68910
+ * An entry is either **internal** (a short list of cap/wait steps kept in the
68911
+ * hub's `actions` record kind) or declared by an addon through the
68912
+ * `action-source` collection (a Home Assistant script, a scene …). The one
68913
+ * surface the UI and automations talk to is the `actions` cap; it keeps no
68914
+ * catalog of its own (D3) — every `list` asks the providers live.
68915
+ *
68916
+ * This file is the data half: the entry schema, the variables an entry may
68917
+ * declare, and the pure resolution of the values a run was given. It holds no
68918
+ * runtime.
68919
+ */
68920
+ /** What a variable's value is. `select` takes one of `options`. */
68921
+ var ActionVariableTypeSchema = _enum([
68922
+ "string",
68923
+ "number",
68924
+ "boolean",
68925
+ "select"
68926
+ ]);
68927
+ var ACTION_VARIABLE_NAME_RE = /^[A-Za-z_][A-Za-z0-9_]*$/;
68928
+ var ActionVariableValueSchema = union([
68929
+ string(),
68930
+ number(),
68931
+ boolean()
68932
+ ]);
68933
+ /**
68934
+ * One declared variable. Steps reference it as `{ $var: 'name' }` (the whole
68935
+ * value) or `{{vars.name}}` (inside a string) in their args.
68936
+ */
68937
+ var ActionVariableSchema = object({
68938
+ name: string().regex(ACTION_VARIABLE_NAME_RE),
68939
+ label: string().min(1).max(120).optional(),
68940
+ type: ActionVariableTypeSchema,
68941
+ required: boolean().optional(),
68942
+ default: ActionVariableValueSchema.optional(),
68943
+ /** The allowed values of a `select`. */
68944
+ options: array(string().min(1)).min(1).optional()
68945
+ }).superRefine((v, ctx) => {
68946
+ if (v.type === "select" && v.options === void 0) ctx.addIssue({
68947
+ code: "custom",
68948
+ message: "a select variable needs options",
68949
+ path: ["options"]
68950
+ });
68951
+ if (v.default !== void 0 && variableValueError(v, v.default) !== null) ctx.addIssue({
68952
+ code: "custom",
68953
+ message: variableValueError(v, v.default) ?? "invalid default",
68954
+ path: ["default"]
68955
+ });
68956
+ });
68957
+ /** Why `value` is not a legal value for the variable, or null when it is. */
68958
+ function variableValueError(variable, value) {
68959
+ switch (variable.type) {
68960
+ case "string": return typeof value === "string" ? null : `variable "${variable.name}" must be a string`;
68961
+ case "number": return typeof value === "number" && Number.isFinite(value) ? null : `variable "${variable.name}" must be a number`;
68962
+ case "boolean": return typeof value === "boolean" ? null : `variable "${variable.name}" must be true or false`;
68963
+ case "select": return typeof value === "string" && (variable.options ?? []).includes(value) ? null : `variable "${variable.name}" must be one of ${(variable.options ?? []).join(", ")}`;
68964
+ }
68965
+ }
68966
+ var ActionSourceKindSchema = _enum(["internal", "addon"]);
68967
+ function isInternalActionId(actionId) {
68968
+ return actionId.startsWith("internal:") && actionId.length > 9;
68969
+ }
68970
+ /** The addon behind a `<addonId>:<localId>` id, or null for anything else (internal included). */
68971
+ function addonIdOfActionId(actionId) {
68972
+ if (isInternalActionId(actionId)) return null;
68973
+ const at = actionId.indexOf(":");
68974
+ return at > 0 && at < actionId.length - 1 ? actionId.slice(0, at) : null;
68975
+ }
68976
+ var ActionStatusSchema = object({
68977
+ isRunning: boolean(),
68978
+ /** Ms epoch the latest run STARTED; null when it never ran. */
68979
+ lastRunAt: number().nullable(),
68980
+ /** The latest run's failure; null on success or when it never ran. */
68981
+ lastError: string().nullable()
68982
+ });
68983
+ var ActionEntrySchema = object({
68984
+ /** `internal:<uuid>` or `<addonId>:<localId>`. */
68985
+ id: string().min(3),
68986
+ name: string().min(1).max(120),
68987
+ /** The addon that declared it; `core-blocks` for an internal one. */
68988
+ addonId: string().min(1),
68989
+ source: ActionSourceKindSchema,
68990
+ /** An addon-declared entry is edited where it was declared. */
68991
+ readOnly: boolean(),
68992
+ /** Internal only: cap and wait steps. No action-in-action, so no cycles. */
68993
+ steps: array(ActionStepSchema).min(1).max(50).optional(),
68994
+ variables: array(ActionVariableSchema).max(20).optional(),
68995
+ status: ActionStatusSchema
68996
+ }).superRefine((entry, ctx) => {
68997
+ if (entry.source === "internal") {
68998
+ if (!isInternalActionId(entry.id)) ctx.addIssue({
68999
+ code: "custom",
69000
+ message: "an internal action id is internal:<uuid>",
69001
+ path: ["id"]
69002
+ });
69003
+ return;
69004
+ }
69005
+ if (entry.steps !== void 0) ctx.addIssue({
69006
+ code: "custom",
69007
+ message: "only an internal action carries steps",
69008
+ path: ["steps"]
69009
+ });
69010
+ if (!entry.readOnly) ctx.addIssue({
69011
+ code: "custom",
69012
+ message: "an addon action is read-only here",
69013
+ path: ["readOnly"]
69014
+ });
69015
+ if (addonIdOfActionId(entry.id) === null) ctx.addIssue({
69016
+ code: "custom",
69017
+ message: "an addon action id is <addonId>:<localId>",
69018
+ path: ["id"]
69019
+ });
69020
+ });
69021
+ /** What an operator writes for an internal action. */
69022
+ var ActionInputSchema = object({
69023
+ name: string().min(1).max(120),
69024
+ steps: array(ActionStepSchema).min(1).max(50),
69025
+ variables: array(ActionVariableSchema).max(20).optional()
69026
+ });
69027
+ /** Who started a run. Absent = the operator pressed it. */
69028
+ var ActionRunOriginSchema = object({
69029
+ /** The automation block whose step started it. */
69030
+ blockId: string().min(1).optional() });
69031
+ /** `started` = the caller did not ask to wait. */
69032
+ var ActionRunOutcomeSchema = union([AutomationRunOutcomeSchema, literal("started")]);
69033
+ var ActionRunResultSchema = object({
69034
+ runId: string().min(1),
69035
+ outcome: ActionRunOutcomeSchema,
69036
+ /** Why it did not run, or what failed. */
69037
+ message: string().optional()
69038
+ });
69039
+ _enum([
69040
+ "not-found",
69041
+ "read-only",
69042
+ "invalid-variables",
69043
+ "invalid-steps",
69044
+ "not-linked",
69045
+ "busy"
69046
+ ]);
69047
+ object({
69048
+ actionId: string(),
69049
+ runId: string(),
69050
+ isRunning: boolean(),
69051
+ /** Set when the run ended. */
69052
+ outcome: AutomationRunOutcomeSchema.optional(),
69053
+ lastRunAt: number().nullable(),
69054
+ lastError: string().nullable()
68650
69055
  });
68651
69056
  /**
68652
69057
  * `addon-pages` — system-scoped singleton aggregator cap. Public-facing
@@ -71333,7 +71738,7 @@ var CompositionEventPlanSchema = object({
71333
71738
  stateField: string(),
71334
71739
  fieldKey: string()
71335
71740
  });
71336
- /** One refusal, named (D391). `path` points into the composition, e.g. `features[0].fields.celsius`. */
71741
+ /** One refusal, named (D391). `path` points into the composition, e.g. `features[0].fields.temperature`. */
71337
71742
  var CompositionProblemSchema = object({
71338
71743
  code: _enum([
71339
71744
  "reserved-variant",
@@ -71652,7 +72057,7 @@ var GROUP_TEMPLATES = [
71652
72057
  summary: "Mean, median, min, max or sum of temperatures. Offline and unknown members are left out, never read as 0 °C.",
71653
72058
  role: DeviceRole.TemperatureSensor,
71654
72059
  outputCap: "temperature-sensor",
71655
- outputField: "celsius",
72060
+ outputField: "temperature",
71656
72061
  quantity: "temperature",
71657
72062
  defaultAggregate: "mean",
71658
72063
  empty: "null",
@@ -71663,7 +72068,7 @@ var GROUP_TEMPLATES = [
71663
72068
  summary: "Humidity in percent. Offline and unknown members are left out, never read as 0 %.",
71664
72069
  role: DeviceRole.HumiditySensor,
71665
72070
  outputCap: "humidity-sensor",
71666
- outputField: "percent",
72071
+ outputField: "humidity",
71667
72072
  quantity: "humidity",
71668
72073
  defaultAggregate: "mean",
71669
72074
  empty: "null",
@@ -71674,7 +72079,7 @@ var GROUP_TEMPLATES = [
71674
72079
  summary: "Pressure in hectopascals. Offline and unknown members are left out, never read as 0 hPa.",
71675
72080
  role: DeviceRole.PressureSensor,
71676
72081
  outputCap: "pressure-sensor",
71677
- outputField: "hpa",
72082
+ outputField: "pressure",
71678
72083
  quantity: "pressure",
71679
72084
  defaultAggregate: "mean",
71680
72085
  empty: "null",
@@ -71685,7 +72090,7 @@ var GROUP_TEMPLATES = [
71685
72090
  summary: "Illuminance in lux. Offline and unknown members are left out, never read as 0 lx.",
71686
72091
  role: DeviceRole.AmbientLightSensor,
71687
72092
  outputCap: "ambient-light-sensor",
71688
- outputField: "lux",
72093
+ outputField: "illuminance",
71689
72094
  quantity: "illuminance",
71690
72095
  defaultAggregate: "mean",
71691
72096
  empty: "null",
@@ -71696,7 +72101,7 @@ var GROUP_TEMPLATES = [
71696
72101
  summary: "Power in watts, summed by default. Nobody reporting omits the key rather than writing 0 W.",
71697
72102
  role: DeviceRole.PowerSensor,
71698
72103
  outputCap: "power-meter",
71699
- outputField: "watts",
72104
+ outputField: "power",
71700
72105
  quantity: "power",
71701
72106
  defaultAggregate: "sum",
71702
72107
  empty: "omit",
@@ -71707,7 +72112,7 @@ var GROUP_TEMPLATES = [
71707
72112
  summary: "Energy in kilowatt-hours, summed by default. Nobody reporting omits the key rather than writing 0 kWh.",
71708
72113
  role: DeviceRole.EnergySensor,
71709
72114
  outputCap: "power-meter",
71710
- outputField: "kwhTotal",
72115
+ outputField: "energy",
71711
72116
  quantity: "energy",
71712
72117
  defaultAggregate: "sum",
71713
72118
  empty: "omit",
@@ -71718,7 +72123,7 @@ var GROUP_TEMPLATES = [
71718
72123
  summary: "Battery charge in percent. Offline and unknown members are left out, never read as 0 %.",
71719
72124
  role: DeviceRole.BatterySensor,
71720
72125
  outputCap: "battery",
71721
- outputField: "percentage",
72126
+ outputField: "level",
71722
72127
  quantity: "percentage",
71723
72128
  defaultAggregate: "mean",
71724
72129
  empty: "null",
@@ -72296,7 +72701,18 @@ method(object({}), object({ blocks: array(CoreBlockSchema) }), { auth: "admin" }
72296
72701
  kind: "mutation",
72297
72702
  auth: "admin",
72298
72703
  access: "view"
72299
- });
72704
+ }), method(object({
72705
+ blockId: string().min(1),
72706
+ trace: AutomationRunTraceSchema.optional(),
72707
+ runStartedAt: number().optional(),
72708
+ keepTraces: number().int().min(1).max(100).optional()
72709
+ }), _void(), {
72710
+ kind: "mutation",
72711
+ auth: "admin"
72712
+ }), method(object({ blockId: string().min(1) }), AutomationStateDocSchema, { auth: "admin" }), method(object({ blockId: string().min(1) }), object({
72713
+ traces: array(AutomationRunTraceSchema),
72714
+ lastError: string().nullable()
72715
+ }), { auth: "admin" });
72300
72716
  /**
72301
72717
  * `custom-model-registry` — collection cap exposing operator-registered
72302
72718
  * custom detection models. Each provider (today: `addon-model-studio`)
@@ -97684,51 +98100,6 @@ method(object({}), array(TargetKindSchema)), method(object({}), array(TargetSche
97684
98100
  kind: "mutation",
97685
98101
  auth: "admin"
97686
98102
  });
97687
- new Set([
97688
- {
97689
- id: "person",
97690
- name: "Person"
97691
- },
97692
- {
97693
- id: "vehicle",
97694
- name: "Vehicle"
97695
- },
97696
- {
97697
- id: "animal",
97698
- name: "Animal"
97699
- },
97700
- {
97701
- id: "package",
97702
- name: "Package"
97703
- }
97704
- ].map((l) => l.id));
97705
- var COCO_TO_MACRO = {
97706
- mapping: {
97707
- person: "person",
97708
- bicycle: "vehicle",
97709
- car: "vehicle",
97710
- motorcycle: "vehicle",
97711
- airplane: "vehicle",
97712
- bus: "vehicle",
97713
- train: "vehicle",
97714
- truck: "vehicle",
97715
- boat: "vehicle",
97716
- bird: "animal",
97717
- cat: "animal",
97718
- dog: "animal",
97719
- horse: "animal",
97720
- sheep: "animal",
97721
- cow: "animal",
97722
- elephant: "animal",
97723
- bear: "animal",
97724
- zebra: "animal",
97725
- giraffe: "animal",
97726
- suitcase: "package",
97727
- backpack: "package",
97728
- handbag: "package"
97729
- },
97730
- preserveOriginal: false
97731
- };
97732
98103
  var AUDIO_MACRO_LABELS = [
97733
98104
  {
97734
98105
  id: "speech",
@@ -110295,9 +110666,9 @@ method(object({ nodeId: string().optional() }).optional(), array(AddonListItemSc
110295
110666
  */
110296
110667
  var AirQualitySensorStatusSchema = object({
110297
110668
  /** Carbon dioxide concentration in ppm. */
110298
- co2Ppm: number().min(0).optional().meta(readingMeta("concentration-volume")),
110669
+ co2: number().min(0).optional().meta(readingMeta("concentration-volume")),
110299
110670
  /** Total volatile organic compounds in ppb. */
110300
- vocPpb: number().min(0).optional().meta(readingMeta("concentration-ppb")),
110671
+ voc: number().min(0).optional().meta(readingMeta("concentration-ppb")),
110301
110672
  /** Particulate matter ≤ 2.5 μm in µg/m³. */
110302
110673
  pm25: number().min(0).optional().meta(readingMeta("concentration-mass")),
110303
110674
  /** Particulate matter ≤ 10 μm in µg/m³. */
@@ -110330,12 +110701,12 @@ var airQualitySensorCapability = {
110330
110701
  * History (D850): recorded series.
110331
110702
  */
110332
110703
  history: { fields: {
110333
- co2Ppm: {
110704
+ co2: {
110334
110705
  kind: "gauge",
110335
110706
  unit: "ppm",
110336
110707
  deadband: 5
110337
110708
  },
110338
- vocPpb: {
110709
+ voc: {
110339
110710
  kind: "gauge",
110340
110711
  unit: "ppb",
110341
110712
  deadband: 5
@@ -110366,7 +110737,7 @@ var airQualitySensorCapability = {
110366
110737
  */
110367
110738
  var AmbientLightSensorStatusSchema = object({
110368
110739
  /** Current illuminance in lux (lx). `null` when the sample was refused (D393). */
110369
- lux: number().min(0).nullable().meta(readingMeta("illuminance", {
110740
+ illuminance: number().min(0).nullable().meta(readingMeta("illuminance", {
110370
110741
  precision: 0,
110371
110742
  label: "Illuminance"
110372
110743
  })),
@@ -110395,7 +110766,7 @@ var ambientLightSensorCapability = {
110395
110766
  /**
110396
110767
  * History (D850): recorded series. Wide range, so a RELATIVE deadband (10 %).
110397
110768
  */
110398
- history: { fields: { lux: {
110769
+ history: { fields: { illuminance: {
110399
110770
  kind: "gauge",
110400
110771
  unit: "lx",
110401
110772
  deadbandPct: 10,
@@ -110534,6 +110905,84 @@ var audioMetricsCapability = {
110534
110905
  */
110535
110906
  history: "none"
110536
110907
  };
110908
+ method(object({}), array(ActionEntrySchema), { auth: "admin" }), method(object({
110909
+ actionId: string().min(1),
110910
+ variables: record(string(), ActionVariableValueSchema).optional()
110911
+ }), _void(), {
110912
+ kind: "mutation",
110913
+ auth: "admin"
110914
+ }), method(object({ actionId: string().min(1) }), _void(), {
110915
+ kind: "mutation",
110916
+ auth: "admin"
110917
+ });
110918
+ /**
110919
+ * actions — the ONE surface for runnable actions, for the UI and for
110920
+ * automations. Provided by the core-blocks builtin on the hub.
110921
+ *
110922
+ * An action is a LIST ENTRY, not a device (D883): either internal (cap and wait
110923
+ * steps, kept in the hub's `actions` record kind) or declared by an addon over
110924
+ * `action-source`. `list` asks every `action-source` provider LIVE and overlays
110925
+ * the run status from the hub's `runs` ledger; there is no catalog here to
110926
+ * drift (D3).
110927
+ *
110928
+ * **Permissions follow the camera, as linked devices do.** An action is linked
110929
+ * to cameras by the operator (`setLinks`, D356: nothing links itself). A
110930
+ * non-admin may `get` / `run` an action only through a camera they may see that
110931
+ * links it: they name the camera (`deviceId`, a top-level field so the scope
110932
+ * gate sees it) and the provider refuses an action that camera does not link.
110933
+ * An admin, and an in-process caller (an automation block), may run any action.
110934
+ * Every refusal is typed (`[not-linked] …`) and logged.
110935
+ *
110936
+ * A second `run` of an action that is already running is REFUSED — traced and
110937
+ * logged — never queued.
110938
+ */
110939
+ var ActionIdInput = object({ actionId: string().min(1) });
110940
+ var ActionLinksSchema = object({
110941
+ deviceId: number().int().nonnegative(),
110942
+ actionIds: array(string().min(1)).max(100)
110943
+ });
110944
+ method(object({}), array(ActionEntrySchema), { auth: "admin" }), method(object({
110945
+ actionId: string().min(1),
110946
+ deviceId: number().int().nonnegative().optional()
110947
+ }), object({ action: ActionEntrySchema.nullable() }), {
110948
+ role: "viewer",
110949
+ auth: "protected",
110950
+ kind: "query",
110951
+ caller: "required"
110952
+ }), method(object({
110953
+ actionId: string().min(1),
110954
+ variables: record(string(), ActionVariableValueSchema).optional(),
110955
+ wait: boolean().optional(),
110956
+ origin: ActionRunOriginSchema.optional(),
110957
+ deviceId: number().int().nonnegative().optional()
110958
+ }), ActionRunResultSchema, {
110959
+ role: "viewer",
110960
+ auth: "protected",
110961
+ kind: "mutation",
110962
+ caller: "required"
110963
+ }), method(ActionIdInput, _void(), {
110964
+ kind: "mutation",
110965
+ auth: "admin"
110966
+ }), method(ActionIdInput, object({ traces: array(AutomationRunTraceSchema) }), { auth: "admin" }), method(object({ action: ActionInputSchema }), object({ action: ActionEntrySchema }), {
110967
+ kind: "mutation",
110968
+ auth: "admin"
110969
+ }), method(object({
110970
+ actionId: string().min(1),
110971
+ action: ActionInputSchema.partial()
110972
+ }), object({ action: ActionEntrySchema }), {
110973
+ kind: "mutation",
110974
+ auth: "admin"
110975
+ }), method(ActionIdInput, _void(), {
110976
+ kind: "mutation",
110977
+ auth: "admin"
110978
+ }), method(object({ deviceId: number().int().nonnegative() }), array(ActionEntrySchema), {
110979
+ role: "viewer",
110980
+ auth: "protected",
110981
+ kind: "query"
110982
+ }), method(object({ deviceId: number().int().nonnegative() }), ActionLinksSchema, { auth: "admin" }), method(ActionLinksSchema, ActionLinksSchema, {
110983
+ kind: "mutation",
110984
+ auth: "admin"
110985
+ });
110537
110986
  /**
110538
110987
  * Automation-control cap. Models HA `automation.*` entities on
110539
110988
  * `DeviceType.Automation`. An automation is a trigger+condition+
@@ -110630,7 +111079,7 @@ var BatteryStatusSchema = object({
110630
111079
  * Consumers must SKIP a null rather than coerce it — `battery-band` already
110631
111080
  * declines to band a non-finite reading, which is the correct shape.
110632
111081
  */
110633
- percentage: number().min(0).max(100).nullable().meta(readingMeta("percentage", { label: "Charge" })),
111082
+ level: number().min(0).max(100).nullable().meta(readingMeta("percentage", { label: "Charge" })),
110634
111083
  /**
110635
111084
  * Charging source. `'dc'` covers wall/USB adapters; `'solar'` is
110636
111085
  * Reolink-specific for the Solar Panel 2 accessory (will become
@@ -110640,7 +111089,7 @@ var BatteryStatusSchema = object({
110640
111089
  * `'unknown'` is the absence of an answer: the provider has not read the
110641
111090
  * power fields yet, or the firmware reported neither. Before it existed,
110642
111091
  * two unreadable fields produced `'none'`, and a camera nobody had reached
110643
- * was drawn "on battery alone" — the same D315 defect `percentage` closed
111092
+ * was drawn "on battery alone" — the same D315 defect `level` closed
110644
111093
  * by going nullable. Consumers SKIP it (no charger state published, no
110645
111094
  * glyph, no HomeKit ChargingState) rather than coerce it either way.
110646
111095
  */
@@ -110664,7 +111113,7 @@ var BatteryStatusSchema = object({
110664
111113
  * that only report charge. A second reading on this cap, not a second
110665
111114
  * `numeric-sensor` on the same device.
110666
111115
  */
110667
- volts: number().nullable().optional().meta(readingMeta("voltage", { label: "Voltage" })),
111116
+ voltage: number().nullable().optional().meta(readingMeta("voltage", { label: "Voltage" })),
110668
111117
  /**
110669
111118
  * Ms epoch of the last time the device PROVED it was reachable — a
110670
111119
  * completed firmware round-trip, an observed wake, or an inbound push
@@ -110698,7 +111147,7 @@ var BatteryStatusSchema = object({
110698
111147
  /**
110699
111148
  * True when the source is a BINARY low-battery indicator (HA
110700
111149
  * `binary_sensor` device_class=battery / `LOW_BAT`) that has no real
110701
- * charge level — `percentage` is then a coarse stand-in (100 = normal,
111150
+ * charge level — `level` is then a coarse stand-in (100 = normal,
110702
111151
  * sub-threshold = low). UI MUST render "Normal"/"Low" instead of a
110703
111152
  * misleading exact percentage. Absent/false → genuine 0–100 % reading.
110704
111153
  */
@@ -110718,15 +111167,15 @@ var BATTERY_DEVICE_TYPES = [
110718
111167
  var batteryCustomize = {
110719
111168
  version: 1,
110720
111169
  targets: [{
110721
- id: "percentage",
111170
+ id: "level",
110722
111171
  cardinality: "scalar",
110723
111172
  vocabulary: {
110724
111173
  cap: "battery",
110725
- stateField: "percentage"
111174
+ stateField: "level"
110726
111175
  },
110727
111176
  deviceTypes: BATTERY_DEVICE_TYPES,
110728
111177
  apply: "add",
110729
- path: "percentage",
111178
+ path: "level",
110730
111179
  sources: [{
110731
111180
  kind: "field",
110732
111181
  valueKinds: ["number", "string"],
@@ -110820,7 +111269,7 @@ onStatusChanged: { data: object({
110820
111269
  schema: BatteryStatusSchema,
110821
111270
  kind: "push",
110822
111271
  empty: {
110823
- percentage: null,
111272
+ level: null,
110824
111273
  charging: "unknown",
110825
111274
  sleeping: false,
110826
111275
  lastUpdated: 0
@@ -110847,11 +111296,11 @@ onStatusChanged: { data: object({
110847
111296
  * History (D850): recorded series. The generic capture replaces the `battery` series of `pipeline-analytics:stats-5m`.
110848
111297
  */
110849
111298
  history: { fields: {
110850
- percentage: {
111299
+ level: {
110851
111300
  kind: "gauge",
110852
111301
  unit: "%"
110853
111302
  },
110854
- volts: {
111303
+ voltage: {
110855
111304
  kind: "gauge",
110856
111305
  unit: "V"
110857
111306
  }
@@ -110911,14 +111360,14 @@ var binaryCapability = {
110911
111360
  * controls to this single-method surface.
110912
111361
  *
110913
111362
  * The cap is intentionally minimal: a single `setBrightness({deviceId,
110914
- * percentage})` mutation plus the auto-injected `getStatus`. Drivers
111363
+ * level})` mutation plus the auto-injected `getStatus`. Drivers
110915
111364
  * that expose richer controls (color temperature, scenes, schedules)
110916
111365
  * should surface those via the device's `getSettingsUISchema()`
110917
111366
  * instead of bloating this cap.
110918
111367
  */
110919
111368
  var BrightnessStatusSchema = object({
110920
111369
  /** Current level as 0..100 inclusive. Firmware-reported. */
110921
- percentage: number().min(0).max(100).meta(readingMeta("percentage")),
111370
+ level: number().min(0).max(100).meta(readingMeta("percentage")),
110922
111371
  /** Ms epoch of the last operator-driven change. Useful for UI freshness. */
110923
111372
  lastChangedAt: number()
110924
111373
  });
@@ -110930,7 +111379,7 @@ var brightnessCapability = {
110930
111379
  deviceTypes: [DeviceType.Light],
110931
111380
  methods: { setBrightness: method(object({
110932
111381
  deviceId: number().int().nonnegative(),
110933
- percentage: number().min(0).max(100)
111382
+ level: number().min(0).max(100)
110934
111383
  }), _void(), {
110935
111384
  kind: "mutation",
110936
111385
  auth: "protected"
@@ -110943,7 +111392,7 @@ var brightnessCapability = {
110943
111392
  */
110944
111393
  onBrightnessChanged: { data: object({
110945
111394
  deviceId: number(),
110946
- percentage: number().min(0).max(100),
111395
+ level: number().min(0).max(100),
110947
111396
  lastChangedAt: number()
110948
111397
  }) } },
110949
111398
  status: {
@@ -110966,7 +111415,7 @@ onBrightnessChanged: { data: object({
110966
111415
  /**
110967
111416
  * History (D850): recorded series. Actuator tier: level keeps every change.
110968
111417
  */
110969
- history: { fields: { percentage: {
111418
+ history: { fields: { level: {
110970
111419
  kind: "gauge",
110971
111420
  unit: "%",
110972
111421
  minIntervalMs: 0
@@ -111580,11 +112029,11 @@ var ColorInputSchema = discriminatedUnion("mode", [
111580
112029
  }),
111581
112030
  object({
111582
112031
  mode: literal("mired"),
111583
- mireds: number().int().min(50).max(1e3)
112032
+ colorTemperature: number().int().min(50).max(1e3)
111584
112033
  })
111585
112034
  ]);
111586
112035
  var ColorStatusSchema = object({
111587
- /** Active color mode — which of `rgb` / `hsv` / `mireds` reflects
112036
+ /** Active color mode — which of `rgb` / `hsv` / `colorTemperature` reflects
111588
112037
  * the bulb's current state. */
111589
112038
  mode: _enum([
111590
112039
  "rgb",
@@ -111596,7 +112045,7 @@ var ColorStatusSchema = object({
111596
112045
  /** Populated when `mode === 'hsv'`. */
111597
112046
  hsv: HsvTripletSchema.optional(),
111598
112047
  /** Populated when `mode === 'mired'`. */
111599
- mireds: number().int().optional().meta(readingMeta("colour-temperature")),
112048
+ colorTemperature: number().int().optional().meta(readingMeta("colour-temperature")),
111600
112049
  /** Ms epoch of the last operator-driven change. */
111601
112050
  lastChangedAt: number()
111602
112051
  });
@@ -111628,7 +112077,7 @@ onColorChanged: { data: object({
111628
112077
  ]),
111629
112078
  rgb: RgbTripletSchema.optional(),
111630
112079
  hsv: HsvTripletSchema.optional(),
111631
- mireds: number().int().optional(),
112080
+ colorTemperature: number().int().optional(),
111632
112081
  lastChangedAt: number()
111633
112082
  }) } },
111634
112083
  status: {
@@ -112517,11 +112966,11 @@ var DishwasherStatusSchema = object({
112517
112966
  /** Feature keys of the programs the appliance currently offers — drives the UI selector. */
112518
112967
  availablePrograms: array(string()),
112519
112968
  /** Seconds remaining on the active cycle, or null when not running / not known. */
112520
- remainingSeconds: number().int().nonnegative().nullable().meta(readingMeta("duration")),
112969
+ remaining: number().int().nonnegative().nullable().meta(readingMeta("duration")),
112521
112970
  /** Seconds the delayed start is set to (time until the cycle starts), or null. */
112522
- delayedStartSeconds: number().int().nonnegative().nullable().meta(readingMeta("duration")),
112971
+ delayedStart: number().int().nonnegative().nullable().meta(readingMeta("duration")),
112523
112972
  /** Cycle completion 0..100, or null when not applicable. */
112524
- progressPercent: number().min(0).max(100).nullable().meta(readingMeta("percentage")),
112973
+ progress: number().min(0).max(100).nullable().meta(readingMeta("percentage")),
112525
112974
  /** True when the door is reported open; null when the door state is not known. */
112526
112975
  doorOpen: boolean().nullable(),
112527
112976
  /** True when the door is latched (locked) during a cycle; null when not reported. */
@@ -112740,14 +113189,14 @@ var dishwasherCapability = {
112740
113189
  runtimeState: DishwasherStatusSchema,
112741
113190
  durability: "session",
112742
113191
  history: { fields: {
112743
- progressPercent: {
113192
+ progress: {
112744
113193
  kind: "gauge",
112745
113194
  unit: "%",
112746
113195
  deadband: 2
112747
113196
  },
112748
113197
  state: { kind: "enum" },
112749
113198
  operationState: { kind: "enum" },
112750
- remainingSeconds: {
113199
+ remaining: {
112751
113200
  kind: "gauge",
112752
113201
  unit: "s",
112753
113202
  minIntervalMs: 6e4
@@ -113338,7 +113787,7 @@ var FanDirectionSchema = _enum(["forward", "reverse"]);
113338
113787
  var FanControlStatusSchema = object({
113339
113788
  /** Active speed as 0..100 inclusive. Null when the device has no
113340
113789
  * speed surface (single-speed fan). */
113341
- percentage: number().min(0).max(100).nullable().meta(readingMeta("percentage")),
113790
+ speed: number().min(0).max(100).nullable().meta(readingMeta("percentage")),
113342
113791
  /** Speed granularity the device accepts (HA `percentage_step`, e.g. 25 for
113343
113792
  * a 4-speed fan). Absent when unknown — the UI then falls back to step 1. */
113344
113793
  percentageStep: number().positive().optional().meta(readingMeta("percentage")),
@@ -113365,7 +113814,7 @@ var fanControlCapability = {
113365
113814
  methods: {
113366
113815
  setPercentage: method(object({
113367
113816
  deviceId: number().int().nonnegative(),
113368
- percentage: number().min(0).max(100)
113817
+ speed: number().min(0).max(100)
113369
113818
  }), _void(), {
113370
113819
  kind: "mutation",
113371
113820
  auth: "protected"
@@ -113398,7 +113847,7 @@ var fanControlCapability = {
113398
113847
  },
113399
113848
  /**
113400
113849
  * Runtime-state slice — mirrored by the kernel. UI fan speed
113401
- * sliders read `percentage` for live updates.
113850
+ * sliders read `speed` for live updates.
113402
113851
  */
113403
113852
  runtimeState: FanControlStatusSchema,
113404
113853
  /**
@@ -113412,7 +113861,7 @@ var fanControlCapability = {
113412
113861
  * History (D850): recorded series. Actuator tier: speed keeps every change.
113413
113862
  */
113414
113863
  history: { fields: {
113415
- percentage: {
113864
+ speed: {
113416
113865
  kind: "gauge",
113417
113866
  unit: "%",
113418
113867
  minIntervalMs: 0
@@ -113857,7 +114306,7 @@ var humidifierCapability = {
113857
114306
  */
113858
114307
  var HumiditySensorStatusSchema = object({
113859
114308
  /** Current relative humidity, 0..100. `null` when the sample was refused (D393). */
113860
- percent: number().min(0).max(100).nullable().meta(readingMeta("humidity", {
114309
+ humidity: number().min(0).max(100).nullable().meta(readingMeta("humidity", {
113861
114310
  precision: 0,
113862
114311
  label: "Humidity"
113863
114312
  })),
@@ -113886,7 +114335,7 @@ var humiditySensorCapability = {
113886
114335
  /**
113887
114336
  * History (D850): recorded series.
113888
114337
  */
113889
- history: { fields: { percent: {
114338
+ history: { fields: { humidity: {
113890
114339
  kind: "gauge",
113891
114340
  unit: "%",
113892
114341
  deadband: .5
@@ -114526,7 +114975,7 @@ var LawnMowerControlStatusSchema = object({
114526
114975
  batteryLevel: number().min(0).max(100).nullable().meta(readingMeta("percentage")),
114527
114976
  /** 0..100 mowing-completion percentage of the current task, or null when no
114528
114977
  * task is active / progress is unavailable. */
114529
- progressPercent: number().min(0).max(100).nullable().meta(readingMeta("percentage")),
114978
+ progress: number().min(0).max(100).nullable().meta(readingMeta("percentage")),
114530
114979
  /** Current device/event code (dynamic — mostly status, sometimes an error),
114531
114980
  * or null when unknown. */
114532
114981
  currentCode: number().nullable().meta(readingMeta("none")),
@@ -115026,7 +115475,7 @@ var lockControlCapability = {
115026
115475
  volatileStateFields: ["lastChangedAt"]
115027
115476
  };
115028
115477
  /**
115029
- * A monotonic total between resets (D944). Energy uses `power-meter.kwhTotal`
115478
+ * A monotonic total between resets (D944). Energy uses `power-meter.energy`
115030
115479
  * so dashboards keep one series; every other instantaneous reading lands here.
115031
115480
  * `value` is the time integral in the source quantity's accumulated base
115032
115481
  * (base-unit seconds, or kWh on the power meter). `null` is a gap, never zero.
@@ -115120,10 +115569,10 @@ var MediaPlayerStatusSchema = object({
115120
115569
  currentMedia: MediaInfoSchema.nullable(),
115121
115570
  /** Current playback position in ms. Null when not seekable or
115122
115571
  * nothing is playing. Pair with `DeviceFeature.MediaPlayerSeek`. */
115123
- positionMs: number().int().nonnegative().nullable().meta(readingMeta("duration-ms")),
115572
+ position: number().int().nonnegative().nullable().meta(readingMeta("duration-ms")),
115124
115573
  /** Total duration of the current media in ms. Null when unknown
115125
115574
  * (live stream) or nothing is playing. */
115126
- durationMs: number().int().nonnegative().nullable().meta(readingMeta("duration-ms")),
115575
+ duration: number().int().nonnegative().nullable().meta(readingMeta("duration-ms")),
115127
115576
  /** Shuffle toggle. Null when no shuffle surface.
115128
115577
  * Pair with `DeviceFeature.MediaPlayerShuffle`. */
115129
115578
  shuffle: boolean().nullable(),
@@ -115162,7 +115611,7 @@ var mediaPlayerCapability = {
115162
115611
  }),
115163
115612
  seek: method(object({
115164
115613
  deviceId: number().int().nonnegative(),
115165
- positionMs: number().int().nonnegative()
115614
+ position: number().int().nonnegative()
115166
115615
  }), _void(), {
115167
115616
  kind: "mutation",
115168
115617
  auth: "protected"
@@ -115234,7 +115683,7 @@ var mediaPlayerCapability = {
115234
115683
  /**
115235
115684
  * History (D850): volume (a level an operator or an automation sets; the
115236
115685
  * default 10 s floor folds a slider drag into its end value) and mute.
115237
- * `positionMs` is not recorded: it moves continuously while playing.
115686
+ * `position` is not recorded: it moves continuously while playing.
115238
115687
  */
115239
115688
  history: { fields: {
115240
115689
  volumeLevel: {
@@ -116333,9 +116782,9 @@ var NetworkLinkStatusSchema = object({
116333
116782
  * one whose reading has not landed must not be drawn at 0 %. Consumers
116334
116783
  * SKIP a null rather than coerce it.
116335
116784
  */
116336
- signalPercent: number().min(0).max(100).nullable().meta(readingMeta("percentage")),
116785
+ signal: number().min(0).max(100).nullable().meta(readingMeta("percentage")),
116337
116786
  /** Raw received signal strength in dBm, when the firmware reports one. */
116338
- rssiDbm: number().optional().meta(readingMeta("signal-level")),
116787
+ rssi: number().optional().meta(readingMeta("signal-level")),
116339
116788
  /** Network name of a wireless link, when the firmware reports it. */
116340
116789
  ssid: string().optional(),
116341
116790
  /** Ms epoch of the last observation. Lets consumers reason about freshness. */
@@ -116344,7 +116793,7 @@ var NetworkLinkStatusSchema = object({
116344
116793
  /** The slice a provider seeds before its first read: nothing is known yet. */
116345
116794
  var NETWORK_LINK_UNKNOWN = {
116346
116795
  type: "unknown",
116347
- signalPercent: null,
116796
+ signal: null,
116348
116797
  lastUpdated: 0
116349
116798
  };
116350
116799
  /**
@@ -116405,13 +116854,13 @@ onStatusChanged: { data: object({
116405
116854
  * History (D850): recorded series.
116406
116855
  */
116407
116856
  history: { fields: {
116408
- signalPercent: {
116857
+ signal: {
116409
116858
  kind: "gauge",
116410
116859
  unit: "%",
116411
116860
  deadband: 3,
116412
116861
  minIntervalMs: 6e4
116413
116862
  },
116414
- rssiDbm: {
116863
+ rssi: {
116415
116864
  kind: "gauge",
116416
116865
  unit: "dBm",
116417
116866
  deadband: 2,
@@ -117731,16 +118180,16 @@ method(_void(), PlatformCapabilitiesSchema), method(_void(), HardwareInfoSchema)
117731
118180
  * in real-world devices.
117732
118181
  */
117733
118182
  var PowerMeterStatusSchema = object({
117734
- /** Instantaneous power draw in watts. */
117735
- watts: number().optional().meta(readingMeta("power", { label: "Power" })),
118183
+ /** Instantaneous power draw (watts). */
118184
+ power: number().optional().meta(readingMeta("power", { label: "Power" })),
117736
118185
  /** Cumulative energy in kilowatt-hours since the meter was reset. */
117737
- kwhTotal: number().optional().meta(readingMeta("energy", { label: "Energy" })),
117738
- /** Voltage in volts. */
117739
- volts: number().optional().meta(readingMeta("voltage", { label: "Voltage" })),
118186
+ energy: number().optional().meta(readingMeta("energy", { label: "Energy" })),
118187
+ /** Voltage (volts). */
118188
+ voltage: number().optional().meta(readingMeta("voltage", { label: "Voltage" })),
117740
118189
  /** Current in amperes. */
117741
- amps: number().optional().meta(readingMeta("current", { label: "Current" })),
118190
+ current: number().optional().meta(readingMeta("current", { label: "Current" })),
117742
118191
  /** Apparent power in volt-amperes. */
117743
- va: number().optional().meta(readingMeta("apparent-power")),
118192
+ apparentPower: number().optional().meta(readingMeta("apparent-power")),
117744
118193
  /** Power factor (0..1) if reported. */
117745
118194
  powerFactor: number().min(0).max(1).optional().meta(readingMeta("ratio")),
117746
118195
  /** Ms epoch when the slice was last updated. */
@@ -117798,14 +118247,14 @@ function powerTarget(id, unit) {
117798
118247
  advancedExpression: true
117799
118248
  };
117800
118249
  }
117801
- /** "Power and energy" extension: watts, energy, voltage and current from other devices (D951). */
118250
+ /** "Power and energy" extension: power, energy, voltage and current from other devices (D951). */
117802
118251
  var powerMeterCustomize = {
117803
118252
  version: 1,
117804
118253
  targets: [
117805
- powerTarget("watts", "W"),
117806
- powerTarget("kwhTotal", "kWh"),
117807
- powerTarget("volts", "V"),
117808
- powerTarget("amps", "A")
118254
+ powerTarget("power", "W"),
118255
+ powerTarget("energy", "kWh"),
118256
+ powerTarget("voltage", "V"),
118257
+ powerTarget("current", "A")
117809
118258
  ]
117810
118259
  };
117811
118260
  var powerMeterCapability = {
@@ -117821,41 +118270,41 @@ var powerMeterCapability = {
117821
118270
  },
117822
118271
  runtimeState: PowerMeterStatusSchema,
117823
118272
  /**
117824
- * Runtime-state durability: **restored** — as `numeric-sensor`; `kwhTotal` is an accumulator whose restored value is the baseline.
118273
+ * Runtime-state durability: **restored** — as `numeric-sensor`; `energy` is an accumulator whose restored value is the baseline.
117825
118274
  *
117826
118275
  * See `RuntimeStateDurability`. Enforced by
117827
118276
  * `scripts/check-runtime-state-durability.ts`.
117828
118277
  */
117829
118278
  durability: "restored",
117830
118279
  /**
117831
- * History (D850): recorded series. Gauges are floored at 30 s; `kwhTotal` is the one monotonic meter (counter).
118280
+ * History (D850): recorded series. Gauges are floored at 30 s; `energy` is the one monotonic meter (counter).
117832
118281
  */
117833
118282
  history: { fields: {
117834
- watts: {
118283
+ power: {
117835
118284
  kind: "gauge",
117836
118285
  unit: "W",
117837
118286
  deadband: 1,
117838
118287
  minIntervalMs: 3e4
117839
118288
  },
117840
- kwhTotal: {
118289
+ energy: {
117841
118290
  kind: "counter",
117842
118291
  unit: "kWh",
117843
118292
  deadband: .01,
117844
118293
  minIntervalMs: 6e4
117845
118294
  },
117846
- volts: {
118295
+ voltage: {
117847
118296
  kind: "gauge",
117848
118297
  unit: "V",
117849
118298
  deadband: 1,
117850
118299
  minIntervalMs: 3e4
117851
118300
  },
117852
- amps: {
118301
+ current: {
117853
118302
  kind: "gauge",
117854
118303
  unit: "A",
117855
118304
  deadband: .05,
117856
118305
  minIntervalMs: 3e4
117857
118306
  },
117858
- va: {
118307
+ apparentPower: {
117859
118308
  kind: "gauge",
117860
118309
  unit: "VA",
117861
118310
  deadband: 1,
@@ -117905,9 +118354,6 @@ var PresenceStatusSchema = object({
117905
118354
  location: string().nullable(),
117906
118355
  /** GPS coordinates when available. Null otherwise. */
117907
118356
  gps: GpsLocationSchema.nullable(),
117908
- /** Optional battery level of the tracking device (0..100). Null
117909
- * when not reported. */
117910
- batteryPercent: number().min(0).max(100).nullable().meta(readingMeta("percentage")),
117911
118357
  /** Ms epoch when the slice was last updated. */
117912
118358
  lastChangedAt: number()
117913
118359
  });
@@ -117936,14 +118382,7 @@ var presenceCapability = {
117936
118382
  * `scripts/check-runtime-state-durability.ts`.
117937
118383
  */
117938
118384
  durability: "restored",
117939
- /**
117940
- * History (D850): recorded series.
117941
- */
117942
- history: { fields: { batteryPercent: {
117943
- kind: "gauge",
117944
- unit: "%",
117945
- deadband: 1
117946
- } } },
118385
+ history: "none",
117947
118386
  /** Clock fields: written, but excluded from the compare that decides
117948
118387
  * whether persisting is worth a SQLite commit. */
117949
118388
  volatileStateFields: ["lastChangedAt"]
@@ -117958,7 +118397,7 @@ var presenceCapability = {
117958
118397
  */
117959
118398
  var PressureSensorStatusSchema = object({
117960
118399
  /** Current pressure in hPa. `null` when the sample was refused (D393). */
117961
- hpa: number().nullable().meta(readingMeta("pressure", { precision: 0 })),
118400
+ pressure: number().nullable().meta(readingMeta("pressure", { precision: 0 })),
117962
118401
  /** Ms epoch when the slice was last updated. */
117963
118402
  lastFetchedAt: number()
117964
118403
  });
@@ -117984,7 +118423,7 @@ var pressureSensorCapability = {
117984
118423
  /**
117985
118424
  * History (D850): recorded series.
117986
118425
  */
117987
- history: { fields: { hpa: {
118426
+ history: { fields: { pressure: {
117988
118427
  kind: "gauge",
117989
118428
  unit: "hPa",
117990
118429
  deadband: .1
@@ -121691,7 +122130,7 @@ var tamperCapability = {
121691
122130
  */
121692
122131
  var TemperatureSensorStatusSchema = object({
121693
122132
  /** Current temperature in Celsius. `null` when the sample was refused (D393). */
121694
- celsius: number().nullable().meta(readingMeta("temperature", {
122133
+ temperature: number().nullable().meta(readingMeta("temperature", {
121695
122134
  precision: 1,
121696
122135
  label: "Temperature"
121697
122136
  })),
@@ -121720,7 +122159,7 @@ var temperatureSensorCapability = {
121720
122159
  /**
121721
122160
  * History (D850): recorded series.
121722
122161
  */
121723
- history: { fields: { celsius: {
122162
+ history: { fields: { temperature: {
121724
122163
  kind: "gauge",
121725
122164
  unit: "°C",
121726
122165
  deadband: .1
@@ -122017,7 +122456,7 @@ var VacuumControlStatusSchema = object({
122017
122456
  /** Speed tokens the hardware accepts — drives the UI selector. */
122018
122457
  availableFanSpeeds: array(string()),
122019
122458
  /** 0..100 cleaning-completion percentage of the current task, or null. */
122020
- progressPercent: number().min(0).max(100).nullable().meta(readingMeta("percentage")),
122459
+ progress: number().min(0).max(100).nullable().meta(readingMeta("percentage")),
122021
122460
  /** Current error code (0 / null = no error). */
122022
122461
  errorCode: number().nullable().meta(readingMeta("none")),
122023
122462
  /** Human label for {@link errorCode}, or null when none / undecodable. */
@@ -124099,7 +124538,7 @@ var BaseDevice = class {
124099
124538
  * `device-local-state.ts`. Drivers access via:
124100
124539
  *
124101
124540
  * `this.state.battery.sleeping = true` // patches the battery slice
124102
- * `const pct = this.state.battery.percentage` // reads the battery slice
124541
+ * `const pct = this.state.battery.level` // reads the battery slice
124103
124542
  * `this.state.deviceStatus.online = true` // mirrors `markOnline(true)`
124104
124543
  *
124105
124544
  * Adding a new cap with `runtimeState:` automatically extends this
@@ -124139,7 +124578,7 @@ var BaseDevice = class {
124139
124578
  * shape; drivers don't redeclare it, they just write through.
124140
124579
  *
124141
124580
  * Read: `this.runtimeState.getCapState('battery')` →
124142
- * `{percentage, charging, sleeping, lastUpdated}` for any
124581
+ * `{level, charging, sleeping, lastUpdated}` for any
124143
124582
  * provider that registers `batteryCapability`.
124144
124583
  * Write: `this.runtimeState.setCapState('battery', { … })`.
124145
124584
  *
@@ -125329,6 +125768,104 @@ Object.freeze({
125329
125768
  access: "create",
125330
125769
  role: "admin"
125331
125770
  },
125771
+ "actions.create": {
125772
+ capName: "actions",
125773
+ capScope: "system",
125774
+ addonId: null,
125775
+ access: "create",
125776
+ role: "admin"
125777
+ },
125778
+ "actions.delete": {
125779
+ capName: "actions",
125780
+ capScope: "system",
125781
+ addonId: null,
125782
+ access: "delete",
125783
+ role: "admin"
125784
+ },
125785
+ "actions.get": {
125786
+ capName: "actions",
125787
+ capScope: "system",
125788
+ addonId: null,
125789
+ access: "view",
125790
+ role: "viewer"
125791
+ },
125792
+ "actions.getLinks": {
125793
+ capName: "actions",
125794
+ capScope: "system",
125795
+ addonId: null,
125796
+ access: "view",
125797
+ role: "admin"
125798
+ },
125799
+ "actions.getTraces": {
125800
+ capName: "actions",
125801
+ capScope: "system",
125802
+ addonId: null,
125803
+ access: "view",
125804
+ role: "admin"
125805
+ },
125806
+ "actions.list": {
125807
+ capName: "actions",
125808
+ capScope: "system",
125809
+ addonId: null,
125810
+ access: "view",
125811
+ role: "admin"
125812
+ },
125813
+ "actions.listForCamera": {
125814
+ capName: "actions",
125815
+ capScope: "system",
125816
+ addonId: null,
125817
+ access: "view",
125818
+ role: "viewer"
125819
+ },
125820
+ "actions.run": {
125821
+ capName: "actions",
125822
+ capScope: "system",
125823
+ addonId: null,
125824
+ access: "create",
125825
+ role: "viewer"
125826
+ },
125827
+ "actions.setLinks": {
125828
+ capName: "actions",
125829
+ capScope: "system",
125830
+ addonId: null,
125831
+ access: "create",
125832
+ role: "admin"
125833
+ },
125834
+ "actions.stop": {
125835
+ capName: "actions",
125836
+ capScope: "system",
125837
+ addonId: null,
125838
+ access: "create",
125839
+ role: "admin"
125840
+ },
125841
+ "actions.update": {
125842
+ capName: "actions",
125843
+ capScope: "system",
125844
+ addonId: null,
125845
+ access: "create",
125846
+ role: "admin"
125847
+ },
125848
+ "actionSource.listActions": {
125849
+ capName: "action-source",
125850
+ capScope: "system",
125851
+ addonId: null,
125852
+ access: "view",
125853
+ role: "admin"
125854
+ },
125855
+ "actionSource.runAction": {
125856
+ capName: "action-source",
125857
+ capScope: "system",
125858
+ addonId: null,
125859
+ access: "create",
125860
+ role: "admin"
125861
+ },
125862
+ "actionSource.stopAction": {
125863
+ capName: "action-source",
125864
+ capScope: "system",
125865
+ addonId: null,
125866
+ access: "create",
125867
+ role: "admin"
125868
+ },
125332
125869
  "addonPages.listPages": {
125333
125870
  capName: "addon-pages",
125334
125871
  capScope: "system",
@@ -126610,6 +127147,13 @@ Object.freeze({
126610
127147
  access: "view",
126611
127148
  role: "admin"
126612
127149
  },
127150
+ "coreBlocks.getAutomationRunState": {
127151
+ capName: "core-blocks",
127152
+ capScope: "system",
127153
+ addonId: null,
127154
+ access: "view",
127155
+ role: "admin"
127156
+ },
126613
127157
  "coreBlocks.getBlockSettings": {
126614
127158
  capName: "core-blocks",
126615
127159
  capScope: "system",
@@ -126631,6 +127175,13 @@ Object.freeze({
126631
127175
  access: "view",
126632
127176
  role: "admin"
126633
127177
  },
127178
+ "coreBlocks.getTraces": {
127179
+ capName: "core-blocks",
127180
+ capScope: "system",
127181
+ addonId: null,
127182
+ access: "view",
127183
+ role: "admin"
127184
+ },
126634
127185
  "coreBlocks.getTypeDefs": {
126635
127186
  capName: "core-blocks",
126636
127187
  capScope: "system",
@@ -126666,6 +127217,13 @@ Object.freeze({
126666
127217
  access: "create",
126667
127218
  role: "admin"
126668
127219
  },
127220
+ "coreBlocks.reportAutomationRun": {
127221
+ capName: "core-blocks",
127222
+ capScope: "system",
127223
+ addonId: null,
127224
+ access: "create",
127225
+ role: "admin"
127226
+ },
126669
127227
  "coreBlocks.restart": {
126670
127228
  capName: "core-blocks",
126671
127229
  capScope: "system",
@@ -135239,7 +135797,7 @@ var ACTUATOR_CAP_VOCABULARY = {
135239
135797
  label: t("Full brightness", "Luminosità massima"),
135240
135798
  phrase: t("Set {device} to full brightness", "Porta {device} alla massima luminosità"),
135241
135799
  icon: "sun",
135242
- args: { percentage: 100 }
135800
+ args: { level: 100 }
135243
135801
  },
135244
135802
  {
135245
135803
  id: "brightness.half",
@@ -135247,7 +135805,7 @@ var ACTUATOR_CAP_VOCABULARY = {
135247
135805
  label: t("Half brightness", "Luminosità a metà"),
135248
135806
  phrase: t("Dim {device} to half", "Abbassa {device} a metà luminosità"),
135249
135807
  icon: "sun-dim",
135250
- args: { percentage: 50 }
135808
+ args: { level: 50 }
135251
135809
  },
135252
135810
  {
135253
135811
  id: "brightness.off",
@@ -135255,16 +135813,16 @@ var ACTUATOR_CAP_VOCABULARY = {
135255
135813
  label: t("Brightness off", "Luminosità a zero"),
135256
135814
  phrase: t("Dim {device} to zero", "Abbassa {device} a zero"),
135257
135815
  icon: "lightbulb-off",
135258
- args: { percentage: 0 }
135816
+ args: { level: 0 }
135259
135817
  },
135260
135818
  {
135261
135819
  id: "brightness.set",
135262
135820
  method: "setBrightness",
135263
135821
  label: t("Set brightness", "Imposta la luminosità"),
135264
- phrase: t("Set the brightness of {device} to {percentage}", "Imposta la luminosità di {device} a {percentage}"),
135822
+ phrase: t("Set the brightness of {device} to {level}", "Imposta la luminosità di {device} a {level}"),
135265
135823
  icon: "sun-medium",
135266
135824
  openArgs: [{
135267
- key: "percentage",
135825
+ key: "level",
135268
135826
  label: PERCENT,
135269
135827
  offer: {
135270
135828
  kind: "number",
@@ -135278,13 +135836,13 @@ var ACTUATOR_CAP_VOCABULARY = {
135278
135836
  ],
135279
135837
  state: [{
135280
135838
  kind: "numeric",
135281
- field: "percentage",
135839
+ field: "level",
135282
135840
  quantity: t("the brightness", "la luminosità"),
135283
135841
  label: t("Brightness", "Luminosità"),
135284
135842
  unit: "%",
135285
135843
  icon: "sun"
135286
135844
  }],
135287
- events: { onBrightnessChanged: { notOperatorFacing: "the `percentage` state trigger reads the same change" } }
135845
+ events: { onBrightnessChanged: { notOperatorFacing: "the `level` state trigger reads the same change" } }
135288
135846
  },
135289
135847
  color: {
135290
135848
  scope: "device",
@@ -135298,7 +135856,7 @@ var ACTUATOR_CAP_VOCABULARY = {
135298
135856
  icon: "sun",
135299
135857
  args: { color: {
135300
135858
  mode: "mired",
135301
- mireds: 370
135859
+ colorTemperature: 370
135302
135860
  } }
135303
135861
  },
135304
135862
  {
@@ -135309,7 +135867,7 @@ var ACTUATOR_CAP_VOCABULARY = {
135309
135867
  icon: "snowflake",
135310
135868
  args: { color: {
135311
135869
  mode: "mired",
135312
- mireds: 200
135870
+ colorTemperature: 200
135313
135871
  } }
135314
135872
  },
135315
135873
  {
@@ -135923,7 +136481,7 @@ var ACTUATOR_CAP_VOCABULARY = {
135923
136481
  label: t("Fan off", "Ventilatore spento"),
135924
136482
  phrase: t("Stop {device}", "Ferma {device}"),
135925
136483
  icon: "power-off",
135926
- args: { percentage: 0 }
136484
+ args: { speed: 0 }
135927
136485
  },
135928
136486
  {
135929
136487
  id: "fan.full",
@@ -135931,16 +136489,16 @@ var ACTUATOR_CAP_VOCABULARY = {
135931
136489
  label: t("Fan full", "Ventilatore al massimo"),
135932
136490
  phrase: t("Run {device} at full speed", "Porta {device} alla massima velocità"),
135933
136491
  icon: "fan",
135934
- args: { percentage: 100 }
136492
+ args: { speed: 100 }
135935
136493
  },
135936
136494
  {
135937
136495
  id: "fan.speed",
135938
136496
  method: "setPercentage",
135939
136497
  label: t("Set speed", "Imposta la velocità"),
135940
- phrase: t("Set the speed of {device} to {percentage}", "Imposta la velocità di {device} al {percentage}"),
136498
+ phrase: t("Set the speed of {device} to {speed}", "Imposta la velocità di {device} al {speed}"),
135941
136499
  icon: "fan",
135942
136500
  openArgs: [{
135943
- key: "percentage",
136501
+ key: "speed",
135944
136502
  label: t("Speed", "Velocità"),
135945
136503
  offer: {
135946
136504
  kind: "number",
@@ -136002,7 +136560,7 @@ var ACTUATOR_CAP_VOCABULARY = {
136002
136560
  ],
136003
136561
  state: [{
136004
136562
  kind: "numeric",
136005
- field: "percentage",
136563
+ field: "speed",
136006
136564
  quantity: t("the speed", "la velocità"),
136007
136565
  label: t("Speed", "Velocità"),
136008
136566
  unit: "%",
@@ -136800,7 +137358,7 @@ var APPLIANCE_CAP_VOCABULARY = {
136800
137358
  },
136801
137359
  {
136802
137360
  kind: "numeric",
136803
- field: "progressPercent",
137361
+ field: "progress",
136804
137362
  quantity: t("the cycle progress", "l’avanzamento del lavaggio"),
136805
137363
  label: t("Progress", "Avanzamento"),
136806
137364
  unit: "%",
@@ -137158,10 +137716,10 @@ var APPLIANCE_CAP_VOCABULARY = {
137158
137716
  id: "media.seek",
137159
137717
  method: "seek",
137160
137718
  label: t("Seek", "Vai al punto"),
137161
- phrase: t("Move {device} to {positionMs}", "Porta {device} al punto {positionMs}"),
137719
+ phrase: t("Move {device} to {position}", "Porta {device} al punto {position}"),
137162
137720
  icon: "fast-forward",
137163
137721
  openArgs: [{
137164
- key: "positionMs",
137722
+ key: "position",
137165
137723
  label: t("Position", "Posizione"),
137166
137724
  offer: {
137167
137725
  kind: "number",
@@ -138210,7 +138768,7 @@ var SENSOR_CAP_VOCABULARY = {
138210
138768
  state: [
138211
138769
  {
138212
138770
  kind: "numeric",
138213
- field: "co2Ppm",
138771
+ field: "co2",
138214
138772
  quantity: t("the CO₂", "la CO₂"),
138215
138773
  label: t("CO₂", "CO₂"),
138216
138774
  unit: "ppm",
@@ -138218,7 +138776,7 @@ var SENSOR_CAP_VOCABULARY = {
138218
138776
  },
138219
138777
  {
138220
138778
  kind: "numeric",
138221
- field: "vocPpb",
138779
+ field: "voc",
138222
138780
  quantity: t("the VOC level", "il livello di COV"),
138223
138781
  label: t("VOC", "COV"),
138224
138782
  unit: "ppb",
@@ -138254,7 +138812,7 @@ var SENSOR_CAP_VOCABULARY = {
138254
138812
  noun: t("Light level", "Luminosità ambiente"),
138255
138813
  state: [{
138256
138814
  kind: "numeric",
138257
- field: "lux",
138815
+ field: "illuminance",
138258
138816
  quantity: t("the light level", "la luminosità"),
138259
138817
  label: t("Illuminance", "Illuminamento"),
138260
138818
  unit: "lx",
@@ -138457,7 +139015,7 @@ var SENSOR_CAP_VOCABULARY = {
138457
139015
  noun: t("Humidity", "Umidità"),
138458
139016
  state: [{
138459
139017
  kind: "numeric",
138460
- field: "percent",
139018
+ field: "humidity",
138461
139019
  quantity: t("the humidity", "l’umidità"),
138462
139020
  label: t("Humidity", "Umidità"),
138463
139021
  unit: "%",
@@ -138505,7 +139063,7 @@ var SENSOR_CAP_VOCABULARY = {
138505
139063
  state: [
138506
139064
  {
138507
139065
  kind: "numeric",
138508
- field: "watts",
139066
+ field: "power",
138509
139067
  quantity: t("the power draw", "il consumo"),
138510
139068
  label: t("Power", "Potenza"),
138511
139069
  unit: "W",
@@ -138513,7 +139071,7 @@ var SENSOR_CAP_VOCABULARY = {
138513
139071
  },
138514
139072
  {
138515
139073
  kind: "numeric",
138516
- field: "kwhTotal",
139074
+ field: "energy",
138517
139075
  quantity: t("the energy used", "l’energia consumata"),
138518
139076
  label: t("Energy", "Energia"),
138519
139077
  unit: "kWh",
@@ -138521,7 +139079,7 @@ var SENSOR_CAP_VOCABULARY = {
138521
139079
  },
138522
139080
  {
138523
139081
  kind: "numeric",
138524
- field: "volts",
139082
+ field: "voltage",
138525
139083
  quantity: t("the voltage", "la tensione"),
138526
139084
  label: t("Voltage", "Tensione"),
138527
139085
  unit: "V",
@@ -138529,7 +139087,7 @@ var SENSOR_CAP_VOCABULARY = {
138529
139087
  },
138530
139088
  {
138531
139089
  kind: "numeric",
138532
- field: "amps",
139090
+ field: "current",
138533
139091
  quantity: t("the current", "la corrente"),
138534
139092
  label: t("Current", "Corrente"),
138535
139093
  unit: "A",
@@ -138546,13 +139104,6 @@ var SENSOR_CAP_VOCABULARY = {
138546
139104
  label: t("Where", "Posizione"),
138547
139105
  phrase: t("When {device} is at {value}", "Quando {device} è in {value}"),
138548
139106
  icon: "map-pin"
138549
- }, {
138550
- kind: "numeric",
138551
- field: "batteryPercent",
138552
- quantity: t("the phone battery", "la batteria del telefono"),
138553
- label: t("Battery", "Batteria"),
138554
- unit: "%",
138555
- icon: "battery"
138556
139107
  }]
138557
139108
  },
138558
139109
  "pressure-sensor": {
@@ -138560,7 +139111,7 @@ var SENSOR_CAP_VOCABULARY = {
138560
139111
  noun: t("Pressure", "Pressione"),
138561
139112
  state: [{
138562
139113
  kind: "numeric",
138563
- field: "hpa",
139114
+ field: "pressure",
138564
139115
  quantity: t("the pressure", "la pressione"),
138565
139116
  label: t("Pressure", "Pressione"),
138566
139117
  unit: "hPa",
@@ -138596,7 +139147,7 @@ var SENSOR_CAP_VOCABULARY = {
138596
139147
  noun: t("Temperature", "Temperatura"),
138597
139148
  state: [{
138598
139149
  kind: "numeric",
138599
- field: "celsius",
139150
+ field: "temperature",
138600
139151
  quantity: t("the temperature", "la temperatura"),
138601
139152
  label: t("Temperature", "Temperatura"),
138602
139153
  unit: "°C",
@@ -138667,7 +139218,7 @@ var SENSOR_CAP_VOCABULARY = {
138667
139218
  state: [
138668
139219
  {
138669
139220
  kind: "numeric",
138670
- field: "percentage",
139221
+ field: "level",
138671
139222
  quantity: t("the battery level", "il livello della batteria"),
138672
139223
  label: t("Battery", "Batteria"),
138673
139224
  unit: "%",
@@ -138709,7 +139260,7 @@ var SENSOR_CAP_VOCABULARY = {
138709
139260
  icon: "battery-charging"
138710
139261
  }
138711
139262
  ],
138712
- events: { onStatusChanged: { notOperatorFacing: "the percentage / sleeping / charging state triggers each read one field of this change" } },
139263
+ events: { onStatusChanged: { notOperatorFacing: "the level / sleeping / charging state triggers each read one field of this change" } },
138713
139264
  extraEvents: [{
138714
139265
  category: EventCategory.DeviceAwake,
138715
139266
  label: t("Woke up", "Risvegliato"),
@@ -138727,7 +139278,7 @@ var SENSOR_CAP_VOCABULARY = {
138727
139278
  noun: t("Network link", "Collegamento di rete"),
138728
139279
  state: [{
138729
139280
  kind: "numeric",
138730
- field: "signalPercent",
139281
+ field: "signal",
138731
139282
  quantity: t("the signal strength", "l’intensità del segnale"),
138732
139283
  label: t("Signal", "Segnale"),
138733
139284
  unit: "%",
@@ -138820,6 +139371,8 @@ var ML = "model and training machinery — no device action";
138820
139371
  var INTERNAL = "internal plumbing between addons — never operator-facing";
138821
139372
  /** Every system-scoped cap, with why it is not a device's vocabulary. */
138822
139373
  var SYSTEM_CAP_VOCABULARY = {
139374
+ "action-source": system(INTERNAL),
139375
+ actions: system("declared actions — run from the Actions page and from the cameras that link them"),
138823
139376
  "addon-pages": system(UI_PLUMBING),
138824
139377
  "addon-pages-source": system(UI_PLUMBING),
138825
139378
  "addon-routes": system(UI_PLUMBING),
@@ -139016,6 +139569,31 @@ Object.freeze({
139016
139569
  form: "single",
139017
139570
  optional: false
139018
139571
  }],
139572
+ "actions.get": [{
139573
+ name: "deviceId",
139574
+ form: "single",
139575
+ optional: true
139576
+ }],
139577
+ "actions.getLinks": [{
139578
+ name: "deviceId",
139579
+ form: "single",
139580
+ optional: false
139581
+ }],
139582
+ "actions.listForCamera": [{
139583
+ name: "deviceId",
139584
+ form: "single",
139585
+ optional: false
139586
+ }],
139587
+ "actions.run": [{
139588
+ name: "deviceId",
139589
+ form: "single",
139590
+ optional: true
139591
+ }],
139592
+ "actions.setLinks": [{
139593
+ name: "deviceId",
139594
+ form: "single",
139595
+ optional: false
139596
+ }],
139019
139597
  "addonSettings.getDeviceSettings": [{
139020
139598
  name: "deviceId",
139021
139599
  form: "single",
@@ -143187,6 +143765,7 @@ function ingestReading(input) {
143187
143765
  value: converted
143188
143766
  };
143189
143767
  }
143768
+ String.raw`(?<=^|["'(\s>,\[{])`;
143190
143769
  var MB = 1024 * 1024;
143191
143770
  1024 * MB, 3072 * MB;
143192
143771
  //#endregion
@@ -143313,6 +143892,334 @@ function buildConnectionFormSchema() {
143313
143892
  ] };
143314
143893
  }
143315
143894
  //#endregion
143895
+ //#region src/dreame-vacuum-decode.ts
143896
+ /**
143897
+ * Pure decode helpers for the Dreame vacuum. Translate the library's MIoT enum
143898
+ * values into the camstack `vacuum-control` slice shape. Unit-tested in
143899
+ * isolation (no facade, no network) — mirrors `homematic-decode.ts`.
143900
+ */
143901
+ /**
143902
+ * Map a Dreame {@link MiotState} (siid 2 piid 1) onto the camstack canonical
143903
+ * `VacuumState` lifecycle (`idle` / `cleaning` / `paused` / `returning` /
143904
+ * `docked` / `error`). A `null` state (not yet observed) settles on `idle`.
143905
+ *
143906
+ * The MIoT state space is large (~50 values); we collapse it onto the six
143907
+ * canonical states by family. Anything unrecognised falls through to `idle`
143908
+ * (the safe neutral) so a firmware-added code never throws.
143909
+ */
143910
+ function mapMiotStateToVacuumState(state) {
143911
+ if (state === null) return "idle";
143912
+ switch (state) {
143913
+ case MiotState.Charging:
143914
+ case MiotState.ChargingComplete:
143915
+ case MiotState.IntelligentCharging:
143916
+ case MiotState.CleanWashboardBase:
143917
+ case MiotState.AutoRepairBase:
143918
+ case MiotState.WaterSupplyDrainTest:
143919
+ case MiotState.CleanMopRefillWater:
143920
+ case MiotState.AutoEmptying:
143921
+ case MiotState.Emptying:
143922
+ case MiotState.Draining:
143923
+ case MiotState.AutoWaterDraining:
143924
+ case MiotState.Sanitizing: return "docked";
143925
+ case MiotState.MopDrying:
143926
+ case MiotState.DustBagDrying:
143927
+ case MiotState.SanitizingWithDry: return "drying";
143928
+ case MiotState.ReturningToCharge:
143929
+ case MiotState.ReturningToWash:
143930
+ case MiotState.ReturnInstallMop:
143931
+ case MiotState.ReturnRemoveMop:
143932
+ case MiotState.ReturnToEmpty:
143933
+ case MiotState.ReturningToDrain: return "returning";
143934
+ case MiotState.Paused:
143935
+ case MiotState.PausedAlt:
143936
+ case MiotState.PausedCleaning:
143937
+ case MiotState.DustBagDryingPaused:
143938
+ case MiotState.CameraMonitoringPaused:
143939
+ case MiotState.FindingPetPaused:
143940
+ case MiotState.InitialDeepCleanPaused:
143941
+ case MiotState.ChangingMopPaused:
143942
+ case MiotState.FloorMaintainingPaused: return "paused";
143943
+ case MiotState.Standby:
143944
+ case MiotState.WaitingForTask: return "idle";
143945
+ default: return "cleaning";
143946
+ }
143947
+ }
143948
+ /**
143949
+ * The camstack `fanSpeed` token for a Dreame {@link SuctionLevel} — the enum
143950
+ * member NAME, lower-cased (`Quiet` → `'quiet'`). Returns `null` for an
143951
+ * unobserved / unknown level.
143952
+ */
143953
+ function suctionLevelToken(level) {
143954
+ if (level === null) return null;
143955
+ const name = SuctionLevel[level];
143956
+ return typeof name === "string" ? name.toLowerCase() : null;
143957
+ }
143958
+ /**
143959
+ * Resolve a camstack `fanSpeed` token back to the Dreame {@link SuctionLevel}
143960
+ * enum value, validated against the model's `supported` levels. Returns null
143961
+ * when the token doesn't match a supported level (the caller rejects the set).
143962
+ */
143963
+ function tokenToSuctionLevel(token, supported) {
143964
+ const wanted = token.trim().toLowerCase();
143965
+ for (const level of supported) if (suctionLevelToken(level) === wanted) return level;
143966
+ return null;
143967
+ }
143968
+ /** The list of accepted `fanSpeed` tokens for a model's supported suction set. */
143969
+ function supportedFanSpeedTokens(supported) {
143970
+ const tokens = [];
143971
+ for (const level of supported) {
143972
+ const token = suctionLevelToken(level);
143973
+ if (token !== null) tokens.push(token);
143974
+ }
143975
+ return tokens;
143976
+ }
143977
+ /**
143978
+ * Clamp a raw battery reading (already a 0..100 percentage from the library)
143979
+ * to the `vacuum-control` slice's `batteryLevel` field. Returns null for a
143980
+ * null/NaN/out-of-band reading so the slice stays valid (the schema rejects
143981
+ * a number outside 0..100).
143982
+ */
143983
+ function clampBatteryLevel$1(value) {
143984
+ if (value === null || !Number.isFinite(value)) return null;
143985
+ return Math.max(0, Math.min(100, value));
143986
+ }
143987
+ /**
143988
+ * MiotError codes that are BENIGN completion / reminder states, not active
143989
+ * faults. A fresh cloud-shadow read of an idle docked robot legitimately
143990
+ * returns `TaskComplete` (68); it is not an error to show. The Error sensor
143991
+ * (`dreame-entity-catalog.ts`) and the vacuum card both read this set.
143992
+ * Extend it if other non-fault codes surface as spurious "errors".
143993
+ */
143994
+ var BENIGN_ERROR_CODES = new Set([MiotError.TaskComplete]);
143995
+ /** The card's error label: null for no error and for a benign completion code. */
143996
+ function vacuumErrorLabel(code) {
143997
+ if (!code || BENIGN_ERROR_CODES.has(code)) return null;
143998
+ return `Error ${code}`;
143999
+ }
144000
+ /** Clamp a 0..100 percentage (progress, coverage, …), null-safe. */
144001
+ function clampPercent$1(value) {
144002
+ if (value === null || !Number.isFinite(value)) return null;
144003
+ return Math.max(0, Math.min(100, value));
144004
+ }
144005
+ /**
144006
+ * Where each {@link VacuumTankProps} field lives (siid/piid). Property ids and
144007
+ * enum values are the HA Dreame integration's (Tasshack `dreame-vacuum`
144008
+ * `types.py`, `DreameVacuumProperty` → siid/piid map; same on `dev` and
144009
+ * v2.0.0b25, checked 2026-09-26). 27/1 and 27/4 were already read here; 27/2 and
144010
+ * 27/3 are their neighbours on the same station service. The robot pushes them
144011
+ * on change; the consumables refresh also re-reads them from the shadow.
144012
+ */
144013
+ var VACUUM_TANK_PROPS = {
144014
+ cleanWaterTankStatus: {
144015
+ siid: 27,
144016
+ piid: 1
144017
+ },
144018
+ waterTank: {
144019
+ siid: 4,
144020
+ piid: 6
144021
+ },
144022
+ detergentStatus: {
144023
+ siid: 27,
144024
+ piid: 4
144025
+ },
144026
+ dustCollection: {
144027
+ siid: 15,
144028
+ piid: 3
144029
+ },
144030
+ dirtyWaterTankStatus: {
144031
+ siid: 27,
144032
+ piid: 2
144033
+ },
144034
+ dustBagStatus: {
144035
+ siid: 27,
144036
+ piid: 3
144037
+ }
144038
+ };
144039
+ /**
144040
+ * Clean-water tank `status`: prefer the richer clean-water-tank-status (27/1),
144041
+ * fall back to the coarser water-tank presence (4/6). `null` when neither maps
144042
+ * to a known good/low reading (NOT_INSTALLED / CHECKING / unknown). Pure.
144043
+ */
144044
+ function cleanWaterTankStatus(props) {
144045
+ if (props.cleanWaterTankStatus !== null) {
144046
+ if (props.cleanWaterTankStatus === 0) return "ok";
144047
+ if (props.cleanWaterTankStatus === 2) return "low";
144048
+ return null;
144049
+ }
144050
+ if (props.waterTank === 1 || props.waterTank === 10) return "ok";
144051
+ return null;
144052
+ }
144053
+ /** Detergent tank `status` from detergent-status (27/4). Pure. */
144054
+ function detergentTankStatus(value) {
144055
+ if (value === 0) return "ok";
144056
+ if (value === 2) return "low";
144057
+ return null;
144058
+ }
144059
+ /**
144060
+ * Dust-bin `status` from dust-collection (15/3); OVER_USE reads as `full`. A dust
144061
+ * bag that is missing or asks to be checked (27/3 NOT_INSTALLED / CHECK) is never
144062
+ * `ok` — the enum does not say "full" either, so it is unknown. Pure.
144063
+ */
144064
+ function dustBinTankStatus(dustCollection, dustBag) {
144065
+ if (dustCollection === 2) return "full";
144066
+ if (dustBag !== null && dustBag !== 0) return null;
144067
+ if (dustCollection === 1) return "ok";
144068
+ return null;
144069
+ }
144070
+ /**
144071
+ * Dirty-water tank `status` from dirty-water-tank-status (27/2). The robot has
144072
+ * ONE value for "not installed or full" and cannot tell them apart; lifting the
144073
+ * tank out to EMPTY it reads exactly like a full one, so that value is unknown
144074
+ * (`null`) — never `full`, which would raise a false "tank full" at the moment
144075
+ * someone is fixing it (controller ruling 2026-09-26). Absent or any other
144076
+ * value is unknown too, never `ok`.
144077
+ *
144078
+ * The unambiguous "full" is the robot's own error `MiotError.DirtyWaterTankFull`
144079
+ * (86, the library's `errorCode`, siid 2 piid 2): while it is raised the tank is
144080
+ * `full` whatever 27/2 says; once it clears, 27/2 decides again. Pure.
144081
+ */
144082
+ function dirtyWaterTankStatus(value, errorCode) {
144083
+ if (errorCode === MiotError.DirtyWaterTankFull) return "full";
144084
+ if (value === 0) return "ok";
144085
+ return null;
144086
+ }
144087
+ /**
144088
+ * The four tanks (clean-water / dirty-water / detergent / dust-bin) decoded from the model's {@link VacuumCapabilities}, with the
144089
+ * discrete `status` populated from the live MIoT enum props when supplied. Pure.
144090
+ *
144091
+ * The nodedreame library exposes NO tank FILL-LEVEL telemetry — the cloud shadow
144092
+ * (verified against a real `dreame.vacuum.r2538z`) carries only the water-VOLUME
144093
+ * *setting* (a mop-flow level, NOT a tank fill). We therefore never fabricate a
144094
+ * `level`; `level` is always `null`. The cap contract is honoured on PRESENCE,
144095
+ * with `status` derived from the tank-status enums:
144096
+ *
144097
+ * - a field is `null` when the hardware HAS NO such tank (capability flags), and
144098
+ * - `{ level: null, status }` when the tank EXISTS — `status` from the enum
144099
+ * prop ('ok' / 'low' / 'full'), or `null` when the device reports none.
144100
+ *
144101
+ * Presence is derived from the capability record:
144102
+ * - clean-water ← `canMop` (a mopping robot carries a clean-water reservoir)
144103
+ * - dirty-water ← `canMopWash` (a self-wash dock has a recovery/dirty tank)
144104
+ * - detergent ← `hasDetergentReservoir`
144105
+ * - dust-bin ← `canAutoEmpty` (an auto-empty robot has an onboard dust bin)
144106
+ */
144107
+ function decodeVacuumTanks(caps, props, errorCode = null) {
144108
+ const tank = (present, status) => present ? {
144109
+ level: null,
144110
+ status
144111
+ } : null;
144112
+ return {
144113
+ cleanWater: tank(Boolean(caps?.canMop), props ? cleanWaterTankStatus(props) : null),
144114
+ dirtyWater: tank(Boolean(caps?.canMopWash), dirtyWaterTankStatus(props?.dirtyWaterTankStatus ?? null, errorCode)),
144115
+ detergent: tank(Boolean(caps?.hasDetergentReservoir), props ? detergentTankStatus(props.detergentStatus) : null),
144116
+ dustBin: tank(Boolean(caps?.canAutoEmpty), props ? dustBinTankStatus(props.dustCollection, props.dustBagStatus) : null)
144117
+ };
144118
+ }
144119
+ /** How each tank is named and what it is, in the order the panel lists them. */
144120
+ var TANK_ITEMS = [
144121
+ {
144122
+ key: "cleanWater",
144123
+ label: "Clean water tank",
144124
+ kind: "supply",
144125
+ form: "tank"
144126
+ },
144127
+ {
144128
+ key: "dirtyWater",
144129
+ label: "Dirty water tank",
144130
+ kind: "waste",
144131
+ form: "tank"
144132
+ },
144133
+ {
144134
+ key: "detergent",
144135
+ label: "Detergent",
144136
+ kind: "supply",
144137
+ form: "product"
144138
+ },
144139
+ {
144140
+ key: "dustBin",
144141
+ label: "Dust bin",
144142
+ kind: "waste",
144143
+ form: "bin"
144144
+ }
144145
+ ];
144146
+ /**
144147
+ * The tanks as `consumables` items (D915): every PRESENT tank becomes a
144148
+ * `supply` (clean water, detergent) or `waste` (dirty water, dust bin) item
144149
+ * keyed by its old field name, drawn as a `tank` (the two water tanks), a
144150
+ * `product` (detergent) or a `bin` (the dust bin) — the key the notification centre's tank
144151
+ * episodes were built on, so an open episode carries on. A tank is never
144152
+ * resettable: emptying or refilling it is the reset. Pure.
144153
+ */
144154
+ function vacuumTankItems(tanks) {
144155
+ return TANK_ITEMS.flatMap(({ key, label, kind, form }) => {
144156
+ const tank = tanks[key];
144157
+ if (tank === null) return [];
144158
+ return [{
144159
+ key,
144160
+ label,
144161
+ level: tank.level,
144162
+ status: tank.status,
144163
+ kind,
144164
+ form,
144165
+ lastResetAt: null,
144166
+ resettable: false
144167
+ }];
144168
+ });
144169
+ }
144170
+ /**
144171
+ * Clamp a raw consumable percentage to `[0, 100]`, or return `null` when the
144172
+ * value is absent / non-finite. The library reports remaining life as a 0..100
144173
+ * integer; we clamp defensively so an out-of-range firmware value never violates
144174
+ * the cap schema.
144175
+ */
144176
+ function clampConsumablePct(value) {
144177
+ if (value === null || !Number.isFinite(value)) return null;
144178
+ return Math.max(0, Math.min(100, value));
144179
+ }
144180
+ /**
144181
+ * The `consumables` cap items for the consumables a vacuum REPORTS.
144182
+ *
144183
+ * PRESENCE-driven (mirrors HA): the library's `supportedConsumables` enumerates
144184
+ * ONLY the consumables this model reports (a present MIoT life property), each
144185
+ * with its remaining-life % and whether a reset action exists. No hardcoded
144186
+ * per-model list: a consumable the device does not expose never appears.
144187
+ *
144188
+ * `status` is `null` — the provider reports the level, the notification rule
144189
+ * decides what is low. The slice clocks are `nextConsumablesSlice`'s job. Pure.
144190
+ */
144191
+ function buildVacuumConsumableItems(readings) {
144192
+ return readings.map((c) => ({
144193
+ key: c.key,
144194
+ label: c.label,
144195
+ level: clampConsumablePct(c.leftPct),
144196
+ status: null,
144197
+ lastResetAt: null,
144198
+ resettable: c.resettable,
144199
+ form: "part"
144200
+ }));
144201
+ }
144202
+ /** Structural equality of two JSON-shaped values, key order ignored. */
144203
+ function jsonEqual(a, b) {
144204
+ if (a === b) return true;
144205
+ if (Array.isArray(a) || Array.isArray(b)) return Array.isArray(a) && Array.isArray(b) && a.length === b.length && a.every((v, i) => jsonEqual(v, b[i]));
144206
+ if (a === null || b === null || typeof a !== "object" || typeof b !== "object") return false;
144207
+ const ka = Object.keys(a);
144208
+ const kb = Object.keys(b);
144209
+ return ka.length === kb.length && ka.every((k) => jsonEqual(Reflect.get(a, k), Reflect.get(b, k)));
144210
+ }
144211
+ /**
144212
+ * The `vacuum-control` slice to write, or `null` when only the clock moved.
144213
+ * `recomputeSlice` runs on EVERY push; without this each one was a write (and a
144214
+ * `device.state-changed`) even when nothing the robot reported had changed. Pure.
144215
+ */
144216
+ function nextVacuumControlSlice(prev, next) {
144217
+ if (prev === void 0) return next;
144218
+ const { lastChangedAt: _prevAt, ...prevContent } = prev;
144219
+ const { lastChangedAt: _nextAt, ...nextContent } = next;
144220
+ return jsonEqual(prevContent, nextContent) ? null : next;
144221
+ }
144222
+ //#endregion
143316
144223
  //#region src/dreame-entity-catalog.ts
143317
144224
  var STATS = "Statistics";
143318
144225
  var MAINT = "Maintenance";
@@ -143959,14 +144866,6 @@ function enumOptions(e) {
143959
144866
  label: humanizeEnumName(name)
143960
144867
  }));
143961
144868
  }
143962
- /**
143963
- * MiotError codes that are BENIGN completion / reminder states, not active
143964
- * faults. HA does not surface these as errors — a fresh cloud-shadow read of an
143965
- * idle docked robot legitimately returns `TaskComplete` (68). We relabel them to
143966
- * the `Clear` label so the Error sensor only shows real faults (HA parity).
143967
- * Extend this set if other non-fault codes surface as spurious "errors".
143968
- */
143969
- var BENIGN_ERROR_CODES = new Set([MiotError.TaskComplete]);
143970
144869
  /** `MiotError` options with the benign completion codes remapped to the `Clear`
143971
144870
  * label, so the Error sensor reads as no-error for them (matches HA). */
143972
144871
  function errorEnumOptions() {
@@ -145324,321 +146223,6 @@ var DreameButtonDevice = class extends DreameChildDevice {
145324
146223
  recomputeSlice() {}
145325
146224
  };
145326
146225
  //#endregion
145327
- //#region src/dreame-vacuum-decode.ts
145328
- /**
145329
- * Pure decode helpers for the Dreame vacuum. Translate the library's MIoT enum
145330
- * values into the camstack `vacuum-control` slice shape. Unit-tested in
145331
- * isolation (no facade, no network) — mirrors `homematic-decode.ts`.
145332
- */
145333
- /**
145334
- * Map a Dreame {@link MiotState} (siid 2 piid 1) onto the camstack canonical
145335
- * `VacuumState` lifecycle (`idle` / `cleaning` / `paused` / `returning` /
145336
- * `docked` / `error`). A `null` state (not yet observed) settles on `idle`.
145337
- *
145338
- * The MIoT state space is large (~50 values); we collapse it onto the six
145339
- * canonical states by family. Anything unrecognised falls through to `idle`
145340
- * (the safe neutral) so a firmware-added code never throws.
145341
- */
145342
- function mapMiotStateToVacuumState(state) {
145343
- if (state === null) return "idle";
145344
- switch (state) {
145345
- case MiotState.Charging:
145346
- case MiotState.ChargingComplete:
145347
- case MiotState.IntelligentCharging:
145348
- case MiotState.CleanWashboardBase:
145349
- case MiotState.AutoRepairBase:
145350
- case MiotState.WaterSupplyDrainTest:
145351
- case MiotState.CleanMopRefillWater:
145352
- case MiotState.AutoEmptying:
145353
- case MiotState.Emptying:
145354
- case MiotState.Draining:
145355
- case MiotState.AutoWaterDraining:
145356
- case MiotState.Sanitizing: return "docked";
145357
- case MiotState.MopDrying:
145358
- case MiotState.DustBagDrying:
145359
- case MiotState.SanitizingWithDry: return "drying";
145360
- case MiotState.ReturningToCharge:
145361
- case MiotState.ReturningToWash:
145362
- case MiotState.ReturnInstallMop:
145363
- case MiotState.ReturnRemoveMop:
145364
- case MiotState.ReturnToEmpty:
145365
- case MiotState.ReturningToDrain: return "returning";
145366
- case MiotState.Paused:
145367
- case MiotState.PausedAlt:
145368
- case MiotState.PausedCleaning:
145369
- case MiotState.DustBagDryingPaused:
145370
- case MiotState.CameraMonitoringPaused:
145371
- case MiotState.FindingPetPaused:
145372
- case MiotState.InitialDeepCleanPaused:
145373
- case MiotState.ChangingMopPaused:
145374
- case MiotState.FloorMaintainingPaused: return "paused";
145375
- case MiotState.Standby:
145376
- case MiotState.WaitingForTask: return "idle";
145377
- default: return "cleaning";
145378
- }
145379
- }
145380
- /**
145381
- * The camstack `fanSpeed` token for a Dreame {@link SuctionLevel} — the enum
145382
- * member NAME, lower-cased (`Quiet` → `'quiet'`). Returns `null` for an
145383
- * unobserved / unknown level.
145384
- */
145385
- function suctionLevelToken(level) {
145386
- if (level === null) return null;
145387
- const name = SuctionLevel[level];
145388
- return typeof name === "string" ? name.toLowerCase() : null;
145389
- }
145390
- /**
145391
- * Resolve a camstack `fanSpeed` token back to the Dreame {@link SuctionLevel}
145392
- * enum value, validated against the model's `supported` levels. Returns null
145393
- * when the token doesn't match a supported level (the caller rejects the set).
145394
- */
145395
- function tokenToSuctionLevel(token, supported) {
145396
- const wanted = token.trim().toLowerCase();
145397
- for (const level of supported) if (suctionLevelToken(level) === wanted) return level;
145398
- return null;
145399
- }
145400
- /** The list of accepted `fanSpeed` tokens for a model's supported suction set. */
145401
- function supportedFanSpeedTokens(supported) {
145402
- const tokens = [];
145403
- for (const level of supported) {
145404
- const token = suctionLevelToken(level);
145405
- if (token !== null) tokens.push(token);
145406
- }
145407
- return tokens;
145408
- }
145409
- /**
145410
- * Clamp a raw battery reading (already a 0..100 percentage from the library)
145411
- * to the `vacuum-control` slice's `batteryLevel` field. Returns null for a
145412
- * null/NaN/out-of-band reading so the slice stays valid (the schema rejects
145413
- * a number outside 0..100).
145414
- */
145415
- function clampBatteryLevel$1(value) {
145416
- if (value === null || !Number.isFinite(value)) return null;
145417
- return Math.max(0, Math.min(100, value));
145418
- }
145419
- /** Clamp a 0..100 percentage (progress, coverage, …), null-safe. */
145420
- function clampPercent$1(value) {
145421
- if (value === null || !Number.isFinite(value)) return null;
145422
- return Math.max(0, Math.min(100, value));
145423
- }
145424
- /**
145425
- * Where each {@link VacuumTankProps} field lives (siid/piid). Property ids and
145426
- * enum values are the HA Dreame integration's (Tasshack `dreame-vacuum`
145427
- * `types.py`, `DreameVacuumProperty` → siid/piid map; same on `dev` and
145428
- * v2.0.0b25, checked 2026-09-26). 27/1 and 27/4 were already read here; 27/2 and
145429
- * 27/3 are their neighbours on the same station service. The robot pushes them
145430
- * on change; the consumables refresh also re-reads them from the shadow.
145431
- */
145432
- var VACUUM_TANK_PROPS = {
145433
- cleanWaterTankStatus: {
145434
- siid: 27,
145435
- piid: 1
145436
- },
145437
- waterTank: {
145438
- siid: 4,
145439
- piid: 6
145440
- },
145441
- detergentStatus: {
145442
- siid: 27,
145443
- piid: 4
145444
- },
145445
- dustCollection: {
145446
- siid: 15,
145447
- piid: 3
145448
- },
145449
- dirtyWaterTankStatus: {
145450
- siid: 27,
145451
- piid: 2
145452
- },
145453
- dustBagStatus: {
145454
- siid: 27,
145455
- piid: 3
145456
- }
145457
- };
145458
- /**
145459
- * Clean-water tank `status`: prefer the richer clean-water-tank-status (27/1),
145460
- * fall back to the coarser water-tank presence (4/6). `null` when neither maps
145461
- * to a known good/low reading (NOT_INSTALLED / CHECKING / unknown). Pure.
145462
- */
145463
- function cleanWaterTankStatus(props) {
145464
- if (props.cleanWaterTankStatus !== null) {
145465
- if (props.cleanWaterTankStatus === 0) return "ok";
145466
- if (props.cleanWaterTankStatus === 2) return "low";
145467
- return null;
145468
- }
145469
- if (props.waterTank === 1 || props.waterTank === 10) return "ok";
145470
- return null;
145471
- }
145472
- /** Detergent tank `status` from detergent-status (27/4). Pure. */
145473
- function detergentTankStatus(value) {
145474
- if (value === 0) return "ok";
145475
- if (value === 2) return "low";
145476
- return null;
145477
- }
145478
- /**
145479
- * Dust-bin `status` from dust-collection (15/3); OVER_USE reads as `full`. A dust
145480
- * bag that is missing or asks to be checked (27/3 NOT_INSTALLED / CHECK) is never
145481
- * `ok` — the enum does not say "full" either, so it is unknown. Pure.
145482
- */
145483
- function dustBinTankStatus(dustCollection, dustBag) {
145484
- if (dustCollection === 2) return "full";
145485
- if (dustBag !== null && dustBag !== 0) return null;
145486
- if (dustCollection === 1) return "ok";
145487
- return null;
145488
- }
145489
- /**
145490
- * Dirty-water tank `status` from dirty-water-tank-status (27/2). The robot has
145491
- * ONE value for "not installed or full" and cannot tell them apart; lifting the
145492
- * tank out to EMPTY it reads exactly like a full one, so that value is unknown
145493
- * (`null`) — never `full`, which would raise a false "tank full" at the moment
145494
- * someone is fixing it (controller ruling 2026-09-26). Absent or any other
145495
- * value is unknown too, never `ok`.
145496
- *
145497
- * The unambiguous "full" is the robot's own error `MiotError.DirtyWaterTankFull`
145498
- * (86, the library's `errorCode`, siid 2 piid 2): while it is raised the tank is
145499
- * `full` whatever 27/2 says; once it clears, 27/2 decides again. Pure.
145500
- */
145501
- function dirtyWaterTankStatus(value, errorCode) {
145502
- if (errorCode === MiotError.DirtyWaterTankFull) return "full";
145503
- if (value === 0) return "ok";
145504
- return null;
145505
- }
145506
- /**
145507
- * The four tanks (clean-water / dirty-water / detergent / dust-bin) decoded from the model's {@link VacuumCapabilities}, with the
145508
- * discrete `status` populated from the live MIoT enum props when supplied. Pure.
145509
- *
145510
- * The nodedreame library exposes NO tank FILL-LEVEL telemetry — the cloud shadow
145511
- * (verified against a real `dreame.vacuum.r2538z`) carries only the water-VOLUME
145512
- * *setting* (a mop-flow level, NOT a tank fill). We therefore never fabricate a
145513
- * `level`; `level` is always `null`. The cap contract is honoured on PRESENCE,
145514
- * with `status` derived from the tank-status enums:
145515
- *
145516
- * - a field is `null` when the hardware HAS NO such tank (capability flags), and
145517
- * - `{ level: null, status }` when the tank EXISTS — `status` from the enum
145518
- * prop ('ok' / 'low' / 'full'), or `null` when the device reports none.
145519
- *
145520
- * Presence is derived from the capability record:
145521
- * - clean-water ← `canMop` (a mopping robot carries a clean-water reservoir)
145522
- * - dirty-water ← `canMopWash` (a self-wash dock has a recovery/dirty tank)
145523
- * - detergent ← `hasDetergentReservoir`
145524
- * - dust-bin ← `canAutoEmpty` (an auto-empty robot has an onboard dust bin)
145525
- */
145526
- function decodeVacuumTanks(caps, props, errorCode = null) {
145527
- const tank = (present, status) => present ? {
145528
- level: null,
145529
- status
145530
- } : null;
145531
- return {
145532
- cleanWater: tank(Boolean(caps?.canMop), props ? cleanWaterTankStatus(props) : null),
145533
- dirtyWater: tank(Boolean(caps?.canMopWash), dirtyWaterTankStatus(props?.dirtyWaterTankStatus ?? null, errorCode)),
145534
- detergent: tank(Boolean(caps?.hasDetergentReservoir), props ? detergentTankStatus(props.detergentStatus) : null),
145535
- dustBin: tank(Boolean(caps?.canAutoEmpty), props ? dustBinTankStatus(props.dustCollection, props.dustBagStatus) : null)
145536
- };
145537
- }
145538
- /** How each tank is named and what it is, in the order the panel lists them. */
145539
- var TANK_ITEMS = [
145540
- {
145541
- key: "cleanWater",
145542
- label: "Clean water tank",
145543
- kind: "supply",
145544
- form: "tank"
145545
- },
145546
- {
145547
- key: "dirtyWater",
145548
- label: "Dirty water tank",
145549
- kind: "waste",
145550
- form: "tank"
145551
- },
145552
- {
145553
- key: "detergent",
145554
- label: "Detergent",
145555
- kind: "supply",
145556
- form: "product"
145557
- },
145558
- {
145559
- key: "dustBin",
145560
- label: "Dust bin",
145561
- kind: "waste",
145562
- form: "bin"
145563
- }
145564
- ];
145565
- /**
145566
- * The tanks as `consumables` items (D915): every PRESENT tank becomes a
145567
- * `supply` (clean water, detergent) or `waste` (dirty water, dust bin) item
145568
- * keyed by its old field name, drawn as a `tank` (the two water tanks), a
145569
- * `product` (detergent) or a `bin` (the dust bin) — the key the notification centre's tank
145570
- * episodes were built on, so an open episode carries on. A tank is never
145571
- * resettable: emptying or refilling it is the reset. Pure.
145572
- */
145573
- function vacuumTankItems(tanks) {
145574
- return TANK_ITEMS.flatMap(({ key, label, kind, form }) => {
145575
- const tank = tanks[key];
145576
- if (tank === null) return [];
145577
- return [{
145578
- key,
145579
- label,
145580
- level: tank.level,
145581
- status: tank.status,
145582
- kind,
145583
- form,
145584
- lastResetAt: null,
145585
- resettable: false
145586
- }];
145587
- });
145588
- }
145589
- /**
145590
- * Clamp a raw consumable percentage to `[0, 100]`, or return `null` when the
145591
- * value is absent / non-finite. The library reports remaining life as a 0..100
145592
- * integer; we clamp defensively so an out-of-range firmware value never violates
145593
- * the cap schema.
145594
- */
145595
- function clampConsumablePct(value) {
145596
- if (value === null || !Number.isFinite(value)) return null;
145597
- return Math.max(0, Math.min(100, value));
145598
- }
145599
- /**
145600
- * The `consumables` cap items for the consumables a vacuum REPORTS.
145601
- *
145602
- * PRESENCE-driven (mirrors HA): the library's `supportedConsumables` enumerates
145603
- * ONLY the consumables this model reports (a present MIoT life property), each
145604
- * with its remaining-life % and whether a reset action exists. No hardcoded
145605
- * per-model list: a consumable the device does not expose never appears.
145606
- *
145607
- * `status` is `null` — the provider reports the level, the notification rule
145608
- * decides what is low. The slice clocks are `nextConsumablesSlice`'s job. Pure.
145609
- */
145610
- function buildVacuumConsumableItems(readings) {
145611
- return readings.map((c) => ({
145612
- key: c.key,
145613
- label: c.label,
145614
- level: clampConsumablePct(c.leftPct),
145615
- status: null,
145616
- lastResetAt: null,
145617
- resettable: c.resettable,
145618
- form: "part"
145619
- }));
145620
- }
145621
- /** Structural equality of two JSON-shaped values, key order ignored. */
145622
- function jsonEqual(a, b) {
145623
- if (a === b) return true;
145624
- if (Array.isArray(a) || Array.isArray(b)) return Array.isArray(a) && Array.isArray(b) && a.length === b.length && a.every((v, i) => jsonEqual(v, b[i]));
145625
- if (a === null || b === null || typeof a !== "object" || typeof b !== "object") return false;
145626
- const ka = Object.keys(a);
145627
- const kb = Object.keys(b);
145628
- return ka.length === kb.length && ka.every((k) => jsonEqual(Reflect.get(a, k), Reflect.get(b, k)));
145629
- }
145630
- /**
145631
- * The `vacuum-control` slice to write, or `null` when only the clock moved.
145632
- * `recomputeSlice` runs on EVERY push; without this each one was a write (and a
145633
- * `device.state-changed`) even when nothing the robot reported had changed. Pure.
145634
- */
145635
- function nextVacuumControlSlice(prev, next) {
145636
- if (prev === void 0) return next;
145637
- const { lastChangedAt: _prevAt, ...prevContent } = prev;
145638
- const { lastChangedAt: _nextAt, ...nextContent } = next;
145639
- return jsonEqual(prevContent, nextContent) ? null : next;
145640
- }
145641
- //#endregion
145642
146226
  //#region src/devices/dreame-camera-stream.ts
145643
146227
  /**
145644
146228
  * The robot camera as a stream the broker can pull — the pure facts.
@@ -145673,7 +146257,7 @@ function clampLevel(level) {
145673
146257
  function cameraBatterySlice(facts) {
145674
146258
  const charging = facts.charging === null ? "unknown" : facts.charging === CHARGING_STATUS_CHARGING ? "dc" : "none";
145675
146259
  return {
145676
- percentage: clampLevel(facts.level),
146260
+ level: clampLevel(facts.level),
145677
146261
  charging,
145678
146262
  sleeping: !facts.streamOpen,
145679
146263
  lastUpdated: facts.now,
@@ -146082,7 +146666,7 @@ function networkLinkFromBars(bars, now) {
146082
146666
  if (bars === null || !Number.isFinite(bars)) return null;
146083
146667
  return {
146084
146668
  type: "wifi",
146085
- signalPercent: bars === 0 ? null : signalPercentFromBars(bars, 4),
146669
+ signal: bars === 0 ? null : signalPercentFromBars(bars, 4),
146086
146670
  lastUpdated: now
146087
146671
  };
146088
146672
  }
@@ -193285,7 +193869,7 @@ var SHADOW_POLL_MS = 12e4;
193285
193869
  var COLD_START = {
193286
193870
  activity: "idle",
193287
193871
  batteryLevel: null,
193288
- progressPercent: null,
193872
+ progress: null,
193289
193873
  currentCode: null,
193290
193874
  currentCodeLabel: null,
193291
193875
  severity: "info",
@@ -193399,11 +193983,11 @@ var DreameMowerDevice = class extends DreameChildDevice {
193399
193983
  const status = mower.status;
193400
193984
  const code = mower.faultRaw;
193401
193985
  const livePct = clampPercent(mower.mowingProgressPct);
193402
- const prevPct = this.runtimeState.getCapState(CAP_NAME$9)?.progressPercent;
193986
+ const prevPct = this.runtimeState.getCapState(CAP_NAME$9)?.progress;
193403
193987
  const slice = {
193404
193988
  activity: mapMowerStatusToActivity(status),
193405
193989
  batteryLevel: clampBatteryLevel(mower.battery),
193406
- progressPercent: livePct ?? prevPct ?? null,
193990
+ progress: livePct ?? prevPct ?? null,
193407
193991
  currentCode: code,
193408
193992
  currentCodeLabel: code !== null ? MowerFault[code] ?? null : null,
193409
193993
  severity: mower.faultSeverity,
@@ -194343,7 +194927,7 @@ var CONTROL_COLD_START = {
194343
194927
  batteryLevel: null,
194344
194928
  fanSpeed: null,
194345
194929
  availableFanSpeeds: [],
194346
- progressPercent: null,
194930
+ progress: null,
194347
194931
  errorCode: null,
194348
194932
  errorLabel: null,
194349
194933
  lastChangedAt: 0
@@ -194497,9 +195081,9 @@ var DreameVacuumDevice = class extends DreameChildDevice {
194497
195081
  batteryLevel: clampBatteryLevel$1(vacuum.battery),
194498
195082
  fanSpeed: suctionLevelToken(vacuum.suction),
194499
195083
  availableFanSpeeds: supportedFanSpeedTokens(vacuum.supportedSuctionLevels),
194500
- progressPercent: clampPercent$1(vacuum.taskProgressPct),
195084
+ progress: clampPercent$1(vacuum.taskProgressPct),
194501
195085
  errorCode: vacuum.errorCode,
194502
- errorLabel: vacuum.errorCode ? `Error ${vacuum.errorCode}` : null,
195086
+ errorLabel: vacuumErrorLabel(vacuum.errorCode),
194503
195087
  lastChangedAt: Date.now()
194504
195088
  };
194505
195089
  const toWrite = nextVacuumControlSlice(this.runtimeState.getCapState(CAP_NAME$1), controlSlice);