@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.mjs CHANGED
@@ -52899,7 +52899,7 @@ var DreameCameraStream = class extends TypedEmitter {
52899
52899
  }
52900
52900
  };
52901
52901
  //#endregion
52902
- //#region ../types/dist/event-category-CT-hz5Rb.mjs
52902
+ //#region ../types/dist/event-category-DmZDPDf_.mjs
52903
52903
  var EventCategory = /* @__PURE__ */ function(EventCategory) {
52904
52904
  EventCategory["SystemBoot"] = "system.boot";
52905
52905
  EventCategory["SystemAddonsReady"] = "system.addons-ready";
@@ -53526,7 +53526,7 @@ var EventCategory = /* @__PURE__ */ function(EventCategory) {
53526
53526
  * (a track was promoted to a stationary-registry entry) or departs (the
53527
53527
  * object moved / was removed). Telemetry (D8): lossy, drives a UI refresh of
53528
53528
  * the dedicated "Stationary" section — never the live event feed. Payload:
53529
- * `{ deviceId, entryId, className, phase:'appeared'|'departed', timestamp }`.
53529
+ * `{ deviceId, entryId, className, phase:'appeared'|'departed', timestamp, observedAt, zones }`.
53530
53530
  */
53531
53531
  EventCategory["PipelineAnalyticsStationaryChanged"] = "pipeline-analytics.stationary-changed";
53532
53532
  /**
@@ -53664,6 +53664,11 @@ var EventCategory = /* @__PURE__ */ function(EventCategory) {
53664
53664
  */
53665
53665
  EventCategory["AccessoriesChanged"] = "accessories.onAccessoriesChanged";
53666
53666
  /**
53667
+ * An action run started or ended. Emitted by the `actions` surface.
53668
+ * Payload: `ActionRunChanged` (`{ actionId, runId, isRunning, outcome?, lastRunAt, lastError }`).
53669
+ */
53670
+ EventCategory["ActionsRunChanged"] = "actions.onRunChanged";
53671
+ /**
53667
53672
  * Progress update from a running model conversion job.
53668
53673
  * Payload: `{ kind: 'model-convert', phase, sessionId?, pct?, detail? }`.
53669
53674
  * Emitted by `addon-model-studio` on the converting node.
@@ -58342,7 +58347,7 @@ var ZodIssueCode = {
58342
58347
  var ZodFirstPartyTypeKind;
58343
58348
  ZodFirstPartyTypeKind || (ZodFirstPartyTypeKind = {});
58344
58349
  //#endregion
58345
- //#region ../types/dist/sleep-BE7IAR2p.mjs
58350
+ //#region ../types/dist/sleep-Crlmu_tO.mjs
58346
58351
  /**
58347
58352
  * The audio chunk plane's byte format, and the ONE expansion from a coded
58348
58353
  * window to float samples (D455).
@@ -61462,6 +61467,8 @@ Object.freeze({ "doorbell": "lastPressedAt" });
61462
61467
  /** Every capability name, so addon code can check a name without the schema graph. */
61463
61468
  var CAP_NAMES = Object.freeze([
61464
61469
  "accessories",
61470
+ "action-source",
61471
+ "actions",
61465
61472
  "addon-pages",
61466
61473
  "addon-pages-source",
61467
61474
  "addon-routes",
@@ -61650,11 +61657,11 @@ var CAP_NAMES = Object.freeze([
61650
61657
  Object.freeze({
61651
61658
  "air-quality-sensor": [
61652
61659
  {
61653
- "path": "co2Ppm",
61660
+ "path": "co2",
61654
61661
  "valueType": "number"
61655
61662
  },
61656
61663
  {
61657
- "path": "vocPpb",
61664
+ "path": "voc",
61658
61665
  "valueType": "number"
61659
61666
  },
61660
61667
  {
@@ -61704,7 +61711,7 @@ Object.freeze({
61704
61711
  }
61705
61712
  ],
61706
61713
  "ambient-light-sensor": [{
61707
- "path": "lux",
61714
+ "path": "illuminance",
61708
61715
  "valueType": "number"
61709
61716
  }],
61710
61717
  "audio-metrics": [
@@ -61765,7 +61772,7 @@ Object.freeze({
61765
61772
  ],
61766
61773
  "battery": [
61767
61774
  {
61768
- "path": "percentage",
61775
+ "path": "level",
61769
61776
  "valueType": "number"
61770
61777
  },
61771
61778
  {
@@ -61787,7 +61794,7 @@ Object.freeze({
61787
61794
  "valueType": "number"
61788
61795
  },
61789
61796
  {
61790
- "path": "volts",
61797
+ "path": "voltage",
61791
61798
  "valueType": "number"
61792
61799
  },
61793
61800
  {
@@ -61808,7 +61815,7 @@ Object.freeze({
61808
61815
  "valueType": "boolean"
61809
61816
  }],
61810
61817
  "brightness": [{
61811
- "path": "percentage",
61818
+ "path": "level",
61812
61819
  "valueType": "number"
61813
61820
  }],
61814
61821
  "broker": [{
@@ -62009,7 +62016,7 @@ Object.freeze({
62009
62016
  "valueType": "number"
62010
62017
  },
62011
62018
  {
62012
- "path": "mireds",
62019
+ "path": "colorTemperature",
62013
62020
  "valueType": "number"
62014
62021
  }
62015
62022
  ],
@@ -62188,15 +62195,15 @@ Object.freeze({
62188
62195
  "valueType": "string"
62189
62196
  },
62190
62197
  {
62191
- "path": "remainingSeconds",
62198
+ "path": "remaining",
62192
62199
  "valueType": "number"
62193
62200
  },
62194
62201
  {
62195
- "path": "delayedStartSeconds",
62202
+ "path": "delayedStart",
62196
62203
  "valueType": "number"
62197
62204
  },
62198
62205
  {
62199
- "path": "progressPercent",
62206
+ "path": "progress",
62200
62207
  "valueType": "number"
62201
62208
  },
62202
62209
  {
@@ -62310,7 +62317,7 @@ Object.freeze({
62310
62317
  ],
62311
62318
  "fan-control": [
62312
62319
  {
62313
- "path": "percentage",
62320
+ "path": "speed",
62314
62321
  "valueType": "number"
62315
62322
  },
62316
62323
  {
@@ -62410,7 +62417,7 @@ Object.freeze({
62410
62417
  }
62411
62418
  ],
62412
62419
  "humidity-sensor": [{
62413
- "path": "percent",
62420
+ "path": "humidity",
62414
62421
  "valueType": "number"
62415
62422
  }],
62416
62423
  "image": [{
@@ -62520,7 +62527,7 @@ Object.freeze({
62520
62527
  "valueType": "number"
62521
62528
  },
62522
62529
  {
62523
- "path": "progressPercent",
62530
+ "path": "progress",
62524
62531
  "valueType": "number"
62525
62532
  },
62526
62533
  {
@@ -62599,11 +62606,11 @@ Object.freeze({
62599
62606
  "valueType": "string"
62600
62607
  },
62601
62608
  {
62602
- "path": "positionMs",
62609
+ "path": "position",
62603
62610
  "valueType": "number"
62604
62611
  },
62605
62612
  {
62606
- "path": "durationMs",
62613
+ "path": "duration",
62607
62614
  "valueType": "number"
62608
62615
  },
62609
62616
  {
@@ -62705,11 +62712,11 @@ Object.freeze({
62705
62712
  ]
62706
62713
  },
62707
62714
  {
62708
- "path": "signalPercent",
62715
+ "path": "signal",
62709
62716
  "valueType": "number"
62710
62717
  },
62711
62718
  {
62712
- "path": "rssiDbm",
62719
+ "path": "rssi",
62713
62720
  "valueType": "number"
62714
62721
  },
62715
62722
  {
@@ -62840,23 +62847,23 @@ Object.freeze({
62840
62847
  ],
62841
62848
  "power-meter": [
62842
62849
  {
62843
- "path": "watts",
62850
+ "path": "power",
62844
62851
  "valueType": "number"
62845
62852
  },
62846
62853
  {
62847
- "path": "kwhTotal",
62854
+ "path": "energy",
62848
62855
  "valueType": "number"
62849
62856
  },
62850
62857
  {
62851
- "path": "volts",
62858
+ "path": "voltage",
62852
62859
  "valueType": "number"
62853
62860
  },
62854
62861
  {
62855
- "path": "amps",
62862
+ "path": "current",
62856
62863
  "valueType": "number"
62857
62864
  },
62858
62865
  {
62859
- "path": "va",
62866
+ "path": "apparentPower",
62860
62867
  "valueType": "number"
62861
62868
  },
62862
62869
  {
@@ -62884,14 +62891,10 @@ Object.freeze({
62884
62891
  {
62885
62892
  "path": "gps.accuracyMeters",
62886
62893
  "valueType": "number"
62887
- },
62888
- {
62889
- "path": "batteryPercent",
62890
- "valueType": "number"
62891
62894
  }
62892
62895
  ],
62893
62896
  "pressure-sensor": [{
62894
- "path": "hpa",
62897
+ "path": "pressure",
62895
62898
  "valueType": "number"
62896
62899
  }],
62897
62900
  "privacy-mask": [{
@@ -63205,7 +63208,7 @@ Object.freeze({
63205
63208
  "valueType": "boolean"
63206
63209
  }],
63207
63210
  "temperature-sensor": [{
63208
- "path": "celsius",
63211
+ "path": "temperature",
63209
63212
  "valueType": "number"
63210
63213
  }],
63211
63214
  "update": [
@@ -63253,7 +63256,7 @@ Object.freeze({
63253
63256
  "valueType": "string"
63254
63257
  },
63255
63258
  {
63256
- "path": "progressPercent",
63259
+ "path": "progress",
63257
63260
  "valueType": "number"
63258
63261
  },
63259
63262
  {
@@ -63752,7 +63755,7 @@ function errMsg(err) {
63752
63755
  return String(err);
63753
63756
  }
63754
63757
  //#endregion
63755
- //#region ../types/dist/composition-Bb8iwHEJ.mjs
63758
+ //#region ../types/dist/composition-CZImLpOI.mjs
63756
63759
  /**
63757
63760
  * The ffmpeg build every CamStack node downloads, per platform and arch.
63758
63761
  *
@@ -68516,6 +68519,14 @@ var AutomationConditionLeafSchema = object({
68516
68519
  array(union([string(), number()]))
68517
68520
  ])
68518
68521
  });
68522
+ var AutomationConditionStationaryPresentSchema = object({
68523
+ kind: literal("stationary-present"),
68524
+ deviceId: number().int().nonnegative(),
68525
+ /** Class names; a macro class matches every raw class that maps to it. */
68526
+ classes: array(string().min(1)).min(1).optional(),
68527
+ /** The parked object must be in at least one of these zones. */
68528
+ zones: array(string().min(1)).min(1).optional()
68529
+ });
68519
68530
  /**
68520
68531
  * The expression leaf, declared as a plain object rather than an intersection
68521
68532
  * with {@link ExpressionSourceSchema}: a discriminated union has to be able to
@@ -68549,7 +68560,8 @@ var AutomationConditionSchema = lazy(() => discriminatedUnion("kind", [
68549
68560
  child: AutomationConditionSchema
68550
68561
  }),
68551
68562
  AutomationConditionLeafSchema,
68552
- AutomationConditionExpressionSchema
68563
+ AutomationConditionExpressionSchema,
68564
+ AutomationConditionStationaryPresentSchema
68553
68565
  ]));
68554
68566
  /**
68555
68567
  * What starts a run.
@@ -68590,7 +68602,34 @@ var AutomationTriggerSchema = discriminatedUnion("kind", [
68590
68602
  kind: literal("schedule"),
68591
68603
  cron: string().min(1).max(120)
68592
68604
  }),
68593
- object({ kind: literal("manual") })
68605
+ object({ kind: literal("manual") }),
68606
+ object({
68607
+ kind: literal("detection"),
68608
+ /** Class names. A macro class (`animal`, `vehicle`, `person`, `package`)
68609
+ * matches every raw class that maps to it; any other name matches
68610
+ * exactly. */
68611
+ classes: array(string().min(1)).min(1),
68612
+ deviceIds: array(number().int().nonnegative()).min(1),
68613
+ /** The track must have visited at least one of these zones. */
68614
+ zones: array(string().min(1)).min(1).optional(),
68615
+ /** Best-so-far confidence the track must reach. */
68616
+ minScore: number().min(0).max(1).optional()
68617
+ }),
68618
+ object({
68619
+ kind: literal("stationary"),
68620
+ deviceIds: array(number().int().nonnegative()).min(1),
68621
+ /** Class names; a macro class matches like the detection trigger. */
68622
+ classes: array(string().min(1)).min(1).optional(),
68623
+ /** The parked object must be in at least one of these zones. */
68624
+ zones: array(string().min(1)).min(1).optional(),
68625
+ /** `appeared`: it became parked. `departed`: the registry retired it.
68626
+ * The vocabulary of `PipelineAnalyticsStationaryChanged`. */
68627
+ phase: _enum(["appeared", "departed"])
68628
+ }),
68629
+ object({
68630
+ kind: literal("interval"),
68631
+ minutes: number().int().min(1).max(1440)
68632
+ })
68594
68633
  ]);
68595
68634
  /**
68596
68635
  * One action step.
@@ -68610,28 +68649,47 @@ var AutomationTriggerSchema = discriminatedUnion("kind", [
68610
68649
  * is called. The runner still receives a number and is untouched; the
68611
68650
  * resolution is the recipe's job, not the runner's.
68612
68651
  */
68652
+ var AutomationWaitStepSchema = object({
68653
+ kind: literal("wait"),
68654
+ seconds: number().min(0).max(300)
68655
+ });
68656
+ var AutomationCapStepSchema = object({
68657
+ kind: literal("cap"),
68658
+ /**
68659
+ * One device, several same-family devices (locks / climate family / …),
68660
+ * or `{ $var }` for the device that triggered the run. An array is
68661
+ * fan-out at codegen — each id gets the same cap.method call.
68662
+ */
68663
+ deviceId: union([
68664
+ number().int(),
68665
+ array(number().int()).min(1),
68666
+ object({ $var: string().min(1) })
68667
+ ]),
68668
+ cap: string().min(1),
68669
+ method: string().min(1),
68670
+ /** Values may carry `{{vars.x}}` slots, or be exactly `{ $var: 'name' }`, which
68671
+ * SUBSTITUTE and do not evaluate (§3.2.3). Anything beyond substitution is
68672
+ * the expression leaf. */
68673
+ args: record(string(), unknown()).optional()
68674
+ });
68675
+ /**
68676
+ * The steps an internal Action may contain: cap and wait, and nothing that can
68677
+ * call back into an Action — so an Action can never reach itself (no cycles).
68678
+ */
68679
+ var ActionStepSchema = discriminatedUnion("kind", [AutomationWaitStepSchema, AutomationCapStepSchema]);
68613
68680
  var AutomationActionSchema = discriminatedUnion("kind", [
68681
+ AutomationWaitStepSchema,
68682
+ AutomationCapStepSchema,
68614
68683
  object({
68615
- kind: literal("wait"),
68616
- seconds: number().min(0).max(300)
68617
- }),
68618
- object({
68619
- kind: literal("cap"),
68620
- /**
68621
- * One device, several same-family devices (locks / climate family / …),
68622
- * or `{ $var }` for the device that triggered the run. An array is
68623
- * fan-out at codegen — each id gets the same cap.method call.
68624
- */
68625
- deviceId: union([
68626
- number().int(),
68627
- array(number().int()).min(1),
68628
- object({ $var: string().min(1) })
68629
- ]),
68630
- cap: string().min(1),
68631
- method: string().min(1),
68632
- /** Values may carry `{{vars.x}}` slots, which SUBSTITUTE and do not
68633
- * evaluate (§3.2.3). Anything beyond substitution is the expression leaf. */
68634
- args: record(string(), unknown()).optional()
68684
+ kind: literal("action"),
68685
+ /** An entry of the `actions` surface (`internal:<uuid>` or `<addonId>:<localId>`). */
68686
+ actionId: string().min(1),
68687
+ /** Values for the action's declared variables; the others take their default. */
68688
+ variables: record(string(), union([
68689
+ string(),
68690
+ number(),
68691
+ boolean()
68692
+ ])).optional()
68635
68693
  }),
68636
68694
  object({
68637
68695
  kind: literal("code"),
@@ -68640,10 +68698,357 @@ var AutomationActionSchema = discriminatedUnion("kind", [
68640
68698
  code: string().min(1).max(2e4)
68641
68699
  })
68642
68700
  ]);
68701
+ /**
68702
+ * How a trigger that arrives while the automation is busy (or cooling down) is
68703
+ * handled. One of the automation's execution modes, never a property of its
68704
+ * actions. Absent means `single`.
68705
+ *
68706
+ * - `single` — drop the trigger while a run is in progress (traced + logged).
68707
+ * - `restart` — abort the run in progress (at its next step boundary), start fresh.
68708
+ * - `queued` — queue up to `max` triggers, drop the rest (traced).
68709
+ * - `parallel` — up to `max` runs at once, drop the rest (traced).
68710
+ * - `cooldown` — `single`, and no new run until `minutes` after the last run
68711
+ * STARTED; persisted across a restart. A manual trigger bypasses only this
68712
+ * wait, never the single/queued limits.
68713
+ */
68714
+ var AutomationRunModeSchema = discriminatedUnion("mode", [
68715
+ object({ mode: literal("single") }),
68716
+ object({ mode: literal("restart") }),
68717
+ object({
68718
+ mode: literal("queued"),
68719
+ max: number().int().min(1).max(10)
68720
+ }),
68721
+ object({
68722
+ mode: literal("parallel"),
68723
+ max: number().int().min(1).max(10)
68724
+ }),
68725
+ object({
68726
+ mode: literal("cooldown"),
68727
+ minutes: number().int().min(1).max(1440)
68728
+ })
68729
+ ]);
68643
68730
  var AutomationRecipeSchema = object({
68644
68731
  triggers: array(AutomationTriggerSchema),
68645
68732
  conditions: AutomationConditionSchema.optional(),
68646
- actions: array(AutomationActionSchema)
68733
+ actions: array(AutomationActionSchema),
68734
+ runMode: AutomationRunModeSchema.optional(),
68735
+ /** How many run traces the automation keeps (default {@link DEFAULT_TRACE_KEEP}). */
68736
+ keepTraces: number().int().min(1).max(100).optional()
68737
+ });
68738
+ new Set([
68739
+ {
68740
+ id: "person",
68741
+ name: "Person"
68742
+ },
68743
+ {
68744
+ id: "vehicle",
68745
+ name: "Vehicle"
68746
+ },
68747
+ {
68748
+ id: "animal",
68749
+ name: "Animal"
68750
+ },
68751
+ {
68752
+ id: "package",
68753
+ name: "Package"
68754
+ }
68755
+ ].map((l) => l.id));
68756
+ var COCO_TO_MACRO = {
68757
+ mapping: {
68758
+ person: "person",
68759
+ bicycle: "vehicle",
68760
+ car: "vehicle",
68761
+ motorcycle: "vehicle",
68762
+ airplane: "vehicle",
68763
+ bus: "vehicle",
68764
+ train: "vehicle",
68765
+ truck: "vehicle",
68766
+ boat: "vehicle",
68767
+ bird: "animal",
68768
+ cat: "animal",
68769
+ dog: "animal",
68770
+ horse: "animal",
68771
+ sheep: "animal",
68772
+ cow: "animal",
68773
+ elephant: "animal",
68774
+ bear: "animal",
68775
+ zebra: "animal",
68776
+ giraffe: "animal",
68777
+ suitcase: "package",
68778
+ backpack: "package",
68779
+ handbag: "package"
68780
+ },
68781
+ preserveOriginal: false
68782
+ };
68783
+ new Set(Object.values(COCO_TO_MACRO.mapping));
68784
+ /**
68785
+ * Run traces — what an automation (or, later, an Action run) DID, node by node,
68786
+ * so the editor can paint every block green or red with its reason.
68787
+ *
68788
+ * A trace is data, not a log: it covers the WHOLE tree — the trigger stage,
68789
+ * every condition node (containers and leaves, including the ones a
68790
+ * short-circuit never reached), the run-mode verdict and every action step —
68791
+ * each addressed by a stable path (`conditions.0.1`, `actions.2`, or `steps.N`
68792
+ * for an Action run). Skipped
68793
+ * evaluations are traced too, and identical consecutive skips collapse into a
68794
+ * repeat count so a trigger that fails every 5 minutes cannot flush real runs.
68795
+ *
68796
+ * {@link AutomationStepTraceSchema} is deliberately free of automation
68797
+ * vocabulary so an Action run records the same step shape.
68798
+ */
68799
+ /** A value a trace may show: JSON primitives and short lists, nothing deeper. */
68800
+ var TraceValueSchema = union([
68801
+ string(),
68802
+ number(),
68803
+ boolean(),
68804
+ _null(),
68805
+ array(union([string(), number()]))
68806
+ ]);
68807
+ var AutomationRunOutcomeSchema = _enum([
68808
+ "ran",
68809
+ "skipped-trigger",
68810
+ "skipped-conditions",
68811
+ "skipped-cooldown",
68812
+ "skipped-overlap",
68813
+ "failed",
68814
+ "aborted"
68815
+ ]);
68816
+ /** `not-evaluated` = a short-circuit never reached the node. */
68817
+ var TraceNodeOutcomeSchema = _enum([
68818
+ "pass",
68819
+ "fail",
68820
+ "not-evaluated"
68821
+ ]);
68822
+ var AutomationTriggerTraceSchema = object({
68823
+ kind: string().min(1),
68824
+ outcome: _enum(["pass", "fail"]),
68825
+ /** Why an event was rejected ("class person not in [dog]") or what fired. */
68826
+ reason: string(),
68827
+ /** deviceId, className, trackId, score … — only what the trigger exposes. */
68828
+ vars: record(string(), TraceValueSchema)
68829
+ });
68830
+ var AutomationConditionTraceNodeSchema = object({
68831
+ /** `conditions` for the root, `conditions.<i>` for its i-th child, … */
68832
+ path: string().min(1),
68833
+ kind: _enum([
68834
+ "all",
68835
+ "any",
68836
+ "not",
68837
+ "condition",
68838
+ "expression",
68839
+ "stationary-present"
68840
+ ]),
68841
+ outcome: TraceNodeOutcomeSchema,
68842
+ reason: string(),
68843
+ deviceId: number().int().optional(),
68844
+ cap: string().optional(),
68845
+ fieldPath: string().optional(),
68846
+ operator: string().optional(),
68847
+ expected: TraceValueSchema.optional(),
68848
+ /** What the mirror held; `null` when it held nothing (unknown). */
68849
+ observed: TraceValueSchema.optional()
68850
+ });
68851
+ var AutomationRunVerdictSchema = object({
68852
+ mode: AutomationRunModeSchema,
68853
+ decision: _enum([
68854
+ "run",
68855
+ "queued",
68856
+ "restart",
68857
+ "drop",
68858
+ "cooldown",
68859
+ "bypass"
68860
+ ]),
68861
+ reason: string()
68862
+ });
68863
+ /** One executed (or not-run) step — shared by automations and Action runs. */
68864
+ var AutomationStepTraceSchema = object({
68865
+ /** `actions.<i>` */
68866
+ path: string().min(1),
68867
+ kind: string().min(1),
68868
+ cap: string().optional(),
68869
+ method: string().optional(),
68870
+ deviceId: number().int().optional(),
68871
+ startedAt: number().optional(),
68872
+ endedAt: number().optional(),
68873
+ outcome: _enum([
68874
+ "ok",
68875
+ "error",
68876
+ "not-run"
68877
+ ]),
68878
+ message: string().optional()
68879
+ });
68880
+ var AutomationRunTraceSchema = object({
68881
+ runId: string().min(1),
68882
+ startedAt: number(),
68883
+ endedAt: number(),
68884
+ trigger: AutomationTriggerTraceSchema,
68885
+ conditions: array(AutomationConditionTraceNodeSchema),
68886
+ verdict: AutomationRunVerdictSchema.optional(),
68887
+ steps: array(AutomationStepTraceSchema),
68888
+ outcome: AutomationRunOutcomeSchema,
68889
+ /** How many identical consecutive skips this trace stands for (>= 1). */
68890
+ repeat: number().int().min(1),
68891
+ /** When the latest of those repeats happened. */
68892
+ lastAt: number()
68893
+ });
68894
+ /**
68895
+ * What the hub's `runs` ledger holds for one owner (`automation:<blockId>` or
68896
+ * `action:<actionId>`): the cooldown stamp and the trace ring.
68897
+ */
68898
+ var AutomationStateDocSchema = object({
68899
+ version: literal(1),
68900
+ lastRunStartedAt: number().nullable(),
68901
+ traces: array(AutomationRunTraceSchema)
68902
+ });
68903
+ /**
68904
+ * Actions — named, runnable things an operator (or an automation) can start,
68905
+ * declared as LIST ENTRIES, never as devices.
68906
+ *
68907
+ * An entry is either **internal** (a short list of cap/wait steps kept in the
68908
+ * hub's `actions` record kind) or declared by an addon through the
68909
+ * `action-source` collection (a Home Assistant script, a scene …). The one
68910
+ * surface the UI and automations talk to is the `actions` cap; it keeps no
68911
+ * catalog of its own (D3) — every `list` asks the providers live.
68912
+ *
68913
+ * This file is the data half: the entry schema, the variables an entry may
68914
+ * declare, and the pure resolution of the values a run was given. It holds no
68915
+ * runtime.
68916
+ */
68917
+ /** What a variable's value is. `select` takes one of `options`. */
68918
+ var ActionVariableTypeSchema = _enum([
68919
+ "string",
68920
+ "number",
68921
+ "boolean",
68922
+ "select"
68923
+ ]);
68924
+ var ACTION_VARIABLE_NAME_RE = /^[A-Za-z_][A-Za-z0-9_]*$/;
68925
+ var ActionVariableValueSchema = union([
68926
+ string(),
68927
+ number(),
68928
+ boolean()
68929
+ ]);
68930
+ /**
68931
+ * One declared variable. Steps reference it as `{ $var: 'name' }` (the whole
68932
+ * value) or `{{vars.name}}` (inside a string) in their args.
68933
+ */
68934
+ var ActionVariableSchema = object({
68935
+ name: string().regex(ACTION_VARIABLE_NAME_RE),
68936
+ label: string().min(1).max(120).optional(),
68937
+ type: ActionVariableTypeSchema,
68938
+ required: boolean().optional(),
68939
+ default: ActionVariableValueSchema.optional(),
68940
+ /** The allowed values of a `select`. */
68941
+ options: array(string().min(1)).min(1).optional()
68942
+ }).superRefine((v, ctx) => {
68943
+ if (v.type === "select" && v.options === void 0) ctx.addIssue({
68944
+ code: "custom",
68945
+ message: "a select variable needs options",
68946
+ path: ["options"]
68947
+ });
68948
+ if (v.default !== void 0 && variableValueError(v, v.default) !== null) ctx.addIssue({
68949
+ code: "custom",
68950
+ message: variableValueError(v, v.default) ?? "invalid default",
68951
+ path: ["default"]
68952
+ });
68953
+ });
68954
+ /** Why `value` is not a legal value for the variable, or null when it is. */
68955
+ function variableValueError(variable, value) {
68956
+ switch (variable.type) {
68957
+ case "string": return typeof value === "string" ? null : `variable "${variable.name}" must be a string`;
68958
+ case "number": return typeof value === "number" && Number.isFinite(value) ? null : `variable "${variable.name}" must be a number`;
68959
+ case "boolean": return typeof value === "boolean" ? null : `variable "${variable.name}" must be true or false`;
68960
+ case "select": return typeof value === "string" && (variable.options ?? []).includes(value) ? null : `variable "${variable.name}" must be one of ${(variable.options ?? []).join(", ")}`;
68961
+ }
68962
+ }
68963
+ var ActionSourceKindSchema = _enum(["internal", "addon"]);
68964
+ function isInternalActionId(actionId) {
68965
+ return actionId.startsWith("internal:") && actionId.length > 9;
68966
+ }
68967
+ /** The addon behind a `<addonId>:<localId>` id, or null for anything else (internal included). */
68968
+ function addonIdOfActionId(actionId) {
68969
+ if (isInternalActionId(actionId)) return null;
68970
+ const at = actionId.indexOf(":");
68971
+ return at > 0 && at < actionId.length - 1 ? actionId.slice(0, at) : null;
68972
+ }
68973
+ var ActionStatusSchema = object({
68974
+ isRunning: boolean(),
68975
+ /** Ms epoch the latest run STARTED; null when it never ran. */
68976
+ lastRunAt: number().nullable(),
68977
+ /** The latest run's failure; null on success or when it never ran. */
68978
+ lastError: string().nullable()
68979
+ });
68980
+ var ActionEntrySchema = object({
68981
+ /** `internal:<uuid>` or `<addonId>:<localId>`. */
68982
+ id: string().min(3),
68983
+ name: string().min(1).max(120),
68984
+ /** The addon that declared it; `core-blocks` for an internal one. */
68985
+ addonId: string().min(1),
68986
+ source: ActionSourceKindSchema,
68987
+ /** An addon-declared entry is edited where it was declared. */
68988
+ readOnly: boolean(),
68989
+ /** Internal only: cap and wait steps. No action-in-action, so no cycles. */
68990
+ steps: array(ActionStepSchema).min(1).max(50).optional(),
68991
+ variables: array(ActionVariableSchema).max(20).optional(),
68992
+ status: ActionStatusSchema
68993
+ }).superRefine((entry, ctx) => {
68994
+ if (entry.source === "internal") {
68995
+ if (!isInternalActionId(entry.id)) ctx.addIssue({
68996
+ code: "custom",
68997
+ message: "an internal action id is internal:<uuid>",
68998
+ path: ["id"]
68999
+ });
69000
+ return;
69001
+ }
69002
+ if (entry.steps !== void 0) ctx.addIssue({
69003
+ code: "custom",
69004
+ message: "only an internal action carries steps",
69005
+ path: ["steps"]
69006
+ });
69007
+ if (!entry.readOnly) ctx.addIssue({
69008
+ code: "custom",
69009
+ message: "an addon action is read-only here",
69010
+ path: ["readOnly"]
69011
+ });
69012
+ if (addonIdOfActionId(entry.id) === null) ctx.addIssue({
69013
+ code: "custom",
69014
+ message: "an addon action id is <addonId>:<localId>",
69015
+ path: ["id"]
69016
+ });
69017
+ });
69018
+ /** What an operator writes for an internal action. */
69019
+ var ActionInputSchema = object({
69020
+ name: string().min(1).max(120),
69021
+ steps: array(ActionStepSchema).min(1).max(50),
69022
+ variables: array(ActionVariableSchema).max(20).optional()
69023
+ });
69024
+ /** Who started a run. Absent = the operator pressed it. */
69025
+ var ActionRunOriginSchema = object({
69026
+ /** The automation block whose step started it. */
69027
+ blockId: string().min(1).optional() });
69028
+ /** `started` = the caller did not ask to wait. */
69029
+ var ActionRunOutcomeSchema = union([AutomationRunOutcomeSchema, literal("started")]);
69030
+ var ActionRunResultSchema = object({
69031
+ runId: string().min(1),
69032
+ outcome: ActionRunOutcomeSchema,
69033
+ /** Why it did not run, or what failed. */
69034
+ message: string().optional()
69035
+ });
69036
+ _enum([
69037
+ "not-found",
69038
+ "read-only",
69039
+ "invalid-variables",
69040
+ "invalid-steps",
69041
+ "not-linked",
69042
+ "busy"
69043
+ ]);
69044
+ object({
69045
+ actionId: string(),
69046
+ runId: string(),
69047
+ isRunning: boolean(),
69048
+ /** Set when the run ended. */
69049
+ outcome: AutomationRunOutcomeSchema.optional(),
69050
+ lastRunAt: number().nullable(),
69051
+ lastError: string().nullable()
68647
69052
  });
68648
69053
  /**
68649
69054
  * `addon-pages` — system-scoped singleton aggregator cap. Public-facing
@@ -71330,7 +71735,7 @@ var CompositionEventPlanSchema = object({
71330
71735
  stateField: string(),
71331
71736
  fieldKey: string()
71332
71737
  });
71333
- /** One refusal, named (D391). `path` points into the composition, e.g. `features[0].fields.celsius`. */
71738
+ /** One refusal, named (D391). `path` points into the composition, e.g. `features[0].fields.temperature`. */
71334
71739
  var CompositionProblemSchema = object({
71335
71740
  code: _enum([
71336
71741
  "reserved-variant",
@@ -71649,7 +72054,7 @@ var GROUP_TEMPLATES = [
71649
72054
  summary: "Mean, median, min, max or sum of temperatures. Offline and unknown members are left out, never read as 0 °C.",
71650
72055
  role: DeviceRole.TemperatureSensor,
71651
72056
  outputCap: "temperature-sensor",
71652
- outputField: "celsius",
72057
+ outputField: "temperature",
71653
72058
  quantity: "temperature",
71654
72059
  defaultAggregate: "mean",
71655
72060
  empty: "null",
@@ -71660,7 +72065,7 @@ var GROUP_TEMPLATES = [
71660
72065
  summary: "Humidity in percent. Offline and unknown members are left out, never read as 0 %.",
71661
72066
  role: DeviceRole.HumiditySensor,
71662
72067
  outputCap: "humidity-sensor",
71663
- outputField: "percent",
72068
+ outputField: "humidity",
71664
72069
  quantity: "humidity",
71665
72070
  defaultAggregate: "mean",
71666
72071
  empty: "null",
@@ -71671,7 +72076,7 @@ var GROUP_TEMPLATES = [
71671
72076
  summary: "Pressure in hectopascals. Offline and unknown members are left out, never read as 0 hPa.",
71672
72077
  role: DeviceRole.PressureSensor,
71673
72078
  outputCap: "pressure-sensor",
71674
- outputField: "hpa",
72079
+ outputField: "pressure",
71675
72080
  quantity: "pressure",
71676
72081
  defaultAggregate: "mean",
71677
72082
  empty: "null",
@@ -71682,7 +72087,7 @@ var GROUP_TEMPLATES = [
71682
72087
  summary: "Illuminance in lux. Offline and unknown members are left out, never read as 0 lx.",
71683
72088
  role: DeviceRole.AmbientLightSensor,
71684
72089
  outputCap: "ambient-light-sensor",
71685
- outputField: "lux",
72090
+ outputField: "illuminance",
71686
72091
  quantity: "illuminance",
71687
72092
  defaultAggregate: "mean",
71688
72093
  empty: "null",
@@ -71693,7 +72098,7 @@ var GROUP_TEMPLATES = [
71693
72098
  summary: "Power in watts, summed by default. Nobody reporting omits the key rather than writing 0 W.",
71694
72099
  role: DeviceRole.PowerSensor,
71695
72100
  outputCap: "power-meter",
71696
- outputField: "watts",
72101
+ outputField: "power",
71697
72102
  quantity: "power",
71698
72103
  defaultAggregate: "sum",
71699
72104
  empty: "omit",
@@ -71704,7 +72109,7 @@ var GROUP_TEMPLATES = [
71704
72109
  summary: "Energy in kilowatt-hours, summed by default. Nobody reporting omits the key rather than writing 0 kWh.",
71705
72110
  role: DeviceRole.EnergySensor,
71706
72111
  outputCap: "power-meter",
71707
- outputField: "kwhTotal",
72112
+ outputField: "energy",
71708
72113
  quantity: "energy",
71709
72114
  defaultAggregate: "sum",
71710
72115
  empty: "omit",
@@ -71715,7 +72120,7 @@ var GROUP_TEMPLATES = [
71715
72120
  summary: "Battery charge in percent. Offline and unknown members are left out, never read as 0 %.",
71716
72121
  role: DeviceRole.BatterySensor,
71717
72122
  outputCap: "battery",
71718
- outputField: "percentage",
72123
+ outputField: "level",
71719
72124
  quantity: "percentage",
71720
72125
  defaultAggregate: "mean",
71721
72126
  empty: "null",
@@ -72293,7 +72698,18 @@ method(object({}), object({ blocks: array(CoreBlockSchema) }), { auth: "admin" }
72293
72698
  kind: "mutation",
72294
72699
  auth: "admin",
72295
72700
  access: "view"
72296
- });
72701
+ }), method(object({
72702
+ blockId: string().min(1),
72703
+ trace: AutomationRunTraceSchema.optional(),
72704
+ runStartedAt: number().optional(),
72705
+ keepTraces: number().int().min(1).max(100).optional()
72706
+ }), _void(), {
72707
+ kind: "mutation",
72708
+ auth: "admin"
72709
+ }), method(object({ blockId: string().min(1) }), AutomationStateDocSchema, { auth: "admin" }), method(object({ blockId: string().min(1) }), object({
72710
+ traces: array(AutomationRunTraceSchema),
72711
+ lastError: string().nullable()
72712
+ }), { auth: "admin" });
72297
72713
  /**
72298
72714
  * `custom-model-registry` — collection cap exposing operator-registered
72299
72715
  * custom detection models. Each provider (today: `addon-model-studio`)
@@ -97681,51 +98097,6 @@ method(object({}), array(TargetKindSchema)), method(object({}), array(TargetSche
97681
98097
  kind: "mutation",
97682
98098
  auth: "admin"
97683
98099
  });
97684
- new Set([
97685
- {
97686
- id: "person",
97687
- name: "Person"
97688
- },
97689
- {
97690
- id: "vehicle",
97691
- name: "Vehicle"
97692
- },
97693
- {
97694
- id: "animal",
97695
- name: "Animal"
97696
- },
97697
- {
97698
- id: "package",
97699
- name: "Package"
97700
- }
97701
- ].map((l) => l.id));
97702
- var COCO_TO_MACRO = {
97703
- mapping: {
97704
- person: "person",
97705
- bicycle: "vehicle",
97706
- car: "vehicle",
97707
- motorcycle: "vehicle",
97708
- airplane: "vehicle",
97709
- bus: "vehicle",
97710
- train: "vehicle",
97711
- truck: "vehicle",
97712
- boat: "vehicle",
97713
- bird: "animal",
97714
- cat: "animal",
97715
- dog: "animal",
97716
- horse: "animal",
97717
- sheep: "animal",
97718
- cow: "animal",
97719
- elephant: "animal",
97720
- bear: "animal",
97721
- zebra: "animal",
97722
- giraffe: "animal",
97723
- suitcase: "package",
97724
- backpack: "package",
97725
- handbag: "package"
97726
- },
97727
- preserveOriginal: false
97728
- };
97729
98100
  var AUDIO_MACRO_LABELS = [
97730
98101
  {
97731
98102
  id: "speech",
@@ -110292,9 +110663,9 @@ method(object({ nodeId: string().optional() }).optional(), array(AddonListItemSc
110292
110663
  */
110293
110664
  var AirQualitySensorStatusSchema = object({
110294
110665
  /** Carbon dioxide concentration in ppm. */
110295
- co2Ppm: number().min(0).optional().meta(readingMeta("concentration-volume")),
110666
+ co2: number().min(0).optional().meta(readingMeta("concentration-volume")),
110296
110667
  /** Total volatile organic compounds in ppb. */
110297
- vocPpb: number().min(0).optional().meta(readingMeta("concentration-ppb")),
110668
+ voc: number().min(0).optional().meta(readingMeta("concentration-ppb")),
110298
110669
  /** Particulate matter ≤ 2.5 μm in µg/m³. */
110299
110670
  pm25: number().min(0).optional().meta(readingMeta("concentration-mass")),
110300
110671
  /** Particulate matter ≤ 10 μm in µg/m³. */
@@ -110327,12 +110698,12 @@ var airQualitySensorCapability = {
110327
110698
  * History (D850): recorded series.
110328
110699
  */
110329
110700
  history: { fields: {
110330
- co2Ppm: {
110701
+ co2: {
110331
110702
  kind: "gauge",
110332
110703
  unit: "ppm",
110333
110704
  deadband: 5
110334
110705
  },
110335
- vocPpb: {
110706
+ voc: {
110336
110707
  kind: "gauge",
110337
110708
  unit: "ppb",
110338
110709
  deadband: 5
@@ -110363,7 +110734,7 @@ var airQualitySensorCapability = {
110363
110734
  */
110364
110735
  var AmbientLightSensorStatusSchema = object({
110365
110736
  /** Current illuminance in lux (lx). `null` when the sample was refused (D393). */
110366
- lux: number().min(0).nullable().meta(readingMeta("illuminance", {
110737
+ illuminance: number().min(0).nullable().meta(readingMeta("illuminance", {
110367
110738
  precision: 0,
110368
110739
  label: "Illuminance"
110369
110740
  })),
@@ -110392,7 +110763,7 @@ var ambientLightSensorCapability = {
110392
110763
  /**
110393
110764
  * History (D850): recorded series. Wide range, so a RELATIVE deadband (10 %).
110394
110765
  */
110395
- history: { fields: { lux: {
110766
+ history: { fields: { illuminance: {
110396
110767
  kind: "gauge",
110397
110768
  unit: "lx",
110398
110769
  deadbandPct: 10,
@@ -110531,6 +110902,84 @@ var audioMetricsCapability = {
110531
110902
  */
110532
110903
  history: "none"
110533
110904
  };
110905
+ method(object({}), array(ActionEntrySchema), { auth: "admin" }), method(object({
110906
+ actionId: string().min(1),
110907
+ variables: record(string(), ActionVariableValueSchema).optional()
110908
+ }), _void(), {
110909
+ kind: "mutation",
110910
+ auth: "admin"
110911
+ }), method(object({ actionId: string().min(1) }), _void(), {
110912
+ kind: "mutation",
110913
+ auth: "admin"
110914
+ });
110915
+ /**
110916
+ * actions — the ONE surface for runnable actions, for the UI and for
110917
+ * automations. Provided by the core-blocks builtin on the hub.
110918
+ *
110919
+ * An action is a LIST ENTRY, not a device (D883): either internal (cap and wait
110920
+ * steps, kept in the hub's `actions` record kind) or declared by an addon over
110921
+ * `action-source`. `list` asks every `action-source` provider LIVE and overlays
110922
+ * the run status from the hub's `runs` ledger; there is no catalog here to
110923
+ * drift (D3).
110924
+ *
110925
+ * **Permissions follow the camera, as linked devices do.** An action is linked
110926
+ * to cameras by the operator (`setLinks`, D356: nothing links itself). A
110927
+ * non-admin may `get` / `run` an action only through a camera they may see that
110928
+ * links it: they name the camera (`deviceId`, a top-level field so the scope
110929
+ * gate sees it) and the provider refuses an action that camera does not link.
110930
+ * An admin, and an in-process caller (an automation block), may run any action.
110931
+ * Every refusal is typed (`[not-linked] …`) and logged.
110932
+ *
110933
+ * A second `run` of an action that is already running is REFUSED — traced and
110934
+ * logged — never queued.
110935
+ */
110936
+ var ActionIdInput = object({ actionId: string().min(1) });
110937
+ var ActionLinksSchema = object({
110938
+ deviceId: number().int().nonnegative(),
110939
+ actionIds: array(string().min(1)).max(100)
110940
+ });
110941
+ method(object({}), array(ActionEntrySchema), { auth: "admin" }), method(object({
110942
+ actionId: string().min(1),
110943
+ deviceId: number().int().nonnegative().optional()
110944
+ }), object({ action: ActionEntrySchema.nullable() }), {
110945
+ role: "viewer",
110946
+ auth: "protected",
110947
+ kind: "query",
110948
+ caller: "required"
110949
+ }), method(object({
110950
+ actionId: string().min(1),
110951
+ variables: record(string(), ActionVariableValueSchema).optional(),
110952
+ wait: boolean().optional(),
110953
+ origin: ActionRunOriginSchema.optional(),
110954
+ deviceId: number().int().nonnegative().optional()
110955
+ }), ActionRunResultSchema, {
110956
+ role: "viewer",
110957
+ auth: "protected",
110958
+ kind: "mutation",
110959
+ caller: "required"
110960
+ }), method(ActionIdInput, _void(), {
110961
+ kind: "mutation",
110962
+ auth: "admin"
110963
+ }), method(ActionIdInput, object({ traces: array(AutomationRunTraceSchema) }), { auth: "admin" }), method(object({ action: ActionInputSchema }), object({ action: ActionEntrySchema }), {
110964
+ kind: "mutation",
110965
+ auth: "admin"
110966
+ }), method(object({
110967
+ actionId: string().min(1),
110968
+ action: ActionInputSchema.partial()
110969
+ }), object({ action: ActionEntrySchema }), {
110970
+ kind: "mutation",
110971
+ auth: "admin"
110972
+ }), method(ActionIdInput, _void(), {
110973
+ kind: "mutation",
110974
+ auth: "admin"
110975
+ }), method(object({ deviceId: number().int().nonnegative() }), array(ActionEntrySchema), {
110976
+ role: "viewer",
110977
+ auth: "protected",
110978
+ kind: "query"
110979
+ }), method(object({ deviceId: number().int().nonnegative() }), ActionLinksSchema, { auth: "admin" }), method(ActionLinksSchema, ActionLinksSchema, {
110980
+ kind: "mutation",
110981
+ auth: "admin"
110982
+ });
110534
110983
  /**
110535
110984
  * Automation-control cap. Models HA `automation.*` entities on
110536
110985
  * `DeviceType.Automation`. An automation is a trigger+condition+
@@ -110627,7 +111076,7 @@ var BatteryStatusSchema = object({
110627
111076
  * Consumers must SKIP a null rather than coerce it — `battery-band` already
110628
111077
  * declines to band a non-finite reading, which is the correct shape.
110629
111078
  */
110630
- percentage: number().min(0).max(100).nullable().meta(readingMeta("percentage", { label: "Charge" })),
111079
+ level: number().min(0).max(100).nullable().meta(readingMeta("percentage", { label: "Charge" })),
110631
111080
  /**
110632
111081
  * Charging source. `'dc'` covers wall/USB adapters; `'solar'` is
110633
111082
  * Reolink-specific for the Solar Panel 2 accessory (will become
@@ -110637,7 +111086,7 @@ var BatteryStatusSchema = object({
110637
111086
  * `'unknown'` is the absence of an answer: the provider has not read the
110638
111087
  * power fields yet, or the firmware reported neither. Before it existed,
110639
111088
  * two unreadable fields produced `'none'`, and a camera nobody had reached
110640
- * was drawn "on battery alone" — the same D315 defect `percentage` closed
111089
+ * was drawn "on battery alone" — the same D315 defect `level` closed
110641
111090
  * by going nullable. Consumers SKIP it (no charger state published, no
110642
111091
  * glyph, no HomeKit ChargingState) rather than coerce it either way.
110643
111092
  */
@@ -110661,7 +111110,7 @@ var BatteryStatusSchema = object({
110661
111110
  * that only report charge. A second reading on this cap, not a second
110662
111111
  * `numeric-sensor` on the same device.
110663
111112
  */
110664
- volts: number().nullable().optional().meta(readingMeta("voltage", { label: "Voltage" })),
111113
+ voltage: number().nullable().optional().meta(readingMeta("voltage", { label: "Voltage" })),
110665
111114
  /**
110666
111115
  * Ms epoch of the last time the device PROVED it was reachable — a
110667
111116
  * completed firmware round-trip, an observed wake, or an inbound push
@@ -110695,7 +111144,7 @@ var BatteryStatusSchema = object({
110695
111144
  /**
110696
111145
  * True when the source is a BINARY low-battery indicator (HA
110697
111146
  * `binary_sensor` device_class=battery / `LOW_BAT`) that has no real
110698
- * charge level — `percentage` is then a coarse stand-in (100 = normal,
111147
+ * charge level — `level` is then a coarse stand-in (100 = normal,
110699
111148
  * sub-threshold = low). UI MUST render "Normal"/"Low" instead of a
110700
111149
  * misleading exact percentage. Absent/false → genuine 0–100 % reading.
110701
111150
  */
@@ -110715,15 +111164,15 @@ var BATTERY_DEVICE_TYPES = [
110715
111164
  var batteryCustomize = {
110716
111165
  version: 1,
110717
111166
  targets: [{
110718
- id: "percentage",
111167
+ id: "level",
110719
111168
  cardinality: "scalar",
110720
111169
  vocabulary: {
110721
111170
  cap: "battery",
110722
- stateField: "percentage"
111171
+ stateField: "level"
110723
111172
  },
110724
111173
  deviceTypes: BATTERY_DEVICE_TYPES,
110725
111174
  apply: "add",
110726
- path: "percentage",
111175
+ path: "level",
110727
111176
  sources: [{
110728
111177
  kind: "field",
110729
111178
  valueKinds: ["number", "string"],
@@ -110817,7 +111266,7 @@ onStatusChanged: { data: object({
110817
111266
  schema: BatteryStatusSchema,
110818
111267
  kind: "push",
110819
111268
  empty: {
110820
- percentage: null,
111269
+ level: null,
110821
111270
  charging: "unknown",
110822
111271
  sleeping: false,
110823
111272
  lastUpdated: 0
@@ -110844,11 +111293,11 @@ onStatusChanged: { data: object({
110844
111293
  * History (D850): recorded series. The generic capture replaces the `battery` series of `pipeline-analytics:stats-5m`.
110845
111294
  */
110846
111295
  history: { fields: {
110847
- percentage: {
111296
+ level: {
110848
111297
  kind: "gauge",
110849
111298
  unit: "%"
110850
111299
  },
110851
- volts: {
111300
+ voltage: {
110852
111301
  kind: "gauge",
110853
111302
  unit: "V"
110854
111303
  }
@@ -110908,14 +111357,14 @@ var binaryCapability = {
110908
111357
  * controls to this single-method surface.
110909
111358
  *
110910
111359
  * The cap is intentionally minimal: a single `setBrightness({deviceId,
110911
- * percentage})` mutation plus the auto-injected `getStatus`. Drivers
111360
+ * level})` mutation plus the auto-injected `getStatus`. Drivers
110912
111361
  * that expose richer controls (color temperature, scenes, schedules)
110913
111362
  * should surface those via the device's `getSettingsUISchema()`
110914
111363
  * instead of bloating this cap.
110915
111364
  */
110916
111365
  var BrightnessStatusSchema = object({
110917
111366
  /** Current level as 0..100 inclusive. Firmware-reported. */
110918
- percentage: number().min(0).max(100).meta(readingMeta("percentage")),
111367
+ level: number().min(0).max(100).meta(readingMeta("percentage")),
110919
111368
  /** Ms epoch of the last operator-driven change. Useful for UI freshness. */
110920
111369
  lastChangedAt: number()
110921
111370
  });
@@ -110927,7 +111376,7 @@ var brightnessCapability = {
110927
111376
  deviceTypes: [DeviceType.Light],
110928
111377
  methods: { setBrightness: method(object({
110929
111378
  deviceId: number().int().nonnegative(),
110930
- percentage: number().min(0).max(100)
111379
+ level: number().min(0).max(100)
110931
111380
  }), _void(), {
110932
111381
  kind: "mutation",
110933
111382
  auth: "protected"
@@ -110940,7 +111389,7 @@ var brightnessCapability = {
110940
111389
  */
110941
111390
  onBrightnessChanged: { data: object({
110942
111391
  deviceId: number(),
110943
- percentage: number().min(0).max(100),
111392
+ level: number().min(0).max(100),
110944
111393
  lastChangedAt: number()
110945
111394
  }) } },
110946
111395
  status: {
@@ -110963,7 +111412,7 @@ onBrightnessChanged: { data: object({
110963
111412
  /**
110964
111413
  * History (D850): recorded series. Actuator tier: level keeps every change.
110965
111414
  */
110966
- history: { fields: { percentage: {
111415
+ history: { fields: { level: {
110967
111416
  kind: "gauge",
110968
111417
  unit: "%",
110969
111418
  minIntervalMs: 0
@@ -111577,11 +112026,11 @@ var ColorInputSchema = discriminatedUnion("mode", [
111577
112026
  }),
111578
112027
  object({
111579
112028
  mode: literal("mired"),
111580
- mireds: number().int().min(50).max(1e3)
112029
+ colorTemperature: number().int().min(50).max(1e3)
111581
112030
  })
111582
112031
  ]);
111583
112032
  var ColorStatusSchema = object({
111584
- /** Active color mode — which of `rgb` / `hsv` / `mireds` reflects
112033
+ /** Active color mode — which of `rgb` / `hsv` / `colorTemperature` reflects
111585
112034
  * the bulb's current state. */
111586
112035
  mode: _enum([
111587
112036
  "rgb",
@@ -111593,7 +112042,7 @@ var ColorStatusSchema = object({
111593
112042
  /** Populated when `mode === 'hsv'`. */
111594
112043
  hsv: HsvTripletSchema.optional(),
111595
112044
  /** Populated when `mode === 'mired'`. */
111596
- mireds: number().int().optional().meta(readingMeta("colour-temperature")),
112045
+ colorTemperature: number().int().optional().meta(readingMeta("colour-temperature")),
111597
112046
  /** Ms epoch of the last operator-driven change. */
111598
112047
  lastChangedAt: number()
111599
112048
  });
@@ -111625,7 +112074,7 @@ onColorChanged: { data: object({
111625
112074
  ]),
111626
112075
  rgb: RgbTripletSchema.optional(),
111627
112076
  hsv: HsvTripletSchema.optional(),
111628
- mireds: number().int().optional(),
112077
+ colorTemperature: number().int().optional(),
111629
112078
  lastChangedAt: number()
111630
112079
  }) } },
111631
112080
  status: {
@@ -112514,11 +112963,11 @@ var DishwasherStatusSchema = object({
112514
112963
  /** Feature keys of the programs the appliance currently offers — drives the UI selector. */
112515
112964
  availablePrograms: array(string()),
112516
112965
  /** Seconds remaining on the active cycle, or null when not running / not known. */
112517
- remainingSeconds: number().int().nonnegative().nullable().meta(readingMeta("duration")),
112966
+ remaining: number().int().nonnegative().nullable().meta(readingMeta("duration")),
112518
112967
  /** Seconds the delayed start is set to (time until the cycle starts), or null. */
112519
- delayedStartSeconds: number().int().nonnegative().nullable().meta(readingMeta("duration")),
112968
+ delayedStart: number().int().nonnegative().nullable().meta(readingMeta("duration")),
112520
112969
  /** Cycle completion 0..100, or null when not applicable. */
112521
- progressPercent: number().min(0).max(100).nullable().meta(readingMeta("percentage")),
112970
+ progress: number().min(0).max(100).nullable().meta(readingMeta("percentage")),
112522
112971
  /** True when the door is reported open; null when the door state is not known. */
112523
112972
  doorOpen: boolean().nullable(),
112524
112973
  /** True when the door is latched (locked) during a cycle; null when not reported. */
@@ -112737,14 +113186,14 @@ var dishwasherCapability = {
112737
113186
  runtimeState: DishwasherStatusSchema,
112738
113187
  durability: "session",
112739
113188
  history: { fields: {
112740
- progressPercent: {
113189
+ progress: {
112741
113190
  kind: "gauge",
112742
113191
  unit: "%",
112743
113192
  deadband: 2
112744
113193
  },
112745
113194
  state: { kind: "enum" },
112746
113195
  operationState: { kind: "enum" },
112747
- remainingSeconds: {
113196
+ remaining: {
112748
113197
  kind: "gauge",
112749
113198
  unit: "s",
112750
113199
  minIntervalMs: 6e4
@@ -113335,7 +113784,7 @@ var FanDirectionSchema = _enum(["forward", "reverse"]);
113335
113784
  var FanControlStatusSchema = object({
113336
113785
  /** Active speed as 0..100 inclusive. Null when the device has no
113337
113786
  * speed surface (single-speed fan). */
113338
- percentage: number().min(0).max(100).nullable().meta(readingMeta("percentage")),
113787
+ speed: number().min(0).max(100).nullable().meta(readingMeta("percentage")),
113339
113788
  /** Speed granularity the device accepts (HA `percentage_step`, e.g. 25 for
113340
113789
  * a 4-speed fan). Absent when unknown — the UI then falls back to step 1. */
113341
113790
  percentageStep: number().positive().optional().meta(readingMeta("percentage")),
@@ -113362,7 +113811,7 @@ var fanControlCapability = {
113362
113811
  methods: {
113363
113812
  setPercentage: method(object({
113364
113813
  deviceId: number().int().nonnegative(),
113365
- percentage: number().min(0).max(100)
113814
+ speed: number().min(0).max(100)
113366
113815
  }), _void(), {
113367
113816
  kind: "mutation",
113368
113817
  auth: "protected"
@@ -113395,7 +113844,7 @@ var fanControlCapability = {
113395
113844
  },
113396
113845
  /**
113397
113846
  * Runtime-state slice — mirrored by the kernel. UI fan speed
113398
- * sliders read `percentage` for live updates.
113847
+ * sliders read `speed` for live updates.
113399
113848
  */
113400
113849
  runtimeState: FanControlStatusSchema,
113401
113850
  /**
@@ -113409,7 +113858,7 @@ var fanControlCapability = {
113409
113858
  * History (D850): recorded series. Actuator tier: speed keeps every change.
113410
113859
  */
113411
113860
  history: { fields: {
113412
- percentage: {
113861
+ speed: {
113413
113862
  kind: "gauge",
113414
113863
  unit: "%",
113415
113864
  minIntervalMs: 0
@@ -113854,7 +114303,7 @@ var humidifierCapability = {
113854
114303
  */
113855
114304
  var HumiditySensorStatusSchema = object({
113856
114305
  /** Current relative humidity, 0..100. `null` when the sample was refused (D393). */
113857
- percent: number().min(0).max(100).nullable().meta(readingMeta("humidity", {
114306
+ humidity: number().min(0).max(100).nullable().meta(readingMeta("humidity", {
113858
114307
  precision: 0,
113859
114308
  label: "Humidity"
113860
114309
  })),
@@ -113883,7 +114332,7 @@ var humiditySensorCapability = {
113883
114332
  /**
113884
114333
  * History (D850): recorded series.
113885
114334
  */
113886
- history: { fields: { percent: {
114335
+ history: { fields: { humidity: {
113887
114336
  kind: "gauge",
113888
114337
  unit: "%",
113889
114338
  deadband: .5
@@ -114523,7 +114972,7 @@ var LawnMowerControlStatusSchema = object({
114523
114972
  batteryLevel: number().min(0).max(100).nullable().meta(readingMeta("percentage")),
114524
114973
  /** 0..100 mowing-completion percentage of the current task, or null when no
114525
114974
  * task is active / progress is unavailable. */
114526
- progressPercent: number().min(0).max(100).nullable().meta(readingMeta("percentage")),
114975
+ progress: number().min(0).max(100).nullable().meta(readingMeta("percentage")),
114527
114976
  /** Current device/event code (dynamic — mostly status, sometimes an error),
114528
114977
  * or null when unknown. */
114529
114978
  currentCode: number().nullable().meta(readingMeta("none")),
@@ -115023,7 +115472,7 @@ var lockControlCapability = {
115023
115472
  volatileStateFields: ["lastChangedAt"]
115024
115473
  };
115025
115474
  /**
115026
- * A monotonic total between resets (D944). Energy uses `power-meter.kwhTotal`
115475
+ * A monotonic total between resets (D944). Energy uses `power-meter.energy`
115027
115476
  * so dashboards keep one series; every other instantaneous reading lands here.
115028
115477
  * `value` is the time integral in the source quantity's accumulated base
115029
115478
  * (base-unit seconds, or kWh on the power meter). `null` is a gap, never zero.
@@ -115117,10 +115566,10 @@ var MediaPlayerStatusSchema = object({
115117
115566
  currentMedia: MediaInfoSchema.nullable(),
115118
115567
  /** Current playback position in ms. Null when not seekable or
115119
115568
  * nothing is playing. Pair with `DeviceFeature.MediaPlayerSeek`. */
115120
- positionMs: number().int().nonnegative().nullable().meta(readingMeta("duration-ms")),
115569
+ position: number().int().nonnegative().nullable().meta(readingMeta("duration-ms")),
115121
115570
  /** Total duration of the current media in ms. Null when unknown
115122
115571
  * (live stream) or nothing is playing. */
115123
- durationMs: number().int().nonnegative().nullable().meta(readingMeta("duration-ms")),
115572
+ duration: number().int().nonnegative().nullable().meta(readingMeta("duration-ms")),
115124
115573
  /** Shuffle toggle. Null when no shuffle surface.
115125
115574
  * Pair with `DeviceFeature.MediaPlayerShuffle`. */
115126
115575
  shuffle: boolean().nullable(),
@@ -115159,7 +115608,7 @@ var mediaPlayerCapability = {
115159
115608
  }),
115160
115609
  seek: method(object({
115161
115610
  deviceId: number().int().nonnegative(),
115162
- positionMs: number().int().nonnegative()
115611
+ position: number().int().nonnegative()
115163
115612
  }), _void(), {
115164
115613
  kind: "mutation",
115165
115614
  auth: "protected"
@@ -115231,7 +115680,7 @@ var mediaPlayerCapability = {
115231
115680
  /**
115232
115681
  * History (D850): volume (a level an operator or an automation sets; the
115233
115682
  * default 10 s floor folds a slider drag into its end value) and mute.
115234
- * `positionMs` is not recorded: it moves continuously while playing.
115683
+ * `position` is not recorded: it moves continuously while playing.
115235
115684
  */
115236
115685
  history: { fields: {
115237
115686
  volumeLevel: {
@@ -116330,9 +116779,9 @@ var NetworkLinkStatusSchema = object({
116330
116779
  * one whose reading has not landed must not be drawn at 0 %. Consumers
116331
116780
  * SKIP a null rather than coerce it.
116332
116781
  */
116333
- signalPercent: number().min(0).max(100).nullable().meta(readingMeta("percentage")),
116782
+ signal: number().min(0).max(100).nullable().meta(readingMeta("percentage")),
116334
116783
  /** Raw received signal strength in dBm, when the firmware reports one. */
116335
- rssiDbm: number().optional().meta(readingMeta("signal-level")),
116784
+ rssi: number().optional().meta(readingMeta("signal-level")),
116336
116785
  /** Network name of a wireless link, when the firmware reports it. */
116337
116786
  ssid: string().optional(),
116338
116787
  /** Ms epoch of the last observation. Lets consumers reason about freshness. */
@@ -116341,7 +116790,7 @@ var NetworkLinkStatusSchema = object({
116341
116790
  /** The slice a provider seeds before its first read: nothing is known yet. */
116342
116791
  var NETWORK_LINK_UNKNOWN = {
116343
116792
  type: "unknown",
116344
- signalPercent: null,
116793
+ signal: null,
116345
116794
  lastUpdated: 0
116346
116795
  };
116347
116796
  /**
@@ -116402,13 +116851,13 @@ onStatusChanged: { data: object({
116402
116851
  * History (D850): recorded series.
116403
116852
  */
116404
116853
  history: { fields: {
116405
- signalPercent: {
116854
+ signal: {
116406
116855
  kind: "gauge",
116407
116856
  unit: "%",
116408
116857
  deadband: 3,
116409
116858
  minIntervalMs: 6e4
116410
116859
  },
116411
- rssiDbm: {
116860
+ rssi: {
116412
116861
  kind: "gauge",
116413
116862
  unit: "dBm",
116414
116863
  deadband: 2,
@@ -117728,16 +118177,16 @@ method(_void(), PlatformCapabilitiesSchema), method(_void(), HardwareInfoSchema)
117728
118177
  * in real-world devices.
117729
118178
  */
117730
118179
  var PowerMeterStatusSchema = object({
117731
- /** Instantaneous power draw in watts. */
117732
- watts: number().optional().meta(readingMeta("power", { label: "Power" })),
118180
+ /** Instantaneous power draw (watts). */
118181
+ power: number().optional().meta(readingMeta("power", { label: "Power" })),
117733
118182
  /** Cumulative energy in kilowatt-hours since the meter was reset. */
117734
- kwhTotal: number().optional().meta(readingMeta("energy", { label: "Energy" })),
117735
- /** Voltage in volts. */
117736
- volts: number().optional().meta(readingMeta("voltage", { label: "Voltage" })),
118183
+ energy: number().optional().meta(readingMeta("energy", { label: "Energy" })),
118184
+ /** Voltage (volts). */
118185
+ voltage: number().optional().meta(readingMeta("voltage", { label: "Voltage" })),
117737
118186
  /** Current in amperes. */
117738
- amps: number().optional().meta(readingMeta("current", { label: "Current" })),
118187
+ current: number().optional().meta(readingMeta("current", { label: "Current" })),
117739
118188
  /** Apparent power in volt-amperes. */
117740
- va: number().optional().meta(readingMeta("apparent-power")),
118189
+ apparentPower: number().optional().meta(readingMeta("apparent-power")),
117741
118190
  /** Power factor (0..1) if reported. */
117742
118191
  powerFactor: number().min(0).max(1).optional().meta(readingMeta("ratio")),
117743
118192
  /** Ms epoch when the slice was last updated. */
@@ -117795,14 +118244,14 @@ function powerTarget(id, unit) {
117795
118244
  advancedExpression: true
117796
118245
  };
117797
118246
  }
117798
- /** "Power and energy" extension: watts, energy, voltage and current from other devices (D951). */
118247
+ /** "Power and energy" extension: power, energy, voltage and current from other devices (D951). */
117799
118248
  var powerMeterCustomize = {
117800
118249
  version: 1,
117801
118250
  targets: [
117802
- powerTarget("watts", "W"),
117803
- powerTarget("kwhTotal", "kWh"),
117804
- powerTarget("volts", "V"),
117805
- powerTarget("amps", "A")
118251
+ powerTarget("power", "W"),
118252
+ powerTarget("energy", "kWh"),
118253
+ powerTarget("voltage", "V"),
118254
+ powerTarget("current", "A")
117806
118255
  ]
117807
118256
  };
117808
118257
  var powerMeterCapability = {
@@ -117818,41 +118267,41 @@ var powerMeterCapability = {
117818
118267
  },
117819
118268
  runtimeState: PowerMeterStatusSchema,
117820
118269
  /**
117821
- * Runtime-state durability: **restored** — as `numeric-sensor`; `kwhTotal` is an accumulator whose restored value is the baseline.
118270
+ * Runtime-state durability: **restored** — as `numeric-sensor`; `energy` is an accumulator whose restored value is the baseline.
117822
118271
  *
117823
118272
  * See `RuntimeStateDurability`. Enforced by
117824
118273
  * `scripts/check-runtime-state-durability.ts`.
117825
118274
  */
117826
118275
  durability: "restored",
117827
118276
  /**
117828
- * History (D850): recorded series. Gauges are floored at 30 s; `kwhTotal` is the one monotonic meter (counter).
118277
+ * History (D850): recorded series. Gauges are floored at 30 s; `energy` is the one monotonic meter (counter).
117829
118278
  */
117830
118279
  history: { fields: {
117831
- watts: {
118280
+ power: {
117832
118281
  kind: "gauge",
117833
118282
  unit: "W",
117834
118283
  deadband: 1,
117835
118284
  minIntervalMs: 3e4
117836
118285
  },
117837
- kwhTotal: {
118286
+ energy: {
117838
118287
  kind: "counter",
117839
118288
  unit: "kWh",
117840
118289
  deadband: .01,
117841
118290
  minIntervalMs: 6e4
117842
118291
  },
117843
- volts: {
118292
+ voltage: {
117844
118293
  kind: "gauge",
117845
118294
  unit: "V",
117846
118295
  deadband: 1,
117847
118296
  minIntervalMs: 3e4
117848
118297
  },
117849
- amps: {
118298
+ current: {
117850
118299
  kind: "gauge",
117851
118300
  unit: "A",
117852
118301
  deadband: .05,
117853
118302
  minIntervalMs: 3e4
117854
118303
  },
117855
- va: {
118304
+ apparentPower: {
117856
118305
  kind: "gauge",
117857
118306
  unit: "VA",
117858
118307
  deadband: 1,
@@ -117902,9 +118351,6 @@ var PresenceStatusSchema = object({
117902
118351
  location: string().nullable(),
117903
118352
  /** GPS coordinates when available. Null otherwise. */
117904
118353
  gps: GpsLocationSchema.nullable(),
117905
- /** Optional battery level of the tracking device (0..100). Null
117906
- * when not reported. */
117907
- batteryPercent: number().min(0).max(100).nullable().meta(readingMeta("percentage")),
117908
118354
  /** Ms epoch when the slice was last updated. */
117909
118355
  lastChangedAt: number()
117910
118356
  });
@@ -117933,14 +118379,7 @@ var presenceCapability = {
117933
118379
  * `scripts/check-runtime-state-durability.ts`.
117934
118380
  */
117935
118381
  durability: "restored",
117936
- /**
117937
- * History (D850): recorded series.
117938
- */
117939
- history: { fields: { batteryPercent: {
117940
- kind: "gauge",
117941
- unit: "%",
117942
- deadband: 1
117943
- } } },
118382
+ history: "none",
117944
118383
  /** Clock fields: written, but excluded from the compare that decides
117945
118384
  * whether persisting is worth a SQLite commit. */
117946
118385
  volatileStateFields: ["lastChangedAt"]
@@ -117955,7 +118394,7 @@ var presenceCapability = {
117955
118394
  */
117956
118395
  var PressureSensorStatusSchema = object({
117957
118396
  /** Current pressure in hPa. `null` when the sample was refused (D393). */
117958
- hpa: number().nullable().meta(readingMeta("pressure", { precision: 0 })),
118397
+ pressure: number().nullable().meta(readingMeta("pressure", { precision: 0 })),
117959
118398
  /** Ms epoch when the slice was last updated. */
117960
118399
  lastFetchedAt: number()
117961
118400
  });
@@ -117981,7 +118420,7 @@ var pressureSensorCapability = {
117981
118420
  /**
117982
118421
  * History (D850): recorded series.
117983
118422
  */
117984
- history: { fields: { hpa: {
118423
+ history: { fields: { pressure: {
117985
118424
  kind: "gauge",
117986
118425
  unit: "hPa",
117987
118426
  deadband: .1
@@ -121688,7 +122127,7 @@ var tamperCapability = {
121688
122127
  */
121689
122128
  var TemperatureSensorStatusSchema = object({
121690
122129
  /** Current temperature in Celsius. `null` when the sample was refused (D393). */
121691
- celsius: number().nullable().meta(readingMeta("temperature", {
122130
+ temperature: number().nullable().meta(readingMeta("temperature", {
121692
122131
  precision: 1,
121693
122132
  label: "Temperature"
121694
122133
  })),
@@ -121717,7 +122156,7 @@ var temperatureSensorCapability = {
121717
122156
  /**
121718
122157
  * History (D850): recorded series.
121719
122158
  */
121720
- history: { fields: { celsius: {
122159
+ history: { fields: { temperature: {
121721
122160
  kind: "gauge",
121722
122161
  unit: "°C",
121723
122162
  deadband: .1
@@ -122014,7 +122453,7 @@ var VacuumControlStatusSchema = object({
122014
122453
  /** Speed tokens the hardware accepts — drives the UI selector. */
122015
122454
  availableFanSpeeds: array(string()),
122016
122455
  /** 0..100 cleaning-completion percentage of the current task, or null. */
122017
- progressPercent: number().min(0).max(100).nullable().meta(readingMeta("percentage")),
122456
+ progress: number().min(0).max(100).nullable().meta(readingMeta("percentage")),
122018
122457
  /** Current error code (0 / null = no error). */
122019
122458
  errorCode: number().nullable().meta(readingMeta("none")),
122020
122459
  /** Human label for {@link errorCode}, or null when none / undecodable. */
@@ -124096,7 +124535,7 @@ var BaseDevice = class {
124096
124535
  * `device-local-state.ts`. Drivers access via:
124097
124536
  *
124098
124537
  * `this.state.battery.sleeping = true` // patches the battery slice
124099
- * `const pct = this.state.battery.percentage` // reads the battery slice
124538
+ * `const pct = this.state.battery.level` // reads the battery slice
124100
124539
  * `this.state.deviceStatus.online = true` // mirrors `markOnline(true)`
124101
124540
  *
124102
124541
  * Adding a new cap with `runtimeState:` automatically extends this
@@ -124136,7 +124575,7 @@ var BaseDevice = class {
124136
124575
  * shape; drivers don't redeclare it, they just write through.
124137
124576
  *
124138
124577
  * Read: `this.runtimeState.getCapState('battery')` →
124139
- * `{percentage, charging, sleeping, lastUpdated}` for any
124578
+ * `{level, charging, sleeping, lastUpdated}` for any
124140
124579
  * provider that registers `batteryCapability`.
124141
124580
  * Write: `this.runtimeState.setCapState('battery', { … })`.
124142
124581
  *
@@ -125326,6 +125765,104 @@ Object.freeze({
125326
125765
  access: "create",
125327
125766
  role: "admin"
125328
125767
  },
125768
+ "actions.create": {
125769
+ capName: "actions",
125770
+ capScope: "system",
125771
+ addonId: null,
125772
+ access: "create",
125773
+ role: "admin"
125774
+ },
125775
+ "actions.delete": {
125776
+ capName: "actions",
125777
+ capScope: "system",
125778
+ addonId: null,
125779
+ access: "delete",
125780
+ role: "admin"
125781
+ },
125782
+ "actions.get": {
125783
+ capName: "actions",
125784
+ capScope: "system",
125785
+ addonId: null,
125786
+ access: "view",
125787
+ role: "viewer"
125788
+ },
125789
+ "actions.getLinks": {
125790
+ capName: "actions",
125791
+ capScope: "system",
125792
+ addonId: null,
125793
+ access: "view",
125794
+ role: "admin"
125795
+ },
125796
+ "actions.getTraces": {
125797
+ capName: "actions",
125798
+ capScope: "system",
125799
+ addonId: null,
125800
+ access: "view",
125801
+ role: "admin"
125802
+ },
125803
+ "actions.list": {
125804
+ capName: "actions",
125805
+ capScope: "system",
125806
+ addonId: null,
125807
+ access: "view",
125808
+ role: "admin"
125809
+ },
125810
+ "actions.listForCamera": {
125811
+ capName: "actions",
125812
+ capScope: "system",
125813
+ addonId: null,
125814
+ access: "view",
125815
+ role: "viewer"
125816
+ },
125817
+ "actions.run": {
125818
+ capName: "actions",
125819
+ capScope: "system",
125820
+ addonId: null,
125821
+ access: "create",
125822
+ role: "viewer"
125823
+ },
125824
+ "actions.setLinks": {
125825
+ capName: "actions",
125826
+ capScope: "system",
125827
+ addonId: null,
125828
+ access: "create",
125829
+ role: "admin"
125830
+ },
125831
+ "actions.stop": {
125832
+ capName: "actions",
125833
+ capScope: "system",
125834
+ addonId: null,
125835
+ access: "create",
125836
+ role: "admin"
125837
+ },
125838
+ "actions.update": {
125839
+ capName: "actions",
125840
+ capScope: "system",
125841
+ addonId: null,
125842
+ access: "create",
125843
+ role: "admin"
125844
+ },
125845
+ "actionSource.listActions": {
125846
+ capName: "action-source",
125847
+ capScope: "system",
125848
+ addonId: null,
125849
+ access: "view",
125850
+ role: "admin"
125851
+ },
125852
+ "actionSource.runAction": {
125853
+ capName: "action-source",
125854
+ capScope: "system",
125855
+ addonId: null,
125856
+ access: "create",
125857
+ role: "admin"
125858
+ },
125859
+ "actionSource.stopAction": {
125860
+ capName: "action-source",
125861
+ capScope: "system",
125862
+ addonId: null,
125863
+ access: "create",
125864
+ role: "admin"
125865
+ },
125329
125866
  "addonPages.listPages": {
125330
125867
  capName: "addon-pages",
125331
125868
  capScope: "system",
@@ -126607,6 +127144,13 @@ Object.freeze({
126607
127144
  access: "view",
126608
127145
  role: "admin"
126609
127146
  },
127147
+ "coreBlocks.getAutomationRunState": {
127148
+ capName: "core-blocks",
127149
+ capScope: "system",
127150
+ addonId: null,
127151
+ access: "view",
127152
+ role: "admin"
127153
+ },
126610
127154
  "coreBlocks.getBlockSettings": {
126611
127155
  capName: "core-blocks",
126612
127156
  capScope: "system",
@@ -126628,6 +127172,13 @@ Object.freeze({
126628
127172
  access: "view",
126629
127173
  role: "admin"
126630
127174
  },
127175
+ "coreBlocks.getTraces": {
127176
+ capName: "core-blocks",
127177
+ capScope: "system",
127178
+ addonId: null,
127179
+ access: "view",
127180
+ role: "admin"
127181
+ },
126631
127182
  "coreBlocks.getTypeDefs": {
126632
127183
  capName: "core-blocks",
126633
127184
  capScope: "system",
@@ -126663,6 +127214,13 @@ Object.freeze({
126663
127214
  access: "create",
126664
127215
  role: "admin"
126665
127216
  },
127217
+ "coreBlocks.reportAutomationRun": {
127218
+ capName: "core-blocks",
127219
+ capScope: "system",
127220
+ addonId: null,
127221
+ access: "create",
127222
+ role: "admin"
127223
+ },
126666
127224
  "coreBlocks.restart": {
126667
127225
  capName: "core-blocks",
126668
127226
  capScope: "system",
@@ -135236,7 +135794,7 @@ var ACTUATOR_CAP_VOCABULARY = {
135236
135794
  label: t("Full brightness", "Luminosità massima"),
135237
135795
  phrase: t("Set {device} to full brightness", "Porta {device} alla massima luminosità"),
135238
135796
  icon: "sun",
135239
- args: { percentage: 100 }
135797
+ args: { level: 100 }
135240
135798
  },
135241
135799
  {
135242
135800
  id: "brightness.half",
@@ -135244,7 +135802,7 @@ var ACTUATOR_CAP_VOCABULARY = {
135244
135802
  label: t("Half brightness", "Luminosità a metà"),
135245
135803
  phrase: t("Dim {device} to half", "Abbassa {device} a metà luminosità"),
135246
135804
  icon: "sun-dim",
135247
- args: { percentage: 50 }
135805
+ args: { level: 50 }
135248
135806
  },
135249
135807
  {
135250
135808
  id: "brightness.off",
@@ -135252,16 +135810,16 @@ var ACTUATOR_CAP_VOCABULARY = {
135252
135810
  label: t("Brightness off", "Luminosità a zero"),
135253
135811
  phrase: t("Dim {device} to zero", "Abbassa {device} a zero"),
135254
135812
  icon: "lightbulb-off",
135255
- args: { percentage: 0 }
135813
+ args: { level: 0 }
135256
135814
  },
135257
135815
  {
135258
135816
  id: "brightness.set",
135259
135817
  method: "setBrightness",
135260
135818
  label: t("Set brightness", "Imposta la luminosità"),
135261
- phrase: t("Set the brightness of {device} to {percentage}", "Imposta la luminosità di {device} a {percentage}"),
135819
+ phrase: t("Set the brightness of {device} to {level}", "Imposta la luminosità di {device} a {level}"),
135262
135820
  icon: "sun-medium",
135263
135821
  openArgs: [{
135264
- key: "percentage",
135822
+ key: "level",
135265
135823
  label: PERCENT,
135266
135824
  offer: {
135267
135825
  kind: "number",
@@ -135275,13 +135833,13 @@ var ACTUATOR_CAP_VOCABULARY = {
135275
135833
  ],
135276
135834
  state: [{
135277
135835
  kind: "numeric",
135278
- field: "percentage",
135836
+ field: "level",
135279
135837
  quantity: t("the brightness", "la luminosità"),
135280
135838
  label: t("Brightness", "Luminosità"),
135281
135839
  unit: "%",
135282
135840
  icon: "sun"
135283
135841
  }],
135284
- events: { onBrightnessChanged: { notOperatorFacing: "the `percentage` state trigger reads the same change" } }
135842
+ events: { onBrightnessChanged: { notOperatorFacing: "the `level` state trigger reads the same change" } }
135285
135843
  },
135286
135844
  color: {
135287
135845
  scope: "device",
@@ -135295,7 +135853,7 @@ var ACTUATOR_CAP_VOCABULARY = {
135295
135853
  icon: "sun",
135296
135854
  args: { color: {
135297
135855
  mode: "mired",
135298
- mireds: 370
135856
+ colorTemperature: 370
135299
135857
  } }
135300
135858
  },
135301
135859
  {
@@ -135306,7 +135864,7 @@ var ACTUATOR_CAP_VOCABULARY = {
135306
135864
  icon: "snowflake",
135307
135865
  args: { color: {
135308
135866
  mode: "mired",
135309
- mireds: 200
135867
+ colorTemperature: 200
135310
135868
  } }
135311
135869
  },
135312
135870
  {
@@ -135920,7 +136478,7 @@ var ACTUATOR_CAP_VOCABULARY = {
135920
136478
  label: t("Fan off", "Ventilatore spento"),
135921
136479
  phrase: t("Stop {device}", "Ferma {device}"),
135922
136480
  icon: "power-off",
135923
- args: { percentage: 0 }
136481
+ args: { speed: 0 }
135924
136482
  },
135925
136483
  {
135926
136484
  id: "fan.full",
@@ -135928,16 +136486,16 @@ var ACTUATOR_CAP_VOCABULARY = {
135928
136486
  label: t("Fan full", "Ventilatore al massimo"),
135929
136487
  phrase: t("Run {device} at full speed", "Porta {device} alla massima velocità"),
135930
136488
  icon: "fan",
135931
- args: { percentage: 100 }
136489
+ args: { speed: 100 }
135932
136490
  },
135933
136491
  {
135934
136492
  id: "fan.speed",
135935
136493
  method: "setPercentage",
135936
136494
  label: t("Set speed", "Imposta la velocità"),
135937
- phrase: t("Set the speed of {device} to {percentage}", "Imposta la velocità di {device} al {percentage}"),
136495
+ phrase: t("Set the speed of {device} to {speed}", "Imposta la velocità di {device} al {speed}"),
135938
136496
  icon: "fan",
135939
136497
  openArgs: [{
135940
- key: "percentage",
136498
+ key: "speed",
135941
136499
  label: t("Speed", "Velocità"),
135942
136500
  offer: {
135943
136501
  kind: "number",
@@ -135999,7 +136557,7 @@ var ACTUATOR_CAP_VOCABULARY = {
135999
136557
  ],
136000
136558
  state: [{
136001
136559
  kind: "numeric",
136002
- field: "percentage",
136560
+ field: "speed",
136003
136561
  quantity: t("the speed", "la velocità"),
136004
136562
  label: t("Speed", "Velocità"),
136005
136563
  unit: "%",
@@ -136797,7 +137355,7 @@ var APPLIANCE_CAP_VOCABULARY = {
136797
137355
  },
136798
137356
  {
136799
137357
  kind: "numeric",
136800
- field: "progressPercent",
137358
+ field: "progress",
136801
137359
  quantity: t("the cycle progress", "l’avanzamento del lavaggio"),
136802
137360
  label: t("Progress", "Avanzamento"),
136803
137361
  unit: "%",
@@ -137155,10 +137713,10 @@ var APPLIANCE_CAP_VOCABULARY = {
137155
137713
  id: "media.seek",
137156
137714
  method: "seek",
137157
137715
  label: t("Seek", "Vai al punto"),
137158
- phrase: t("Move {device} to {positionMs}", "Porta {device} al punto {positionMs}"),
137716
+ phrase: t("Move {device} to {position}", "Porta {device} al punto {position}"),
137159
137717
  icon: "fast-forward",
137160
137718
  openArgs: [{
137161
- key: "positionMs",
137719
+ key: "position",
137162
137720
  label: t("Position", "Posizione"),
137163
137721
  offer: {
137164
137722
  kind: "number",
@@ -138207,7 +138765,7 @@ var SENSOR_CAP_VOCABULARY = {
138207
138765
  state: [
138208
138766
  {
138209
138767
  kind: "numeric",
138210
- field: "co2Ppm",
138768
+ field: "co2",
138211
138769
  quantity: t("the CO₂", "la CO₂"),
138212
138770
  label: t("CO₂", "CO₂"),
138213
138771
  unit: "ppm",
@@ -138215,7 +138773,7 @@ var SENSOR_CAP_VOCABULARY = {
138215
138773
  },
138216
138774
  {
138217
138775
  kind: "numeric",
138218
- field: "vocPpb",
138776
+ field: "voc",
138219
138777
  quantity: t("the VOC level", "il livello di COV"),
138220
138778
  label: t("VOC", "COV"),
138221
138779
  unit: "ppb",
@@ -138251,7 +138809,7 @@ var SENSOR_CAP_VOCABULARY = {
138251
138809
  noun: t("Light level", "Luminosità ambiente"),
138252
138810
  state: [{
138253
138811
  kind: "numeric",
138254
- field: "lux",
138812
+ field: "illuminance",
138255
138813
  quantity: t("the light level", "la luminosità"),
138256
138814
  label: t("Illuminance", "Illuminamento"),
138257
138815
  unit: "lx",
@@ -138454,7 +139012,7 @@ var SENSOR_CAP_VOCABULARY = {
138454
139012
  noun: t("Humidity", "Umidità"),
138455
139013
  state: [{
138456
139014
  kind: "numeric",
138457
- field: "percent",
139015
+ field: "humidity",
138458
139016
  quantity: t("the humidity", "l’umidità"),
138459
139017
  label: t("Humidity", "Umidità"),
138460
139018
  unit: "%",
@@ -138502,7 +139060,7 @@ var SENSOR_CAP_VOCABULARY = {
138502
139060
  state: [
138503
139061
  {
138504
139062
  kind: "numeric",
138505
- field: "watts",
139063
+ field: "power",
138506
139064
  quantity: t("the power draw", "il consumo"),
138507
139065
  label: t("Power", "Potenza"),
138508
139066
  unit: "W",
@@ -138510,7 +139068,7 @@ var SENSOR_CAP_VOCABULARY = {
138510
139068
  },
138511
139069
  {
138512
139070
  kind: "numeric",
138513
- field: "kwhTotal",
139071
+ field: "energy",
138514
139072
  quantity: t("the energy used", "l’energia consumata"),
138515
139073
  label: t("Energy", "Energia"),
138516
139074
  unit: "kWh",
@@ -138518,7 +139076,7 @@ var SENSOR_CAP_VOCABULARY = {
138518
139076
  },
138519
139077
  {
138520
139078
  kind: "numeric",
138521
- field: "volts",
139079
+ field: "voltage",
138522
139080
  quantity: t("the voltage", "la tensione"),
138523
139081
  label: t("Voltage", "Tensione"),
138524
139082
  unit: "V",
@@ -138526,7 +139084,7 @@ var SENSOR_CAP_VOCABULARY = {
138526
139084
  },
138527
139085
  {
138528
139086
  kind: "numeric",
138529
- field: "amps",
139087
+ field: "current",
138530
139088
  quantity: t("the current", "la corrente"),
138531
139089
  label: t("Current", "Corrente"),
138532
139090
  unit: "A",
@@ -138543,13 +139101,6 @@ var SENSOR_CAP_VOCABULARY = {
138543
139101
  label: t("Where", "Posizione"),
138544
139102
  phrase: t("When {device} is at {value}", "Quando {device} è in {value}"),
138545
139103
  icon: "map-pin"
138546
- }, {
138547
- kind: "numeric",
138548
- field: "batteryPercent",
138549
- quantity: t("the phone battery", "la batteria del telefono"),
138550
- label: t("Battery", "Batteria"),
138551
- unit: "%",
138552
- icon: "battery"
138553
139104
  }]
138554
139105
  },
138555
139106
  "pressure-sensor": {
@@ -138557,7 +139108,7 @@ var SENSOR_CAP_VOCABULARY = {
138557
139108
  noun: t("Pressure", "Pressione"),
138558
139109
  state: [{
138559
139110
  kind: "numeric",
138560
- field: "hpa",
139111
+ field: "pressure",
138561
139112
  quantity: t("the pressure", "la pressione"),
138562
139113
  label: t("Pressure", "Pressione"),
138563
139114
  unit: "hPa",
@@ -138593,7 +139144,7 @@ var SENSOR_CAP_VOCABULARY = {
138593
139144
  noun: t("Temperature", "Temperatura"),
138594
139145
  state: [{
138595
139146
  kind: "numeric",
138596
- field: "celsius",
139147
+ field: "temperature",
138597
139148
  quantity: t("the temperature", "la temperatura"),
138598
139149
  label: t("Temperature", "Temperatura"),
138599
139150
  unit: "°C",
@@ -138664,7 +139215,7 @@ var SENSOR_CAP_VOCABULARY = {
138664
139215
  state: [
138665
139216
  {
138666
139217
  kind: "numeric",
138667
- field: "percentage",
139218
+ field: "level",
138668
139219
  quantity: t("the battery level", "il livello della batteria"),
138669
139220
  label: t("Battery", "Batteria"),
138670
139221
  unit: "%",
@@ -138706,7 +139257,7 @@ var SENSOR_CAP_VOCABULARY = {
138706
139257
  icon: "battery-charging"
138707
139258
  }
138708
139259
  ],
138709
- events: { onStatusChanged: { notOperatorFacing: "the percentage / sleeping / charging state triggers each read one field of this change" } },
139260
+ events: { onStatusChanged: { notOperatorFacing: "the level / sleeping / charging state triggers each read one field of this change" } },
138710
139261
  extraEvents: [{
138711
139262
  category: EventCategory.DeviceAwake,
138712
139263
  label: t("Woke up", "Risvegliato"),
@@ -138724,7 +139275,7 @@ var SENSOR_CAP_VOCABULARY = {
138724
139275
  noun: t("Network link", "Collegamento di rete"),
138725
139276
  state: [{
138726
139277
  kind: "numeric",
138727
- field: "signalPercent",
139278
+ field: "signal",
138728
139279
  quantity: t("the signal strength", "l’intensità del segnale"),
138729
139280
  label: t("Signal", "Segnale"),
138730
139281
  unit: "%",
@@ -138817,6 +139368,8 @@ var ML = "model and training machinery — no device action";
138817
139368
  var INTERNAL = "internal plumbing between addons — never operator-facing";
138818
139369
  /** Every system-scoped cap, with why it is not a device's vocabulary. */
138819
139370
  var SYSTEM_CAP_VOCABULARY = {
139371
+ "action-source": system(INTERNAL),
139372
+ actions: system("declared actions — run from the Actions page and from the cameras that link them"),
138820
139373
  "addon-pages": system(UI_PLUMBING),
138821
139374
  "addon-pages-source": system(UI_PLUMBING),
138822
139375
  "addon-routes": system(UI_PLUMBING),
@@ -139013,6 +139566,31 @@ Object.freeze({
139013
139566
  form: "single",
139014
139567
  optional: false
139015
139568
  }],
139569
+ "actions.get": [{
139570
+ name: "deviceId",
139571
+ form: "single",
139572
+ optional: true
139573
+ }],
139574
+ "actions.getLinks": [{
139575
+ name: "deviceId",
139576
+ form: "single",
139577
+ optional: false
139578
+ }],
139579
+ "actions.listForCamera": [{
139580
+ name: "deviceId",
139581
+ form: "single",
139582
+ optional: false
139583
+ }],
139584
+ "actions.run": [{
139585
+ name: "deviceId",
139586
+ form: "single",
139587
+ optional: true
139588
+ }],
139589
+ "actions.setLinks": [{
139590
+ name: "deviceId",
139591
+ form: "single",
139592
+ optional: false
139593
+ }],
139016
139594
  "addonSettings.getDeviceSettings": [{
139017
139595
  name: "deviceId",
139018
139596
  form: "single",
@@ -143184,6 +143762,7 @@ function ingestReading(input) {
143184
143762
  value: converted
143185
143763
  };
143186
143764
  }
143765
+ String.raw`(?<=^|["'(\s>,\[{])`;
143187
143766
  var MB = 1024 * 1024;
143188
143767
  1024 * MB, 3072 * MB;
143189
143768
  //#endregion
@@ -143310,6 +143889,334 @@ function buildConnectionFormSchema() {
143310
143889
  ] };
143311
143890
  }
143312
143891
  //#endregion
143892
+ //#region src/dreame-vacuum-decode.ts
143893
+ /**
143894
+ * Pure decode helpers for the Dreame vacuum. Translate the library's MIoT enum
143895
+ * values into the camstack `vacuum-control` slice shape. Unit-tested in
143896
+ * isolation (no facade, no network) — mirrors `homematic-decode.ts`.
143897
+ */
143898
+ /**
143899
+ * Map a Dreame {@link MiotState} (siid 2 piid 1) onto the camstack canonical
143900
+ * `VacuumState` lifecycle (`idle` / `cleaning` / `paused` / `returning` /
143901
+ * `docked` / `error`). A `null` state (not yet observed) settles on `idle`.
143902
+ *
143903
+ * The MIoT state space is large (~50 values); we collapse it onto the six
143904
+ * canonical states by family. Anything unrecognised falls through to `idle`
143905
+ * (the safe neutral) so a firmware-added code never throws.
143906
+ */
143907
+ function mapMiotStateToVacuumState(state) {
143908
+ if (state === null) return "idle";
143909
+ switch (state) {
143910
+ case MiotState.Charging:
143911
+ case MiotState.ChargingComplete:
143912
+ case MiotState.IntelligentCharging:
143913
+ case MiotState.CleanWashboardBase:
143914
+ case MiotState.AutoRepairBase:
143915
+ case MiotState.WaterSupplyDrainTest:
143916
+ case MiotState.CleanMopRefillWater:
143917
+ case MiotState.AutoEmptying:
143918
+ case MiotState.Emptying:
143919
+ case MiotState.Draining:
143920
+ case MiotState.AutoWaterDraining:
143921
+ case MiotState.Sanitizing: return "docked";
143922
+ case MiotState.MopDrying:
143923
+ case MiotState.DustBagDrying:
143924
+ case MiotState.SanitizingWithDry: return "drying";
143925
+ case MiotState.ReturningToCharge:
143926
+ case MiotState.ReturningToWash:
143927
+ case MiotState.ReturnInstallMop:
143928
+ case MiotState.ReturnRemoveMop:
143929
+ case MiotState.ReturnToEmpty:
143930
+ case MiotState.ReturningToDrain: return "returning";
143931
+ case MiotState.Paused:
143932
+ case MiotState.PausedAlt:
143933
+ case MiotState.PausedCleaning:
143934
+ case MiotState.DustBagDryingPaused:
143935
+ case MiotState.CameraMonitoringPaused:
143936
+ case MiotState.FindingPetPaused:
143937
+ case MiotState.InitialDeepCleanPaused:
143938
+ case MiotState.ChangingMopPaused:
143939
+ case MiotState.FloorMaintainingPaused: return "paused";
143940
+ case MiotState.Standby:
143941
+ case MiotState.WaitingForTask: return "idle";
143942
+ default: return "cleaning";
143943
+ }
143944
+ }
143945
+ /**
143946
+ * The camstack `fanSpeed` token for a Dreame {@link SuctionLevel} — the enum
143947
+ * member NAME, lower-cased (`Quiet` → `'quiet'`). Returns `null` for an
143948
+ * unobserved / unknown level.
143949
+ */
143950
+ function suctionLevelToken(level) {
143951
+ if (level === null) return null;
143952
+ const name = SuctionLevel[level];
143953
+ return typeof name === "string" ? name.toLowerCase() : null;
143954
+ }
143955
+ /**
143956
+ * Resolve a camstack `fanSpeed` token back to the Dreame {@link SuctionLevel}
143957
+ * enum value, validated against the model's `supported` levels. Returns null
143958
+ * when the token doesn't match a supported level (the caller rejects the set).
143959
+ */
143960
+ function tokenToSuctionLevel(token, supported) {
143961
+ const wanted = token.trim().toLowerCase();
143962
+ for (const level of supported) if (suctionLevelToken(level) === wanted) return level;
143963
+ return null;
143964
+ }
143965
+ /** The list of accepted `fanSpeed` tokens for a model's supported suction set. */
143966
+ function supportedFanSpeedTokens(supported) {
143967
+ const tokens = [];
143968
+ for (const level of supported) {
143969
+ const token = suctionLevelToken(level);
143970
+ if (token !== null) tokens.push(token);
143971
+ }
143972
+ return tokens;
143973
+ }
143974
+ /**
143975
+ * Clamp a raw battery reading (already a 0..100 percentage from the library)
143976
+ * to the `vacuum-control` slice's `batteryLevel` field. Returns null for a
143977
+ * null/NaN/out-of-band reading so the slice stays valid (the schema rejects
143978
+ * a number outside 0..100).
143979
+ */
143980
+ function clampBatteryLevel$1(value) {
143981
+ if (value === null || !Number.isFinite(value)) return null;
143982
+ return Math.max(0, Math.min(100, value));
143983
+ }
143984
+ /**
143985
+ * MiotError codes that are BENIGN completion / reminder states, not active
143986
+ * faults. A fresh cloud-shadow read of an idle docked robot legitimately
143987
+ * returns `TaskComplete` (68); it is not an error to show. The Error sensor
143988
+ * (`dreame-entity-catalog.ts`) and the vacuum card both read this set.
143989
+ * Extend it if other non-fault codes surface as spurious "errors".
143990
+ */
143991
+ var BENIGN_ERROR_CODES = new Set([MiotError.TaskComplete]);
143992
+ /** The card's error label: null for no error and for a benign completion code. */
143993
+ function vacuumErrorLabel(code) {
143994
+ if (!code || BENIGN_ERROR_CODES.has(code)) return null;
143995
+ return `Error ${code}`;
143996
+ }
143997
+ /** Clamp a 0..100 percentage (progress, coverage, …), null-safe. */
143998
+ function clampPercent$1(value) {
143999
+ if (value === null || !Number.isFinite(value)) return null;
144000
+ return Math.max(0, Math.min(100, value));
144001
+ }
144002
+ /**
144003
+ * Where each {@link VacuumTankProps} field lives (siid/piid). Property ids and
144004
+ * enum values are the HA Dreame integration's (Tasshack `dreame-vacuum`
144005
+ * `types.py`, `DreameVacuumProperty` → siid/piid map; same on `dev` and
144006
+ * v2.0.0b25, checked 2026-09-26). 27/1 and 27/4 were already read here; 27/2 and
144007
+ * 27/3 are their neighbours on the same station service. The robot pushes them
144008
+ * on change; the consumables refresh also re-reads them from the shadow.
144009
+ */
144010
+ var VACUUM_TANK_PROPS = {
144011
+ cleanWaterTankStatus: {
144012
+ siid: 27,
144013
+ piid: 1
144014
+ },
144015
+ waterTank: {
144016
+ siid: 4,
144017
+ piid: 6
144018
+ },
144019
+ detergentStatus: {
144020
+ siid: 27,
144021
+ piid: 4
144022
+ },
144023
+ dustCollection: {
144024
+ siid: 15,
144025
+ piid: 3
144026
+ },
144027
+ dirtyWaterTankStatus: {
144028
+ siid: 27,
144029
+ piid: 2
144030
+ },
144031
+ dustBagStatus: {
144032
+ siid: 27,
144033
+ piid: 3
144034
+ }
144035
+ };
144036
+ /**
144037
+ * Clean-water tank `status`: prefer the richer clean-water-tank-status (27/1),
144038
+ * fall back to the coarser water-tank presence (4/6). `null` when neither maps
144039
+ * to a known good/low reading (NOT_INSTALLED / CHECKING / unknown). Pure.
144040
+ */
144041
+ function cleanWaterTankStatus(props) {
144042
+ if (props.cleanWaterTankStatus !== null) {
144043
+ if (props.cleanWaterTankStatus === 0) return "ok";
144044
+ if (props.cleanWaterTankStatus === 2) return "low";
144045
+ return null;
144046
+ }
144047
+ if (props.waterTank === 1 || props.waterTank === 10) return "ok";
144048
+ return null;
144049
+ }
144050
+ /** Detergent tank `status` from detergent-status (27/4). Pure. */
144051
+ function detergentTankStatus(value) {
144052
+ if (value === 0) return "ok";
144053
+ if (value === 2) return "low";
144054
+ return null;
144055
+ }
144056
+ /**
144057
+ * Dust-bin `status` from dust-collection (15/3); OVER_USE reads as `full`. A dust
144058
+ * bag that is missing or asks to be checked (27/3 NOT_INSTALLED / CHECK) is never
144059
+ * `ok` — the enum does not say "full" either, so it is unknown. Pure.
144060
+ */
144061
+ function dustBinTankStatus(dustCollection, dustBag) {
144062
+ if (dustCollection === 2) return "full";
144063
+ if (dustBag !== null && dustBag !== 0) return null;
144064
+ if (dustCollection === 1) return "ok";
144065
+ return null;
144066
+ }
144067
+ /**
144068
+ * Dirty-water tank `status` from dirty-water-tank-status (27/2). The robot has
144069
+ * ONE value for "not installed or full" and cannot tell them apart; lifting the
144070
+ * tank out to EMPTY it reads exactly like a full one, so that value is unknown
144071
+ * (`null`) — never `full`, which would raise a false "tank full" at the moment
144072
+ * someone is fixing it (controller ruling 2026-09-26). Absent or any other
144073
+ * value is unknown too, never `ok`.
144074
+ *
144075
+ * The unambiguous "full" is the robot's own error `MiotError.DirtyWaterTankFull`
144076
+ * (86, the library's `errorCode`, siid 2 piid 2): while it is raised the tank is
144077
+ * `full` whatever 27/2 says; once it clears, 27/2 decides again. Pure.
144078
+ */
144079
+ function dirtyWaterTankStatus(value, errorCode) {
144080
+ if (errorCode === MiotError.DirtyWaterTankFull) return "full";
144081
+ if (value === 0) return "ok";
144082
+ return null;
144083
+ }
144084
+ /**
144085
+ * The four tanks (clean-water / dirty-water / detergent / dust-bin) decoded from the model's {@link VacuumCapabilities}, with the
144086
+ * discrete `status` populated from the live MIoT enum props when supplied. Pure.
144087
+ *
144088
+ * The nodedreame library exposes NO tank FILL-LEVEL telemetry — the cloud shadow
144089
+ * (verified against a real `dreame.vacuum.r2538z`) carries only the water-VOLUME
144090
+ * *setting* (a mop-flow level, NOT a tank fill). We therefore never fabricate a
144091
+ * `level`; `level` is always `null`. The cap contract is honoured on PRESENCE,
144092
+ * with `status` derived from the tank-status enums:
144093
+ *
144094
+ * - a field is `null` when the hardware HAS NO such tank (capability flags), and
144095
+ * - `{ level: null, status }` when the tank EXISTS — `status` from the enum
144096
+ * prop ('ok' / 'low' / 'full'), or `null` when the device reports none.
144097
+ *
144098
+ * Presence is derived from the capability record:
144099
+ * - clean-water ← `canMop` (a mopping robot carries a clean-water reservoir)
144100
+ * - dirty-water ← `canMopWash` (a self-wash dock has a recovery/dirty tank)
144101
+ * - detergent ← `hasDetergentReservoir`
144102
+ * - dust-bin ← `canAutoEmpty` (an auto-empty robot has an onboard dust bin)
144103
+ */
144104
+ function decodeVacuumTanks(caps, props, errorCode = null) {
144105
+ const tank = (present, status) => present ? {
144106
+ level: null,
144107
+ status
144108
+ } : null;
144109
+ return {
144110
+ cleanWater: tank(Boolean(caps?.canMop), props ? cleanWaterTankStatus(props) : null),
144111
+ dirtyWater: tank(Boolean(caps?.canMopWash), dirtyWaterTankStatus(props?.dirtyWaterTankStatus ?? null, errorCode)),
144112
+ detergent: tank(Boolean(caps?.hasDetergentReservoir), props ? detergentTankStatus(props.detergentStatus) : null),
144113
+ dustBin: tank(Boolean(caps?.canAutoEmpty), props ? dustBinTankStatus(props.dustCollection, props.dustBagStatus) : null)
144114
+ };
144115
+ }
144116
+ /** How each tank is named and what it is, in the order the panel lists them. */
144117
+ var TANK_ITEMS = [
144118
+ {
144119
+ key: "cleanWater",
144120
+ label: "Clean water tank",
144121
+ kind: "supply",
144122
+ form: "tank"
144123
+ },
144124
+ {
144125
+ key: "dirtyWater",
144126
+ label: "Dirty water tank",
144127
+ kind: "waste",
144128
+ form: "tank"
144129
+ },
144130
+ {
144131
+ key: "detergent",
144132
+ label: "Detergent",
144133
+ kind: "supply",
144134
+ form: "product"
144135
+ },
144136
+ {
144137
+ key: "dustBin",
144138
+ label: "Dust bin",
144139
+ kind: "waste",
144140
+ form: "bin"
144141
+ }
144142
+ ];
144143
+ /**
144144
+ * The tanks as `consumables` items (D915): every PRESENT tank becomes a
144145
+ * `supply` (clean water, detergent) or `waste` (dirty water, dust bin) item
144146
+ * keyed by its old field name, drawn as a `tank` (the two water tanks), a
144147
+ * `product` (detergent) or a `bin` (the dust bin) — the key the notification centre's tank
144148
+ * episodes were built on, so an open episode carries on. A tank is never
144149
+ * resettable: emptying or refilling it is the reset. Pure.
144150
+ */
144151
+ function vacuumTankItems(tanks) {
144152
+ return TANK_ITEMS.flatMap(({ key, label, kind, form }) => {
144153
+ const tank = tanks[key];
144154
+ if (tank === null) return [];
144155
+ return [{
144156
+ key,
144157
+ label,
144158
+ level: tank.level,
144159
+ status: tank.status,
144160
+ kind,
144161
+ form,
144162
+ lastResetAt: null,
144163
+ resettable: false
144164
+ }];
144165
+ });
144166
+ }
144167
+ /**
144168
+ * Clamp a raw consumable percentage to `[0, 100]`, or return `null` when the
144169
+ * value is absent / non-finite. The library reports remaining life as a 0..100
144170
+ * integer; we clamp defensively so an out-of-range firmware value never violates
144171
+ * the cap schema.
144172
+ */
144173
+ function clampConsumablePct(value) {
144174
+ if (value === null || !Number.isFinite(value)) return null;
144175
+ return Math.max(0, Math.min(100, value));
144176
+ }
144177
+ /**
144178
+ * The `consumables` cap items for the consumables a vacuum REPORTS.
144179
+ *
144180
+ * PRESENCE-driven (mirrors HA): the library's `supportedConsumables` enumerates
144181
+ * ONLY the consumables this model reports (a present MIoT life property), each
144182
+ * with its remaining-life % and whether a reset action exists. No hardcoded
144183
+ * per-model list: a consumable the device does not expose never appears.
144184
+ *
144185
+ * `status` is `null` — the provider reports the level, the notification rule
144186
+ * decides what is low. The slice clocks are `nextConsumablesSlice`'s job. Pure.
144187
+ */
144188
+ function buildVacuumConsumableItems(readings) {
144189
+ return readings.map((c) => ({
144190
+ key: c.key,
144191
+ label: c.label,
144192
+ level: clampConsumablePct(c.leftPct),
144193
+ status: null,
144194
+ lastResetAt: null,
144195
+ resettable: c.resettable,
144196
+ form: "part"
144197
+ }));
144198
+ }
144199
+ /** Structural equality of two JSON-shaped values, key order ignored. */
144200
+ function jsonEqual(a, b) {
144201
+ if (a === b) return true;
144202
+ 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]));
144203
+ if (a === null || b === null || typeof a !== "object" || typeof b !== "object") return false;
144204
+ const ka = Object.keys(a);
144205
+ const kb = Object.keys(b);
144206
+ return ka.length === kb.length && ka.every((k) => jsonEqual(Reflect.get(a, k), Reflect.get(b, k)));
144207
+ }
144208
+ /**
144209
+ * The `vacuum-control` slice to write, or `null` when only the clock moved.
144210
+ * `recomputeSlice` runs on EVERY push; without this each one was a write (and a
144211
+ * `device.state-changed`) even when nothing the robot reported had changed. Pure.
144212
+ */
144213
+ function nextVacuumControlSlice(prev, next) {
144214
+ if (prev === void 0) return next;
144215
+ const { lastChangedAt: _prevAt, ...prevContent } = prev;
144216
+ const { lastChangedAt: _nextAt, ...nextContent } = next;
144217
+ return jsonEqual(prevContent, nextContent) ? null : next;
144218
+ }
144219
+ //#endregion
143313
144220
  //#region src/dreame-entity-catalog.ts
143314
144221
  var STATS = "Statistics";
143315
144222
  var MAINT = "Maintenance";
@@ -143956,14 +144863,6 @@ function enumOptions(e) {
143956
144863
  label: humanizeEnumName(name)
143957
144864
  }));
143958
144865
  }
143959
- /**
143960
- * MiotError codes that are BENIGN completion / reminder states, not active
143961
- * faults. HA does not surface these as errors — a fresh cloud-shadow read of an
143962
- * idle docked robot legitimately returns `TaskComplete` (68). We relabel them to
143963
- * the `Clear` label so the Error sensor only shows real faults (HA parity).
143964
- * Extend this set if other non-fault codes surface as spurious "errors".
143965
- */
143966
- var BENIGN_ERROR_CODES = new Set([MiotError.TaskComplete]);
143967
144866
  /** `MiotError` options with the benign completion codes remapped to the `Clear`
143968
144867
  * label, so the Error sensor reads as no-error for them (matches HA). */
143969
144868
  function errorEnumOptions() {
@@ -145321,321 +146220,6 @@ var DreameButtonDevice = class extends DreameChildDevice {
145321
146220
  recomputeSlice() {}
145322
146221
  };
145323
146222
  //#endregion
145324
- //#region src/dreame-vacuum-decode.ts
145325
- /**
145326
- * Pure decode helpers for the Dreame vacuum. Translate the library's MIoT enum
145327
- * values into the camstack `vacuum-control` slice shape. Unit-tested in
145328
- * isolation (no facade, no network) — mirrors `homematic-decode.ts`.
145329
- */
145330
- /**
145331
- * Map a Dreame {@link MiotState} (siid 2 piid 1) onto the camstack canonical
145332
- * `VacuumState` lifecycle (`idle` / `cleaning` / `paused` / `returning` /
145333
- * `docked` / `error`). A `null` state (not yet observed) settles on `idle`.
145334
- *
145335
- * The MIoT state space is large (~50 values); we collapse it onto the six
145336
- * canonical states by family. Anything unrecognised falls through to `idle`
145337
- * (the safe neutral) so a firmware-added code never throws.
145338
- */
145339
- function mapMiotStateToVacuumState(state) {
145340
- if (state === null) return "idle";
145341
- switch (state) {
145342
- case MiotState.Charging:
145343
- case MiotState.ChargingComplete:
145344
- case MiotState.IntelligentCharging:
145345
- case MiotState.CleanWashboardBase:
145346
- case MiotState.AutoRepairBase:
145347
- case MiotState.WaterSupplyDrainTest:
145348
- case MiotState.CleanMopRefillWater:
145349
- case MiotState.AutoEmptying:
145350
- case MiotState.Emptying:
145351
- case MiotState.Draining:
145352
- case MiotState.AutoWaterDraining:
145353
- case MiotState.Sanitizing: return "docked";
145354
- case MiotState.MopDrying:
145355
- case MiotState.DustBagDrying:
145356
- case MiotState.SanitizingWithDry: return "drying";
145357
- case MiotState.ReturningToCharge:
145358
- case MiotState.ReturningToWash:
145359
- case MiotState.ReturnInstallMop:
145360
- case MiotState.ReturnRemoveMop:
145361
- case MiotState.ReturnToEmpty:
145362
- case MiotState.ReturningToDrain: return "returning";
145363
- case MiotState.Paused:
145364
- case MiotState.PausedAlt:
145365
- case MiotState.PausedCleaning:
145366
- case MiotState.DustBagDryingPaused:
145367
- case MiotState.CameraMonitoringPaused:
145368
- case MiotState.FindingPetPaused:
145369
- case MiotState.InitialDeepCleanPaused:
145370
- case MiotState.ChangingMopPaused:
145371
- case MiotState.FloorMaintainingPaused: return "paused";
145372
- case MiotState.Standby:
145373
- case MiotState.WaitingForTask: return "idle";
145374
- default: return "cleaning";
145375
- }
145376
- }
145377
- /**
145378
- * The camstack `fanSpeed` token for a Dreame {@link SuctionLevel} — the enum
145379
- * member NAME, lower-cased (`Quiet` → `'quiet'`). Returns `null` for an
145380
- * unobserved / unknown level.
145381
- */
145382
- function suctionLevelToken(level) {
145383
- if (level === null) return null;
145384
- const name = SuctionLevel[level];
145385
- return typeof name === "string" ? name.toLowerCase() : null;
145386
- }
145387
- /**
145388
- * Resolve a camstack `fanSpeed` token back to the Dreame {@link SuctionLevel}
145389
- * enum value, validated against the model's `supported` levels. Returns null
145390
- * when the token doesn't match a supported level (the caller rejects the set).
145391
- */
145392
- function tokenToSuctionLevel(token, supported) {
145393
- const wanted = token.trim().toLowerCase();
145394
- for (const level of supported) if (suctionLevelToken(level) === wanted) return level;
145395
- return null;
145396
- }
145397
- /** The list of accepted `fanSpeed` tokens for a model's supported suction set. */
145398
- function supportedFanSpeedTokens(supported) {
145399
- const tokens = [];
145400
- for (const level of supported) {
145401
- const token = suctionLevelToken(level);
145402
- if (token !== null) tokens.push(token);
145403
- }
145404
- return tokens;
145405
- }
145406
- /**
145407
- * Clamp a raw battery reading (already a 0..100 percentage from the library)
145408
- * to the `vacuum-control` slice's `batteryLevel` field. Returns null for a
145409
- * null/NaN/out-of-band reading so the slice stays valid (the schema rejects
145410
- * a number outside 0..100).
145411
- */
145412
- function clampBatteryLevel$1(value) {
145413
- if (value === null || !Number.isFinite(value)) return null;
145414
- return Math.max(0, Math.min(100, value));
145415
- }
145416
- /** Clamp a 0..100 percentage (progress, coverage, …), null-safe. */
145417
- function clampPercent$1(value) {
145418
- if (value === null || !Number.isFinite(value)) return null;
145419
- return Math.max(0, Math.min(100, value));
145420
- }
145421
- /**
145422
- * Where each {@link VacuumTankProps} field lives (siid/piid). Property ids and
145423
- * enum values are the HA Dreame integration's (Tasshack `dreame-vacuum`
145424
- * `types.py`, `DreameVacuumProperty` → siid/piid map; same on `dev` and
145425
- * v2.0.0b25, checked 2026-09-26). 27/1 and 27/4 were already read here; 27/2 and
145426
- * 27/3 are their neighbours on the same station service. The robot pushes them
145427
- * on change; the consumables refresh also re-reads them from the shadow.
145428
- */
145429
- var VACUUM_TANK_PROPS = {
145430
- cleanWaterTankStatus: {
145431
- siid: 27,
145432
- piid: 1
145433
- },
145434
- waterTank: {
145435
- siid: 4,
145436
- piid: 6
145437
- },
145438
- detergentStatus: {
145439
- siid: 27,
145440
- piid: 4
145441
- },
145442
- dustCollection: {
145443
- siid: 15,
145444
- piid: 3
145445
- },
145446
- dirtyWaterTankStatus: {
145447
- siid: 27,
145448
- piid: 2
145449
- },
145450
- dustBagStatus: {
145451
- siid: 27,
145452
- piid: 3
145453
- }
145454
- };
145455
- /**
145456
- * Clean-water tank `status`: prefer the richer clean-water-tank-status (27/1),
145457
- * fall back to the coarser water-tank presence (4/6). `null` when neither maps
145458
- * to a known good/low reading (NOT_INSTALLED / CHECKING / unknown). Pure.
145459
- */
145460
- function cleanWaterTankStatus(props) {
145461
- if (props.cleanWaterTankStatus !== null) {
145462
- if (props.cleanWaterTankStatus === 0) return "ok";
145463
- if (props.cleanWaterTankStatus === 2) return "low";
145464
- return null;
145465
- }
145466
- if (props.waterTank === 1 || props.waterTank === 10) return "ok";
145467
- return null;
145468
- }
145469
- /** Detergent tank `status` from detergent-status (27/4). Pure. */
145470
- function detergentTankStatus(value) {
145471
- if (value === 0) return "ok";
145472
- if (value === 2) return "low";
145473
- return null;
145474
- }
145475
- /**
145476
- * Dust-bin `status` from dust-collection (15/3); OVER_USE reads as `full`. A dust
145477
- * bag that is missing or asks to be checked (27/3 NOT_INSTALLED / CHECK) is never
145478
- * `ok` — the enum does not say "full" either, so it is unknown. Pure.
145479
- */
145480
- function dustBinTankStatus(dustCollection, dustBag) {
145481
- if (dustCollection === 2) return "full";
145482
- if (dustBag !== null && dustBag !== 0) return null;
145483
- if (dustCollection === 1) return "ok";
145484
- return null;
145485
- }
145486
- /**
145487
- * Dirty-water tank `status` from dirty-water-tank-status (27/2). The robot has
145488
- * ONE value for "not installed or full" and cannot tell them apart; lifting the
145489
- * tank out to EMPTY it reads exactly like a full one, so that value is unknown
145490
- * (`null`) — never `full`, which would raise a false "tank full" at the moment
145491
- * someone is fixing it (controller ruling 2026-09-26). Absent or any other
145492
- * value is unknown too, never `ok`.
145493
- *
145494
- * The unambiguous "full" is the robot's own error `MiotError.DirtyWaterTankFull`
145495
- * (86, the library's `errorCode`, siid 2 piid 2): while it is raised the tank is
145496
- * `full` whatever 27/2 says; once it clears, 27/2 decides again. Pure.
145497
- */
145498
- function dirtyWaterTankStatus(value, errorCode) {
145499
- if (errorCode === MiotError.DirtyWaterTankFull) return "full";
145500
- if (value === 0) return "ok";
145501
- return null;
145502
- }
145503
- /**
145504
- * The four tanks (clean-water / dirty-water / detergent / dust-bin) decoded from the model's {@link VacuumCapabilities}, with the
145505
- * discrete `status` populated from the live MIoT enum props when supplied. Pure.
145506
- *
145507
- * The nodedreame library exposes NO tank FILL-LEVEL telemetry — the cloud shadow
145508
- * (verified against a real `dreame.vacuum.r2538z`) carries only the water-VOLUME
145509
- * *setting* (a mop-flow level, NOT a tank fill). We therefore never fabricate a
145510
- * `level`; `level` is always `null`. The cap contract is honoured on PRESENCE,
145511
- * with `status` derived from the tank-status enums:
145512
- *
145513
- * - a field is `null` when the hardware HAS NO such tank (capability flags), and
145514
- * - `{ level: null, status }` when the tank EXISTS — `status` from the enum
145515
- * prop ('ok' / 'low' / 'full'), or `null` when the device reports none.
145516
- *
145517
- * Presence is derived from the capability record:
145518
- * - clean-water ← `canMop` (a mopping robot carries a clean-water reservoir)
145519
- * - dirty-water ← `canMopWash` (a self-wash dock has a recovery/dirty tank)
145520
- * - detergent ← `hasDetergentReservoir`
145521
- * - dust-bin ← `canAutoEmpty` (an auto-empty robot has an onboard dust bin)
145522
- */
145523
- function decodeVacuumTanks(caps, props, errorCode = null) {
145524
- const tank = (present, status) => present ? {
145525
- level: null,
145526
- status
145527
- } : null;
145528
- return {
145529
- cleanWater: tank(Boolean(caps?.canMop), props ? cleanWaterTankStatus(props) : null),
145530
- dirtyWater: tank(Boolean(caps?.canMopWash), dirtyWaterTankStatus(props?.dirtyWaterTankStatus ?? null, errorCode)),
145531
- detergent: tank(Boolean(caps?.hasDetergentReservoir), props ? detergentTankStatus(props.detergentStatus) : null),
145532
- dustBin: tank(Boolean(caps?.canAutoEmpty), props ? dustBinTankStatus(props.dustCollection, props.dustBagStatus) : null)
145533
- };
145534
- }
145535
- /** How each tank is named and what it is, in the order the panel lists them. */
145536
- var TANK_ITEMS = [
145537
- {
145538
- key: "cleanWater",
145539
- label: "Clean water tank",
145540
- kind: "supply",
145541
- form: "tank"
145542
- },
145543
- {
145544
- key: "dirtyWater",
145545
- label: "Dirty water tank",
145546
- kind: "waste",
145547
- form: "tank"
145548
- },
145549
- {
145550
- key: "detergent",
145551
- label: "Detergent",
145552
- kind: "supply",
145553
- form: "product"
145554
- },
145555
- {
145556
- key: "dustBin",
145557
- label: "Dust bin",
145558
- kind: "waste",
145559
- form: "bin"
145560
- }
145561
- ];
145562
- /**
145563
- * The tanks as `consumables` items (D915): every PRESENT tank becomes a
145564
- * `supply` (clean water, detergent) or `waste` (dirty water, dust bin) item
145565
- * keyed by its old field name, drawn as a `tank` (the two water tanks), a
145566
- * `product` (detergent) or a `bin` (the dust bin) — the key the notification centre's tank
145567
- * episodes were built on, so an open episode carries on. A tank is never
145568
- * resettable: emptying or refilling it is the reset. Pure.
145569
- */
145570
- function vacuumTankItems(tanks) {
145571
- return TANK_ITEMS.flatMap(({ key, label, kind, form }) => {
145572
- const tank = tanks[key];
145573
- if (tank === null) return [];
145574
- return [{
145575
- key,
145576
- label,
145577
- level: tank.level,
145578
- status: tank.status,
145579
- kind,
145580
- form,
145581
- lastResetAt: null,
145582
- resettable: false
145583
- }];
145584
- });
145585
- }
145586
- /**
145587
- * Clamp a raw consumable percentage to `[0, 100]`, or return `null` when the
145588
- * value is absent / non-finite. The library reports remaining life as a 0..100
145589
- * integer; we clamp defensively so an out-of-range firmware value never violates
145590
- * the cap schema.
145591
- */
145592
- function clampConsumablePct(value) {
145593
- if (value === null || !Number.isFinite(value)) return null;
145594
- return Math.max(0, Math.min(100, value));
145595
- }
145596
- /**
145597
- * The `consumables` cap items for the consumables a vacuum REPORTS.
145598
- *
145599
- * PRESENCE-driven (mirrors HA): the library's `supportedConsumables` enumerates
145600
- * ONLY the consumables this model reports (a present MIoT life property), each
145601
- * with its remaining-life % and whether a reset action exists. No hardcoded
145602
- * per-model list: a consumable the device does not expose never appears.
145603
- *
145604
- * `status` is `null` — the provider reports the level, the notification rule
145605
- * decides what is low. The slice clocks are `nextConsumablesSlice`'s job. Pure.
145606
- */
145607
- function buildVacuumConsumableItems(readings) {
145608
- return readings.map((c) => ({
145609
- key: c.key,
145610
- label: c.label,
145611
- level: clampConsumablePct(c.leftPct),
145612
- status: null,
145613
- lastResetAt: null,
145614
- resettable: c.resettable,
145615
- form: "part"
145616
- }));
145617
- }
145618
- /** Structural equality of two JSON-shaped values, key order ignored. */
145619
- function jsonEqual(a, b) {
145620
- if (a === b) return true;
145621
- 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]));
145622
- if (a === null || b === null || typeof a !== "object" || typeof b !== "object") return false;
145623
- const ka = Object.keys(a);
145624
- const kb = Object.keys(b);
145625
- return ka.length === kb.length && ka.every((k) => jsonEqual(Reflect.get(a, k), Reflect.get(b, k)));
145626
- }
145627
- /**
145628
- * The `vacuum-control` slice to write, or `null` when only the clock moved.
145629
- * `recomputeSlice` runs on EVERY push; without this each one was a write (and a
145630
- * `device.state-changed`) even when nothing the robot reported had changed. Pure.
145631
- */
145632
- function nextVacuumControlSlice(prev, next) {
145633
- if (prev === void 0) return next;
145634
- const { lastChangedAt: _prevAt, ...prevContent } = prev;
145635
- const { lastChangedAt: _nextAt, ...nextContent } = next;
145636
- return jsonEqual(prevContent, nextContent) ? null : next;
145637
- }
145638
- //#endregion
145639
146223
  //#region src/devices/dreame-camera-stream.ts
145640
146224
  /**
145641
146225
  * The robot camera as a stream the broker can pull — the pure facts.
@@ -145670,7 +146254,7 @@ function clampLevel(level) {
145670
146254
  function cameraBatterySlice(facts) {
145671
146255
  const charging = facts.charging === null ? "unknown" : facts.charging === CHARGING_STATUS_CHARGING ? "dc" : "none";
145672
146256
  return {
145673
- percentage: clampLevel(facts.level),
146257
+ level: clampLevel(facts.level),
145674
146258
  charging,
145675
146259
  sleeping: !facts.streamOpen,
145676
146260
  lastUpdated: facts.now,
@@ -146079,7 +146663,7 @@ function networkLinkFromBars(bars, now) {
146079
146663
  if (bars === null || !Number.isFinite(bars)) return null;
146080
146664
  return {
146081
146665
  type: "wifi",
146082
- signalPercent: bars === 0 ? null : signalPercentFromBars(bars, 4),
146666
+ signal: bars === 0 ? null : signalPercentFromBars(bars, 4),
146083
146667
  lastUpdated: now
146084
146668
  };
146085
146669
  }
@@ -193282,7 +193866,7 @@ var SHADOW_POLL_MS = 12e4;
193282
193866
  var COLD_START = {
193283
193867
  activity: "idle",
193284
193868
  batteryLevel: null,
193285
- progressPercent: null,
193869
+ progress: null,
193286
193870
  currentCode: null,
193287
193871
  currentCodeLabel: null,
193288
193872
  severity: "info",
@@ -193396,11 +193980,11 @@ var DreameMowerDevice = class extends DreameChildDevice {
193396
193980
  const status = mower.status;
193397
193981
  const code = mower.faultRaw;
193398
193982
  const livePct = clampPercent(mower.mowingProgressPct);
193399
- const prevPct = this.runtimeState.getCapState(CAP_NAME$9)?.progressPercent;
193983
+ const prevPct = this.runtimeState.getCapState(CAP_NAME$9)?.progress;
193400
193984
  const slice = {
193401
193985
  activity: mapMowerStatusToActivity(status),
193402
193986
  batteryLevel: clampBatteryLevel(mower.battery),
193403
- progressPercent: livePct ?? prevPct ?? null,
193987
+ progress: livePct ?? prevPct ?? null,
193404
193988
  currentCode: code,
193405
193989
  currentCodeLabel: code !== null ? MowerFault[code] ?? null : null,
193406
193990
  severity: mower.faultSeverity,
@@ -194340,7 +194924,7 @@ var CONTROL_COLD_START = {
194340
194924
  batteryLevel: null,
194341
194925
  fanSpeed: null,
194342
194926
  availableFanSpeeds: [],
194343
- progressPercent: null,
194927
+ progress: null,
194344
194928
  errorCode: null,
194345
194929
  errorLabel: null,
194346
194930
  lastChangedAt: 0
@@ -194494,9 +195078,9 @@ var DreameVacuumDevice = class extends DreameChildDevice {
194494
195078
  batteryLevel: clampBatteryLevel$1(vacuum.battery),
194495
195079
  fanSpeed: suctionLevelToken(vacuum.suction),
194496
195080
  availableFanSpeeds: supportedFanSpeedTokens(vacuum.supportedSuctionLevels),
194497
- progressPercent: clampPercent$1(vacuum.taskProgressPct),
195081
+ progress: clampPercent$1(vacuum.taskProgressPct),
194498
195082
  errorCode: vacuum.errorCode,
194499
- errorLabel: vacuum.errorCode ? `Error ${vacuum.errorCode}` : null,
195083
+ errorLabel: vacuumErrorLabel(vacuum.errorCode),
194500
195084
  lastChangedAt: Date.now()
194501
195085
  };
194502
195086
  const toWrite = nextVacuumControlSlice(this.runtimeState.getCapState(CAP_NAME$1), controlSlice);
@@ -196465,4 +197049,4 @@ var DreameProviderAddon = class extends BaseDeviceProvider {
196465
197049
  }
196466
197050
  };
196467
197051
  //#endregion
196468
- export { DreameProviderAddon, tokenToSuctionLevel as a, deviceTypeForKind as c, dreameConfigSchema as d, toNodreameOptions as f, supportedFanSpeedTokens as i, buildConnectionFormSchema as l, mapMiotStateToVacuumState as n, classifyDreameModel as o, suctionLevelToken as r, controlCapForKind as s, mapMowerStatusToActivity as t, connectionFromSettings as u };
197052
+ export { DreameProviderAddon, mapMiotStateToVacuumState as a, tokenToSuctionLevel as c, dreameConfigSchema as d, toNodreameOptions as f, deviceTypeForKind as i, buildConnectionFormSchema as l, classifyDreameModel as n, suctionLevelToken as o, controlCapForKind as r, supportedFanSpeedTokens as s, mapMowerStatusToActivity as t, connectionFromSettings as u };