@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.mjs
CHANGED
|
@@ -52899,7 +52899,7 @@ var DreameCameraStream = class extends TypedEmitter {
|
|
|
52899
52899
|
}
|
|
52900
52900
|
};
|
|
52901
52901
|
//#endregion
|
|
52902
|
-
//#region ../types/dist/event-category-
|
|
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-
|
|
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("
|
|
68616
|
-
|
|
68617
|
-
|
|
68618
|
-
|
|
68619
|
-
|
|
68620
|
-
|
|
68621
|
-
|
|
68622
|
-
|
|
68623
|
-
|
|
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
|
|
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,
|
|
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 };
|