@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/CHANGELOG.md +6 -0
- package/dist/addon.js +978 -394
- package/dist/addon.mjs +979 -395
- package/dist/index.mjs +1 -1
- package/package.json +1 -1
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-
|
|
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-
|
|
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("
|
|
68619
|
-
|
|
68620
|
-
|
|
68621
|
-
|
|
68622
|
-
|
|
68623
|
-
|
|
68624
|
-
|
|
68625
|
-
|
|
68626
|
-
|
|
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
|
|
195086
|
+
errorLabel: vacuumErrorLabel(vacuum.errorCode),
|
|
194503
195087
|
lastChangedAt: Date.now()
|
|
194504
195088
|
};
|
|
194505
195089
|
const toWrite = nextVacuumControlSlice(this.runtimeState.getCapState(CAP_NAME$1), controlSlice);
|