@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.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-hdMR7CTW.mjs
52903
52903
  var EventCategory = /* @__PURE__ */ function(EventCategory) {
52904
52904
  EventCategory["SystemBoot"] = "system.boot";
52905
52905
  EventCategory["SystemAddonsReady"] = "system.addons-ready";
@@ -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-BHjNez61.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",
@@ -68590,7 +68597,23 @@ var AutomationTriggerSchema = discriminatedUnion("kind", [
68590
68597
  kind: literal("schedule"),
68591
68598
  cron: string().min(1).max(120)
68592
68599
  }),
68593
- object({ kind: literal("manual") })
68600
+ object({ kind: literal("manual") }),
68601
+ object({
68602
+ kind: literal("detection"),
68603
+ /** Class names. A macro class (`animal`, `vehicle`, `person`, `package`)
68604
+ * matches every raw class that maps to it; any other name matches
68605
+ * exactly. */
68606
+ classes: array(string().min(1)).min(1),
68607
+ deviceIds: array(number().int().nonnegative()).min(1),
68608
+ /** The track must have visited at least one of these zones. */
68609
+ zones: array(string().min(1)).min(1).optional(),
68610
+ /** Best-so-far confidence the track must reach. */
68611
+ minScore: number().min(0).max(1).optional()
68612
+ }),
68613
+ object({
68614
+ kind: literal("interval"),
68615
+ minutes: number().int().min(1).max(1440)
68616
+ })
68594
68617
  ]);
68595
68618
  /**
68596
68619
  * One action step.
@@ -68610,28 +68633,47 @@ var AutomationTriggerSchema = discriminatedUnion("kind", [
68610
68633
  * is called. The runner still receives a number and is untouched; the
68611
68634
  * resolution is the recipe's job, not the runner's.
68612
68635
  */
68636
+ var AutomationWaitStepSchema = object({
68637
+ kind: literal("wait"),
68638
+ seconds: number().min(0).max(300)
68639
+ });
68640
+ var AutomationCapStepSchema = object({
68641
+ kind: literal("cap"),
68642
+ /**
68643
+ * One device, several same-family devices (locks / climate family / …),
68644
+ * or `{ $var }` for the device that triggered the run. An array is
68645
+ * fan-out at codegen — each id gets the same cap.method call.
68646
+ */
68647
+ deviceId: union([
68648
+ number().int(),
68649
+ array(number().int()).min(1),
68650
+ object({ $var: string().min(1) })
68651
+ ]),
68652
+ cap: string().min(1),
68653
+ method: string().min(1),
68654
+ /** Values may carry `{{vars.x}}` slots, or be exactly `{ $var: 'name' }`, which
68655
+ * SUBSTITUTE and do not evaluate (§3.2.3). Anything beyond substitution is
68656
+ * the expression leaf. */
68657
+ args: record(string(), unknown()).optional()
68658
+ });
68659
+ /**
68660
+ * The steps an internal Action may contain: cap and wait, and nothing that can
68661
+ * call back into an Action — so an Action can never reach itself (no cycles).
68662
+ */
68663
+ var ActionStepSchema = discriminatedUnion("kind", [AutomationWaitStepSchema, AutomationCapStepSchema]);
68613
68664
  var AutomationActionSchema = discriminatedUnion("kind", [
68665
+ AutomationWaitStepSchema,
68666
+ AutomationCapStepSchema,
68614
68667
  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()
68668
+ kind: literal("action"),
68669
+ /** An entry of the `actions` surface (`internal:<uuid>` or `<addonId>:<localId>`). */
68670
+ actionId: string().min(1),
68671
+ /** Values for the action's declared variables; the others take their default. */
68672
+ variables: record(string(), union([
68673
+ string(),
68674
+ number(),
68675
+ boolean()
68676
+ ])).optional()
68635
68677
  }),
68636
68678
  object({
68637
68679
  kind: literal("code"),
@@ -68640,10 +68682,356 @@ var AutomationActionSchema = discriminatedUnion("kind", [
68640
68682
  code: string().min(1).max(2e4)
68641
68683
  })
68642
68684
  ]);
68685
+ /**
68686
+ * How a trigger that arrives while the automation is busy (or cooling down) is
68687
+ * handled. One of the automation's execution modes, never a property of its
68688
+ * actions. Absent means `single`.
68689
+ *
68690
+ * - `single` — drop the trigger while a run is in progress (traced + logged).
68691
+ * - `restart` — abort the run in progress (at its next step boundary), start fresh.
68692
+ * - `queued` — queue up to `max` triggers, drop the rest (traced).
68693
+ * - `parallel` — up to `max` runs at once, drop the rest (traced).
68694
+ * - `cooldown` — `single`, and no new run until `minutes` after the last run
68695
+ * STARTED; persisted across a restart. A manual trigger bypasses only this
68696
+ * wait, never the single/queued limits.
68697
+ */
68698
+ var AutomationRunModeSchema = discriminatedUnion("mode", [
68699
+ object({ mode: literal("single") }),
68700
+ object({ mode: literal("restart") }),
68701
+ object({
68702
+ mode: literal("queued"),
68703
+ max: number().int().min(1).max(10)
68704
+ }),
68705
+ object({
68706
+ mode: literal("parallel"),
68707
+ max: number().int().min(1).max(10)
68708
+ }),
68709
+ object({
68710
+ mode: literal("cooldown"),
68711
+ minutes: number().int().min(1).max(1440)
68712
+ })
68713
+ ]);
68643
68714
  var AutomationRecipeSchema = object({
68644
68715
  triggers: array(AutomationTriggerSchema),
68645
68716
  conditions: AutomationConditionSchema.optional(),
68646
- actions: array(AutomationActionSchema)
68717
+ actions: array(AutomationActionSchema),
68718
+ runMode: AutomationRunModeSchema.optional(),
68719
+ /** How many run traces the automation keeps (default {@link DEFAULT_TRACE_KEEP}). */
68720
+ keepTraces: number().int().min(1).max(100).optional()
68721
+ });
68722
+ new Set([
68723
+ {
68724
+ id: "person",
68725
+ name: "Person"
68726
+ },
68727
+ {
68728
+ id: "vehicle",
68729
+ name: "Vehicle"
68730
+ },
68731
+ {
68732
+ id: "animal",
68733
+ name: "Animal"
68734
+ },
68735
+ {
68736
+ id: "package",
68737
+ name: "Package"
68738
+ }
68739
+ ].map((l) => l.id));
68740
+ var COCO_TO_MACRO = {
68741
+ mapping: {
68742
+ person: "person",
68743
+ bicycle: "vehicle",
68744
+ car: "vehicle",
68745
+ motorcycle: "vehicle",
68746
+ airplane: "vehicle",
68747
+ bus: "vehicle",
68748
+ train: "vehicle",
68749
+ truck: "vehicle",
68750
+ boat: "vehicle",
68751
+ bird: "animal",
68752
+ cat: "animal",
68753
+ dog: "animal",
68754
+ horse: "animal",
68755
+ sheep: "animal",
68756
+ cow: "animal",
68757
+ elephant: "animal",
68758
+ bear: "animal",
68759
+ zebra: "animal",
68760
+ giraffe: "animal",
68761
+ suitcase: "package",
68762
+ backpack: "package",
68763
+ handbag: "package"
68764
+ },
68765
+ preserveOriginal: false
68766
+ };
68767
+ new Set(Object.values(COCO_TO_MACRO.mapping));
68768
+ /**
68769
+ * Run traces — what an automation (or, later, an Action run) DID, node by node,
68770
+ * so the editor can paint every block green or red with its reason.
68771
+ *
68772
+ * A trace is data, not a log: it covers the WHOLE tree — the trigger stage,
68773
+ * every condition node (containers and leaves, including the ones a
68774
+ * short-circuit never reached), the run-mode verdict and every action step —
68775
+ * each addressed by a stable path (`conditions.0.1`, `actions.2`, or `steps.N`
68776
+ * for an Action run). Skipped
68777
+ * evaluations are traced too, and identical consecutive skips collapse into a
68778
+ * repeat count so a trigger that fails every 5 minutes cannot flush real runs.
68779
+ *
68780
+ * {@link AutomationStepTraceSchema} is deliberately free of automation
68781
+ * vocabulary so an Action run records the same step shape.
68782
+ */
68783
+ /** A value a trace may show: JSON primitives and short lists, nothing deeper. */
68784
+ var TraceValueSchema = union([
68785
+ string(),
68786
+ number(),
68787
+ boolean(),
68788
+ _null(),
68789
+ array(union([string(), number()]))
68790
+ ]);
68791
+ var AutomationRunOutcomeSchema = _enum([
68792
+ "ran",
68793
+ "skipped-trigger",
68794
+ "skipped-conditions",
68795
+ "skipped-cooldown",
68796
+ "skipped-overlap",
68797
+ "failed",
68798
+ "aborted"
68799
+ ]);
68800
+ /** `not-evaluated` = a short-circuit never reached the node. */
68801
+ var TraceNodeOutcomeSchema = _enum([
68802
+ "pass",
68803
+ "fail",
68804
+ "not-evaluated"
68805
+ ]);
68806
+ var AutomationTriggerTraceSchema = object({
68807
+ kind: string().min(1),
68808
+ outcome: _enum(["pass", "fail"]),
68809
+ /** Why an event was rejected ("class person not in [dog]") or what fired. */
68810
+ reason: string(),
68811
+ /** deviceId, className, trackId, score … — only what the trigger exposes. */
68812
+ vars: record(string(), TraceValueSchema)
68813
+ });
68814
+ var AutomationConditionTraceNodeSchema = object({
68815
+ /** `conditions` for the root, `conditions.<i>` for its i-th child, … */
68816
+ path: string().min(1),
68817
+ kind: _enum([
68818
+ "all",
68819
+ "any",
68820
+ "not",
68821
+ "condition",
68822
+ "expression"
68823
+ ]),
68824
+ outcome: TraceNodeOutcomeSchema,
68825
+ reason: string(),
68826
+ deviceId: number().int().optional(),
68827
+ cap: string().optional(),
68828
+ fieldPath: string().optional(),
68829
+ operator: string().optional(),
68830
+ expected: TraceValueSchema.optional(),
68831
+ /** What the mirror held; `null` when it held nothing (unknown). */
68832
+ observed: TraceValueSchema.optional()
68833
+ });
68834
+ var AutomationRunVerdictSchema = object({
68835
+ mode: AutomationRunModeSchema,
68836
+ decision: _enum([
68837
+ "run",
68838
+ "queued",
68839
+ "restart",
68840
+ "drop",
68841
+ "cooldown",
68842
+ "bypass"
68843
+ ]),
68844
+ reason: string()
68845
+ });
68846
+ /** One executed (or not-run) step — shared by automations and Action runs. */
68847
+ var AutomationStepTraceSchema = object({
68848
+ /** `actions.<i>` */
68849
+ path: string().min(1),
68850
+ kind: string().min(1),
68851
+ cap: string().optional(),
68852
+ method: string().optional(),
68853
+ deviceId: number().int().optional(),
68854
+ startedAt: number().optional(),
68855
+ endedAt: number().optional(),
68856
+ outcome: _enum([
68857
+ "ok",
68858
+ "error",
68859
+ "not-run"
68860
+ ]),
68861
+ message: string().optional()
68862
+ });
68863
+ var AutomationRunTraceSchema = object({
68864
+ runId: string().min(1),
68865
+ startedAt: number(),
68866
+ endedAt: number(),
68867
+ trigger: AutomationTriggerTraceSchema,
68868
+ conditions: array(AutomationConditionTraceNodeSchema),
68869
+ verdict: AutomationRunVerdictSchema.optional(),
68870
+ steps: array(AutomationStepTraceSchema),
68871
+ outcome: AutomationRunOutcomeSchema,
68872
+ /** How many identical consecutive skips this trace stands for (>= 1). */
68873
+ repeat: number().int().min(1),
68874
+ /** When the latest of those repeats happened. */
68875
+ lastAt: number()
68876
+ });
68877
+ /**
68878
+ * What the hub's `runs` ledger holds for one owner (`automation:<blockId>` or
68879
+ * `action:<actionId>`): the cooldown stamp and the trace ring.
68880
+ */
68881
+ var AutomationStateDocSchema = object({
68882
+ version: literal(1),
68883
+ lastRunStartedAt: number().nullable(),
68884
+ traces: array(AutomationRunTraceSchema)
68885
+ });
68886
+ /**
68887
+ * Actions — named, runnable things an operator (or an automation) can start,
68888
+ * declared as LIST ENTRIES, never as devices.
68889
+ *
68890
+ * An entry is either **internal** (a short list of cap/wait steps kept in the
68891
+ * hub's `actions` record kind) or declared by an addon through the
68892
+ * `action-source` collection (a Home Assistant script, a scene …). The one
68893
+ * surface the UI and automations talk to is the `actions` cap; it keeps no
68894
+ * catalog of its own (D3) — every `list` asks the providers live.
68895
+ *
68896
+ * This file is the data half: the entry schema, the variables an entry may
68897
+ * declare, and the pure resolution of the values a run was given. It holds no
68898
+ * runtime.
68899
+ */
68900
+ /** What a variable's value is. `select` takes one of `options`. */
68901
+ var ActionVariableTypeSchema = _enum([
68902
+ "string",
68903
+ "number",
68904
+ "boolean",
68905
+ "select"
68906
+ ]);
68907
+ var ACTION_VARIABLE_NAME_RE = /^[A-Za-z_][A-Za-z0-9_]*$/;
68908
+ var ActionVariableValueSchema = union([
68909
+ string(),
68910
+ number(),
68911
+ boolean()
68912
+ ]);
68913
+ /**
68914
+ * One declared variable. Steps reference it as `{ $var: 'name' }` (the whole
68915
+ * value) or `{{vars.name}}` (inside a string) in their args.
68916
+ */
68917
+ var ActionVariableSchema = object({
68918
+ name: string().regex(ACTION_VARIABLE_NAME_RE),
68919
+ label: string().min(1).max(120).optional(),
68920
+ type: ActionVariableTypeSchema,
68921
+ required: boolean().optional(),
68922
+ default: ActionVariableValueSchema.optional(),
68923
+ /** The allowed values of a `select`. */
68924
+ options: array(string().min(1)).min(1).optional()
68925
+ }).superRefine((v, ctx) => {
68926
+ if (v.type === "select" && v.options === void 0) ctx.addIssue({
68927
+ code: "custom",
68928
+ message: "a select variable needs options",
68929
+ path: ["options"]
68930
+ });
68931
+ if (v.default !== void 0 && variableValueError(v, v.default) !== null) ctx.addIssue({
68932
+ code: "custom",
68933
+ message: variableValueError(v, v.default) ?? "invalid default",
68934
+ path: ["default"]
68935
+ });
68936
+ });
68937
+ /** Why `value` is not a legal value for the variable, or null when it is. */
68938
+ function variableValueError(variable, value) {
68939
+ switch (variable.type) {
68940
+ case "string": return typeof value === "string" ? null : `variable "${variable.name}" must be a string`;
68941
+ case "number": return typeof value === "number" && Number.isFinite(value) ? null : `variable "${variable.name}" must be a number`;
68942
+ case "boolean": return typeof value === "boolean" ? null : `variable "${variable.name}" must be true or false`;
68943
+ case "select": return typeof value === "string" && (variable.options ?? []).includes(value) ? null : `variable "${variable.name}" must be one of ${(variable.options ?? []).join(", ")}`;
68944
+ }
68945
+ }
68946
+ var ActionSourceKindSchema = _enum(["internal", "addon"]);
68947
+ function isInternalActionId(actionId) {
68948
+ return actionId.startsWith("internal:") && actionId.length > 9;
68949
+ }
68950
+ /** The addon behind a `<addonId>:<localId>` id, or null for anything else (internal included). */
68951
+ function addonIdOfActionId(actionId) {
68952
+ if (isInternalActionId(actionId)) return null;
68953
+ const at = actionId.indexOf(":");
68954
+ return at > 0 && at < actionId.length - 1 ? actionId.slice(0, at) : null;
68955
+ }
68956
+ var ActionStatusSchema = object({
68957
+ isRunning: boolean(),
68958
+ /** Ms epoch the latest run STARTED; null when it never ran. */
68959
+ lastRunAt: number().nullable(),
68960
+ /** The latest run's failure; null on success or when it never ran. */
68961
+ lastError: string().nullable()
68962
+ });
68963
+ var ActionEntrySchema = object({
68964
+ /** `internal:<uuid>` or `<addonId>:<localId>`. */
68965
+ id: string().min(3),
68966
+ name: string().min(1).max(120),
68967
+ /** The addon that declared it; `core-blocks` for an internal one. */
68968
+ addonId: string().min(1),
68969
+ source: ActionSourceKindSchema,
68970
+ /** An addon-declared entry is edited where it was declared. */
68971
+ readOnly: boolean(),
68972
+ /** Internal only: cap and wait steps. No action-in-action, so no cycles. */
68973
+ steps: array(ActionStepSchema).min(1).max(50).optional(),
68974
+ variables: array(ActionVariableSchema).max(20).optional(),
68975
+ status: ActionStatusSchema
68976
+ }).superRefine((entry, ctx) => {
68977
+ if (entry.source === "internal") {
68978
+ if (!isInternalActionId(entry.id)) ctx.addIssue({
68979
+ code: "custom",
68980
+ message: "an internal action id is internal:<uuid>",
68981
+ path: ["id"]
68982
+ });
68983
+ return;
68984
+ }
68985
+ if (entry.steps !== void 0) ctx.addIssue({
68986
+ code: "custom",
68987
+ message: "only an internal action carries steps",
68988
+ path: ["steps"]
68989
+ });
68990
+ if (!entry.readOnly) ctx.addIssue({
68991
+ code: "custom",
68992
+ message: "an addon action is read-only here",
68993
+ path: ["readOnly"]
68994
+ });
68995
+ if (addonIdOfActionId(entry.id) === null) ctx.addIssue({
68996
+ code: "custom",
68997
+ message: "an addon action id is <addonId>:<localId>",
68998
+ path: ["id"]
68999
+ });
69000
+ });
69001
+ /** What an operator writes for an internal action. */
69002
+ var ActionInputSchema = object({
69003
+ name: string().min(1).max(120),
69004
+ steps: array(ActionStepSchema).min(1).max(50),
69005
+ variables: array(ActionVariableSchema).max(20).optional()
69006
+ });
69007
+ /** Who started a run. Absent = the operator pressed it. */
69008
+ var ActionRunOriginSchema = object({
69009
+ /** The automation block whose step started it. */
69010
+ blockId: string().min(1).optional() });
69011
+ /** `started` = the caller did not ask to wait. */
69012
+ var ActionRunOutcomeSchema = union([AutomationRunOutcomeSchema, literal("started")]);
69013
+ var ActionRunResultSchema = object({
69014
+ runId: string().min(1),
69015
+ outcome: ActionRunOutcomeSchema,
69016
+ /** Why it did not run, or what failed. */
69017
+ message: string().optional()
69018
+ });
69019
+ _enum([
69020
+ "not-found",
69021
+ "read-only",
69022
+ "invalid-variables",
69023
+ "invalid-steps",
69024
+ "not-linked",
69025
+ "busy"
69026
+ ]);
69027
+ object({
69028
+ actionId: string(),
69029
+ runId: string(),
69030
+ isRunning: boolean(),
69031
+ /** Set when the run ended. */
69032
+ outcome: AutomationRunOutcomeSchema.optional(),
69033
+ lastRunAt: number().nullable(),
69034
+ lastError: string().nullable()
68647
69035
  });
68648
69036
  /**
68649
69037
  * `addon-pages` — system-scoped singleton aggregator cap. Public-facing
@@ -72293,7 +72681,18 @@ method(object({}), object({ blocks: array(CoreBlockSchema) }), { auth: "admin" }
72293
72681
  kind: "mutation",
72294
72682
  auth: "admin",
72295
72683
  access: "view"
72296
- });
72684
+ }), method(object({
72685
+ blockId: string().min(1),
72686
+ trace: AutomationRunTraceSchema.optional(),
72687
+ runStartedAt: number().optional(),
72688
+ keepTraces: number().int().min(1).max(100).optional()
72689
+ }), _void(), {
72690
+ kind: "mutation",
72691
+ auth: "admin"
72692
+ }), method(object({ blockId: string().min(1) }), AutomationStateDocSchema, { auth: "admin" }), method(object({ blockId: string().min(1) }), object({
72693
+ traces: array(AutomationRunTraceSchema),
72694
+ lastError: string().nullable()
72695
+ }), { auth: "admin" });
72297
72696
  /**
72298
72697
  * `custom-model-registry` — collection cap exposing operator-registered
72299
72698
  * custom detection models. Each provider (today: `addon-model-studio`)
@@ -97681,51 +98080,6 @@ method(object({}), array(TargetKindSchema)), method(object({}), array(TargetSche
97681
98080
  kind: "mutation",
97682
98081
  auth: "admin"
97683
98082
  });
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
98083
  var AUDIO_MACRO_LABELS = [
97730
98084
  {
97731
98085
  id: "speech",
@@ -110531,6 +110885,84 @@ var audioMetricsCapability = {
110531
110885
  */
110532
110886
  history: "none"
110533
110887
  };
110888
+ method(object({}), array(ActionEntrySchema), { auth: "admin" }), method(object({
110889
+ actionId: string().min(1),
110890
+ variables: record(string(), ActionVariableValueSchema).optional()
110891
+ }), _void(), {
110892
+ kind: "mutation",
110893
+ auth: "admin"
110894
+ }), method(object({ actionId: string().min(1) }), _void(), {
110895
+ kind: "mutation",
110896
+ auth: "admin"
110897
+ });
110898
+ /**
110899
+ * actions — the ONE surface for runnable actions, for the UI and for
110900
+ * automations. Provided by the core-blocks builtin on the hub.
110901
+ *
110902
+ * An action is a LIST ENTRY, not a device (D883): either internal (cap and wait
110903
+ * steps, kept in the hub's `actions` record kind) or declared by an addon over
110904
+ * `action-source`. `list` asks every `action-source` provider LIVE and overlays
110905
+ * the run status from the hub's `runs` ledger; there is no catalog here to
110906
+ * drift (D3).
110907
+ *
110908
+ * **Permissions follow the camera, as linked devices do.** An action is linked
110909
+ * to cameras by the operator (`setLinks`, D356: nothing links itself). A
110910
+ * non-admin may `get` / `run` an action only through a camera they may see that
110911
+ * links it: they name the camera (`deviceId`, a top-level field so the scope
110912
+ * gate sees it) and the provider refuses an action that camera does not link.
110913
+ * An admin, and an in-process caller (an automation block), may run any action.
110914
+ * Every refusal is typed (`[not-linked] …`) and logged.
110915
+ *
110916
+ * A second `run` of an action that is already running is REFUSED — traced and
110917
+ * logged — never queued.
110918
+ */
110919
+ var ActionIdInput = object({ actionId: string().min(1) });
110920
+ var ActionLinksSchema = object({
110921
+ deviceId: number().int().nonnegative(),
110922
+ actionIds: array(string().min(1)).max(100)
110923
+ });
110924
+ method(object({}), array(ActionEntrySchema), { auth: "admin" }), method(object({
110925
+ actionId: string().min(1),
110926
+ deviceId: number().int().nonnegative().optional()
110927
+ }), object({ action: ActionEntrySchema.nullable() }), {
110928
+ role: "viewer",
110929
+ auth: "protected",
110930
+ kind: "query",
110931
+ caller: "required"
110932
+ }), method(object({
110933
+ actionId: string().min(1),
110934
+ variables: record(string(), ActionVariableValueSchema).optional(),
110935
+ wait: boolean().optional(),
110936
+ origin: ActionRunOriginSchema.optional(),
110937
+ deviceId: number().int().nonnegative().optional()
110938
+ }), ActionRunResultSchema, {
110939
+ role: "viewer",
110940
+ auth: "protected",
110941
+ kind: "mutation",
110942
+ caller: "required"
110943
+ }), method(ActionIdInput, _void(), {
110944
+ kind: "mutation",
110945
+ auth: "admin"
110946
+ }), method(ActionIdInput, object({ traces: array(AutomationRunTraceSchema) }), { auth: "admin" }), method(object({ action: ActionInputSchema }), object({ action: ActionEntrySchema }), {
110947
+ kind: "mutation",
110948
+ auth: "admin"
110949
+ }), method(object({
110950
+ actionId: string().min(1),
110951
+ action: ActionInputSchema.partial()
110952
+ }), object({ action: ActionEntrySchema }), {
110953
+ kind: "mutation",
110954
+ auth: "admin"
110955
+ }), method(ActionIdInput, _void(), {
110956
+ kind: "mutation",
110957
+ auth: "admin"
110958
+ }), method(object({ deviceId: number().int().nonnegative() }), array(ActionEntrySchema), {
110959
+ role: "viewer",
110960
+ auth: "protected",
110961
+ kind: "query"
110962
+ }), method(object({ deviceId: number().int().nonnegative() }), ActionLinksSchema, { auth: "admin" }), method(ActionLinksSchema, ActionLinksSchema, {
110963
+ kind: "mutation",
110964
+ auth: "admin"
110965
+ });
110534
110966
  /**
110535
110967
  * Automation-control cap. Models HA `automation.*` entities on
110536
110968
  * `DeviceType.Automation`. An automation is a trigger+condition+
@@ -125326,6 +125758,104 @@ Object.freeze({
125326
125758
  access: "create",
125327
125759
  role: "admin"
125328
125760
  },
125761
+ "actions.create": {
125762
+ capName: "actions",
125763
+ capScope: "system",
125764
+ addonId: null,
125765
+ access: "create",
125766
+ role: "admin"
125767
+ },
125768
+ "actions.delete": {
125769
+ capName: "actions",
125770
+ capScope: "system",
125771
+ addonId: null,
125772
+ access: "delete",
125773
+ role: "admin"
125774
+ },
125775
+ "actions.get": {
125776
+ capName: "actions",
125777
+ capScope: "system",
125778
+ addonId: null,
125779
+ access: "view",
125780
+ role: "viewer"
125781
+ },
125782
+ "actions.getLinks": {
125783
+ capName: "actions",
125784
+ capScope: "system",
125785
+ addonId: null,
125786
+ access: "view",
125787
+ role: "admin"
125788
+ },
125789
+ "actions.getTraces": {
125790
+ capName: "actions",
125791
+ capScope: "system",
125792
+ addonId: null,
125793
+ access: "view",
125794
+ role: "admin"
125795
+ },
125796
+ "actions.list": {
125797
+ capName: "actions",
125798
+ capScope: "system",
125799
+ addonId: null,
125800
+ access: "view",
125801
+ role: "admin"
125802
+ },
125803
+ "actions.listForCamera": {
125804
+ capName: "actions",
125805
+ capScope: "system",
125806
+ addonId: null,
125807
+ access: "view",
125808
+ role: "viewer"
125809
+ },
125810
+ "actions.run": {
125811
+ capName: "actions",
125812
+ capScope: "system",
125813
+ addonId: null,
125814
+ access: "create",
125815
+ role: "viewer"
125816
+ },
125817
+ "actions.setLinks": {
125818
+ capName: "actions",
125819
+ capScope: "system",
125820
+ addonId: null,
125821
+ access: "create",
125822
+ role: "admin"
125823
+ },
125824
+ "actions.stop": {
125825
+ capName: "actions",
125826
+ capScope: "system",
125827
+ addonId: null,
125828
+ access: "create",
125829
+ role: "admin"
125830
+ },
125831
+ "actions.update": {
125832
+ capName: "actions",
125833
+ capScope: "system",
125834
+ addonId: null,
125835
+ access: "create",
125836
+ role: "admin"
125837
+ },
125838
+ "actionSource.listActions": {
125839
+ capName: "action-source",
125840
+ capScope: "system",
125841
+ addonId: null,
125842
+ access: "view",
125843
+ role: "admin"
125844
+ },
125845
+ "actionSource.runAction": {
125846
+ capName: "action-source",
125847
+ capScope: "system",
125848
+ addonId: null,
125849
+ access: "create",
125850
+ role: "admin"
125851
+ },
125852
+ "actionSource.stopAction": {
125853
+ capName: "action-source",
125854
+ capScope: "system",
125855
+ addonId: null,
125856
+ access: "create",
125857
+ role: "admin"
125858
+ },
125329
125859
  "addonPages.listPages": {
125330
125860
  capName: "addon-pages",
125331
125861
  capScope: "system",
@@ -126607,6 +127137,13 @@ Object.freeze({
126607
127137
  access: "view",
126608
127138
  role: "admin"
126609
127139
  },
127140
+ "coreBlocks.getAutomationRunState": {
127141
+ capName: "core-blocks",
127142
+ capScope: "system",
127143
+ addonId: null,
127144
+ access: "view",
127145
+ role: "admin"
127146
+ },
126610
127147
  "coreBlocks.getBlockSettings": {
126611
127148
  capName: "core-blocks",
126612
127149
  capScope: "system",
@@ -126628,6 +127165,13 @@ Object.freeze({
126628
127165
  access: "view",
126629
127166
  role: "admin"
126630
127167
  },
127168
+ "coreBlocks.getTraces": {
127169
+ capName: "core-blocks",
127170
+ capScope: "system",
127171
+ addonId: null,
127172
+ access: "view",
127173
+ role: "admin"
127174
+ },
126631
127175
  "coreBlocks.getTypeDefs": {
126632
127176
  capName: "core-blocks",
126633
127177
  capScope: "system",
@@ -126663,6 +127207,13 @@ Object.freeze({
126663
127207
  access: "create",
126664
127208
  role: "admin"
126665
127209
  },
127210
+ "coreBlocks.reportAutomationRun": {
127211
+ capName: "core-blocks",
127212
+ capScope: "system",
127213
+ addonId: null,
127214
+ access: "create",
127215
+ role: "admin"
127216
+ },
126666
127217
  "coreBlocks.restart": {
126667
127218
  capName: "core-blocks",
126668
127219
  capScope: "system",
@@ -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.
@@ -194496,7 +195080,7 @@ var DreameVacuumDevice = class extends DreameChildDevice {
194496
195080
  availableFanSpeeds: supportedFanSpeedTokens(vacuum.supportedSuctionLevels),
194497
195081
  progressPercent: 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 };