@camstack/addon-provider-dreame 0.2.159 → 0.2.160

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/addon.js CHANGED
@@ -52902,7 +52902,7 @@ var DreameCameraStream = class extends TypedEmitter {
52902
52902
  }
52903
52903
  };
52904
52904
  //#endregion
52905
- //#region ../types/dist/event-category-CT-hz5Rb.mjs
52905
+ //#region ../types/dist/event-category-hdMR7CTW.mjs
52906
52906
  var EventCategory = /* @__PURE__ */ function(EventCategory) {
52907
52907
  EventCategory["SystemBoot"] = "system.boot";
52908
52908
  EventCategory["SystemAddonsReady"] = "system.addons-ready";
@@ -53667,6 +53667,11 @@ var EventCategory = /* @__PURE__ */ function(EventCategory) {
53667
53667
  */
53668
53668
  EventCategory["AccessoriesChanged"] = "accessories.onAccessoriesChanged";
53669
53669
  /**
53670
+ * An action run started or ended. Emitted by the `actions` surface.
53671
+ * Payload: `ActionRunChanged` (`{ actionId, runId, isRunning, outcome?, lastRunAt, lastError }`).
53672
+ */
53673
+ EventCategory["ActionsRunChanged"] = "actions.onRunChanged";
53674
+ /**
53670
53675
  * Progress update from a running model conversion job.
53671
53676
  * Payload: `{ kind: 'model-convert', phase, sessionId?, pct?, detail? }`.
53672
53677
  * Emitted by `addon-model-studio` on the converting node.
@@ -58345,7 +58350,7 @@ var ZodIssueCode = {
58345
58350
  var ZodFirstPartyTypeKind;
58346
58351
  ZodFirstPartyTypeKind || (ZodFirstPartyTypeKind = {});
58347
58352
  //#endregion
58348
- //#region ../types/dist/sleep-BE7IAR2p.mjs
58353
+ //#region ../types/dist/sleep-BHjNez61.mjs
58349
58354
  /**
58350
58355
  * The audio chunk plane's byte format, and the ONE expansion from a coded
58351
58356
  * window to float samples (D455).
@@ -61465,6 +61470,8 @@ Object.freeze({ "doorbell": "lastPressedAt" });
61465
61470
  /** Every capability name, so addon code can check a name without the schema graph. */
61466
61471
  var CAP_NAMES = Object.freeze([
61467
61472
  "accessories",
61473
+ "action-source",
61474
+ "actions",
61468
61475
  "addon-pages",
61469
61476
  "addon-pages-source",
61470
61477
  "addon-routes",
@@ -68593,7 +68600,23 @@ var AutomationTriggerSchema = discriminatedUnion("kind", [
68593
68600
  kind: literal("schedule"),
68594
68601
  cron: string().min(1).max(120)
68595
68602
  }),
68596
- object({ kind: literal("manual") })
68603
+ object({ kind: literal("manual") }),
68604
+ object({
68605
+ kind: literal("detection"),
68606
+ /** Class names. A macro class (`animal`, `vehicle`, `person`, `package`)
68607
+ * matches every raw class that maps to it; any other name matches
68608
+ * exactly. */
68609
+ classes: array(string().min(1)).min(1),
68610
+ deviceIds: array(number().int().nonnegative()).min(1),
68611
+ /** The track must have visited at least one of these zones. */
68612
+ zones: array(string().min(1)).min(1).optional(),
68613
+ /** Best-so-far confidence the track must reach. */
68614
+ minScore: number().min(0).max(1).optional()
68615
+ }),
68616
+ object({
68617
+ kind: literal("interval"),
68618
+ minutes: number().int().min(1).max(1440)
68619
+ })
68597
68620
  ]);
68598
68621
  /**
68599
68622
  * One action step.
@@ -68613,28 +68636,47 @@ var AutomationTriggerSchema = discriminatedUnion("kind", [
68613
68636
  * is called. The runner still receives a number and is untouched; the
68614
68637
  * resolution is the recipe's job, not the runner's.
68615
68638
  */
68639
+ var AutomationWaitStepSchema = object({
68640
+ kind: literal("wait"),
68641
+ seconds: number().min(0).max(300)
68642
+ });
68643
+ var AutomationCapStepSchema = object({
68644
+ kind: literal("cap"),
68645
+ /**
68646
+ * One device, several same-family devices (locks / climate family / …),
68647
+ * or `{ $var }` for the device that triggered the run. An array is
68648
+ * fan-out at codegen — each id gets the same cap.method call.
68649
+ */
68650
+ deviceId: union([
68651
+ number().int(),
68652
+ array(number().int()).min(1),
68653
+ object({ $var: string().min(1) })
68654
+ ]),
68655
+ cap: string().min(1),
68656
+ method: string().min(1),
68657
+ /** Values may carry `{{vars.x}}` slots, or be exactly `{ $var: 'name' }`, which
68658
+ * SUBSTITUTE and do not evaluate (§3.2.3). Anything beyond substitution is
68659
+ * the expression leaf. */
68660
+ args: record(string(), unknown()).optional()
68661
+ });
68662
+ /**
68663
+ * The steps an internal Action may contain: cap and wait, and nothing that can
68664
+ * call back into an Action — so an Action can never reach itself (no cycles).
68665
+ */
68666
+ var ActionStepSchema = discriminatedUnion("kind", [AutomationWaitStepSchema, AutomationCapStepSchema]);
68616
68667
  var AutomationActionSchema = discriminatedUnion("kind", [
68668
+ AutomationWaitStepSchema,
68669
+ AutomationCapStepSchema,
68617
68670
  object({
68618
- kind: literal("wait"),
68619
- seconds: number().min(0).max(300)
68620
- }),
68621
- object({
68622
- kind: literal("cap"),
68623
- /**
68624
- * One device, several same-family devices (locks / climate family / …),
68625
- * or `{ $var }` for the device that triggered the run. An array is
68626
- * fan-out at codegen — each id gets the same cap.method call.
68627
- */
68628
- deviceId: union([
68629
- number().int(),
68630
- array(number().int()).min(1),
68631
- object({ $var: string().min(1) })
68632
- ]),
68633
- cap: string().min(1),
68634
- method: string().min(1),
68635
- /** Values may carry `{{vars.x}}` slots, which SUBSTITUTE and do not
68636
- * evaluate (§3.2.3). Anything beyond substitution is the expression leaf. */
68637
- args: record(string(), unknown()).optional()
68671
+ kind: literal("action"),
68672
+ /** An entry of the `actions` surface (`internal:<uuid>` or `<addonId>:<localId>`). */
68673
+ actionId: string().min(1),
68674
+ /** Values for the action's declared variables; the others take their default. */
68675
+ variables: record(string(), union([
68676
+ string(),
68677
+ number(),
68678
+ boolean()
68679
+ ])).optional()
68638
68680
  }),
68639
68681
  object({
68640
68682
  kind: literal("code"),
@@ -68643,10 +68685,356 @@ var AutomationActionSchema = discriminatedUnion("kind", [
68643
68685
  code: string().min(1).max(2e4)
68644
68686
  })
68645
68687
  ]);
68688
+ /**
68689
+ * How a trigger that arrives while the automation is busy (or cooling down) is
68690
+ * handled. One of the automation's execution modes, never a property of its
68691
+ * actions. Absent means `single`.
68692
+ *
68693
+ * - `single` — drop the trigger while a run is in progress (traced + logged).
68694
+ * - `restart` — abort the run in progress (at its next step boundary), start fresh.
68695
+ * - `queued` — queue up to `max` triggers, drop the rest (traced).
68696
+ * - `parallel` — up to `max` runs at once, drop the rest (traced).
68697
+ * - `cooldown` — `single`, and no new run until `minutes` after the last run
68698
+ * STARTED; persisted across a restart. A manual trigger bypasses only this
68699
+ * wait, never the single/queued limits.
68700
+ */
68701
+ var AutomationRunModeSchema = discriminatedUnion("mode", [
68702
+ object({ mode: literal("single") }),
68703
+ object({ mode: literal("restart") }),
68704
+ object({
68705
+ mode: literal("queued"),
68706
+ max: number().int().min(1).max(10)
68707
+ }),
68708
+ object({
68709
+ mode: literal("parallel"),
68710
+ max: number().int().min(1).max(10)
68711
+ }),
68712
+ object({
68713
+ mode: literal("cooldown"),
68714
+ minutes: number().int().min(1).max(1440)
68715
+ })
68716
+ ]);
68646
68717
  var AutomationRecipeSchema = object({
68647
68718
  triggers: array(AutomationTriggerSchema),
68648
68719
  conditions: AutomationConditionSchema.optional(),
68649
- actions: array(AutomationActionSchema)
68720
+ actions: array(AutomationActionSchema),
68721
+ runMode: AutomationRunModeSchema.optional(),
68722
+ /** How many run traces the automation keeps (default {@link DEFAULT_TRACE_KEEP}). */
68723
+ keepTraces: number().int().min(1).max(100).optional()
68724
+ });
68725
+ new Set([
68726
+ {
68727
+ id: "person",
68728
+ name: "Person"
68729
+ },
68730
+ {
68731
+ id: "vehicle",
68732
+ name: "Vehicle"
68733
+ },
68734
+ {
68735
+ id: "animal",
68736
+ name: "Animal"
68737
+ },
68738
+ {
68739
+ id: "package",
68740
+ name: "Package"
68741
+ }
68742
+ ].map((l) => l.id));
68743
+ var COCO_TO_MACRO = {
68744
+ mapping: {
68745
+ person: "person",
68746
+ bicycle: "vehicle",
68747
+ car: "vehicle",
68748
+ motorcycle: "vehicle",
68749
+ airplane: "vehicle",
68750
+ bus: "vehicle",
68751
+ train: "vehicle",
68752
+ truck: "vehicle",
68753
+ boat: "vehicle",
68754
+ bird: "animal",
68755
+ cat: "animal",
68756
+ dog: "animal",
68757
+ horse: "animal",
68758
+ sheep: "animal",
68759
+ cow: "animal",
68760
+ elephant: "animal",
68761
+ bear: "animal",
68762
+ zebra: "animal",
68763
+ giraffe: "animal",
68764
+ suitcase: "package",
68765
+ backpack: "package",
68766
+ handbag: "package"
68767
+ },
68768
+ preserveOriginal: false
68769
+ };
68770
+ new Set(Object.values(COCO_TO_MACRO.mapping));
68771
+ /**
68772
+ * Run traces — what an automation (or, later, an Action run) DID, node by node,
68773
+ * so the editor can paint every block green or red with its reason.
68774
+ *
68775
+ * A trace is data, not a log: it covers the WHOLE tree — the trigger stage,
68776
+ * every condition node (containers and leaves, including the ones a
68777
+ * short-circuit never reached), the run-mode verdict and every action step —
68778
+ * each addressed by a stable path (`conditions.0.1`, `actions.2`, or `steps.N`
68779
+ * for an Action run). Skipped
68780
+ * evaluations are traced too, and identical consecutive skips collapse into a
68781
+ * repeat count so a trigger that fails every 5 minutes cannot flush real runs.
68782
+ *
68783
+ * {@link AutomationStepTraceSchema} is deliberately free of automation
68784
+ * vocabulary so an Action run records the same step shape.
68785
+ */
68786
+ /** A value a trace may show: JSON primitives and short lists, nothing deeper. */
68787
+ var TraceValueSchema = union([
68788
+ string(),
68789
+ number(),
68790
+ boolean(),
68791
+ _null(),
68792
+ array(union([string(), number()]))
68793
+ ]);
68794
+ var AutomationRunOutcomeSchema = _enum([
68795
+ "ran",
68796
+ "skipped-trigger",
68797
+ "skipped-conditions",
68798
+ "skipped-cooldown",
68799
+ "skipped-overlap",
68800
+ "failed",
68801
+ "aborted"
68802
+ ]);
68803
+ /** `not-evaluated` = a short-circuit never reached the node. */
68804
+ var TraceNodeOutcomeSchema = _enum([
68805
+ "pass",
68806
+ "fail",
68807
+ "not-evaluated"
68808
+ ]);
68809
+ var AutomationTriggerTraceSchema = object({
68810
+ kind: string().min(1),
68811
+ outcome: _enum(["pass", "fail"]),
68812
+ /** Why an event was rejected ("class person not in [dog]") or what fired. */
68813
+ reason: string(),
68814
+ /** deviceId, className, trackId, score … — only what the trigger exposes. */
68815
+ vars: record(string(), TraceValueSchema)
68816
+ });
68817
+ var AutomationConditionTraceNodeSchema = object({
68818
+ /** `conditions` for the root, `conditions.<i>` for its i-th child, … */
68819
+ path: string().min(1),
68820
+ kind: _enum([
68821
+ "all",
68822
+ "any",
68823
+ "not",
68824
+ "condition",
68825
+ "expression"
68826
+ ]),
68827
+ outcome: TraceNodeOutcomeSchema,
68828
+ reason: string(),
68829
+ deviceId: number().int().optional(),
68830
+ cap: string().optional(),
68831
+ fieldPath: string().optional(),
68832
+ operator: string().optional(),
68833
+ expected: TraceValueSchema.optional(),
68834
+ /** What the mirror held; `null` when it held nothing (unknown). */
68835
+ observed: TraceValueSchema.optional()
68836
+ });
68837
+ var AutomationRunVerdictSchema = object({
68838
+ mode: AutomationRunModeSchema,
68839
+ decision: _enum([
68840
+ "run",
68841
+ "queued",
68842
+ "restart",
68843
+ "drop",
68844
+ "cooldown",
68845
+ "bypass"
68846
+ ]),
68847
+ reason: string()
68848
+ });
68849
+ /** One executed (or not-run) step — shared by automations and Action runs. */
68850
+ var AutomationStepTraceSchema = object({
68851
+ /** `actions.<i>` */
68852
+ path: string().min(1),
68853
+ kind: string().min(1),
68854
+ cap: string().optional(),
68855
+ method: string().optional(),
68856
+ deviceId: number().int().optional(),
68857
+ startedAt: number().optional(),
68858
+ endedAt: number().optional(),
68859
+ outcome: _enum([
68860
+ "ok",
68861
+ "error",
68862
+ "not-run"
68863
+ ]),
68864
+ message: string().optional()
68865
+ });
68866
+ var AutomationRunTraceSchema = object({
68867
+ runId: string().min(1),
68868
+ startedAt: number(),
68869
+ endedAt: number(),
68870
+ trigger: AutomationTriggerTraceSchema,
68871
+ conditions: array(AutomationConditionTraceNodeSchema),
68872
+ verdict: AutomationRunVerdictSchema.optional(),
68873
+ steps: array(AutomationStepTraceSchema),
68874
+ outcome: AutomationRunOutcomeSchema,
68875
+ /** How many identical consecutive skips this trace stands for (>= 1). */
68876
+ repeat: number().int().min(1),
68877
+ /** When the latest of those repeats happened. */
68878
+ lastAt: number()
68879
+ });
68880
+ /**
68881
+ * What the hub's `runs` ledger holds for one owner (`automation:<blockId>` or
68882
+ * `action:<actionId>`): the cooldown stamp and the trace ring.
68883
+ */
68884
+ var AutomationStateDocSchema = object({
68885
+ version: literal(1),
68886
+ lastRunStartedAt: number().nullable(),
68887
+ traces: array(AutomationRunTraceSchema)
68888
+ });
68889
+ /**
68890
+ * Actions — named, runnable things an operator (or an automation) can start,
68891
+ * declared as LIST ENTRIES, never as devices.
68892
+ *
68893
+ * An entry is either **internal** (a short list of cap/wait steps kept in the
68894
+ * hub's `actions` record kind) or declared by an addon through the
68895
+ * `action-source` collection (a Home Assistant script, a scene …). The one
68896
+ * surface the UI and automations talk to is the `actions` cap; it keeps no
68897
+ * catalog of its own (D3) — every `list` asks the providers live.
68898
+ *
68899
+ * This file is the data half: the entry schema, the variables an entry may
68900
+ * declare, and the pure resolution of the values a run was given. It holds no
68901
+ * runtime.
68902
+ */
68903
+ /** What a variable's value is. `select` takes one of `options`. */
68904
+ var ActionVariableTypeSchema = _enum([
68905
+ "string",
68906
+ "number",
68907
+ "boolean",
68908
+ "select"
68909
+ ]);
68910
+ var ACTION_VARIABLE_NAME_RE = /^[A-Za-z_][A-Za-z0-9_]*$/;
68911
+ var ActionVariableValueSchema = union([
68912
+ string(),
68913
+ number(),
68914
+ boolean()
68915
+ ]);
68916
+ /**
68917
+ * One declared variable. Steps reference it as `{ $var: 'name' }` (the whole
68918
+ * value) or `{{vars.name}}` (inside a string) in their args.
68919
+ */
68920
+ var ActionVariableSchema = object({
68921
+ name: string().regex(ACTION_VARIABLE_NAME_RE),
68922
+ label: string().min(1).max(120).optional(),
68923
+ type: ActionVariableTypeSchema,
68924
+ required: boolean().optional(),
68925
+ default: ActionVariableValueSchema.optional(),
68926
+ /** The allowed values of a `select`. */
68927
+ options: array(string().min(1)).min(1).optional()
68928
+ }).superRefine((v, ctx) => {
68929
+ if (v.type === "select" && v.options === void 0) ctx.addIssue({
68930
+ code: "custom",
68931
+ message: "a select variable needs options",
68932
+ path: ["options"]
68933
+ });
68934
+ if (v.default !== void 0 && variableValueError(v, v.default) !== null) ctx.addIssue({
68935
+ code: "custom",
68936
+ message: variableValueError(v, v.default) ?? "invalid default",
68937
+ path: ["default"]
68938
+ });
68939
+ });
68940
+ /** Why `value` is not a legal value for the variable, or null when it is. */
68941
+ function variableValueError(variable, value) {
68942
+ switch (variable.type) {
68943
+ case "string": return typeof value === "string" ? null : `variable "${variable.name}" must be a string`;
68944
+ case "number": return typeof value === "number" && Number.isFinite(value) ? null : `variable "${variable.name}" must be a number`;
68945
+ case "boolean": return typeof value === "boolean" ? null : `variable "${variable.name}" must be true or false`;
68946
+ case "select": return typeof value === "string" && (variable.options ?? []).includes(value) ? null : `variable "${variable.name}" must be one of ${(variable.options ?? []).join(", ")}`;
68947
+ }
68948
+ }
68949
+ var ActionSourceKindSchema = _enum(["internal", "addon"]);
68950
+ function isInternalActionId(actionId) {
68951
+ return actionId.startsWith("internal:") && actionId.length > 9;
68952
+ }
68953
+ /** The addon behind a `<addonId>:<localId>` id, or null for anything else (internal included). */
68954
+ function addonIdOfActionId(actionId) {
68955
+ if (isInternalActionId(actionId)) return null;
68956
+ const at = actionId.indexOf(":");
68957
+ return at > 0 && at < actionId.length - 1 ? actionId.slice(0, at) : null;
68958
+ }
68959
+ var ActionStatusSchema = object({
68960
+ isRunning: boolean(),
68961
+ /** Ms epoch the latest run STARTED; null when it never ran. */
68962
+ lastRunAt: number().nullable(),
68963
+ /** The latest run's failure; null on success or when it never ran. */
68964
+ lastError: string().nullable()
68965
+ });
68966
+ var ActionEntrySchema = object({
68967
+ /** `internal:<uuid>` or `<addonId>:<localId>`. */
68968
+ id: string().min(3),
68969
+ name: string().min(1).max(120),
68970
+ /** The addon that declared it; `core-blocks` for an internal one. */
68971
+ addonId: string().min(1),
68972
+ source: ActionSourceKindSchema,
68973
+ /** An addon-declared entry is edited where it was declared. */
68974
+ readOnly: boolean(),
68975
+ /** Internal only: cap and wait steps. No action-in-action, so no cycles. */
68976
+ steps: array(ActionStepSchema).min(1).max(50).optional(),
68977
+ variables: array(ActionVariableSchema).max(20).optional(),
68978
+ status: ActionStatusSchema
68979
+ }).superRefine((entry, ctx) => {
68980
+ if (entry.source === "internal") {
68981
+ if (!isInternalActionId(entry.id)) ctx.addIssue({
68982
+ code: "custom",
68983
+ message: "an internal action id is internal:<uuid>",
68984
+ path: ["id"]
68985
+ });
68986
+ return;
68987
+ }
68988
+ if (entry.steps !== void 0) ctx.addIssue({
68989
+ code: "custom",
68990
+ message: "only an internal action carries steps",
68991
+ path: ["steps"]
68992
+ });
68993
+ if (!entry.readOnly) ctx.addIssue({
68994
+ code: "custom",
68995
+ message: "an addon action is read-only here",
68996
+ path: ["readOnly"]
68997
+ });
68998
+ if (addonIdOfActionId(entry.id) === null) ctx.addIssue({
68999
+ code: "custom",
69000
+ message: "an addon action id is <addonId>:<localId>",
69001
+ path: ["id"]
69002
+ });
69003
+ });
69004
+ /** What an operator writes for an internal action. */
69005
+ var ActionInputSchema = object({
69006
+ name: string().min(1).max(120),
69007
+ steps: array(ActionStepSchema).min(1).max(50),
69008
+ variables: array(ActionVariableSchema).max(20).optional()
69009
+ });
69010
+ /** Who started a run. Absent = the operator pressed it. */
69011
+ var ActionRunOriginSchema = object({
69012
+ /** The automation block whose step started it. */
69013
+ blockId: string().min(1).optional() });
69014
+ /** `started` = the caller did not ask to wait. */
69015
+ var ActionRunOutcomeSchema = union([AutomationRunOutcomeSchema, literal("started")]);
69016
+ var ActionRunResultSchema = object({
69017
+ runId: string().min(1),
69018
+ outcome: ActionRunOutcomeSchema,
69019
+ /** Why it did not run, or what failed. */
69020
+ message: string().optional()
69021
+ });
69022
+ _enum([
69023
+ "not-found",
69024
+ "read-only",
69025
+ "invalid-variables",
69026
+ "invalid-steps",
69027
+ "not-linked",
69028
+ "busy"
69029
+ ]);
69030
+ object({
69031
+ actionId: string(),
69032
+ runId: string(),
69033
+ isRunning: boolean(),
69034
+ /** Set when the run ended. */
69035
+ outcome: AutomationRunOutcomeSchema.optional(),
69036
+ lastRunAt: number().nullable(),
69037
+ lastError: string().nullable()
68650
69038
  });
68651
69039
  /**
68652
69040
  * `addon-pages` — system-scoped singleton aggregator cap. Public-facing
@@ -72296,7 +72684,18 @@ method(object({}), object({ blocks: array(CoreBlockSchema) }), { auth: "admin" }
72296
72684
  kind: "mutation",
72297
72685
  auth: "admin",
72298
72686
  access: "view"
72299
- });
72687
+ }), method(object({
72688
+ blockId: string().min(1),
72689
+ trace: AutomationRunTraceSchema.optional(),
72690
+ runStartedAt: number().optional(),
72691
+ keepTraces: number().int().min(1).max(100).optional()
72692
+ }), _void(), {
72693
+ kind: "mutation",
72694
+ auth: "admin"
72695
+ }), method(object({ blockId: string().min(1) }), AutomationStateDocSchema, { auth: "admin" }), method(object({ blockId: string().min(1) }), object({
72696
+ traces: array(AutomationRunTraceSchema),
72697
+ lastError: string().nullable()
72698
+ }), { auth: "admin" });
72300
72699
  /**
72301
72700
  * `custom-model-registry` — collection cap exposing operator-registered
72302
72701
  * custom detection models. Each provider (today: `addon-model-studio`)
@@ -97684,51 +98083,6 @@ method(object({}), array(TargetKindSchema)), method(object({}), array(TargetSche
97684
98083
  kind: "mutation",
97685
98084
  auth: "admin"
97686
98085
  });
97687
- new Set([
97688
- {
97689
- id: "person",
97690
- name: "Person"
97691
- },
97692
- {
97693
- id: "vehicle",
97694
- name: "Vehicle"
97695
- },
97696
- {
97697
- id: "animal",
97698
- name: "Animal"
97699
- },
97700
- {
97701
- id: "package",
97702
- name: "Package"
97703
- }
97704
- ].map((l) => l.id));
97705
- var COCO_TO_MACRO = {
97706
- mapping: {
97707
- person: "person",
97708
- bicycle: "vehicle",
97709
- car: "vehicle",
97710
- motorcycle: "vehicle",
97711
- airplane: "vehicle",
97712
- bus: "vehicle",
97713
- train: "vehicle",
97714
- truck: "vehicle",
97715
- boat: "vehicle",
97716
- bird: "animal",
97717
- cat: "animal",
97718
- dog: "animal",
97719
- horse: "animal",
97720
- sheep: "animal",
97721
- cow: "animal",
97722
- elephant: "animal",
97723
- bear: "animal",
97724
- zebra: "animal",
97725
- giraffe: "animal",
97726
- suitcase: "package",
97727
- backpack: "package",
97728
- handbag: "package"
97729
- },
97730
- preserveOriginal: false
97731
- };
97732
98086
  var AUDIO_MACRO_LABELS = [
97733
98087
  {
97734
98088
  id: "speech",
@@ -110534,6 +110888,84 @@ var audioMetricsCapability = {
110534
110888
  */
110535
110889
  history: "none"
110536
110890
  };
110891
+ method(object({}), array(ActionEntrySchema), { auth: "admin" }), method(object({
110892
+ actionId: string().min(1),
110893
+ variables: record(string(), ActionVariableValueSchema).optional()
110894
+ }), _void(), {
110895
+ kind: "mutation",
110896
+ auth: "admin"
110897
+ }), method(object({ actionId: string().min(1) }), _void(), {
110898
+ kind: "mutation",
110899
+ auth: "admin"
110900
+ });
110901
+ /**
110902
+ * actions — the ONE surface for runnable actions, for the UI and for
110903
+ * automations. Provided by the core-blocks builtin on the hub.
110904
+ *
110905
+ * An action is a LIST ENTRY, not a device (D883): either internal (cap and wait
110906
+ * steps, kept in the hub's `actions` record kind) or declared by an addon over
110907
+ * `action-source`. `list` asks every `action-source` provider LIVE and overlays
110908
+ * the run status from the hub's `runs` ledger; there is no catalog here to
110909
+ * drift (D3).
110910
+ *
110911
+ * **Permissions follow the camera, as linked devices do.** An action is linked
110912
+ * to cameras by the operator (`setLinks`, D356: nothing links itself). A
110913
+ * non-admin may `get` / `run` an action only through a camera they may see that
110914
+ * links it: they name the camera (`deviceId`, a top-level field so the scope
110915
+ * gate sees it) and the provider refuses an action that camera does not link.
110916
+ * An admin, and an in-process caller (an automation block), may run any action.
110917
+ * Every refusal is typed (`[not-linked] …`) and logged.
110918
+ *
110919
+ * A second `run` of an action that is already running is REFUSED — traced and
110920
+ * logged — never queued.
110921
+ */
110922
+ var ActionIdInput = object({ actionId: string().min(1) });
110923
+ var ActionLinksSchema = object({
110924
+ deviceId: number().int().nonnegative(),
110925
+ actionIds: array(string().min(1)).max(100)
110926
+ });
110927
+ method(object({}), array(ActionEntrySchema), { auth: "admin" }), method(object({
110928
+ actionId: string().min(1),
110929
+ deviceId: number().int().nonnegative().optional()
110930
+ }), object({ action: ActionEntrySchema.nullable() }), {
110931
+ role: "viewer",
110932
+ auth: "protected",
110933
+ kind: "query",
110934
+ caller: "required"
110935
+ }), method(object({
110936
+ actionId: string().min(1),
110937
+ variables: record(string(), ActionVariableValueSchema).optional(),
110938
+ wait: boolean().optional(),
110939
+ origin: ActionRunOriginSchema.optional(),
110940
+ deviceId: number().int().nonnegative().optional()
110941
+ }), ActionRunResultSchema, {
110942
+ role: "viewer",
110943
+ auth: "protected",
110944
+ kind: "mutation",
110945
+ caller: "required"
110946
+ }), method(ActionIdInput, _void(), {
110947
+ kind: "mutation",
110948
+ auth: "admin"
110949
+ }), method(ActionIdInput, object({ traces: array(AutomationRunTraceSchema) }), { auth: "admin" }), method(object({ action: ActionInputSchema }), object({ action: ActionEntrySchema }), {
110950
+ kind: "mutation",
110951
+ auth: "admin"
110952
+ }), method(object({
110953
+ actionId: string().min(1),
110954
+ action: ActionInputSchema.partial()
110955
+ }), object({ action: ActionEntrySchema }), {
110956
+ kind: "mutation",
110957
+ auth: "admin"
110958
+ }), method(ActionIdInput, _void(), {
110959
+ kind: "mutation",
110960
+ auth: "admin"
110961
+ }), method(object({ deviceId: number().int().nonnegative() }), array(ActionEntrySchema), {
110962
+ role: "viewer",
110963
+ auth: "protected",
110964
+ kind: "query"
110965
+ }), method(object({ deviceId: number().int().nonnegative() }), ActionLinksSchema, { auth: "admin" }), method(ActionLinksSchema, ActionLinksSchema, {
110966
+ kind: "mutation",
110967
+ auth: "admin"
110968
+ });
110537
110969
  /**
110538
110970
  * Automation-control cap. Models HA `automation.*` entities on
110539
110971
  * `DeviceType.Automation`. An automation is a trigger+condition+
@@ -125329,6 +125761,104 @@ Object.freeze({
125329
125761
  access: "create",
125330
125762
  role: "admin"
125331
125763
  },
125764
+ "actions.create": {
125765
+ capName: "actions",
125766
+ capScope: "system",
125767
+ addonId: null,
125768
+ access: "create",
125769
+ role: "admin"
125770
+ },
125771
+ "actions.delete": {
125772
+ capName: "actions",
125773
+ capScope: "system",
125774
+ addonId: null,
125775
+ access: "delete",
125776
+ role: "admin"
125777
+ },
125778
+ "actions.get": {
125779
+ capName: "actions",
125780
+ capScope: "system",
125781
+ addonId: null,
125782
+ access: "view",
125783
+ role: "viewer"
125784
+ },
125785
+ "actions.getLinks": {
125786
+ capName: "actions",
125787
+ capScope: "system",
125788
+ addonId: null,
125789
+ access: "view",
125790
+ role: "admin"
125791
+ },
125792
+ "actions.getTraces": {
125793
+ capName: "actions",
125794
+ capScope: "system",
125795
+ addonId: null,
125796
+ access: "view",
125797
+ role: "admin"
125798
+ },
125799
+ "actions.list": {
125800
+ capName: "actions",
125801
+ capScope: "system",
125802
+ addonId: null,
125803
+ access: "view",
125804
+ role: "admin"
125805
+ },
125806
+ "actions.listForCamera": {
125807
+ capName: "actions",
125808
+ capScope: "system",
125809
+ addonId: null,
125810
+ access: "view",
125811
+ role: "viewer"
125812
+ },
125813
+ "actions.run": {
125814
+ capName: "actions",
125815
+ capScope: "system",
125816
+ addonId: null,
125817
+ access: "create",
125818
+ role: "viewer"
125819
+ },
125820
+ "actions.setLinks": {
125821
+ capName: "actions",
125822
+ capScope: "system",
125823
+ addonId: null,
125824
+ access: "create",
125825
+ role: "admin"
125826
+ },
125827
+ "actions.stop": {
125828
+ capName: "actions",
125829
+ capScope: "system",
125830
+ addonId: null,
125831
+ access: "create",
125832
+ role: "admin"
125833
+ },
125834
+ "actions.update": {
125835
+ capName: "actions",
125836
+ capScope: "system",
125837
+ addonId: null,
125838
+ access: "create",
125839
+ role: "admin"
125840
+ },
125841
+ "actionSource.listActions": {
125842
+ capName: "action-source",
125843
+ capScope: "system",
125844
+ addonId: null,
125845
+ access: "view",
125846
+ role: "admin"
125847
+ },
125848
+ "actionSource.runAction": {
125849
+ capName: "action-source",
125850
+ capScope: "system",
125851
+ addonId: null,
125852
+ access: "create",
125853
+ role: "admin"
125854
+ },
125855
+ "actionSource.stopAction": {
125856
+ capName: "action-source",
125857
+ capScope: "system",
125858
+ addonId: null,
125859
+ access: "create",
125860
+ role: "admin"
125861
+ },
125332
125862
  "addonPages.listPages": {
125333
125863
  capName: "addon-pages",
125334
125864
  capScope: "system",
@@ -126610,6 +127140,13 @@ Object.freeze({
126610
127140
  access: "view",
126611
127141
  role: "admin"
126612
127142
  },
127143
+ "coreBlocks.getAutomationRunState": {
127144
+ capName: "core-blocks",
127145
+ capScope: "system",
127146
+ addonId: null,
127147
+ access: "view",
127148
+ role: "admin"
127149
+ },
126613
127150
  "coreBlocks.getBlockSettings": {
126614
127151
  capName: "core-blocks",
126615
127152
  capScope: "system",
@@ -126631,6 +127168,13 @@ Object.freeze({
126631
127168
  access: "view",
126632
127169
  role: "admin"
126633
127170
  },
127171
+ "coreBlocks.getTraces": {
127172
+ capName: "core-blocks",
127173
+ capScope: "system",
127174
+ addonId: null,
127175
+ access: "view",
127176
+ role: "admin"
127177
+ },
126634
127178
  "coreBlocks.getTypeDefs": {
126635
127179
  capName: "core-blocks",
126636
127180
  capScope: "system",
@@ -126666,6 +127210,13 @@ Object.freeze({
126666
127210
  access: "create",
126667
127211
  role: "admin"
126668
127212
  },
127213
+ "coreBlocks.reportAutomationRun": {
127214
+ capName: "core-blocks",
127215
+ capScope: "system",
127216
+ addonId: null,
127217
+ access: "create",
127218
+ role: "admin"
127219
+ },
126669
127220
  "coreBlocks.restart": {
126670
127221
  capName: "core-blocks",
126671
127222
  capScope: "system",
@@ -138820,6 +139371,8 @@ var ML = "model and training machinery — no device action";
138820
139371
  var INTERNAL = "internal plumbing between addons — never operator-facing";
138821
139372
  /** Every system-scoped cap, with why it is not a device's vocabulary. */
138822
139373
  var SYSTEM_CAP_VOCABULARY = {
139374
+ "action-source": system(INTERNAL),
139375
+ actions: system("declared actions — run from the Actions page and from the cameras that link them"),
138823
139376
  "addon-pages": system(UI_PLUMBING),
138824
139377
  "addon-pages-source": system(UI_PLUMBING),
138825
139378
  "addon-routes": system(UI_PLUMBING),
@@ -139016,6 +139569,31 @@ Object.freeze({
139016
139569
  form: "single",
139017
139570
  optional: false
139018
139571
  }],
139572
+ "actions.get": [{
139573
+ name: "deviceId",
139574
+ form: "single",
139575
+ optional: true
139576
+ }],
139577
+ "actions.getLinks": [{
139578
+ name: "deviceId",
139579
+ form: "single",
139580
+ optional: false
139581
+ }],
139582
+ "actions.listForCamera": [{
139583
+ name: "deviceId",
139584
+ form: "single",
139585
+ optional: false
139586
+ }],
139587
+ "actions.run": [{
139588
+ name: "deviceId",
139589
+ form: "single",
139590
+ optional: true
139591
+ }],
139592
+ "actions.setLinks": [{
139593
+ name: "deviceId",
139594
+ form: "single",
139595
+ optional: false
139596
+ }],
139019
139597
  "addonSettings.getDeviceSettings": [{
139020
139598
  name: "deviceId",
139021
139599
  form: "single",
@@ -143187,6 +143765,7 @@ function ingestReading(input) {
143187
143765
  value: converted
143188
143766
  };
143189
143767
  }
143768
+ String.raw`(?<=^|["'(\s>,\[{])`;
143190
143769
  var MB = 1024 * 1024;
143191
143770
  1024 * MB, 3072 * MB;
143192
143771
  //#endregion
@@ -143313,6 +143892,334 @@ function buildConnectionFormSchema() {
143313
143892
  ] };
143314
143893
  }
143315
143894
  //#endregion
143895
+ //#region src/dreame-vacuum-decode.ts
143896
+ /**
143897
+ * Pure decode helpers for the Dreame vacuum. Translate the library's MIoT enum
143898
+ * values into the camstack `vacuum-control` slice shape. Unit-tested in
143899
+ * isolation (no facade, no network) — mirrors `homematic-decode.ts`.
143900
+ */
143901
+ /**
143902
+ * Map a Dreame {@link MiotState} (siid 2 piid 1) onto the camstack canonical
143903
+ * `VacuumState` lifecycle (`idle` / `cleaning` / `paused` / `returning` /
143904
+ * `docked` / `error`). A `null` state (not yet observed) settles on `idle`.
143905
+ *
143906
+ * The MIoT state space is large (~50 values); we collapse it onto the six
143907
+ * canonical states by family. Anything unrecognised falls through to `idle`
143908
+ * (the safe neutral) so a firmware-added code never throws.
143909
+ */
143910
+ function mapMiotStateToVacuumState(state) {
143911
+ if (state === null) return "idle";
143912
+ switch (state) {
143913
+ case MiotState.Charging:
143914
+ case MiotState.ChargingComplete:
143915
+ case MiotState.IntelligentCharging:
143916
+ case MiotState.CleanWashboardBase:
143917
+ case MiotState.AutoRepairBase:
143918
+ case MiotState.WaterSupplyDrainTest:
143919
+ case MiotState.CleanMopRefillWater:
143920
+ case MiotState.AutoEmptying:
143921
+ case MiotState.Emptying:
143922
+ case MiotState.Draining:
143923
+ case MiotState.AutoWaterDraining:
143924
+ case MiotState.Sanitizing: return "docked";
143925
+ case MiotState.MopDrying:
143926
+ case MiotState.DustBagDrying:
143927
+ case MiotState.SanitizingWithDry: return "drying";
143928
+ case MiotState.ReturningToCharge:
143929
+ case MiotState.ReturningToWash:
143930
+ case MiotState.ReturnInstallMop:
143931
+ case MiotState.ReturnRemoveMop:
143932
+ case MiotState.ReturnToEmpty:
143933
+ case MiotState.ReturningToDrain: return "returning";
143934
+ case MiotState.Paused:
143935
+ case MiotState.PausedAlt:
143936
+ case MiotState.PausedCleaning:
143937
+ case MiotState.DustBagDryingPaused:
143938
+ case MiotState.CameraMonitoringPaused:
143939
+ case MiotState.FindingPetPaused:
143940
+ case MiotState.InitialDeepCleanPaused:
143941
+ case MiotState.ChangingMopPaused:
143942
+ case MiotState.FloorMaintainingPaused: return "paused";
143943
+ case MiotState.Standby:
143944
+ case MiotState.WaitingForTask: return "idle";
143945
+ default: return "cleaning";
143946
+ }
143947
+ }
143948
+ /**
143949
+ * The camstack `fanSpeed` token for a Dreame {@link SuctionLevel} — the enum
143950
+ * member NAME, lower-cased (`Quiet` → `'quiet'`). Returns `null` for an
143951
+ * unobserved / unknown level.
143952
+ */
143953
+ function suctionLevelToken(level) {
143954
+ if (level === null) return null;
143955
+ const name = SuctionLevel[level];
143956
+ return typeof name === "string" ? name.toLowerCase() : null;
143957
+ }
143958
+ /**
143959
+ * Resolve a camstack `fanSpeed` token back to the Dreame {@link SuctionLevel}
143960
+ * enum value, validated against the model's `supported` levels. Returns null
143961
+ * when the token doesn't match a supported level (the caller rejects the set).
143962
+ */
143963
+ function tokenToSuctionLevel(token, supported) {
143964
+ const wanted = token.trim().toLowerCase();
143965
+ for (const level of supported) if (suctionLevelToken(level) === wanted) return level;
143966
+ return null;
143967
+ }
143968
+ /** The list of accepted `fanSpeed` tokens for a model's supported suction set. */
143969
+ function supportedFanSpeedTokens(supported) {
143970
+ const tokens = [];
143971
+ for (const level of supported) {
143972
+ const token = suctionLevelToken(level);
143973
+ if (token !== null) tokens.push(token);
143974
+ }
143975
+ return tokens;
143976
+ }
143977
+ /**
143978
+ * Clamp a raw battery reading (already a 0..100 percentage from the library)
143979
+ * to the `vacuum-control` slice's `batteryLevel` field. Returns null for a
143980
+ * null/NaN/out-of-band reading so the slice stays valid (the schema rejects
143981
+ * a number outside 0..100).
143982
+ */
143983
+ function clampBatteryLevel$1(value) {
143984
+ if (value === null || !Number.isFinite(value)) return null;
143985
+ return Math.max(0, Math.min(100, value));
143986
+ }
143987
+ /**
143988
+ * MiotError codes that are BENIGN completion / reminder states, not active
143989
+ * faults. A fresh cloud-shadow read of an idle docked robot legitimately
143990
+ * returns `TaskComplete` (68); it is not an error to show. The Error sensor
143991
+ * (`dreame-entity-catalog.ts`) and the vacuum card both read this set.
143992
+ * Extend it if other non-fault codes surface as spurious "errors".
143993
+ */
143994
+ var BENIGN_ERROR_CODES = new Set([MiotError.TaskComplete]);
143995
+ /** The card's error label: null for no error and for a benign completion code. */
143996
+ function vacuumErrorLabel(code) {
143997
+ if (!code || BENIGN_ERROR_CODES.has(code)) return null;
143998
+ return `Error ${code}`;
143999
+ }
144000
+ /** Clamp a 0..100 percentage (progress, coverage, …), null-safe. */
144001
+ function clampPercent$1(value) {
144002
+ if (value === null || !Number.isFinite(value)) return null;
144003
+ return Math.max(0, Math.min(100, value));
144004
+ }
144005
+ /**
144006
+ * Where each {@link VacuumTankProps} field lives (siid/piid). Property ids and
144007
+ * enum values are the HA Dreame integration's (Tasshack `dreame-vacuum`
144008
+ * `types.py`, `DreameVacuumProperty` → siid/piid map; same on `dev` and
144009
+ * v2.0.0b25, checked 2026-09-26). 27/1 and 27/4 were already read here; 27/2 and
144010
+ * 27/3 are their neighbours on the same station service. The robot pushes them
144011
+ * on change; the consumables refresh also re-reads them from the shadow.
144012
+ */
144013
+ var VACUUM_TANK_PROPS = {
144014
+ cleanWaterTankStatus: {
144015
+ siid: 27,
144016
+ piid: 1
144017
+ },
144018
+ waterTank: {
144019
+ siid: 4,
144020
+ piid: 6
144021
+ },
144022
+ detergentStatus: {
144023
+ siid: 27,
144024
+ piid: 4
144025
+ },
144026
+ dustCollection: {
144027
+ siid: 15,
144028
+ piid: 3
144029
+ },
144030
+ dirtyWaterTankStatus: {
144031
+ siid: 27,
144032
+ piid: 2
144033
+ },
144034
+ dustBagStatus: {
144035
+ siid: 27,
144036
+ piid: 3
144037
+ }
144038
+ };
144039
+ /**
144040
+ * Clean-water tank `status`: prefer the richer clean-water-tank-status (27/1),
144041
+ * fall back to the coarser water-tank presence (4/6). `null` when neither maps
144042
+ * to a known good/low reading (NOT_INSTALLED / CHECKING / unknown). Pure.
144043
+ */
144044
+ function cleanWaterTankStatus(props) {
144045
+ if (props.cleanWaterTankStatus !== null) {
144046
+ if (props.cleanWaterTankStatus === 0) return "ok";
144047
+ if (props.cleanWaterTankStatus === 2) return "low";
144048
+ return null;
144049
+ }
144050
+ if (props.waterTank === 1 || props.waterTank === 10) return "ok";
144051
+ return null;
144052
+ }
144053
+ /** Detergent tank `status` from detergent-status (27/4). Pure. */
144054
+ function detergentTankStatus(value) {
144055
+ if (value === 0) return "ok";
144056
+ if (value === 2) return "low";
144057
+ return null;
144058
+ }
144059
+ /**
144060
+ * Dust-bin `status` from dust-collection (15/3); OVER_USE reads as `full`. A dust
144061
+ * bag that is missing or asks to be checked (27/3 NOT_INSTALLED / CHECK) is never
144062
+ * `ok` — the enum does not say "full" either, so it is unknown. Pure.
144063
+ */
144064
+ function dustBinTankStatus(dustCollection, dustBag) {
144065
+ if (dustCollection === 2) return "full";
144066
+ if (dustBag !== null && dustBag !== 0) return null;
144067
+ if (dustCollection === 1) return "ok";
144068
+ return null;
144069
+ }
144070
+ /**
144071
+ * Dirty-water tank `status` from dirty-water-tank-status (27/2). The robot has
144072
+ * ONE value for "not installed or full" and cannot tell them apart; lifting the
144073
+ * tank out to EMPTY it reads exactly like a full one, so that value is unknown
144074
+ * (`null`) — never `full`, which would raise a false "tank full" at the moment
144075
+ * someone is fixing it (controller ruling 2026-09-26). Absent or any other
144076
+ * value is unknown too, never `ok`.
144077
+ *
144078
+ * The unambiguous "full" is the robot's own error `MiotError.DirtyWaterTankFull`
144079
+ * (86, the library's `errorCode`, siid 2 piid 2): while it is raised the tank is
144080
+ * `full` whatever 27/2 says; once it clears, 27/2 decides again. Pure.
144081
+ */
144082
+ function dirtyWaterTankStatus(value, errorCode) {
144083
+ if (errorCode === MiotError.DirtyWaterTankFull) return "full";
144084
+ if (value === 0) return "ok";
144085
+ return null;
144086
+ }
144087
+ /**
144088
+ * The four tanks (clean-water / dirty-water / detergent / dust-bin) decoded from the model's {@link VacuumCapabilities}, with the
144089
+ * discrete `status` populated from the live MIoT enum props when supplied. Pure.
144090
+ *
144091
+ * The nodedreame library exposes NO tank FILL-LEVEL telemetry — the cloud shadow
144092
+ * (verified against a real `dreame.vacuum.r2538z`) carries only the water-VOLUME
144093
+ * *setting* (a mop-flow level, NOT a tank fill). We therefore never fabricate a
144094
+ * `level`; `level` is always `null`. The cap contract is honoured on PRESENCE,
144095
+ * with `status` derived from the tank-status enums:
144096
+ *
144097
+ * - a field is `null` when the hardware HAS NO such tank (capability flags), and
144098
+ * - `{ level: null, status }` when the tank EXISTS — `status` from the enum
144099
+ * prop ('ok' / 'low' / 'full'), or `null` when the device reports none.
144100
+ *
144101
+ * Presence is derived from the capability record:
144102
+ * - clean-water ← `canMop` (a mopping robot carries a clean-water reservoir)
144103
+ * - dirty-water ← `canMopWash` (a self-wash dock has a recovery/dirty tank)
144104
+ * - detergent ← `hasDetergentReservoir`
144105
+ * - dust-bin ← `canAutoEmpty` (an auto-empty robot has an onboard dust bin)
144106
+ */
144107
+ function decodeVacuumTanks(caps, props, errorCode = null) {
144108
+ const tank = (present, status) => present ? {
144109
+ level: null,
144110
+ status
144111
+ } : null;
144112
+ return {
144113
+ cleanWater: tank(Boolean(caps?.canMop), props ? cleanWaterTankStatus(props) : null),
144114
+ dirtyWater: tank(Boolean(caps?.canMopWash), dirtyWaterTankStatus(props?.dirtyWaterTankStatus ?? null, errorCode)),
144115
+ detergent: tank(Boolean(caps?.hasDetergentReservoir), props ? detergentTankStatus(props.detergentStatus) : null),
144116
+ dustBin: tank(Boolean(caps?.canAutoEmpty), props ? dustBinTankStatus(props.dustCollection, props.dustBagStatus) : null)
144117
+ };
144118
+ }
144119
+ /** How each tank is named and what it is, in the order the panel lists them. */
144120
+ var TANK_ITEMS = [
144121
+ {
144122
+ key: "cleanWater",
144123
+ label: "Clean water tank",
144124
+ kind: "supply",
144125
+ form: "tank"
144126
+ },
144127
+ {
144128
+ key: "dirtyWater",
144129
+ label: "Dirty water tank",
144130
+ kind: "waste",
144131
+ form: "tank"
144132
+ },
144133
+ {
144134
+ key: "detergent",
144135
+ label: "Detergent",
144136
+ kind: "supply",
144137
+ form: "product"
144138
+ },
144139
+ {
144140
+ key: "dustBin",
144141
+ label: "Dust bin",
144142
+ kind: "waste",
144143
+ form: "bin"
144144
+ }
144145
+ ];
144146
+ /**
144147
+ * The tanks as `consumables` items (D915): every PRESENT tank becomes a
144148
+ * `supply` (clean water, detergent) or `waste` (dirty water, dust bin) item
144149
+ * keyed by its old field name, drawn as a `tank` (the two water tanks), a
144150
+ * `product` (detergent) or a `bin` (the dust bin) — the key the notification centre's tank
144151
+ * episodes were built on, so an open episode carries on. A tank is never
144152
+ * resettable: emptying or refilling it is the reset. Pure.
144153
+ */
144154
+ function vacuumTankItems(tanks) {
144155
+ return TANK_ITEMS.flatMap(({ key, label, kind, form }) => {
144156
+ const tank = tanks[key];
144157
+ if (tank === null) return [];
144158
+ return [{
144159
+ key,
144160
+ label,
144161
+ level: tank.level,
144162
+ status: tank.status,
144163
+ kind,
144164
+ form,
144165
+ lastResetAt: null,
144166
+ resettable: false
144167
+ }];
144168
+ });
144169
+ }
144170
+ /**
144171
+ * Clamp a raw consumable percentage to `[0, 100]`, or return `null` when the
144172
+ * value is absent / non-finite. The library reports remaining life as a 0..100
144173
+ * integer; we clamp defensively so an out-of-range firmware value never violates
144174
+ * the cap schema.
144175
+ */
144176
+ function clampConsumablePct(value) {
144177
+ if (value === null || !Number.isFinite(value)) return null;
144178
+ return Math.max(0, Math.min(100, value));
144179
+ }
144180
+ /**
144181
+ * The `consumables` cap items for the consumables a vacuum REPORTS.
144182
+ *
144183
+ * PRESENCE-driven (mirrors HA): the library's `supportedConsumables` enumerates
144184
+ * ONLY the consumables this model reports (a present MIoT life property), each
144185
+ * with its remaining-life % and whether a reset action exists. No hardcoded
144186
+ * per-model list: a consumable the device does not expose never appears.
144187
+ *
144188
+ * `status` is `null` — the provider reports the level, the notification rule
144189
+ * decides what is low. The slice clocks are `nextConsumablesSlice`'s job. Pure.
144190
+ */
144191
+ function buildVacuumConsumableItems(readings) {
144192
+ return readings.map((c) => ({
144193
+ key: c.key,
144194
+ label: c.label,
144195
+ level: clampConsumablePct(c.leftPct),
144196
+ status: null,
144197
+ lastResetAt: null,
144198
+ resettable: c.resettable,
144199
+ form: "part"
144200
+ }));
144201
+ }
144202
+ /** Structural equality of two JSON-shaped values, key order ignored. */
144203
+ function jsonEqual(a, b) {
144204
+ if (a === b) return true;
144205
+ if (Array.isArray(a) || Array.isArray(b)) return Array.isArray(a) && Array.isArray(b) && a.length === b.length && a.every((v, i) => jsonEqual(v, b[i]));
144206
+ if (a === null || b === null || typeof a !== "object" || typeof b !== "object") return false;
144207
+ const ka = Object.keys(a);
144208
+ const kb = Object.keys(b);
144209
+ return ka.length === kb.length && ka.every((k) => jsonEqual(Reflect.get(a, k), Reflect.get(b, k)));
144210
+ }
144211
+ /**
144212
+ * The `vacuum-control` slice to write, or `null` when only the clock moved.
144213
+ * `recomputeSlice` runs on EVERY push; without this each one was a write (and a
144214
+ * `device.state-changed`) even when nothing the robot reported had changed. Pure.
144215
+ */
144216
+ function nextVacuumControlSlice(prev, next) {
144217
+ if (prev === void 0) return next;
144218
+ const { lastChangedAt: _prevAt, ...prevContent } = prev;
144219
+ const { lastChangedAt: _nextAt, ...nextContent } = next;
144220
+ return jsonEqual(prevContent, nextContent) ? null : next;
144221
+ }
144222
+ //#endregion
143316
144223
  //#region src/dreame-entity-catalog.ts
143317
144224
  var STATS = "Statistics";
143318
144225
  var MAINT = "Maintenance";
@@ -143959,14 +144866,6 @@ function enumOptions(e) {
143959
144866
  label: humanizeEnumName(name)
143960
144867
  }));
143961
144868
  }
143962
- /**
143963
- * MiotError codes that are BENIGN completion / reminder states, not active
143964
- * faults. HA does not surface these as errors — a fresh cloud-shadow read of an
143965
- * idle docked robot legitimately returns `TaskComplete` (68). We relabel them to
143966
- * the `Clear` label so the Error sensor only shows real faults (HA parity).
143967
- * Extend this set if other non-fault codes surface as spurious "errors".
143968
- */
143969
- var BENIGN_ERROR_CODES = new Set([MiotError.TaskComplete]);
143970
144869
  /** `MiotError` options with the benign completion codes remapped to the `Clear`
143971
144870
  * label, so the Error sensor reads as no-error for them (matches HA). */
143972
144871
  function errorEnumOptions() {
@@ -145324,321 +146223,6 @@ var DreameButtonDevice = class extends DreameChildDevice {
145324
146223
  recomputeSlice() {}
145325
146224
  };
145326
146225
  //#endregion
145327
- //#region src/dreame-vacuum-decode.ts
145328
- /**
145329
- * Pure decode helpers for the Dreame vacuum. Translate the library's MIoT enum
145330
- * values into the camstack `vacuum-control` slice shape. Unit-tested in
145331
- * isolation (no facade, no network) — mirrors `homematic-decode.ts`.
145332
- */
145333
- /**
145334
- * Map a Dreame {@link MiotState} (siid 2 piid 1) onto the camstack canonical
145335
- * `VacuumState` lifecycle (`idle` / `cleaning` / `paused` / `returning` /
145336
- * `docked` / `error`). A `null` state (not yet observed) settles on `idle`.
145337
- *
145338
- * The MIoT state space is large (~50 values); we collapse it onto the six
145339
- * canonical states by family. Anything unrecognised falls through to `idle`
145340
- * (the safe neutral) so a firmware-added code never throws.
145341
- */
145342
- function mapMiotStateToVacuumState(state) {
145343
- if (state === null) return "idle";
145344
- switch (state) {
145345
- case MiotState.Charging:
145346
- case MiotState.ChargingComplete:
145347
- case MiotState.IntelligentCharging:
145348
- case MiotState.CleanWashboardBase:
145349
- case MiotState.AutoRepairBase:
145350
- case MiotState.WaterSupplyDrainTest:
145351
- case MiotState.CleanMopRefillWater:
145352
- case MiotState.AutoEmptying:
145353
- case MiotState.Emptying:
145354
- case MiotState.Draining:
145355
- case MiotState.AutoWaterDraining:
145356
- case MiotState.Sanitizing: return "docked";
145357
- case MiotState.MopDrying:
145358
- case MiotState.DustBagDrying:
145359
- case MiotState.SanitizingWithDry: return "drying";
145360
- case MiotState.ReturningToCharge:
145361
- case MiotState.ReturningToWash:
145362
- case MiotState.ReturnInstallMop:
145363
- case MiotState.ReturnRemoveMop:
145364
- case MiotState.ReturnToEmpty:
145365
- case MiotState.ReturningToDrain: return "returning";
145366
- case MiotState.Paused:
145367
- case MiotState.PausedAlt:
145368
- case MiotState.PausedCleaning:
145369
- case MiotState.DustBagDryingPaused:
145370
- case MiotState.CameraMonitoringPaused:
145371
- case MiotState.FindingPetPaused:
145372
- case MiotState.InitialDeepCleanPaused:
145373
- case MiotState.ChangingMopPaused:
145374
- case MiotState.FloorMaintainingPaused: return "paused";
145375
- case MiotState.Standby:
145376
- case MiotState.WaitingForTask: return "idle";
145377
- default: return "cleaning";
145378
- }
145379
- }
145380
- /**
145381
- * The camstack `fanSpeed` token for a Dreame {@link SuctionLevel} — the enum
145382
- * member NAME, lower-cased (`Quiet` → `'quiet'`). Returns `null` for an
145383
- * unobserved / unknown level.
145384
- */
145385
- function suctionLevelToken(level) {
145386
- if (level === null) return null;
145387
- const name = SuctionLevel[level];
145388
- return typeof name === "string" ? name.toLowerCase() : null;
145389
- }
145390
- /**
145391
- * Resolve a camstack `fanSpeed` token back to the Dreame {@link SuctionLevel}
145392
- * enum value, validated against the model's `supported` levels. Returns null
145393
- * when the token doesn't match a supported level (the caller rejects the set).
145394
- */
145395
- function tokenToSuctionLevel(token, supported) {
145396
- const wanted = token.trim().toLowerCase();
145397
- for (const level of supported) if (suctionLevelToken(level) === wanted) return level;
145398
- return null;
145399
- }
145400
- /** The list of accepted `fanSpeed` tokens for a model's supported suction set. */
145401
- function supportedFanSpeedTokens(supported) {
145402
- const tokens = [];
145403
- for (const level of supported) {
145404
- const token = suctionLevelToken(level);
145405
- if (token !== null) tokens.push(token);
145406
- }
145407
- return tokens;
145408
- }
145409
- /**
145410
- * Clamp a raw battery reading (already a 0..100 percentage from the library)
145411
- * to the `vacuum-control` slice's `batteryLevel` field. Returns null for a
145412
- * null/NaN/out-of-band reading so the slice stays valid (the schema rejects
145413
- * a number outside 0..100).
145414
- */
145415
- function clampBatteryLevel$1(value) {
145416
- if (value === null || !Number.isFinite(value)) return null;
145417
- return Math.max(0, Math.min(100, value));
145418
- }
145419
- /** Clamp a 0..100 percentage (progress, coverage, …), null-safe. */
145420
- function clampPercent$1(value) {
145421
- if (value === null || !Number.isFinite(value)) return null;
145422
- return Math.max(0, Math.min(100, value));
145423
- }
145424
- /**
145425
- * Where each {@link VacuumTankProps} field lives (siid/piid). Property ids and
145426
- * enum values are the HA Dreame integration's (Tasshack `dreame-vacuum`
145427
- * `types.py`, `DreameVacuumProperty` → siid/piid map; same on `dev` and
145428
- * v2.0.0b25, checked 2026-09-26). 27/1 and 27/4 were already read here; 27/2 and
145429
- * 27/3 are their neighbours on the same station service. The robot pushes them
145430
- * on change; the consumables refresh also re-reads them from the shadow.
145431
- */
145432
- var VACUUM_TANK_PROPS = {
145433
- cleanWaterTankStatus: {
145434
- siid: 27,
145435
- piid: 1
145436
- },
145437
- waterTank: {
145438
- siid: 4,
145439
- piid: 6
145440
- },
145441
- detergentStatus: {
145442
- siid: 27,
145443
- piid: 4
145444
- },
145445
- dustCollection: {
145446
- siid: 15,
145447
- piid: 3
145448
- },
145449
- dirtyWaterTankStatus: {
145450
- siid: 27,
145451
- piid: 2
145452
- },
145453
- dustBagStatus: {
145454
- siid: 27,
145455
- piid: 3
145456
- }
145457
- };
145458
- /**
145459
- * Clean-water tank `status`: prefer the richer clean-water-tank-status (27/1),
145460
- * fall back to the coarser water-tank presence (4/6). `null` when neither maps
145461
- * to a known good/low reading (NOT_INSTALLED / CHECKING / unknown). Pure.
145462
- */
145463
- function cleanWaterTankStatus(props) {
145464
- if (props.cleanWaterTankStatus !== null) {
145465
- if (props.cleanWaterTankStatus === 0) return "ok";
145466
- if (props.cleanWaterTankStatus === 2) return "low";
145467
- return null;
145468
- }
145469
- if (props.waterTank === 1 || props.waterTank === 10) return "ok";
145470
- return null;
145471
- }
145472
- /** Detergent tank `status` from detergent-status (27/4). Pure. */
145473
- function detergentTankStatus(value) {
145474
- if (value === 0) return "ok";
145475
- if (value === 2) return "low";
145476
- return null;
145477
- }
145478
- /**
145479
- * Dust-bin `status` from dust-collection (15/3); OVER_USE reads as `full`. A dust
145480
- * bag that is missing or asks to be checked (27/3 NOT_INSTALLED / CHECK) is never
145481
- * `ok` — the enum does not say "full" either, so it is unknown. Pure.
145482
- */
145483
- function dustBinTankStatus(dustCollection, dustBag) {
145484
- if (dustCollection === 2) return "full";
145485
- if (dustBag !== null && dustBag !== 0) return null;
145486
- if (dustCollection === 1) return "ok";
145487
- return null;
145488
- }
145489
- /**
145490
- * Dirty-water tank `status` from dirty-water-tank-status (27/2). The robot has
145491
- * ONE value for "not installed or full" and cannot tell them apart; lifting the
145492
- * tank out to EMPTY it reads exactly like a full one, so that value is unknown
145493
- * (`null`) — never `full`, which would raise a false "tank full" at the moment
145494
- * someone is fixing it (controller ruling 2026-09-26). Absent or any other
145495
- * value is unknown too, never `ok`.
145496
- *
145497
- * The unambiguous "full" is the robot's own error `MiotError.DirtyWaterTankFull`
145498
- * (86, the library's `errorCode`, siid 2 piid 2): while it is raised the tank is
145499
- * `full` whatever 27/2 says; once it clears, 27/2 decides again. Pure.
145500
- */
145501
- function dirtyWaterTankStatus(value, errorCode) {
145502
- if (errorCode === MiotError.DirtyWaterTankFull) return "full";
145503
- if (value === 0) return "ok";
145504
- return null;
145505
- }
145506
- /**
145507
- * The four tanks (clean-water / dirty-water / detergent / dust-bin) decoded from the model's {@link VacuumCapabilities}, with the
145508
- * discrete `status` populated from the live MIoT enum props when supplied. Pure.
145509
- *
145510
- * The nodedreame library exposes NO tank FILL-LEVEL telemetry — the cloud shadow
145511
- * (verified against a real `dreame.vacuum.r2538z`) carries only the water-VOLUME
145512
- * *setting* (a mop-flow level, NOT a tank fill). We therefore never fabricate a
145513
- * `level`; `level` is always `null`. The cap contract is honoured on PRESENCE,
145514
- * with `status` derived from the tank-status enums:
145515
- *
145516
- * - a field is `null` when the hardware HAS NO such tank (capability flags), and
145517
- * - `{ level: null, status }` when the tank EXISTS — `status` from the enum
145518
- * prop ('ok' / 'low' / 'full'), or `null` when the device reports none.
145519
- *
145520
- * Presence is derived from the capability record:
145521
- * - clean-water ← `canMop` (a mopping robot carries a clean-water reservoir)
145522
- * - dirty-water ← `canMopWash` (a self-wash dock has a recovery/dirty tank)
145523
- * - detergent ← `hasDetergentReservoir`
145524
- * - dust-bin ← `canAutoEmpty` (an auto-empty robot has an onboard dust bin)
145525
- */
145526
- function decodeVacuumTanks(caps, props, errorCode = null) {
145527
- const tank = (present, status) => present ? {
145528
- level: null,
145529
- status
145530
- } : null;
145531
- return {
145532
- cleanWater: tank(Boolean(caps?.canMop), props ? cleanWaterTankStatus(props) : null),
145533
- dirtyWater: tank(Boolean(caps?.canMopWash), dirtyWaterTankStatus(props?.dirtyWaterTankStatus ?? null, errorCode)),
145534
- detergent: tank(Boolean(caps?.hasDetergentReservoir), props ? detergentTankStatus(props.detergentStatus) : null),
145535
- dustBin: tank(Boolean(caps?.canAutoEmpty), props ? dustBinTankStatus(props.dustCollection, props.dustBagStatus) : null)
145536
- };
145537
- }
145538
- /** How each tank is named and what it is, in the order the panel lists them. */
145539
- var TANK_ITEMS = [
145540
- {
145541
- key: "cleanWater",
145542
- label: "Clean water tank",
145543
- kind: "supply",
145544
- form: "tank"
145545
- },
145546
- {
145547
- key: "dirtyWater",
145548
- label: "Dirty water tank",
145549
- kind: "waste",
145550
- form: "tank"
145551
- },
145552
- {
145553
- key: "detergent",
145554
- label: "Detergent",
145555
- kind: "supply",
145556
- form: "product"
145557
- },
145558
- {
145559
- key: "dustBin",
145560
- label: "Dust bin",
145561
- kind: "waste",
145562
- form: "bin"
145563
- }
145564
- ];
145565
- /**
145566
- * The tanks as `consumables` items (D915): every PRESENT tank becomes a
145567
- * `supply` (clean water, detergent) or `waste` (dirty water, dust bin) item
145568
- * keyed by its old field name, drawn as a `tank` (the two water tanks), a
145569
- * `product` (detergent) or a `bin` (the dust bin) — the key the notification centre's tank
145570
- * episodes were built on, so an open episode carries on. A tank is never
145571
- * resettable: emptying or refilling it is the reset. Pure.
145572
- */
145573
- function vacuumTankItems(tanks) {
145574
- return TANK_ITEMS.flatMap(({ key, label, kind, form }) => {
145575
- const tank = tanks[key];
145576
- if (tank === null) return [];
145577
- return [{
145578
- key,
145579
- label,
145580
- level: tank.level,
145581
- status: tank.status,
145582
- kind,
145583
- form,
145584
- lastResetAt: null,
145585
- resettable: false
145586
- }];
145587
- });
145588
- }
145589
- /**
145590
- * Clamp a raw consumable percentage to `[0, 100]`, or return `null` when the
145591
- * value is absent / non-finite. The library reports remaining life as a 0..100
145592
- * integer; we clamp defensively so an out-of-range firmware value never violates
145593
- * the cap schema.
145594
- */
145595
- function clampConsumablePct(value) {
145596
- if (value === null || !Number.isFinite(value)) return null;
145597
- return Math.max(0, Math.min(100, value));
145598
- }
145599
- /**
145600
- * The `consumables` cap items for the consumables a vacuum REPORTS.
145601
- *
145602
- * PRESENCE-driven (mirrors HA): the library's `supportedConsumables` enumerates
145603
- * ONLY the consumables this model reports (a present MIoT life property), each
145604
- * with its remaining-life % and whether a reset action exists. No hardcoded
145605
- * per-model list: a consumable the device does not expose never appears.
145606
- *
145607
- * `status` is `null` — the provider reports the level, the notification rule
145608
- * decides what is low. The slice clocks are `nextConsumablesSlice`'s job. Pure.
145609
- */
145610
- function buildVacuumConsumableItems(readings) {
145611
- return readings.map((c) => ({
145612
- key: c.key,
145613
- label: c.label,
145614
- level: clampConsumablePct(c.leftPct),
145615
- status: null,
145616
- lastResetAt: null,
145617
- resettable: c.resettable,
145618
- form: "part"
145619
- }));
145620
- }
145621
- /** Structural equality of two JSON-shaped values, key order ignored. */
145622
- function jsonEqual(a, b) {
145623
- if (a === b) return true;
145624
- if (Array.isArray(a) || Array.isArray(b)) return Array.isArray(a) && Array.isArray(b) && a.length === b.length && a.every((v, i) => jsonEqual(v, b[i]));
145625
- if (a === null || b === null || typeof a !== "object" || typeof b !== "object") return false;
145626
- const ka = Object.keys(a);
145627
- const kb = Object.keys(b);
145628
- return ka.length === kb.length && ka.every((k) => jsonEqual(Reflect.get(a, k), Reflect.get(b, k)));
145629
- }
145630
- /**
145631
- * The `vacuum-control` slice to write, or `null` when only the clock moved.
145632
- * `recomputeSlice` runs on EVERY push; without this each one was a write (and a
145633
- * `device.state-changed`) even when nothing the robot reported had changed. Pure.
145634
- */
145635
- function nextVacuumControlSlice(prev, next) {
145636
- if (prev === void 0) return next;
145637
- const { lastChangedAt: _prevAt, ...prevContent } = prev;
145638
- const { lastChangedAt: _nextAt, ...nextContent } = next;
145639
- return jsonEqual(prevContent, nextContent) ? null : next;
145640
- }
145641
- //#endregion
145642
146226
  //#region src/devices/dreame-camera-stream.ts
145643
146227
  /**
145644
146228
  * The robot camera as a stream the broker can pull — the pure facts.
@@ -194499,7 +195083,7 @@ var DreameVacuumDevice = class extends DreameChildDevice {
194499
195083
  availableFanSpeeds: supportedFanSpeedTokens(vacuum.supportedSuctionLevels),
194500
195084
  progressPercent: clampPercent$1(vacuum.taskProgressPct),
194501
195085
  errorCode: vacuum.errorCode,
194502
- errorLabel: vacuum.errorCode ? `Error ${vacuum.errorCode}` : null,
195086
+ errorLabel: vacuumErrorLabel(vacuum.errorCode),
194503
195087
  lastChangedAt: Date.now()
194504
195088
  };
194505
195089
  const toWrite = nextVacuumControlSlice(this.runtimeState.getCapState(CAP_NAME$1), controlSlice);