@camstack/addon-provider-dreame 0.2.117 → 0.2.118
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/addon.js +1671 -278
- package/dist/addon.mjs +1671 -278
- package/package.json +2 -2
package/dist/addon.mjs
CHANGED
|
@@ -46341,6 +46341,11 @@ var VACUUM_CONSUMABLES = [
|
|
|
46341
46341
|
siid: 9,
|
|
46342
46342
|
piid: 2
|
|
46343
46343
|
},
|
|
46344
|
+
timeLeft: {
|
|
46345
|
+
siid: 9,
|
|
46346
|
+
piid: 1,
|
|
46347
|
+
unit: "hours"
|
|
46348
|
+
},
|
|
46344
46349
|
reset: {
|
|
46345
46350
|
siid: 9,
|
|
46346
46351
|
aiid: 1
|
|
@@ -46353,6 +46358,11 @@ var VACUUM_CONSUMABLES = [
|
|
|
46353
46358
|
siid: 10,
|
|
46354
46359
|
piid: 2
|
|
46355
46360
|
},
|
|
46361
|
+
timeLeft: {
|
|
46362
|
+
siid: 10,
|
|
46363
|
+
piid: 1,
|
|
46364
|
+
unit: "hours"
|
|
46365
|
+
},
|
|
46356
46366
|
reset: {
|
|
46357
46367
|
siid: 10,
|
|
46358
46368
|
aiid: 1
|
|
@@ -46365,6 +46375,11 @@ var VACUUM_CONSUMABLES = [
|
|
|
46365
46375
|
siid: 11,
|
|
46366
46376
|
piid: 1
|
|
46367
46377
|
},
|
|
46378
|
+
timeLeft: {
|
|
46379
|
+
siid: 11,
|
|
46380
|
+
piid: 2,
|
|
46381
|
+
unit: "hours"
|
|
46382
|
+
},
|
|
46368
46383
|
reset: {
|
|
46369
46384
|
siid: 11,
|
|
46370
46385
|
aiid: 1
|
|
@@ -46372,11 +46387,16 @@ var VACUUM_CONSUMABLES = [
|
|
|
46372
46387
|
},
|
|
46373
46388
|
{
|
|
46374
46389
|
key: "sensor",
|
|
46375
|
-
label: "Sensor",
|
|
46390
|
+
label: "Sensor Dirty",
|
|
46376
46391
|
life: {
|
|
46377
46392
|
siid: 16,
|
|
46378
46393
|
piid: 1
|
|
46379
46394
|
},
|
|
46395
|
+
timeLeft: {
|
|
46396
|
+
siid: 16,
|
|
46397
|
+
piid: 2,
|
|
46398
|
+
unit: "hours"
|
|
46399
|
+
},
|
|
46380
46400
|
reset: {
|
|
46381
46401
|
siid: 16,
|
|
46382
46402
|
aiid: 1
|
|
@@ -46389,6 +46409,11 @@ var VACUUM_CONSUMABLES = [
|
|
|
46389
46409
|
siid: 17,
|
|
46390
46410
|
piid: 1
|
|
46391
46411
|
},
|
|
46412
|
+
timeLeft: {
|
|
46413
|
+
siid: 17,
|
|
46414
|
+
piid: 2,
|
|
46415
|
+
unit: "hours"
|
|
46416
|
+
},
|
|
46392
46417
|
reset: {
|
|
46393
46418
|
siid: 17,
|
|
46394
46419
|
aiid: 1
|
|
@@ -46401,6 +46426,11 @@ var VACUUM_CONSUMABLES = [
|
|
|
46401
46426
|
siid: 18,
|
|
46402
46427
|
piid: 1
|
|
46403
46428
|
},
|
|
46429
|
+
timeLeft: {
|
|
46430
|
+
siid: 18,
|
|
46431
|
+
piid: 2,
|
|
46432
|
+
unit: "hours"
|
|
46433
|
+
},
|
|
46404
46434
|
reset: {
|
|
46405
46435
|
siid: 18,
|
|
46406
46436
|
aiid: 1
|
|
@@ -46413,6 +46443,11 @@ var VACUUM_CONSUMABLES = [
|
|
|
46413
46443
|
siid: 19,
|
|
46414
46444
|
piid: 2
|
|
46415
46445
|
},
|
|
46446
|
+
timeLeft: {
|
|
46447
|
+
siid: 19,
|
|
46448
|
+
piid: 1,
|
|
46449
|
+
unit: "days"
|
|
46450
|
+
},
|
|
46416
46451
|
reset: {
|
|
46417
46452
|
siid: 19,
|
|
46418
46453
|
aiid: 1
|
|
@@ -46425,6 +46460,11 @@ var VACUUM_CONSUMABLES = [
|
|
|
46425
46460
|
siid: 20,
|
|
46426
46461
|
piid: 1
|
|
46427
46462
|
},
|
|
46463
|
+
timeLeft: {
|
|
46464
|
+
siid: 20,
|
|
46465
|
+
piid: 2,
|
|
46466
|
+
unit: "days"
|
|
46467
|
+
},
|
|
46428
46468
|
reset: {
|
|
46429
46469
|
siid: 20,
|
|
46430
46470
|
aiid: 1
|
|
@@ -46437,6 +46477,11 @@ var VACUUM_CONSUMABLES = [
|
|
|
46437
46477
|
siid: 24,
|
|
46438
46478
|
piid: 1
|
|
46439
46479
|
},
|
|
46480
|
+
timeLeft: {
|
|
46481
|
+
siid: 24,
|
|
46482
|
+
piid: 2,
|
|
46483
|
+
unit: "days"
|
|
46484
|
+
},
|
|
46440
46485
|
reset: {
|
|
46441
46486
|
siid: 24,
|
|
46442
46487
|
aiid: 1
|
|
@@ -46449,6 +46494,11 @@ var VACUUM_CONSUMABLES = [
|
|
|
46449
46494
|
siid: 29,
|
|
46450
46495
|
piid: 2
|
|
46451
46496
|
},
|
|
46497
|
+
timeLeft: {
|
|
46498
|
+
siid: 29,
|
|
46499
|
+
piid: 1,
|
|
46500
|
+
unit: "days"
|
|
46501
|
+
},
|
|
46452
46502
|
reset: {
|
|
46453
46503
|
siid: 29,
|
|
46454
46504
|
aiid: 1
|
|
@@ -46456,11 +46506,16 @@ var VACUUM_CONSUMABLES = [
|
|
|
46456
46506
|
},
|
|
46457
46507
|
{
|
|
46458
46508
|
key: "wheel",
|
|
46459
|
-
label: "Wheel",
|
|
46509
|
+
label: "Wheel Dirty",
|
|
46460
46510
|
life: {
|
|
46461
46511
|
siid: 30,
|
|
46462
46512
|
piid: 2
|
|
46463
46513
|
},
|
|
46514
|
+
timeLeft: {
|
|
46515
|
+
siid: 30,
|
|
46516
|
+
piid: 1,
|
|
46517
|
+
unit: "days"
|
|
46518
|
+
},
|
|
46464
46519
|
reset: {
|
|
46465
46520
|
siid: 30,
|
|
46466
46521
|
aiid: 1
|
|
@@ -46473,6 +46528,11 @@ var VACUUM_CONSUMABLES = [
|
|
|
46473
46528
|
siid: 31,
|
|
46474
46529
|
piid: 2
|
|
46475
46530
|
},
|
|
46531
|
+
timeLeft: {
|
|
46532
|
+
siid: 31,
|
|
46533
|
+
piid: 1,
|
|
46534
|
+
unit: "days"
|
|
46535
|
+
},
|
|
46476
46536
|
reset: {
|
|
46477
46537
|
siid: 31,
|
|
46478
46538
|
aiid: 1
|
|
@@ -46485,15 +46545,13 @@ var VACUUM_CONSUMABLES = [
|
|
|
46485
46545
|
siid: 27,
|
|
46486
46546
|
piid: 17
|
|
46487
46547
|
},
|
|
46548
|
+
timeLeft: null,
|
|
46488
46549
|
reset: null
|
|
46489
46550
|
}
|
|
46490
46551
|
];
|
|
46491
46552
|
function consumableSpec(key2) {
|
|
46492
46553
|
return VACUUM_CONSUMABLES.find((c) => c.key === key2);
|
|
46493
46554
|
}
|
|
46494
|
-
function isDreameConsumableKey(key2) {
|
|
46495
|
-
return VACUUM_CONSUMABLES.some((c) => c.key === key2);
|
|
46496
|
-
}
|
|
46497
46555
|
var X50_AI_FEATURES = [
|
|
46498
46556
|
"obstacleDetection",
|
|
46499
46557
|
"petDetection",
|
|
@@ -49570,6 +49628,15 @@ var OssFetcher = class {
|
|
|
49570
49628
|
this.cache.clear();
|
|
49571
49629
|
}
|
|
49572
49630
|
};
|
|
49631
|
+
function consumableProps(spec) {
|
|
49632
|
+
return spec.timeLeft === null ? [spec.life] : [spec.life, {
|
|
49633
|
+
siid: spec.timeLeft.siid,
|
|
49634
|
+
piid: spec.timeLeft.piid
|
|
49635
|
+
}];
|
|
49636
|
+
}
|
|
49637
|
+
function asError(err) {
|
|
49638
|
+
return err instanceof Error ? err : new Error(String(err));
|
|
49639
|
+
}
|
|
49573
49640
|
var SUCTION = enumLookup([
|
|
49574
49641
|
0,
|
|
49575
49642
|
1,
|
|
@@ -49728,23 +49795,82 @@ var VacuumDevice = class _VacuumDevice extends BaseDevice$1 {
|
|
|
49728
49795
|
});
|
|
49729
49796
|
}
|
|
49730
49797
|
/**
|
|
49731
|
-
*
|
|
49732
|
-
* {@link
|
|
49733
|
-
*
|
|
49734
|
-
|
|
49798
|
+
* Time left before a consumable needs attention, in the unit the robot counts
|
|
49799
|
+
* it in (see {@link ConsumableSpec.timeLeft}), or `null` when the model does
|
|
49800
|
+
* not report it or the key has no time-left twin.
|
|
49801
|
+
*/
|
|
49802
|
+
consumableTimeLeft(key2) {
|
|
49803
|
+
const spec = consumableSpec(key2);
|
|
49804
|
+
if (spec === void 0 || spec.timeLeft === null) return null;
|
|
49805
|
+
const value = this.#num(spec.timeLeft.siid, spec.timeLeft.piid);
|
|
49806
|
+
return value === null ? null : {
|
|
49807
|
+
value,
|
|
49808
|
+
unit: spec.timeLeft.unit
|
|
49809
|
+
};
|
|
49810
|
+
}
|
|
49811
|
+
/**
|
|
49812
|
+
* Seed EVERY consumable life property AND its time-left twin from the cloud
|
|
49813
|
+
* shadow (no robot wake), in one call, so {@link supportedConsumables} and
|
|
49814
|
+
* {@link consumableTimeLeft} reflect the full set on a docked robot. The
|
|
49815
|
+
* robot rarely re-pushes these over MQTT, so a consumer that wants them fresh
|
|
49816
|
+
* must call this again — the library does not poll them.
|
|
49735
49817
|
*/
|
|
49736
49818
|
async refreshConsumables() {
|
|
49737
|
-
await this.refreshCachedProperties(VACUUM_CONSUMABLES.
|
|
49819
|
+
await this.refreshCachedProperties(VACUUM_CONSUMABLES.flatMap(consumableProps));
|
|
49738
49820
|
}
|
|
49739
49821
|
/**
|
|
49740
49822
|
* Reset (mark replaced → life back to 100%) one consumable via its MIoT reset
|
|
49741
|
-
* action
|
|
49742
|
-
*
|
|
49823
|
+
* action, then re-read that consumable's life % and time left ONCE so the
|
|
49824
|
+
* cache — and every getter after this resolves — reflects the reset. The
|
|
49825
|
+
* re-read is live; if the robot does not answer it falls back to the cloud
|
|
49826
|
+
* shadow (which may still hold the pre-reset value until the robot reports),
|
|
49827
|
+
* and `refreshedFrom` says which one answered.
|
|
49828
|
+
*
|
|
49829
|
+
* Rejects when the model exposes no reset action for the key (e.g.
|
|
49830
|
+
* `dust-bag`), the key is unknown, or the action itself fails. A failed
|
|
49831
|
+
* RE-READ does not reject: the reset happened, and the result says the values
|
|
49832
|
+
* are unknown (`refreshedFrom: null`).
|
|
49743
49833
|
*/
|
|
49744
49834
|
async resetConsumable(key2) {
|
|
49745
49835
|
const spec = consumableSpec(key2);
|
|
49746
49836
|
if (spec === void 0 || spec.reset === null) throw new DreameError(`resetConsumable: no reset action for consumable "${key2}"`);
|
|
49747
|
-
|
|
49837
|
+
const actionResult = await this.callAction(spec.reset.siid, spec.reset.aiid, []);
|
|
49838
|
+
const refresh2 = await this.#rereadConsumable(spec);
|
|
49839
|
+
const fresh = refresh2.refreshedFrom !== null;
|
|
49840
|
+
return {
|
|
49841
|
+
key: key2,
|
|
49842
|
+
actionResult,
|
|
49843
|
+
refreshedFrom: refresh2.refreshedFrom,
|
|
49844
|
+
leftPct: fresh ? this.consumableLeftPct(key2) : null,
|
|
49845
|
+
timeLeft: fresh ? this.consumableTimeLeft(key2) : null,
|
|
49846
|
+
refreshError: refresh2.error
|
|
49847
|
+
};
|
|
49848
|
+
}
|
|
49849
|
+
/** One targeted re-read of a consumable's properties: live, else the cloud shadow. */
|
|
49850
|
+
async #rereadConsumable(spec) {
|
|
49851
|
+
const props = consumableProps(spec);
|
|
49852
|
+
let liveError;
|
|
49853
|
+
try {
|
|
49854
|
+
await this.refreshProperties(props);
|
|
49855
|
+
return {
|
|
49856
|
+
refreshedFrom: "device",
|
|
49857
|
+
error: null
|
|
49858
|
+
};
|
|
49859
|
+
} catch (err) {
|
|
49860
|
+
liveError = asError(err);
|
|
49861
|
+
}
|
|
49862
|
+
try {
|
|
49863
|
+
await this.refreshCachedProperties(props);
|
|
49864
|
+
return {
|
|
49865
|
+
refreshedFrom: "cloud-shadow",
|
|
49866
|
+
error: liveError
|
|
49867
|
+
};
|
|
49868
|
+
} catch (err) {
|
|
49869
|
+
return {
|
|
49870
|
+
refreshedFrom: null,
|
|
49871
|
+
error: asError(err)
|
|
49872
|
+
};
|
|
49873
|
+
}
|
|
49748
49874
|
}
|
|
49749
49875
|
/**
|
|
49750
49876
|
* The raw `AI_DETECTION` value (siid 4 piid 22) — an int bitmask or a JSON
|
|
@@ -58134,7 +58260,7 @@ var ZodIssueCode = {
|
|
|
58134
58260
|
var ZodFirstPartyTypeKind;
|
|
58135
58261
|
ZodFirstPartyTypeKind || (ZodFirstPartyTypeKind = {});
|
|
58136
58262
|
//#endregion
|
|
58137
|
-
//#region ../types/dist/sleep-
|
|
58263
|
+
//#region ../types/dist/sleep-DU7mu4wb.mjs
|
|
58138
58264
|
/**
|
|
58139
58265
|
* The audio chunk plane's byte format, and the ONE expansion from a coded
|
|
58140
58266
|
* window to float samples (D455).
|
|
@@ -60115,11 +60241,17 @@ var RawStateResultSchema = object({
|
|
|
60115
60241
|
method(object({ deviceId: number() }), array(StreamSourceEntrySchema)), method(object({ deviceId: number() }), array(ConfigEntrySchema$1)), method(object({
|
|
60116
60242
|
deviceId: number(),
|
|
60117
60243
|
values: record(string(), unknown())
|
|
60118
|
-
}), _void(), {
|
|
60244
|
+
}), _void(), {
|
|
60245
|
+
kind: "mutation",
|
|
60246
|
+
auth: "admin"
|
|
60247
|
+
}), method(object({
|
|
60119
60248
|
deviceId: number(),
|
|
60120
60249
|
action: string().min(1),
|
|
60121
60250
|
input: unknown()
|
|
60122
|
-
}), unknown(), { kind: "mutation" }), method(object({ deviceId: number() }), _void(), {
|
|
60251
|
+
}), unknown(), { kind: "mutation" }), method(object({ deviceId: number() }), _void(), {
|
|
60252
|
+
kind: "mutation",
|
|
60253
|
+
auth: "admin"
|
|
60254
|
+
}), method(object({ deviceId: number() }), unknown().nullable()), method(object({ deviceId: number() }), RawStateResultSchema.nullable(), { auth: "protected" });
|
|
60123
60255
|
//#endregion
|
|
60124
60256
|
//#region ../types/dist/err-msg-IQTHeDzc.mjs
|
|
60125
60257
|
/**
|
|
@@ -64355,7 +64487,7 @@ method(ListInputSchema, array(BrokerInfoSchema$1)), method(GetInputSchema, Broke
|
|
|
64355
64487
|
}), method(UnsubscribeInputSchema, _void(), {
|
|
64356
64488
|
kind: "mutation",
|
|
64357
64489
|
auth: "admin"
|
|
64358
|
-
}), method(GetStateInputSchema, unknown().nullable()), method(_void(), RegistryStatusSchema);
|
|
64490
|
+
}), method(GetStateInputSchema, unknown().nullable(), { auth: "admin" }), method(_void(), RegistryStatusSchema);
|
|
64359
64491
|
DeviceType.Camera;
|
|
64360
64492
|
/** Stream delivery format. (Relocated from the retired `streaming-engine` cap.) */
|
|
64361
64493
|
var StreamFormatSchema = _enum([
|
|
@@ -65538,17 +65670,20 @@ method(object({
|
|
|
65538
65670
|
namespace: string().optional(),
|
|
65539
65671
|
collection: string(),
|
|
65540
65672
|
key: string()
|
|
65541
|
-
}), unknown()), method(object({
|
|
65673
|
+
}), unknown(), { auth: "admin" }), method(object({
|
|
65542
65674
|
namespace: string().optional(),
|
|
65543
65675
|
collection: string(),
|
|
65544
65676
|
key: string(),
|
|
65545
65677
|
value: unknown()
|
|
65546
|
-
}), _void(), {
|
|
65678
|
+
}), _void(), {
|
|
65679
|
+
kind: "mutation",
|
|
65680
|
+
auth: "admin"
|
|
65681
|
+
}), method(object({
|
|
65547
65682
|
namespace: string().optional(),
|
|
65548
65683
|
collection: string(),
|
|
65549
65684
|
filter: QueryFilterSchema.optional(),
|
|
65550
65685
|
columns: array(string()).readonly().optional()
|
|
65551
|
-
}), array(SettingsRecordSchema).readonly()), method(object({
|
|
65686
|
+
}), array(SettingsRecordSchema).readonly(), { auth: "admin" }), method(object({
|
|
65552
65687
|
namespace: string().optional(),
|
|
65553
65688
|
collection: string(),
|
|
65554
65689
|
/** Declared columns identifying the slot; must be covered by a UNIQUE
|
|
@@ -65558,7 +65693,10 @@ method(object({
|
|
|
65558
65693
|
}), object({
|
|
65559
65694
|
/** The id the row HAS: assigned by SQLite on an insert, or the existing
|
|
65560
65695
|
* row's own id on an update. */
|
|
65561
|
-
id: union([string(), number()]) }), {
|
|
65696
|
+
id: union([string(), number()]) }), {
|
|
65697
|
+
kind: "mutation",
|
|
65698
|
+
auth: "admin"
|
|
65699
|
+
}), method(object({
|
|
65562
65700
|
namespace: string().optional(),
|
|
65563
65701
|
collection: string(),
|
|
65564
65702
|
record: object({
|
|
@@ -65571,38 +65709,56 @@ id: union([string(), number()]) }), { kind: "mutation" }), method(object({
|
|
|
65571
65709
|
* for an absent one, or the ROWID SQLite assigned on an `INTEGER`
|
|
65572
65710
|
* primary key — which is the only place an auto key is knowable.
|
|
65573
65711
|
*/
|
|
65574
|
-
id: union([string(), number()]) }), {
|
|
65712
|
+
id: union([string(), number()]) }), {
|
|
65713
|
+
kind: "mutation",
|
|
65714
|
+
auth: "admin"
|
|
65715
|
+
}), method(object({
|
|
65575
65716
|
namespace: string().optional(),
|
|
65576
65717
|
collection: string(),
|
|
65577
65718
|
records: array(BulkRecordSchema).readonly()
|
|
65578
|
-
}), object({ inserted: number().int() }), {
|
|
65719
|
+
}), object({ inserted: number().int() }), {
|
|
65720
|
+
kind: "mutation",
|
|
65721
|
+
auth: "admin"
|
|
65722
|
+
}), method(object({
|
|
65579
65723
|
namespace: string().optional(),
|
|
65580
65724
|
collection: string(),
|
|
65581
65725
|
id: string(),
|
|
65582
65726
|
data: record(string(), unknown())
|
|
65583
|
-
}), _void(), {
|
|
65727
|
+
}), _void(), {
|
|
65728
|
+
kind: "mutation",
|
|
65729
|
+
auth: "admin"
|
|
65730
|
+
}), method(object({
|
|
65584
65731
|
namespace: string().optional(),
|
|
65585
65732
|
collection: string(),
|
|
65586
65733
|
key: string()
|
|
65587
|
-
}), _void(), {
|
|
65734
|
+
}), _void(), {
|
|
65735
|
+
kind: "mutation",
|
|
65736
|
+
auth: "admin"
|
|
65737
|
+
}), method(object({
|
|
65588
65738
|
namespace: string().optional(),
|
|
65589
65739
|
collection: string(),
|
|
65590
65740
|
filter: MutationFilterSchema
|
|
65591
|
-
}), object({ deleted: number().int() }), {
|
|
65741
|
+
}), object({ deleted: number().int() }), {
|
|
65742
|
+
kind: "mutation",
|
|
65743
|
+
auth: "admin"
|
|
65744
|
+
}), method(object({
|
|
65592
65745
|
namespace: string().optional(),
|
|
65593
65746
|
collection: string(),
|
|
65594
65747
|
filter: MutationFilterSchema,
|
|
65595
65748
|
data: record(string(), unknown())
|
|
65596
|
-
}), object({ updated: number().int() }), {
|
|
65749
|
+
}), object({ updated: number().int() }), {
|
|
65750
|
+
kind: "mutation",
|
|
65751
|
+
auth: "admin"
|
|
65752
|
+
}), method(object({
|
|
65597
65753
|
namespace: string().optional(),
|
|
65598
65754
|
collection: string(),
|
|
65599
65755
|
filter: QueryFilterSchema.optional()
|
|
65600
|
-
}), number()), method(object({
|
|
65756
|
+
}), number(), { auth: "admin" }), method(object({
|
|
65601
65757
|
namespace: string().optional(),
|
|
65602
65758
|
collection: string(),
|
|
65603
65759
|
fields: array(AggregateFieldSchema).readonly(),
|
|
65604
65760
|
filter: QueryFilterSchema.optional()
|
|
65605
|
-
}), AggregateResultSchema), method(object({
|
|
65761
|
+
}), AggregateResultSchema, { auth: "admin" }), method(object({
|
|
65606
65762
|
namespace: string().optional(),
|
|
65607
65763
|
collection: string(),
|
|
65608
65764
|
field: string(),
|
|
@@ -65612,15 +65768,18 @@ id: union([string(), number()]) }), { kind: "mutation" }), method(object({
|
|
|
65612
65768
|
}), array(object({
|
|
65613
65769
|
bucket: number().int(),
|
|
65614
65770
|
count: number().int()
|
|
65615
|
-
})).readonly()), method(object({
|
|
65771
|
+
})).readonly(), { auth: "admin" }), method(object({
|
|
65616
65772
|
namespace: string().optional(),
|
|
65617
65773
|
collection: string()
|
|
65618
|
-
}), boolean()), method(object({
|
|
65774
|
+
}), boolean(), { auth: "admin" }), method(object({
|
|
65619
65775
|
namespace: string().optional(),
|
|
65620
65776
|
collection: string(),
|
|
65621
65777
|
columns: array(CollectionColumnSchema).readonly(),
|
|
65622
65778
|
indexes: array(CollectionIndexSchema).readonly().optional()
|
|
65623
|
-
}), _void(), {
|
|
65779
|
+
}), _void(), {
|
|
65780
|
+
kind: "mutation",
|
|
65781
|
+
auth: "admin"
|
|
65782
|
+
});
|
|
65624
65783
|
/**
|
|
65625
65784
|
* What one engine says about itself. The orchestrator uses `kind` to pick
|
|
65626
65785
|
* a registrant for a collection; `engineId` is what a log line names when
|
|
@@ -66428,7 +66587,13 @@ var ExposeInputSchema = object({
|
|
|
66428
66587
|
capabilities: array(string()).optional()
|
|
66429
66588
|
});
|
|
66430
66589
|
var UnexposeInputSchema = object({ deviceId: string() });
|
|
66431
|
-
method(_void(), DeviceExportStatusSchema), method(_void(), array(DeviceKindSchema)), method(_void(), array(ExposedDeviceSchema)), method(ExposeInputSchema, _void(), {
|
|
66590
|
+
method(_void(), DeviceExportStatusSchema), method(_void(), array(DeviceKindSchema)), method(_void(), array(ExposedDeviceSchema)), method(ExposeInputSchema, _void(), {
|
|
66591
|
+
kind: "mutation",
|
|
66592
|
+
auth: "admin"
|
|
66593
|
+
}), method(UnexposeInputSchema, _void(), {
|
|
66594
|
+
kind: "mutation",
|
|
66595
|
+
auth: "admin"
|
|
66596
|
+
});
|
|
66432
66597
|
/** Wire/storage schema for one icon id. */
|
|
66433
66598
|
var LocationIconIdSchema = _enum([
|
|
66434
66599
|
"map-pin",
|
|
@@ -67013,7 +67178,10 @@ method(object({
|
|
|
67013
67178
|
kind: "mutation",
|
|
67014
67179
|
auth: "admin",
|
|
67015
67180
|
timeoutMs: 12 * 6e4
|
|
67016
|
-
}), method(DeviceRegisterPayloadSchema, _void(), { kind: "mutation" }), method(DeviceRemovePayloadSchema, _void(), { kind: "mutation" }), method(DevicePersistConfigPayloadSchema, _void(), {
|
|
67181
|
+
}), method(DeviceRegisterPayloadSchema, _void(), { kind: "mutation" }), method(DeviceRemovePayloadSchema, _void(), { kind: "mutation" }), method(DevicePersistConfigPayloadSchema, _void(), {
|
|
67182
|
+
kind: "mutation",
|
|
67183
|
+
auth: "admin"
|
|
67184
|
+
}), method(object({ deviceId: number() }), record(string(), unknown()), { auth: "admin" }), method(object({ deviceId: number() }), record(string(), unknown()), { auth: "admin" }), method(object({ deviceId: number() }), DeviceMetaSchema.nullable()), method(object({
|
|
67017
67185
|
deviceId: number(),
|
|
67018
67186
|
name: string()
|
|
67019
67187
|
}), _void(), {
|
|
@@ -67218,7 +67386,7 @@ method(object({
|
|
|
67218
67386
|
}), method(object({ deviceId: number() }), array(StreamProbeResultSchema), {
|
|
67219
67387
|
kind: "mutation",
|
|
67220
67388
|
auth: "admin"
|
|
67221
|
-
}), method(object({ deviceId: number() }), DeviceBindingsForDeviceSchema), method(object({ deviceIds: array(number()) }), array(DeviceBindingsForDeviceSchema)), method(object({}), array(DeviceBindingsForDeviceSchema)), method(object({
|
|
67389
|
+
}), method(object({ deviceId: number() }), DeviceBindingsForDeviceSchema), method(object({ deviceIds: array(number()) }), array(DeviceBindingsForDeviceSchema)), method(object({}), array(DeviceBindingsForDeviceSchema)), method(object({ capNames: array(string().min(1)).min(1).max(16) }), array(number())), method(object({
|
|
67222
67390
|
deviceId: number(),
|
|
67223
67391
|
capName: string(),
|
|
67224
67392
|
wrapperAddonId: string(),
|
|
@@ -67573,18 +67741,30 @@ var LlmErrorCodeSchema = _enum([
|
|
|
67573
67741
|
"budget-exceeded",
|
|
67574
67742
|
"adapter-error"
|
|
67575
67743
|
]);
|
|
67576
|
-
var
|
|
67744
|
+
var LlmGenerateOkSchema = object({
|
|
67577
67745
|
ok: literal(true),
|
|
67578
67746
|
text: string(),
|
|
67579
67747
|
model: string(),
|
|
67580
67748
|
usage: LlmUsageSchema,
|
|
67581
67749
|
truncated: boolean(),
|
|
67582
67750
|
latencyMs: number()
|
|
67583
|
-
})
|
|
67751
|
+
});
|
|
67752
|
+
/**
|
|
67753
|
+
* WHO refused, on a `refusal`. `guardrail` = a safety filter in front of the
|
|
67754
|
+
* model rejected the request before it answered (Apple Foundation Models
|
|
67755
|
+
* refuses surveillance-framed prompts in 0.1 s); `model` = the model itself
|
|
67756
|
+
* declined. They are different faults: a guardrail refusal is a property of
|
|
67757
|
+
* the BACKEND, so the provider falls back to the next profile; a model refusal
|
|
67758
|
+
* is an answer. Absent on every other code, and on a refusal a wire cannot
|
|
67759
|
+
* attribute.
|
|
67760
|
+
*/
|
|
67761
|
+
var LlmRefusalSourceSchema = _enum(["guardrail", "model"]);
|
|
67762
|
+
var LlmGenerateResultSchema = discriminatedUnion("ok", [LlmGenerateOkSchema, object({
|
|
67584
67763
|
ok: literal(false),
|
|
67585
67764
|
code: LlmErrorCodeSchema,
|
|
67586
67765
|
message: string(),
|
|
67587
|
-
retryAfterMs: number().optional()
|
|
67766
|
+
retryAfterMs: number().optional(),
|
|
67767
|
+
refusalSource: LlmRefusalSourceSchema.optional()
|
|
67588
67768
|
})]);
|
|
67589
67769
|
/**
|
|
67590
67770
|
* `Uint8Array` is the sanctioned binary convention — superjson + the UDS
|
|
@@ -67814,12 +67994,78 @@ var LlmRuntimeDiskUsageSchema = object({
|
|
|
67814
67994
|
modelsBytes: number(),
|
|
67815
67995
|
freeBytes: number().optional()
|
|
67816
67996
|
});
|
|
67817
|
-
|
|
67997
|
+
var LlmRuntimeCompleteInputSchema = LlmGenerateBaseInputSchema.extend({
|
|
67818
67998
|
images: array(LlmImageSchema).optional(),
|
|
67819
67999
|
runtime: ManagedRuntimeConfigSchema,
|
|
67820
68000
|
/** The managed profile's timeout, threaded by the hub provider. */
|
|
67821
68001
|
timeoutMs: number().int().positive().optional()
|
|
67822
|
-
})
|
|
68002
|
+
});
|
|
68003
|
+
/**
|
|
68004
|
+
* Why Apple Foundation Models cannot run on a node that has it in principle.
|
|
68005
|
+
* The first three are Apple's own `UnavailableReason`s, verbatim; the rest are
|
|
68006
|
+
* ours: the OS predates the framework (macOS < 26), the helper binary is not
|
|
68007
|
+
* in the installed addon, or it crashed past the respawn bound.
|
|
68008
|
+
*/
|
|
68009
|
+
var AppleFmUnavailableReasonSchema = _enum([
|
|
68010
|
+
"deviceNotEligible",
|
|
68011
|
+
"appleIntelligenceNotEnabled",
|
|
68012
|
+
"modelNotReady",
|
|
68013
|
+
"osTooOld",
|
|
68014
|
+
"helperMissing",
|
|
68015
|
+
"helperCrashed"
|
|
68016
|
+
]);
|
|
68017
|
+
/**
|
|
68018
|
+
* Apple Foundation Models on ONE node — four states, never a boolean (D315).
|
|
68019
|
+
*
|
|
68020
|
+
* - `available` — the helper probed it; what it CAN do is reported
|
|
68021
|
+
* here (`imageInput`, `contextSize`, languages),
|
|
68022
|
+
* never typed by an operator.
|
|
68023
|
+
* - `unavailable` — a known absence, with the reason to act on.
|
|
68024
|
+
* - `unsupported-platform` — not a Mac; there is nothing to enable.
|
|
68025
|
+
* - `unknown` — the probe did not answer (not yet run, timed
|
|
68026
|
+
* out, helper crashed mid-probe). A failed
|
|
68027
|
+
* measurement is NEVER `unavailable` (D393).
|
|
68028
|
+
*/
|
|
68029
|
+
var AppleFmAvailabilitySchema = discriminatedUnion("state", [
|
|
68030
|
+
object({
|
|
68031
|
+
state: literal("available"),
|
|
68032
|
+
osVersion: string(),
|
|
68033
|
+
/** Tokens, as the framework reports it. Read at runtime, never assumed. */
|
|
68034
|
+
contextSize: number().int().positive(),
|
|
68035
|
+
/** BCP-47-ish tags (`it-IT`) of the languages the model supports. */
|
|
68036
|
+
languages: array(string()),
|
|
68037
|
+
/** Whether prompts may carry images. `false` on a macOS 26 build. */
|
|
68038
|
+
imageInput: boolean()
|
|
68039
|
+
}),
|
|
68040
|
+
object({
|
|
68041
|
+
state: literal("unavailable"),
|
|
68042
|
+
reason: AppleFmUnavailableReasonSchema,
|
|
68043
|
+
osVersion: string().nullable(),
|
|
68044
|
+
detail: string()
|
|
68045
|
+
}),
|
|
68046
|
+
object({
|
|
68047
|
+
state: literal("unsupported-platform"),
|
|
68048
|
+
platform: string()
|
|
68049
|
+
}),
|
|
68050
|
+
object({
|
|
68051
|
+
state: literal("unknown"),
|
|
68052
|
+
error: string()
|
|
68053
|
+
})
|
|
68054
|
+
]);
|
|
68055
|
+
/**
|
|
68056
|
+
* A generation on the node's Apple Foundation Models helper. A separate verb
|
|
68057
|
+
* from `complete`, whose `runtime` is a llama.cpp config: overloading it would
|
|
68058
|
+
* drag a GGUF model reference into a backend that has none. `images` is part
|
|
68059
|
+
* of the contract so vision needs no cap change later; a node whose model has
|
|
68060
|
+
* no image input REFUSES them (`unavailable`), never drops them.
|
|
68061
|
+
*/
|
|
68062
|
+
var LlmRuntimeAppleCompleteInputSchema = LlmGenerateBaseInputSchema.extend({
|
|
68063
|
+
images: array(LlmImageSchema).optional(),
|
|
68064
|
+
/** The profile's timeout, threaded by the hub provider. */
|
|
68065
|
+
timeoutMs: number().int().positive().optional()
|
|
68066
|
+
});
|
|
68067
|
+
var LlmRuntimeCancelInputSchema = object({ requestId: string() });
|
|
68068
|
+
method(LlmRuntimeCompleteInputSchema, LlmGenerateResultSchema, {
|
|
67823
68069
|
kind: "mutation",
|
|
67824
68070
|
auth: "admin",
|
|
67825
68071
|
timeoutMs: AI_CAP_TRANSPORT_TIMEOUT_MS
|
|
@@ -67837,7 +68083,14 @@ method(LlmGenerateBaseInputSchema.extend({
|
|
|
67837
68083
|
}), method(object({ file: string() }), _void(), {
|
|
67838
68084
|
kind: "mutation",
|
|
67839
68085
|
auth: "admin"
|
|
67840
|
-
}), method(object({}), array(LlmNodeModelSchema), { auth: "admin" }), method(object({}), LlmRuntimeDiskUsageSchema, { auth: "admin" })
|
|
68086
|
+
}), method(object({}), array(LlmNodeModelSchema), { auth: "admin" }), method(object({}), LlmRuntimeDiskUsageSchema, { auth: "admin" }), method(LlmRuntimeCancelInputSchema, _void(), {
|
|
68087
|
+
kind: "mutation",
|
|
68088
|
+
auth: "admin"
|
|
68089
|
+
}), method(object({}), AppleFmAvailabilitySchema, { auth: "admin" }), method(LlmRuntimeAppleCompleteInputSchema, LlmGenerateResultSchema, {
|
|
68090
|
+
kind: "mutation",
|
|
68091
|
+
auth: "admin",
|
|
68092
|
+
timeoutMs: AI_CAP_TRANSPORT_TIMEOUT_MS
|
|
68093
|
+
});
|
|
67841
68094
|
/**
|
|
67842
68095
|
* `llm` — consumer-facing LLM surface (spec §1-§3). Collection-mode: array
|
|
67843
68096
|
* methods concat-fan across providers; single-row methods route to ONE
|
|
@@ -67854,7 +68107,8 @@ var LlmProfileKindSchema = _enum([
|
|
|
67854
68107
|
"openai",
|
|
67855
68108
|
"anthropic",
|
|
67856
68109
|
"google",
|
|
67857
|
-
"managed-local"
|
|
68110
|
+
"managed-local",
|
|
68111
|
+
"apple-foundation"
|
|
67858
68112
|
]);
|
|
67859
68113
|
var LlmProfileSchema = object({
|
|
67860
68114
|
id: string(),
|
|
@@ -67912,7 +68166,14 @@ var LlmProfileSchema = object({
|
|
|
67912
68166
|
toolsEnabled: boolean().default(false),
|
|
67913
68167
|
extraHeaders: record(string(), string()).optional(),
|
|
67914
68168
|
/** kind === 'managed-local' only (spec §4). */
|
|
67915
|
-
runtime: ManagedRuntimeConfigSchema.optional()
|
|
68169
|
+
runtime: ManagedRuntimeConfigSchema.optional(),
|
|
68170
|
+
/**
|
|
68171
|
+
* kind === 'apple-foundation' only: WHERE it runs — the only thing there is
|
|
68172
|
+
* to configure (no key, no URL, no download; `model` is `apple-system`).
|
|
68173
|
+
* `supportsVision` on such a row is stamped by the provider from the node's
|
|
68174
|
+
* reported `imageInput`, never taken from the operator (D62).
|
|
68175
|
+
*/
|
|
68176
|
+
appleFoundation: object({ nodeId: string() }).optional()
|
|
67916
68177
|
});
|
|
67917
68178
|
/** ConfigUISchema tree passed through untyped on the wire (the
|
|
67918
68179
|
* notification-output `ConfigSchemaPassthrough` precedent at
|
|
@@ -67994,7 +68255,9 @@ var LlmRuntimeNodeSchema = object({
|
|
|
67994
68255
|
reachable: boolean(),
|
|
67995
68256
|
status: LlmRuntimeStatusSchema.optional(),
|
|
67996
68257
|
disk: LlmRuntimeDiskUsageSchema.optional(),
|
|
67997
|
-
error: string().optional()
|
|
68258
|
+
error: string().optional(),
|
|
68259
|
+
/** Absent = the node did not answer the question (render as unknown). */
|
|
68260
|
+
apple: AppleFmAvailabilitySchema.optional()
|
|
67998
68261
|
});
|
|
67999
68262
|
var GenerateVisionInputSchema = LlmGenerateBaseInputSchema.extend({ images: array(LlmImageSchema).min(1) });
|
|
68000
68263
|
var ProfileRefInputSchema = object({
|
|
@@ -68765,7 +69028,22 @@ var StatusSchema = object({
|
|
|
68765
69028
|
brokerCount: number(),
|
|
68766
69029
|
embeddedRunning: boolean()
|
|
68767
69030
|
});
|
|
68768
|
-
method(_void(), array(BrokerInfoSchema)), method(IdInputSchema, BrokerConnectionDetailsSchema
|
|
69031
|
+
method(_void(), array(BrokerInfoSchema)), method(IdInputSchema, BrokerConnectionDetailsSchema, { auth: "admin" }), method(AddBrokerInputSchema, AddBrokerResultSchema, {
|
|
69032
|
+
kind: "mutation",
|
|
69033
|
+
auth: "admin"
|
|
69034
|
+
}), method(IdInputSchema, _void(), {
|
|
69035
|
+
kind: "mutation",
|
|
69036
|
+
auth: "admin"
|
|
69037
|
+
}), method(IdInputSchema, TestResultSchema$1, {
|
|
69038
|
+
kind: "mutation",
|
|
69039
|
+
auth: "admin"
|
|
69040
|
+
}), method(StartEmbeddedInputSchema, StartEmbeddedResultSchema, {
|
|
69041
|
+
kind: "mutation",
|
|
69042
|
+
auth: "admin"
|
|
69043
|
+
}), method(IdInputSchema, _void(), {
|
|
69044
|
+
kind: "mutation",
|
|
69045
|
+
auth: "admin"
|
|
69046
|
+
}), method(_void(), StatusSchema);
|
|
68769
69047
|
var NetworkEndpointSchema = object({
|
|
68770
69048
|
url: string(),
|
|
68771
69049
|
hostname: string(),
|
|
@@ -69060,6 +69338,18 @@ var TargetKindSchema = object({
|
|
|
69060
69338
|
*/
|
|
69061
69339
|
iconMediaType: string().optional(),
|
|
69062
69340
|
configSchema: ConfigSchemaPassthrough,
|
|
69341
|
+
/**
|
|
69342
|
+
* The config keys that decide WHERE this kind's secrets are sent (server,
|
|
69343
|
+
* base URL, instance, chat, recipient). A save that changes any of them
|
|
69344
|
+
* while leaving a secret redacted is refused by the provider (D634), so an
|
|
69345
|
+
* editor uses this to ask for the secret on exactly those edits instead of
|
|
69346
|
+
* on every edit.
|
|
69347
|
+
*
|
|
69348
|
+
* Optional: absent from a provider that holds no secret (the Home Assistant
|
|
69349
|
+
* kinds) and from a provider older than this field — an editor that finds
|
|
69350
|
+
* it absent must assume ANY edit may need the secret.
|
|
69351
|
+
*/
|
|
69352
|
+
destinationKeys: array(string()).optional(),
|
|
69063
69353
|
supportsDiscovery: boolean(),
|
|
69064
69354
|
caps: TargetKindCapsSchema
|
|
69065
69355
|
});
|
|
@@ -69098,19 +69388,34 @@ var SendResultSchema = object({
|
|
|
69098
69388
|
});
|
|
69099
69389
|
/** Same shape as SendResult — kept as a distinct name for the test panel. */
|
|
69100
69390
|
var TestResultSchema = SendResultSchema;
|
|
69101
|
-
method(object({}), array(TargetKindSchema)), method(object({}), array(TargetSchema)), method(object({
|
|
69391
|
+
method(object({}), array(TargetKindSchema)), method(object({}), array(TargetSchema), { auth: "admin" }), method(object({
|
|
69102
69392
|
kind: string(),
|
|
69103
69393
|
config: record(string(), unknown()).optional()
|
|
69104
|
-
}), array(DiscoveredTargetSchema)), method(object({
|
|
69394
|
+
}), array(DiscoveredTargetSchema), { auth: "admin" }), method(object({
|
|
69105
69395
|
targetId: string(),
|
|
69106
69396
|
notification: NotificationSchema
|
|
69107
|
-
}), SendResultSchema, {
|
|
69397
|
+
}), SendResultSchema, {
|
|
69398
|
+
kind: "mutation",
|
|
69399
|
+
auth: "admin"
|
|
69400
|
+
}), method(object({
|
|
69108
69401
|
targetId: string(),
|
|
69109
69402
|
sample: NotificationSchema.optional()
|
|
69110
|
-
}), TestResultSchema, {
|
|
69403
|
+
}), TestResultSchema, {
|
|
69404
|
+
kind: "mutation",
|
|
69405
|
+
auth: "admin"
|
|
69406
|
+
}), method(object({ target: TargetSchema }), TargetSchema, {
|
|
69407
|
+
kind: "mutation",
|
|
69408
|
+
auth: "admin"
|
|
69409
|
+
}), method(object({ targetId: string() }), _void(), {
|
|
69410
|
+
kind: "mutation",
|
|
69411
|
+
auth: "admin"
|
|
69412
|
+
}), method(object({
|
|
69111
69413
|
targetId: string(),
|
|
69112
69414
|
enabled: boolean()
|
|
69113
|
-
}), _void(), {
|
|
69415
|
+
}), _void(), {
|
|
69416
|
+
kind: "mutation",
|
|
69417
|
+
auth: "admin"
|
|
69418
|
+
});
|
|
69114
69419
|
new Set([
|
|
69115
69420
|
{
|
|
69116
69421
|
id: "person",
|
|
@@ -69631,6 +69936,7 @@ sub("presence", "sensor", "sensor", "#22c55e", "presence", "Presence");
|
|
|
69631
69936
|
sub("enum-sensor", "sensor", "sensor", TAXONOMY_COLORS.sensor, "generic", "Sensor state");
|
|
69632
69937
|
sub("device-event", "sensor", "sensor", "#10b981", "button", "Device event");
|
|
69633
69938
|
sub("lock", "control", "control", "#0ea5e9", "lock", "Lock");
|
|
69939
|
+
sub("cover", "control", "control", "#0ea5e9", "door", "Cover");
|
|
69634
69940
|
sub("switch", "control", "control", TAXONOMY_COLORS.control, "switch", "Switch");
|
|
69635
69941
|
sub("siren", "control", "control", "#dc2626", "siren", "Siren");
|
|
69636
69942
|
sub("button", "control", "control", "#10b981", "button", "Button");
|
|
@@ -69934,6 +70240,9 @@ var NcSystemEventKindSchema = _enum([
|
|
|
69934
70240
|
"device-battery-normal",
|
|
69935
70241
|
"device-consumable-low",
|
|
69936
70242
|
"device-consumable-normal",
|
|
70243
|
+
"device-tank-low",
|
|
70244
|
+
"device-tank-full",
|
|
70245
|
+
"device-tank-normal",
|
|
69937
70246
|
"stream-online",
|
|
69938
70247
|
"stream-offline",
|
|
69939
70248
|
"node-online",
|
|
@@ -69949,6 +70258,10 @@ var NcSystemEventKindSchema = _enum([
|
|
|
69949
70258
|
"alarm-disarmed",
|
|
69950
70259
|
"alarm-arming",
|
|
69951
70260
|
"alarm-arm-refused",
|
|
70261
|
+
"alarm-pending",
|
|
70262
|
+
"alarm-rearmed",
|
|
70263
|
+
"alarm-sensor-bypass",
|
|
70264
|
+
"alarm-check-failed",
|
|
69952
70265
|
"addon-updated",
|
|
69953
70266
|
"server-updated",
|
|
69954
70267
|
"export-completed",
|
|
@@ -69986,8 +70299,36 @@ var NcSystemEventConditionSchema = object({
|
|
|
69986
70299
|
* does not carry) matches no `deviceTypes` list.
|
|
69987
70300
|
*/
|
|
69988
70301
|
deviceTypes: array(string().min(1)).min(1).optional(),
|
|
70302
|
+
/**
|
|
70303
|
+
* Devices this rule NEVER speaks about, whatever else matched. Scoped to the
|
|
70304
|
+
* same kind family as `deviceIds` (`systemEventFilterApplies(kind,
|
|
70305
|
+
* 'deviceIds')`) and applied AFTER it: include first, then exclude. A
|
|
70306
|
+
* consumable subject (`consumable:<id>:<key>`) is excluded by its device id.
|
|
70307
|
+
*
|
|
70308
|
+
* It never fails closed: a subject that carries no device id is not in the
|
|
70309
|
+
* list, so it is not excluded. "Every battery except the one thermometer that
|
|
70310
|
+
* reads 0 % forever" is the question this answers — expressed as an include
|
|
70311
|
+
* list it would have to name every other battery device, and every device
|
|
70312
|
+
* adopted later would be silently left out (D636).
|
|
70313
|
+
*/
|
|
70314
|
+
excludeDeviceIds: array(number().int()).min(1).optional(),
|
|
69989
70315
|
nodeIds: array(string().min(1)).min(1).optional(),
|
|
69990
|
-
packageNames: array(string().min(1)).min(1).optional()
|
|
70316
|
+
packageNames: array(string().min(1)).min(1).optional(),
|
|
70317
|
+
/**
|
|
70318
|
+
* At or below this remaining-life %, a consumable is LOW for THIS rule
|
|
70319
|
+
* (D642). Absent = {@link NC_CONSUMABLE_LOW_PERCENT_DEFAULT}; at most
|
|
70320
|
+
* {@link NC_CONSUMABLE_LOW_PERCENT_MAX}, so every episode can recover. Scoped to the
|
|
70321
|
+
* consumable kinds (`systemEventFilterApplies`) and to the device-events
|
|
70322
|
+
* family; ignored on every other kind.
|
|
70323
|
+
*
|
|
70324
|
+
* It is a property of the RULE because the operator decided so: a rule at
|
|
70325
|
+
* 25 must be told about a filter at 20 %, a rule at 10 must not. The hub
|
|
70326
|
+
* keeps one episode per (threshold, device, item) — its own hysteresis, its
|
|
70327
|
+
* own reminder — and a rule hears only the episode judged at ITS threshold,
|
|
70328
|
+
* exactly as an occupancy rule hears only the edge of its own count. A
|
|
70329
|
+
* provider's own `replace` verdict is low at every threshold.
|
|
70330
|
+
*/
|
|
70331
|
+
consumableLowPercent: number().int().min(1).max(95).optional()
|
|
69991
70332
|
});
|
|
69992
70333
|
/** Hard ceiling on a window (24h). A snooze that could not expire would be an
|
|
69993
70334
|
* outage the operator asked for once and forgot. */
|
|
@@ -70130,6 +70471,16 @@ var NcCrossingSchema = _enum([
|
|
|
70130
70471
|
"exit",
|
|
70131
70472
|
"any"
|
|
70132
70473
|
]);
|
|
70474
|
+
/**
|
|
70475
|
+
* Which edge of a BOOLEAN sensor fires a device-event rule (D640).
|
|
70476
|
+
* - `raised` — the sensor went high: water detected, smoke, open, tampered.
|
|
70477
|
+
* - `cleared` — it went back: dry, clear, closed.
|
|
70478
|
+
* - absent — both edges, which is what every rule authored before this did.
|
|
70479
|
+
*
|
|
70480
|
+
* There is no third member for "both": absence already says it, and a value
|
|
70481
|
+
* that means the same as absence is a second way to write one rule.
|
|
70482
|
+
*/
|
|
70483
|
+
var NcSensorTransitionSchema = _enum(["raised", "cleared"]);
|
|
70133
70484
|
/** Admin-zone membership condition (zone IDs as stamped by the ZoneEngine). */
|
|
70134
70485
|
var NcZoneConditionSchema = object({
|
|
70135
70486
|
ids: array(string().min(1)).min(1),
|
|
@@ -70319,6 +70670,20 @@ var NcConditionsSchema = object({
|
|
|
70319
70670
|
scene: NcSceneConditionSchema.optional(),
|
|
70320
70671
|
/** Device scope — absent = all devices. */
|
|
70321
70672
|
devices: array(number()).optional(),
|
|
70673
|
+
/**
|
|
70674
|
+
* Device EXCLUSION (D641) — "every device except these". Judged on the SAME
|
|
70675
|
+
* subject ids {@link devices} is judged on (the camera, the sensor that
|
|
70676
|
+
* fired, a linked set's sensor and row cameras), so a sensor fanned out per
|
|
70677
|
+
* linked camera is excluded exactly where an include would have tested it.
|
|
70678
|
+
* `min(1)`: an empty exclusion excludes nothing and is written as absent.
|
|
70679
|
+
*
|
|
70680
|
+
* Not for `system-event` rules — those carry their own
|
|
70681
|
+
* `systemEvent.excludeDeviceIds` (D636), and the hub refuses this key there.
|
|
70682
|
+
* Editors present the two lists as ONE field (All | Only these | All except
|
|
70683
|
+
* these); the engine evaluates each independently, so a legacy rule
|
|
70684
|
+
* carrying both still means "these, minus those".
|
|
70685
|
+
*/
|
|
70686
|
+
devicesExclude: array(number()).min(1).optional(),
|
|
70322
70687
|
/** Detector class names (any overlap with the record's class set). */
|
|
70323
70688
|
classes: array(string().min(1)).optional(),
|
|
70324
70689
|
/** Veto classes — any overlap fails the rule. */
|
|
@@ -70452,6 +70817,16 @@ var NcConditionsSchema = object({
|
|
|
70452
70817
|
*/
|
|
70453
70818
|
sensorKinds: array(string().min(1)).optional(),
|
|
70454
70819
|
/**
|
|
70820
|
+
* DEVICE-EVENT only (D640). Fire on one edge of a boolean sensor —
|
|
70821
|
+
* {@link NcSensorTransitionSchema}. Which field of the slice means "raised"
|
|
70822
|
+
* is `@camstack/types` → `notification/sensor-raised.ts`. A reading the
|
|
70823
|
+
* engine cannot place (a kind with no raise, an unreadable slice) PASSES:
|
|
70824
|
+
* the condition fails open towards notifying, never towards silence. Editors
|
|
70825
|
+
* offer it only when every chosen {@link sensorKinds} member has a raise
|
|
70826
|
+
* (`sensorTransitionOffered`), and the hub refuses it on any other delivery.
|
|
70827
|
+
*/
|
|
70828
|
+
sensorTransition: NcSensorTransitionSchema.optional(),
|
|
70829
|
+
/**
|
|
70455
70830
|
* PACKAGE-EVENT only. Which package phase fires the rule — `delivered`
|
|
70456
70831
|
* (a parked parcel appeared), `picked-up` (it departed), or `both`. Fails
|
|
70457
70832
|
* when the subject's phase does not match (a subject always carries a phase
|
|
@@ -70544,6 +70919,19 @@ var NcMediaFrameSchema = _enum([
|
|
|
70544
70919
|
"full",
|
|
70545
70920
|
"boxed"
|
|
70546
70921
|
]);
|
|
70922
|
+
/**
|
|
70923
|
+
* How a SENSOR notification shows the cameras that link the sensor.
|
|
70924
|
+
*
|
|
70925
|
+
* `mosaic` — one image composed of every linked camera's photograph, taken at
|
|
70926
|
+
* the trigger. `each` — one image per camera where the target takes several
|
|
70927
|
+
* attachments; a target that takes one keeps the first camera's (the degrade
|
|
70928
|
+
* engine drops the rest by order). Either way ONE notification per event.
|
|
70929
|
+
* Absent = `mosaic`, applied by the Notification Center and deliberately NOT
|
|
70930
|
+
* a Zod default (see NcRulePatchSchema: a default materialises on every
|
|
70931
|
+
* partial patch). Meaningless for a rule whose subject is not a sensor, and a
|
|
70932
|
+
* sensor linked to one camera ships that camera's plain snapshot either way.
|
|
70933
|
+
*/
|
|
70934
|
+
var NcLinkedCameraModeSchema = _enum(["mosaic", "each"]);
|
|
70547
70935
|
var NcMediaPolicySchema = object({
|
|
70548
70936
|
attach: _enum([
|
|
70549
70937
|
"best",
|
|
@@ -70589,7 +70977,9 @@ var NcMediaPolicySchema = object({
|
|
|
70589
70977
|
* A profile that is not assigned falls back to the cheapest, and the render
|
|
70590
70978
|
* reports which one actually ran.
|
|
70591
70979
|
*/
|
|
70592
|
-
profile: CamProfileSchema.optional()
|
|
70980
|
+
profile: CamProfileSchema.optional(),
|
|
70981
|
+
/** See {@link NcLinkedCameraModeSchema}. */
|
|
70982
|
+
linkedCameras: NcLinkedCameraModeSchema.optional()
|
|
70593
70983
|
});
|
|
70594
70984
|
/**
|
|
70595
70985
|
* Cooldown GRANULARITY over the subject's class — how much a fired
|
|
@@ -70709,7 +71099,7 @@ var NcRuleInputSchema = object({
|
|
|
70709
71099
|
cooldownSec: 60,
|
|
70710
71100
|
scope: "rule-device"
|
|
70711
71101
|
}),
|
|
70712
|
-
/** `{{var}}`
|
|
71102
|
+
/** `{{var}}` title/body. The variables are declared in `NC_TEMPLATE_VARS` (@camstack/types) and depend on the rule kind — see `templateVarsFor`. */
|
|
70713
71103
|
template: object({
|
|
70714
71104
|
title: string().max(500).optional(),
|
|
70715
71105
|
body: string().max(2e3).optional()
|
|
@@ -70889,6 +71279,22 @@ var NcRulePatchSchema = NcRuleInputSchema.partial().extend({
|
|
|
70889
71279
|
throttle: NcThrottleSchema.optional(),
|
|
70890
71280
|
priority: number().int().min(1).max(5).optional()
|
|
70891
71281
|
});
|
|
71282
|
+
var NcRuleClearableKeySchema = _enum([
|
|
71283
|
+
"template",
|
|
71284
|
+
"schedule",
|
|
71285
|
+
"targetUsers",
|
|
71286
|
+
"snoozeOptions",
|
|
71287
|
+
"snoozeAllowGlobal",
|
|
71288
|
+
"waitForEnhancement",
|
|
71289
|
+
"actions",
|
|
71290
|
+
"confirm",
|
|
71291
|
+
"groupIdleSec"
|
|
71292
|
+
]);
|
|
71293
|
+
var NcRuleUpdateInputSchema = object({
|
|
71294
|
+
ruleId: string(),
|
|
71295
|
+
patch: NcRulePatchSchema,
|
|
71296
|
+
clear: array(NcRuleClearableKeySchema).optional()
|
|
71297
|
+
});
|
|
70892
71298
|
/** A persisted rule. */
|
|
70893
71299
|
var NcRuleSchema = NcRuleInputSchema.extend({
|
|
70894
71300
|
id: string(),
|
|
@@ -70920,6 +71326,105 @@ var NcTestResultSchema = object({
|
|
|
70920
71326
|
className: string().optional(),
|
|
70921
71327
|
label: string().optional()
|
|
70922
71328
|
});
|
|
71329
|
+
/** The rule kinds, as data. `NcRuleKind` in `notification/rule-kinds.ts` is the
|
|
71330
|
+
* same union; the template-vars spec pins the two together. `mixed` is DERIVED
|
|
71331
|
+
* (a stored system rule whose kinds span families, D637) and never authored. */
|
|
71332
|
+
var NcRuleKindSchema = _enum([
|
|
71333
|
+
"detection",
|
|
71334
|
+
"sensor",
|
|
71335
|
+
"occupancy",
|
|
71336
|
+
"sound",
|
|
71337
|
+
"device-events",
|
|
71338
|
+
"system",
|
|
71339
|
+
"alarm",
|
|
71340
|
+
"mixed"
|
|
71341
|
+
]);
|
|
71342
|
+
/**
|
|
71343
|
+
* The kinds a SERVED template-variable descriptor may name: the five that
|
|
71344
|
+
* existed before D637, frozen. `getTemplateCatalog` ships `kinds` to every
|
|
71345
|
+
* installed viewer, and a viewer at `@camstack/types` 1.2.25x parses them with a
|
|
71346
|
+
* strict five-member enum — one `device-events` in one descriptor fails the
|
|
71347
|
+
* whole catalog parse. So the WIRE says `system`, and the MATCH widens it:
|
|
71348
|
+
* `templateVarKindApplies` reads a `system` descriptor as applying to every
|
|
71349
|
+
* system rule kind (`device-events`, `system`, `alarm`, `mixed`).
|
|
71350
|
+
*/
|
|
71351
|
+
var NcTemplateVarKindSchema = _enum([
|
|
71352
|
+
"detection",
|
|
71353
|
+
"sensor",
|
|
71354
|
+
"occupancy",
|
|
71355
|
+
"sound",
|
|
71356
|
+
"system"
|
|
71357
|
+
]);
|
|
71358
|
+
/** Which producer renders a template: an ordinary rule, a timelapse, a digest. */
|
|
71359
|
+
var NcTemplateFamilySchema = _enum([
|
|
71360
|
+
"rule",
|
|
71361
|
+
"timelapse",
|
|
71362
|
+
"summary"
|
|
71363
|
+
]);
|
|
71364
|
+
/** Which text field of that producer. `previewText` is the timelapse frame
|
|
71365
|
+
* caption, `captionText` the digest mosaic caption — both see fewer vars. */
|
|
71366
|
+
var NcTemplateFieldSchema = _enum([
|
|
71367
|
+
"title",
|
|
71368
|
+
"body",
|
|
71369
|
+
"previewText",
|
|
71370
|
+
"captionText"
|
|
71371
|
+
]);
|
|
71372
|
+
var NcTemplateVarGroupSchema = _enum([
|
|
71373
|
+
"subject",
|
|
71374
|
+
"place",
|
|
71375
|
+
"time",
|
|
71376
|
+
"rule",
|
|
71377
|
+
"occupancy",
|
|
71378
|
+
"sound",
|
|
71379
|
+
"sensor",
|
|
71380
|
+
"system",
|
|
71381
|
+
"digest",
|
|
71382
|
+
"ai"
|
|
71383
|
+
]);
|
|
71384
|
+
/**
|
|
71385
|
+
* ONE `{{var}}` a notification template may name.
|
|
71386
|
+
*
|
|
71387
|
+
* `families` / `fields` / `kinds` / `deliveries` / `systemEventKinds` say WHERE
|
|
71388
|
+
* it has a value; absent = no restriction on that axis. `systemEventKinds` is
|
|
71389
|
+
* read only for a system-event rule (any family, or `mixed`). `dynamic` declares a FAMILY of names
|
|
71390
|
+
* (`count_<class>`): the descriptor's `name` is the example member.
|
|
71391
|
+
*/
|
|
71392
|
+
var NcTemplateVarDescriptorSchema = object({
|
|
71393
|
+
name: string().regex(/^\w+$/),
|
|
71394
|
+
label: string(),
|
|
71395
|
+
description: string().optional(),
|
|
71396
|
+
example: string(),
|
|
71397
|
+
group: NcTemplateVarGroupSchema,
|
|
71398
|
+
families: array(NcTemplateFamilySchema).min(1),
|
|
71399
|
+
fields: array(NcTemplateFieldSchema).optional(),
|
|
71400
|
+
kinds: array(NcTemplateVarKindSchema).optional(),
|
|
71401
|
+
deliveries: array(NcDeliverySchema).optional(),
|
|
71402
|
+
systemEventKinds: array(NcSystemEventKindSchema).optional(),
|
|
71403
|
+
dynamic: object({
|
|
71404
|
+
prefix: literal("count_"),
|
|
71405
|
+
from: literal("classes")
|
|
71406
|
+
}).optional()
|
|
71407
|
+
});
|
|
71408
|
+
/** Input to `previewTemplate` — the editor's own context, plus the draft text. */
|
|
71409
|
+
var NcTemplatePreviewInputSchema = object({
|
|
71410
|
+
context: object({
|
|
71411
|
+
family: NcTemplateFamilySchema,
|
|
71412
|
+
field: NcTemplateFieldSchema,
|
|
71413
|
+
kind: NcRuleKindSchema.optional(),
|
|
71414
|
+
delivery: NcDeliverySchema.optional(),
|
|
71415
|
+
systemEventKinds: array(NcSystemEventKindSchema).optional()
|
|
71416
|
+
}),
|
|
71417
|
+
template: object({
|
|
71418
|
+
title: string().max(500).optional(),
|
|
71419
|
+
body: string().max(2e3).optional()
|
|
71420
|
+
})
|
|
71421
|
+
});
|
|
71422
|
+
var NcTemplatePreviewSchema = object({
|
|
71423
|
+
title: string().nullable(),
|
|
71424
|
+
body: string().nullable(),
|
|
71425
|
+
/** Names the template uses that this context never fills — they render empty. */
|
|
71426
|
+
unknown: array(string())
|
|
71427
|
+
});
|
|
70923
71428
|
var NcConditionDescriptorSchema = object({
|
|
70924
71429
|
/** Field id inside `NcConditions` (or `'schedule'` for the rule-level group). */
|
|
70925
71430
|
id: string(),
|
|
@@ -70940,6 +71445,7 @@ var NcConditionDescriptorSchema = object({
|
|
|
70940
71445
|
/** Editor widget the UI renders — never hardcode per-condition forms. */
|
|
70941
71446
|
valueType: _enum([
|
|
70942
71447
|
"deviceIdList",
|
|
71448
|
+
"deviceIdExcludeList",
|
|
70943
71449
|
"stringList",
|
|
70944
71450
|
"number01",
|
|
70945
71451
|
"number",
|
|
@@ -70950,6 +71456,7 @@ var NcConditionDescriptorSchema = object({
|
|
|
70950
71456
|
"plateMatcher",
|
|
70951
71457
|
"packagePhase",
|
|
70952
71458
|
"crossingSelect",
|
|
71459
|
+
"sensorTransition",
|
|
70953
71460
|
"polygonDraw",
|
|
70954
71461
|
"occupancy",
|
|
70955
71462
|
"audio",
|
|
@@ -71100,6 +71607,28 @@ var NcHistoryFilterSchema = object({
|
|
|
71100
71607
|
limit: number().int().min(1).max(500).default(100)
|
|
71101
71608
|
});
|
|
71102
71609
|
/**
|
|
71610
|
+
* Every rule's last run, in one answer.
|
|
71611
|
+
*
|
|
71612
|
+
* A rule ABSENT from `runs` produced no delivery inside `horizonMs` — how far
|
|
71613
|
+
* back this answer looked, in ms before the hub's now. It is the outbox prune
|
|
71614
|
+
* window (7 days by default, operator-configurable), NARROWED when the read hit
|
|
71615
|
+
* its row bound: the answer never claims a window it did not read. `null` means
|
|
71616
|
+
* the operator keeps the outbox forever AND the read reached its end, so absent
|
|
71617
|
+
* means "never on record". A UI derives its "not in the last N days" from this
|
|
71618
|
+
* value, never from a constant — and only when the call ANSWERED (D315).
|
|
71619
|
+
*/
|
|
71620
|
+
var NcRuleLastRunsSchema = object({
|
|
71621
|
+
runs: array(object({
|
|
71622
|
+
ruleId: string(),
|
|
71623
|
+
/** Fire time of the newest row (`createdAt`, epoch ms). */
|
|
71624
|
+
lastAt: number(),
|
|
71625
|
+
status: NcHistoryStatusSchema,
|
|
71626
|
+
/** The camera/device of that row (a digest row files under its first camera). */
|
|
71627
|
+
deviceId: number()
|
|
71628
|
+
})),
|
|
71629
|
+
horizonMs: number().nonnegative().nullable()
|
|
71630
|
+
});
|
|
71631
|
+
/**
|
|
71103
71632
|
* What a snooze covers. Broader scopes win when several overlap, so one window
|
|
71104
71633
|
* leaves ONE digest rather than a rule snooze and a whole-feed snooze both
|
|
71105
71634
|
* summarising the same silence.
|
|
@@ -71185,8 +71714,17 @@ object({
|
|
|
71185
71714
|
lastAt: number()
|
|
71186
71715
|
});
|
|
71187
71716
|
/**
|
|
71188
|
-
* The
|
|
71189
|
-
*
|
|
71717
|
+
* The per-mode lists, at most one per mode. Two lists for one mode have no
|
|
71718
|
+
* meaning a reader could agree on (union? the last one?), so they are refused
|
|
71719
|
+
* at the boundary rather than interpreted.
|
|
71720
|
+
*/
|
|
71721
|
+
var NcAlarmNonBlockingListSchema = array(object({
|
|
71722
|
+
mode: AlarmArmModeSchema,
|
|
71723
|
+
deviceIds: array(number().int()).max(200)
|
|
71724
|
+
})).max(8).refine((lists) => new Set(lists.map((l) => l.mode)).size === lists.length, { message: "nonBlocking: at most one list per arm mode" });
|
|
71725
|
+
/**
|
|
71726
|
+
* The three durations the panel's state machine runs on, who hears about its
|
|
71727
|
+
* transitions, and which openings each mode tolerates.
|
|
71190
71728
|
*
|
|
71191
71729
|
* They live on the NOTIFICATION-RULES cap, not on `alarm-panel`, on purpose:
|
|
71192
71730
|
* `alarm-panel` is `deviceNative` and its other provider mirrors somebody
|
|
@@ -71205,21 +71743,39 @@ var NcAlarmSettingsSchema = object({
|
|
|
71205
71743
|
* existed, and therefore what an untouched install keeps doing.
|
|
71206
71744
|
*/
|
|
71207
71745
|
triggeredDurationSec: number().int().min(0).max(3600),
|
|
71208
|
-
/** Send a notification when a mode takes effect. */
|
|
71209
|
-
announceArm: boolean(),
|
|
71210
71746
|
/**
|
|
71211
|
-
*
|
|
71747
|
+
* Deprecated by D630 — every transition is announced. Parsed, never read;
|
|
71748
|
+
* remove after one release. Optional so an older admin's patch (which still
|
|
71749
|
+
* sends it) parses rather than failing the whole save.
|
|
71750
|
+
*/
|
|
71751
|
+
announceArm: boolean().optional(),
|
|
71752
|
+
/**
|
|
71753
|
+
* Target ids from `notification-output`: every alarm transition goes here.
|
|
71212
71754
|
*
|
|
71213
|
-
* Explicit rather than "everyone": an
|
|
71755
|
+
* Explicit rather than "everyone": an alarm announcement is a household
|
|
71214
71756
|
* message, and broadcasting it to every configured endpoint (including a
|
|
71215
71757
|
* webhook wired to something else) is not a default anybody would choose.
|
|
71216
|
-
* Empty
|
|
71217
|
-
*
|
|
71758
|
+
* Empty sends nothing, and the server logs that — silence must be
|
|
71759
|
+
* attributable.
|
|
71760
|
+
*/
|
|
71761
|
+
announceTargets: array(string().min(1)).max(16),
|
|
71762
|
+
/**
|
|
71763
|
+
* Per arm mode, the covered devices whose being OPEN does not refuse the
|
|
71764
|
+
* arm (a window left ajar for the cat under `home`, say). At most one entry
|
|
71765
|
+
* per mode is meaningful. Defaulted to `[]` so a blob stored before D630
|
|
71766
|
+
* parses as "every opening blocks" — the behaviour it was written under.
|
|
71218
71767
|
*/
|
|
71219
|
-
|
|
71768
|
+
nonBlocking: NcAlarmNonBlockingListSchema.default([])
|
|
71220
71769
|
});
|
|
71221
|
-
/**
|
|
71222
|
-
|
|
71770
|
+
/**
|
|
71771
|
+
* Every field optional — a tab edits one control at a time.
|
|
71772
|
+
*
|
|
71773
|
+
* `nonBlocking` is re-declared WITHOUT its default: `.partial()` does not
|
|
71774
|
+
* remove an inner `.default()`, so a patch that never named the field would
|
|
71775
|
+
* parse to `nonBlocking: []` and wipe every mode's list on an unrelated edit
|
|
71776
|
+
* (the same trap {@link NcRulePatchSchema} documents).
|
|
71777
|
+
*/
|
|
71778
|
+
var NcAlarmSettingsPatchSchema = NcAlarmSettingsSchema.extend({ nonBlocking: NcAlarmNonBlockingListSchema }).partial();
|
|
71223
71779
|
/**
|
|
71224
71780
|
* What one arm mode actually arms, DERIVED from the enabled rules gated on it.
|
|
71225
71781
|
* Never authored, never stored — see `alarm-mode-coverage.ts` for why a stored
|
|
@@ -71253,6 +71809,14 @@ var NcAlarmModeCoverageSchema = object({
|
|
|
71253
71809
|
/** Ids named by the covering rules. A SUBSET when `allDevices` is true. */
|
|
71254
71810
|
deviceIds: array(number().int()),
|
|
71255
71811
|
/**
|
|
71812
|
+
* D641: the ids NOT covered although `allDevices` is true — every unscoped
|
|
71813
|
+
* covering rule EXCLUDES them (`conditions.devicesExclude`) and no named rule
|
|
71814
|
+
* covers them. Always `[]` when `allDevices` is false. Excluded by the rules,
|
|
71815
|
+
* which is not the same as skipped by a switch: never in `skippedDevices`.
|
|
71816
|
+
* Defaulted so an answer computed before this field parses as "none".
|
|
71817
|
+
*/
|
|
71818
|
+
allDevicesExcept: array(number().int()).default([]),
|
|
71819
|
+
/**
|
|
71256
71820
|
* Devices this mode NAMES but cannot actually arm, each with the switch that
|
|
71257
71821
|
* excludes it.
|
|
71258
71822
|
*
|
|
@@ -71264,6 +71828,36 @@ var NcAlarmModeCoverageSchema = object({
|
|
|
71264
71828
|
*/
|
|
71265
71829
|
skippedDevices: array(NcAlarmSkippedDeviceSchema).default([])
|
|
71266
71830
|
});
|
|
71831
|
+
/** One covered device that reads open, and whether that refuses the arm. */
|
|
71832
|
+
var NcAlarmOpeningSchema = object({
|
|
71833
|
+
deviceId: number().int(),
|
|
71834
|
+
name: string(),
|
|
71835
|
+
/** The device's state word as read (`open`, `unlocked`, …). */
|
|
71836
|
+
state: string(),
|
|
71837
|
+
/** False when the mode's non-blocking list names this device. */
|
|
71838
|
+
blocking: boolean()
|
|
71839
|
+
});
|
|
71840
|
+
/** A device the panel is currently ignoring, and which side of it it is on. */
|
|
71841
|
+
var NcAlarmExclusionViewSchema = object({
|
|
71842
|
+
deviceId: number().int(),
|
|
71843
|
+
name: string(),
|
|
71844
|
+
phase: _enum(["open", "closed"])
|
|
71845
|
+
});
|
|
71846
|
+
/** One mode's openings: devices read open, plus those whose state is unknown. */
|
|
71847
|
+
var NcAlarmModeOpeningsSchema = object({
|
|
71848
|
+
mode: AlarmArmModeSchema,
|
|
71849
|
+
devices: array(NcAlarmOpeningSchema),
|
|
71850
|
+
/** Covered devices whose state could not be read (D49): neither open nor closed. */
|
|
71851
|
+
unknown: array(number().int())
|
|
71852
|
+
});
|
|
71853
|
+
var NcAlarmLiveSchema = object({
|
|
71854
|
+
state: AlarmStateSchema,
|
|
71855
|
+
/** The mode being armed into (exit delay) or held; null when disarmed. */
|
|
71856
|
+
targetMode: AlarmArmModeSchema.nullable(),
|
|
71857
|
+
availableModes: array(AlarmArmModeSchema),
|
|
71858
|
+
openings: array(NcAlarmModeOpeningsSchema),
|
|
71859
|
+
exclusions: array(NcAlarmExclusionViewSchema)
|
|
71860
|
+
});
|
|
71267
71861
|
var NcAlarmConfigSchema = object({
|
|
71268
71862
|
/**
|
|
71269
71863
|
* The panel's device id, or null when this install has no panel (the ensure
|
|
@@ -71273,7 +71867,13 @@ var NcAlarmConfigSchema = object({
|
|
|
71273
71867
|
*/
|
|
71274
71868
|
deviceId: number().int().nullable(),
|
|
71275
71869
|
settings: NcAlarmSettingsSchema,
|
|
71276
|
-
coverage: array(NcAlarmModeCoverageSchema)
|
|
71870
|
+
coverage: array(NcAlarmModeCoverageSchema),
|
|
71871
|
+
/**
|
|
71872
|
+
* The panel as it is NOW — state, what each mode would find open, what is
|
|
71873
|
+
* excluded. Optional: a panel-less hub answers without it, and a client
|
|
71874
|
+
* must not read its absence as "nothing is open".
|
|
71875
|
+
*/
|
|
71876
|
+
live: NcAlarmLiveSchema.optional()
|
|
71277
71877
|
});
|
|
71278
71878
|
/**
|
|
71279
71879
|
* ONE rule's demand on ONE camera's clip ring.
|
|
@@ -71321,10 +71921,7 @@ method(object({}), object({ rules: array(NcRuleSchema) }), { auth: "admin" }), m
|
|
|
71321
71921
|
kind: "mutation",
|
|
71322
71922
|
auth: "admin",
|
|
71323
71923
|
caller: "required"
|
|
71324
|
-
}), method(object({
|
|
71325
|
-
ruleId: string(),
|
|
71326
|
-
patch: NcRulePatchSchema
|
|
71327
|
-
}), object({ rule: NcRuleSchema }), {
|
|
71924
|
+
}), method(NcRuleUpdateInputSchema, object({ rule: NcRuleSchema }), {
|
|
71328
71925
|
kind: "mutation",
|
|
71329
71926
|
auth: "admin",
|
|
71330
71927
|
caller: "required"
|
|
@@ -71352,7 +71949,10 @@ method(object({}), object({ rules: array(NcRuleSchema) }), { auth: "admin" }), m
|
|
|
71352
71949
|
}), method(object({}), object({
|
|
71353
71950
|
catalog: array(NcConditionDescriptorSchema),
|
|
71354
71951
|
taxonomy: NcTaxonomySchema.optional()
|
|
71355
|
-
})), method(object({
|
|
71952
|
+
})), method(object({}), object({ vars: array(NcTemplateVarDescriptorSchema) })), method(NcTemplatePreviewInputSchema, NcTemplatePreviewSchema, {
|
|
71953
|
+
kind: "mutation",
|
|
71954
|
+
access: "view"
|
|
71955
|
+
}), method(object({ filter: NcHistoryFilterSchema.default({ limit: 100 }) }), object({ entries: array(NcHistoryEntrySchema) }), { auth: "admin" }), method(object({}), NcRuleLastRunsSchema, { auth: "admin" }), method(object({ artifactId: string().min(1) }), object({ url: string().nullable() }), { auth: "admin" }), method(object({}), object({ snoozes: array(NcSnoozeSchema) }), { caller: "required" }), method(object({ snooze: NcSnoozeInputSchema }), object({ snooze: NcSnoozeSchema }), {
|
|
71356
71956
|
kind: "mutation",
|
|
71357
71957
|
caller: "required"
|
|
71358
71958
|
}), method(object({ snoozeId: string() }), object({ success: literal(true) }), {
|
|
@@ -75825,6 +76425,7 @@ var CAP_TO_KIND = {
|
|
|
75825
76425
|
"enum-sensor": "enum-sensor",
|
|
75826
76426
|
"event-emitter": "device-event",
|
|
75827
76427
|
"lock-control": "lock",
|
|
76428
|
+
cover: "cover",
|
|
75828
76429
|
switch: "switch",
|
|
75829
76430
|
button: "button",
|
|
75830
76431
|
doorbell: "doorbell"
|
|
@@ -75837,7 +76438,11 @@ function buildDescriptor(capName, kind) {
|
|
|
75837
76438
|
icon: legacyIcon(t.iconId)
|
|
75838
76439
|
};
|
|
75839
76440
|
}
|
|
75840
|
-
|
|
76441
|
+
/**
|
|
76442
|
+
* Sensor / control cap name → static event-kind descriptor. A linked device
|
|
76443
|
+
* contributes one entry per bound cap present in this map.
|
|
76444
|
+
*/
|
|
76445
|
+
var EVENT_KIND_BY_CAP = Object.freeze(Object.fromEntries(Object.entries(CAP_TO_KIND).map(([capName, kind]) => [capName, buildDescriptor(capName, kind)])));
|
|
75841
76446
|
/**
|
|
75842
76447
|
* server-management — per-NODE singleton capability for a node's ROOT
|
|
75843
76448
|
* package lifecycle (runtime-updatable node packages).
|
|
@@ -76385,41 +76990,65 @@ var ProviderListEntrySchema = discriminatedUnion("shouldSaveDiskSpace", [object(
|
|
|
76385
76990
|
method(object({
|
|
76386
76991
|
location: StorageLocationRefSchema,
|
|
76387
76992
|
relativePath: string()
|
|
76388
|
-
}), string()), method(object({
|
|
76993
|
+
}), string(), { auth: "admin" }), method(object({
|
|
76389
76994
|
location: StorageLocationRefSchema,
|
|
76390
76995
|
relativePath: string(),
|
|
76391
76996
|
data: _instanceof(Uint8Array)
|
|
76392
|
-
}), _void(), {
|
|
76997
|
+
}), _void(), {
|
|
76998
|
+
kind: "mutation",
|
|
76999
|
+
auth: "admin"
|
|
77000
|
+
}), method(object({
|
|
76393
77001
|
location: StorageLocationRefSchema,
|
|
76394
77002
|
relativePath: string()
|
|
76395
|
-
}), _instanceof(Uint8Array)), method(object({
|
|
77003
|
+
}), _instanceof(Uint8Array), { auth: "admin" }), method(object({
|
|
76396
77004
|
location: StorageLocationRefSchema,
|
|
76397
77005
|
relativePath: string()
|
|
76398
|
-
}), boolean()), method(object({
|
|
77006
|
+
}), boolean(), { auth: "admin" }), method(object({
|
|
76399
77007
|
location: StorageLocationRefSchema,
|
|
76400
77008
|
prefix: string().optional()
|
|
76401
|
-
}), array(string()).readonly()), method(object({
|
|
77009
|
+
}), array(string()).readonly(), { auth: "admin" }), method(object({
|
|
76402
77010
|
location: StorageLocationRefSchema,
|
|
76403
77011
|
relativePath: string()
|
|
76404
|
-
}), _void(), {
|
|
77012
|
+
}), _void(), {
|
|
77013
|
+
kind: "mutation",
|
|
77014
|
+
auth: "admin"
|
|
77015
|
+
}), method(object({ location: StorageLocationRefSchema }), number().nullable()), method(object({
|
|
76405
77016
|
location: StorageLocationRefSchema,
|
|
76406
77017
|
relativePath: string(),
|
|
76407
77018
|
sizeBytes: number().optional()
|
|
76408
|
-
}), object({ uploadId: string() }), {
|
|
77019
|
+
}), object({ uploadId: string() }), {
|
|
77020
|
+
kind: "mutation",
|
|
77021
|
+
auth: "admin"
|
|
77022
|
+
}), method(object({
|
|
76409
77023
|
uploadId: string(),
|
|
76410
77024
|
offset: number(),
|
|
76411
77025
|
data: _instanceof(Uint8Array)
|
|
76412
|
-
}), _void(), {
|
|
77026
|
+
}), _void(), {
|
|
77027
|
+
kind: "mutation",
|
|
77028
|
+
auth: "admin"
|
|
77029
|
+
}), method(object({ uploadId: string() }), _void(), {
|
|
77030
|
+
kind: "mutation",
|
|
77031
|
+
auth: "admin"
|
|
77032
|
+
}), method(object({ uploadId: string() }), _void(), {
|
|
77033
|
+
kind: "mutation",
|
|
77034
|
+
auth: "admin"
|
|
77035
|
+
}), method(object({
|
|
76413
77036
|
location: StorageLocationRefSchema,
|
|
76414
77037
|
relativePath: string()
|
|
76415
77038
|
}), object({
|
|
76416
77039
|
downloadId: string(),
|
|
76417
77040
|
sizeBytes: number()
|
|
76418
|
-
}), {
|
|
77041
|
+
}), {
|
|
77042
|
+
kind: "mutation",
|
|
77043
|
+
auth: "admin"
|
|
77044
|
+
}), method(object({
|
|
76419
77045
|
downloadId: string(),
|
|
76420
77046
|
offset: number(),
|
|
76421
77047
|
length: number()
|
|
76422
|
-
}), _instanceof(Uint8Array)
|
|
77048
|
+
}), _instanceof(Uint8Array), { auth: "admin" }), method(object({ downloadId: string() }), _void(), {
|
|
77049
|
+
kind: "mutation",
|
|
77050
|
+
auth: "admin"
|
|
77051
|
+
}), method(object({ type: StorageLocationTypeSchema.optional() }), array(StorageLocationSchema).readonly()), method(_void(), array(StorageLocationDeclarationSchema).readonly()), method(StorageLocationSchema.omit({
|
|
76423
77052
|
createdAt: true,
|
|
76424
77053
|
updatedAt: true
|
|
76425
77054
|
}), StorageLocationSchema, {
|
|
@@ -78024,7 +78653,67 @@ var WebrtcLadderDecisionSchema = object({
|
|
|
78024
78653
|
serverSending: boolean().nullable(),
|
|
78025
78654
|
/** Epoch ms. The overlay prints its age; a decision from 40 minutes ago
|
|
78026
78655
|
* must not read like one from this second. */
|
|
78027
|
-
at: number()
|
|
78656
|
+
at: number(),
|
|
78657
|
+
/** The broker's own cause when it is finer than `cause` can carry, e.g.
|
|
78658
|
+
* `trial-revert` (reported as `stall`/`loss` for older consumers). Optional:
|
|
78659
|
+
* an older server strips it instead of rejecting the answer. */
|
|
78660
|
+
causeDetail: string().optional()
|
|
78661
|
+
});
|
|
78662
|
+
var WebrtcClimbHoldSchema = object({
|
|
78663
|
+
/** `stall-building` — the client receives ≪ what is sent; `delivery` — the
|
|
78664
|
+
* current rung is not landing whole; `no-evidence` — no receiver feedback;
|
|
78665
|
+
* `warming` — healthy, accumulating the streak; `cooldown` — too soon after
|
|
78666
|
+
* a change; `capacity` — a MEASURED probe below the next rung's bar;
|
|
78667
|
+
* `try-unclean` — no capacity evidence, and the path is not clean enough to
|
|
78668
|
+
* try; `try-backoff` — a try reverted, waiting out its backoff; `trial` —
|
|
78669
|
+
* this rung IS a try, reverting if it does not land; `suspended` —
|
|
78670
|
+
* recorded playback on the track. */
|
|
78671
|
+
code: string(),
|
|
78672
|
+
/** The rung a climb would go to. */
|
|
78673
|
+
nextTier: CamProfileSchema,
|
|
78674
|
+
/** Bytes received ÷ bytes sent over the delivery window (≥ one GOP). */
|
|
78675
|
+
deliveryRatio: number().nullable(),
|
|
78676
|
+
deliveryWindowMs: number().nullable(),
|
|
78677
|
+
/** The ratio a climb requires. */
|
|
78678
|
+
climbDeliveryRatio: number(),
|
|
78679
|
+
/** The receiver's bandwidth estimate (REMB), kbps. Capped by what is sent. */
|
|
78680
|
+
capacityKbps: number().nullable(),
|
|
78681
|
+
/** The viewer's downlink probe for this session, kbps. */
|
|
78682
|
+
probeKbps: number().nullable(),
|
|
78683
|
+
/** The next rung's measured bitrate, kbps. */
|
|
78684
|
+
nextTierKbps: number().nullable(),
|
|
78685
|
+
capacityMargin: number(),
|
|
78686
|
+
/** The capacity a climb to `nextTier` needs, kbps (bitrate × margin, or the
|
|
78687
|
+
* client ladder threshold when the rung has measured nothing). */
|
|
78688
|
+
neededKbps: number().nullable(),
|
|
78689
|
+
/** For `try-unclean`: which clean-path condition failed (a `WebrtcTryBlocker`,
|
|
78690
|
+
* carried as a string for the same reason as `code`). */
|
|
78691
|
+
tryBlockedBy: string().nullable(),
|
|
78692
|
+
/** The delivery ratio and the loss ratio a try requires / must stay under.
|
|
78693
|
+
* Optional, like every field added to this schema after it first shipped:
|
|
78694
|
+
* the router validates output, so a REQUIRED new field makes a newer server
|
|
78695
|
+
* reject an older broker's whole answer (review m1). */
|
|
78696
|
+
tryDeliveryRatio: number().optional(),
|
|
78697
|
+
tryMaxLossRatio: number().optional(),
|
|
78698
|
+
/** For `try-backoff`: time until the next try, ms. */
|
|
78699
|
+
retryInMs: number().nullable(),
|
|
78700
|
+
/** The last try that reverted in this viewer session. */
|
|
78701
|
+
lastRevert: object({
|
|
78702
|
+
tier: CamProfileSchema,
|
|
78703
|
+
deliveryRatio: number().nullable(),
|
|
78704
|
+
stalled: boolean(),
|
|
78705
|
+
at: number()
|
|
78706
|
+
}).nullable(),
|
|
78707
|
+
/** For `trial`: how long this rung has been on trial, and for how long it must hold. */
|
|
78708
|
+
trialForMs: number().nullable(),
|
|
78709
|
+
trialMs: number().nullable(),
|
|
78710
|
+
healthyForMs: number().nullable(),
|
|
78711
|
+
requiredHealthyMs: number().nullable(),
|
|
78712
|
+
/** Epoch ms this hold's code began. */
|
|
78713
|
+
since: number(),
|
|
78714
|
+
/** The same one-line account the broker logs, e.g.
|
|
78715
|
+
* `held: delivery 0.97 over 4.0s ok, REMB 480 kbps < mid 1800 kbps × 1.2`. */
|
|
78716
|
+
summary: string()
|
|
78028
78717
|
});
|
|
78029
78718
|
/** Per-tier count of MEASURED downgrades this viewer session has suffered. */
|
|
78030
78719
|
var WebrtcTierFailureSchema = object({
|
|
@@ -78088,7 +78777,12 @@ var WebrtcSessionDebugSchema = object({
|
|
|
78088
78777
|
tierFailures: array(WebrtcTierFailureSchema),
|
|
78089
78778
|
/** Successful promotions in this viewer session (drives the progressive
|
|
78090
78779
|
* healthy timer). Null when no ladder is running. */
|
|
78091
|
-
upgradeCount: number().nullable()
|
|
78780
|
+
upgradeCount: number().nullable(),
|
|
78781
|
+
/** Why the ladder is not climbing right now. Null when no ladder runs, it
|
|
78782
|
+
* is at the top rung, or it just moved. Optional because this output is
|
|
78783
|
+
* validated by the router: a server carrying this schema must still accept
|
|
78784
|
+
* the answer of a stream-broker deployed before the field existed. */
|
|
78785
|
+
climbHold: WebrtcClimbHoldSchema.nullable().optional()
|
|
78092
78786
|
});
|
|
78093
78787
|
DeviceType.Camera, method(object({ deviceId: number().int().nonnegative() }), array(WebrtcStreamChoiceSchema)), method(object({
|
|
78094
78788
|
deviceId: number().int().nonnegative(),
|
|
@@ -79908,12 +80602,20 @@ reset: method(object({
|
|
|
79908
80602
|
},
|
|
79909
80603
|
runtimeState: ConsumablesStatusSchema.extend({ lastFetchedAt: number() }),
|
|
79910
80604
|
/**
|
|
79911
|
-
* Runtime-state durability: **
|
|
80605
|
+
* Runtime-state durability: **restored** — nothing else holds the value
|
|
80606
|
+
* (D642). A consumable can often be read only in a device's wake window (the
|
|
80607
|
+
* Dreame mower's CMS counters answer only while it charges), so a restart
|
|
80608
|
+
* that wiped the slice left the panel empty and the alerts blind until the
|
|
80609
|
+
* next wake — 2026-09-26, mower 503 idle with `items: []`. A restored item is
|
|
80610
|
+
* the previous observation, not a claim about now; the provider overwrites it
|
|
80611
|
+
* the moment it reads the device again.
|
|
79912
80612
|
*
|
|
79913
80613
|
* See `RuntimeStateDurability`. Enforced by
|
|
79914
80614
|
* `scripts/check-runtime-state-durability.ts`.
|
|
79915
80615
|
*/
|
|
79916
|
-
durability: "
|
|
80616
|
+
durability: "restored",
|
|
80617
|
+
/** Clocks, not content: a re-read that changed no item costs no commit. */
|
|
80618
|
+
volatileStateFields: ["lastChangedAt", "lastFetchedAt"]
|
|
79917
80619
|
};
|
|
79918
80620
|
/**
|
|
79919
80621
|
* Door / window / opening / garage / valve contact sensor. Boolean
|
|
@@ -83457,6 +84159,11 @@ var ClientNetworkStatsSchema = object({
|
|
|
83457
84159
|
jitterMs: number(),
|
|
83458
84160
|
estimatedBandwidthKbps: number(),
|
|
83459
84161
|
packetLossPercent: number().min(0).max(100),
|
|
84162
|
+
/** The viewer's latest `/adaptive-probe` reading, Mbps — whatever it read
|
|
84163
|
+
* (D635 I4). Absent when it never probed or the probe failed (unknown). */
|
|
84164
|
+
downlinkMbps: number().optional(),
|
|
84165
|
+
/** Age of that reading when this report was sent, ms. */
|
|
84166
|
+
downlinkAgeMs: number().optional(),
|
|
83460
84167
|
lastUpdated: number()
|
|
83461
84168
|
});
|
|
83462
84169
|
var DeviceNetworkStatsSchema = object({
|
|
@@ -83469,7 +84176,11 @@ method(object({ deviceId: number() }), DeviceNetworkStatsSchema.nullable()), met
|
|
|
83469
84176
|
rttMs: number().min(0).max(6e4),
|
|
83470
84177
|
jitterMs: number().min(0).max(1e4),
|
|
83471
84178
|
estimatedBandwidthKbps: number().min(0).max(1e6),
|
|
83472
|
-
packetLossPercent: number().min(0).max(100)
|
|
84179
|
+
packetLossPercent: number().min(0).max(100),
|
|
84180
|
+
/** Latest downlink probe, Mbps, whatever it read; absent = never / failed. */
|
|
84181
|
+
downlinkMbps: number().min(0).max(1e5).optional(),
|
|
84182
|
+
/** Age of that probe when this report is sent, ms. */
|
|
84183
|
+
downlinkAgeMs: number().min(0).max(864e5).optional()
|
|
83473
84184
|
}), _void(), { kind: "mutation" });
|
|
83474
84185
|
/**
|
|
83475
84186
|
* nodes — system-scoped singleton capability for cluster topology
|
|
@@ -83588,7 +84299,30 @@ var RenameNodeResultSchema = object({
|
|
|
83588
84299
|
nodeId: string(),
|
|
83589
84300
|
name: string()
|
|
83590
84301
|
});
|
|
83591
|
-
|
|
84302
|
+
/**
|
|
84303
|
+
* The cheap node list. A consumer's rule for a STORED node id (a rule's
|
|
84304
|
+
* `nodeIds`) that is missing from `nodes`, whatever `complete` says: render it
|
|
84305
|
+
* as a removable chip labelled unknown, never drop it (D49). Even a complete
|
|
84306
|
+
* list omits a node that joined and left between two history captures, so a
|
|
84307
|
+
* missing id means "not known to the hub", never "removed". Only the operator
|
|
84308
|
+
* removes it.
|
|
84309
|
+
*
|
|
84310
|
+
* `complete` changes only the WORDING: `false` = no history read has ever
|
|
84311
|
+
* answered, so the list may be missing offline nodes the hub does know (D315).
|
|
84312
|
+
*
|
|
84313
|
+
* `name` is the history row's name (≤ 30 s behind a rename), or the id when
|
|
84314
|
+
* the history has none.
|
|
84315
|
+
*/
|
|
84316
|
+
var NodeDirectorySchema = object({
|
|
84317
|
+
nodes: array(object({
|
|
84318
|
+
id: string(),
|
|
84319
|
+
name: string(),
|
|
84320
|
+
isHub: boolean(),
|
|
84321
|
+
isOnline: boolean()
|
|
84322
|
+
})).readonly(),
|
|
84323
|
+
complete: boolean()
|
|
84324
|
+
});
|
|
84325
|
+
method(_void(), array(TopologyNodeSchema).readonly(), { auth: "admin" }), method(_void(), NodeDirectorySchema, { auth: "admin" }), method(NodeAddonInputSchema, SuccessSchema, {
|
|
83592
84326
|
kind: "mutation",
|
|
83593
84327
|
auth: "admin"
|
|
83594
84328
|
}), method(NodeAddonInputSchema, SuccessSchema, {
|
|
@@ -89088,6 +89822,42 @@ var zoneRulesCapability = {
|
|
|
89088
89822
|
durability: "restored"
|
|
89089
89823
|
};
|
|
89090
89824
|
/**
|
|
89825
|
+
* LA tabella "quale booleano di questo tipo di device conta come ALTO", e il
|
|
89826
|
+
* valutatore puro del suo FRONTE.
|
|
89827
|
+
*
|
|
89828
|
+
* Viveva dentro il builtin virtual-doorbell
|
|
89829
|
+
* (`@camstack/system` — `builtins/doorbell/trigger-engine.ts`) e i suoi
|
|
89830
|
+
* predicati erano privati al modulo. Il recorder ne ha bisogno per il trigger
|
|
89831
|
+
* `RecordingTriggers.sensorDeviceIds`: copiarla avrebbe creato la SECONDA
|
|
89832
|
+
* tabella, che diverge alla prima cap aggiunta e il cui sintomo — "il sensore
|
|
89833
|
+
* fa suonare il campanello ma non registra" — è esattamente D62. Quindi si
|
|
89834
|
+
* SPOSTA qui e il doorbell la ri-esporta.
|
|
89835
|
+
*
|
|
89836
|
+
* ⚠ NON è `DEVICE_STATE_READERS` (`catalogs/device-state-vocabulary.ts`), e le
|
|
89837
|
+
* due non vanno unificate: quella risponde a "qual è la PAROLA di stato per una
|
|
89838
|
+
* regola" (e include `presence`, `cover`, `alarm-panel`), questa a "qual è il
|
|
89839
|
+
* booleano il cui FRONTE conta". Vocabolari deliberatamente diversi.
|
|
89840
|
+
*/
|
|
89841
|
+
/**
|
|
89842
|
+
* Known binary / switch source caps → the boolean slice field whose
|
|
89843
|
+
* false→true rise counts as ACTIVE. Every entry is "fire on active".
|
|
89844
|
+
* Sensors whose "active" reading is not a plain boolean (presence's string
|
|
89845
|
+
* state, connectivity's connected flag) are deliberately excluded — a
|
|
89846
|
+
* reconnect is not a doorbell press, and it is not a recording either.
|
|
89847
|
+
*/
|
|
89848
|
+
var SOURCE_CAP_ACTIVE_FIELD = {
|
|
89849
|
+
contact: "entryOpen",
|
|
89850
|
+
binary: "on",
|
|
89851
|
+
switch: "on",
|
|
89852
|
+
motion: "detected",
|
|
89853
|
+
flood: "flooded",
|
|
89854
|
+
gas: "detected",
|
|
89855
|
+
smoke: "detected",
|
|
89856
|
+
"carbon-monoxide": "detected",
|
|
89857
|
+
vibration: "detected",
|
|
89858
|
+
tamper: "tampered"
|
|
89859
|
+
};
|
|
89860
|
+
/**
|
|
89091
89861
|
* Accessory device helpers — shared across drivers.
|
|
89092
89862
|
*
|
|
89093
89863
|
* Many vendor-specific drivers register accessory child devices on
|
|
@@ -93615,6 +94385,12 @@ Object.freeze({
|
|
|
93615
94385
|
addonId: null,
|
|
93616
94386
|
access: "view"
|
|
93617
94387
|
},
|
|
94388
|
+
"deviceManager.listDeviceIdsByCaps": {
|
|
94389
|
+
capName: "device-manager",
|
|
94390
|
+
capScope: "system",
|
|
94391
|
+
addonId: null,
|
|
94392
|
+
access: "view"
|
|
94393
|
+
},
|
|
93618
94394
|
"deviceManager.listLocationDescriptions": {
|
|
93619
94395
|
capName: "device-manager",
|
|
93620
94396
|
capScope: "system",
|
|
@@ -94575,12 +95351,30 @@ Object.freeze({
|
|
|
94575
95351
|
addonId: null,
|
|
94576
95352
|
access: "create"
|
|
94577
95353
|
},
|
|
95354
|
+
"llmRuntime.appleAvailability": {
|
|
95355
|
+
capName: "llm-runtime",
|
|
95356
|
+
capScope: "system",
|
|
95357
|
+
addonId: null,
|
|
95358
|
+
access: "view"
|
|
95359
|
+
},
|
|
95360
|
+
"llmRuntime.cancel": {
|
|
95361
|
+
capName: "llm-runtime",
|
|
95362
|
+
capScope: "system",
|
|
95363
|
+
addonId: null,
|
|
95364
|
+
access: "create"
|
|
95365
|
+
},
|
|
94578
95366
|
"llmRuntime.complete": {
|
|
94579
95367
|
capName: "llm-runtime",
|
|
94580
95368
|
capScope: "system",
|
|
94581
95369
|
addonId: null,
|
|
94582
95370
|
access: "create"
|
|
94583
95371
|
},
|
|
95372
|
+
"llmRuntime.completeApple": {
|
|
95373
|
+
capName: "llm-runtime",
|
|
95374
|
+
capScope: "system",
|
|
95375
|
+
addonId: null,
|
|
95376
|
+
access: "create"
|
|
95377
|
+
},
|
|
94584
95378
|
"llmRuntime.deleteModel": {
|
|
94585
95379
|
capName: "llm-runtime",
|
|
94586
95380
|
capScope: "system",
|
|
@@ -95253,6 +96047,12 @@ Object.freeze({
|
|
|
95253
96047
|
addonId: null,
|
|
95254
96048
|
access: "create"
|
|
95255
96049
|
},
|
|
96050
|
+
"nodes.directory": {
|
|
96051
|
+
capName: "nodes",
|
|
96052
|
+
capScope: "system",
|
|
96053
|
+
addonId: null,
|
|
96054
|
+
access: "view"
|
|
96055
|
+
},
|
|
95256
96056
|
"nodes.executeQuery": {
|
|
95257
96057
|
capName: "nodes",
|
|
95258
96058
|
capScope: "system",
|
|
@@ -95421,6 +96221,18 @@ Object.freeze({
|
|
|
95421
96221
|
addonId: null,
|
|
95422
96222
|
access: "view"
|
|
95423
96223
|
},
|
|
96224
|
+
"notificationRules.getRuleLastRuns": {
|
|
96225
|
+
capName: "notification-rules",
|
|
96226
|
+
capScope: "system",
|
|
96227
|
+
addonId: null,
|
|
96228
|
+
access: "view"
|
|
96229
|
+
},
|
|
96230
|
+
"notificationRules.getTemplateCatalog": {
|
|
96231
|
+
capName: "notification-rules",
|
|
96232
|
+
capScope: "system",
|
|
96233
|
+
addonId: null,
|
|
96234
|
+
access: "view"
|
|
96235
|
+
},
|
|
95424
96236
|
"notificationRules.listDeviceMutes": {
|
|
95425
96237
|
capName: "notification-rules",
|
|
95426
96238
|
capScope: "system",
|
|
@@ -95439,6 +96251,12 @@ Object.freeze({
|
|
|
95439
96251
|
addonId: null,
|
|
95440
96252
|
access: "view"
|
|
95441
96253
|
},
|
|
96254
|
+
"notificationRules.previewTemplate": {
|
|
96255
|
+
capName: "notification-rules",
|
|
96256
|
+
capScope: "system",
|
|
96257
|
+
addonId: null,
|
|
96258
|
+
access: "view"
|
|
96259
|
+
},
|
|
95442
96260
|
"notificationRules.resolveArtifactUrl": {
|
|
95443
96261
|
capName: "notification-rules",
|
|
95444
96262
|
capScope: "system",
|
|
@@ -101005,8 +101823,173 @@ var NC_AUDIO_DEFAULTS = {
|
|
|
101005
101823
|
samplingSeconds: 10
|
|
101006
101824
|
};
|
|
101007
101825
|
new Set(["devices", "classes"]);
|
|
101826
|
+
/**
|
|
101827
|
+
* WHICH narrowing filter speaks about WHICH system-event kind — one predicate,
|
|
101828
|
+
* consumed by the rule ENGINE and by every rule EDITOR.
|
|
101829
|
+
*
|
|
101830
|
+
* ── The failure this file exists for ───────────────────────────────────────
|
|
101831
|
+
* `NcSystemEventConditionSchema` carries four optional narrowing lists
|
|
101832
|
+
* (`deviceIds`, `deviceTypes`, `nodeIds`, `packageNames`) beside a required
|
|
101833
|
+
* `kinds` gate. They are not four filters over one vocabulary: each belongs to
|
|
101834
|
+
* a FAMILY of kinds and is meaningless outside it. A node going away has no
|
|
101835
|
+
* device type; a completed export has no package; `stream-offline` deliberately
|
|
101836
|
+
* carries no device type at all (a stream belongs to a camera by construction).
|
|
101837
|
+
*
|
|
101838
|
+
* The engine applied all four to every kind, so a filter from one family failed
|
|
101839
|
+
* a kind from another family CLOSED. The operator's live rule "Eventi di
|
|
101840
|
+
* sistema" selected eleven kinds and set `deviceTypes: ['camera']` — the only
|
|
101841
|
+
* way to say "only my cameras" about the two liveness kinds — and NINE of the
|
|
101842
|
+
* eleven silently stopped being deliverable. Four days of history contained
|
|
101843
|
+
* `device-online` and `device-offline` and nothing else, across a window in
|
|
101844
|
+
* which agents were restarted and addons were updated.
|
|
101845
|
+
*
|
|
101846
|
+
* Both editors already computed this scoping, to decide which filter boxes to
|
|
101847
|
+
* render; the cap's own docblock already promised it ("Narrow a `device-*` kind
|
|
101848
|
+
* to these device TYPES"). Only the engine disagreed, and it disagreed in the
|
|
101849
|
+
* destructive direction. So the rule is stated ONCE, here, and the engine and
|
|
101850
|
+
* the editors are readers of it rather than three independent authors.
|
|
101851
|
+
*
|
|
101852
|
+
* ── The direction ─────────────────────────────────────────────────────────
|
|
101853
|
+
* Out of family ⇒ the filter is IGNORED, never fail-closed. An editor that
|
|
101854
|
+
* hides a box has already told the operator this filter does not speak about
|
|
101855
|
+
* these kinds, so honouring it as an exclusion contradicts the only
|
|
101856
|
+
* explanation the operator was ever given. Inside its family the filter still
|
|
101857
|
+
* narrows, and still fails CLOSED on a subject that cannot produce the identity
|
|
101858
|
+
* it tests (a device the mirror cannot name is not evidence of a camera).
|
|
101859
|
+
*/
|
|
101860
|
+
/**
|
|
101861
|
+
* Every filter, for the callers that must cover all of them (guards, tests).
|
|
101862
|
+
*
|
|
101863
|
+
* `consumableLowPercent` (D642) is not a list but it is a filter in exactly
|
|
101864
|
+
* this sense: it belongs to a family of kinds (the consumable pair) and the
|
|
101865
|
+
* engine consults it only there. It differs in one way the readers must know —
|
|
101866
|
+
* its ABSENCE is not "no narrowing" but the default threshold, because a
|
|
101867
|
+
* consumable episode is always judged at SOME threshold.
|
|
101868
|
+
*/
|
|
101869
|
+
var NC_SYSTEM_EVENT_FILTER_KEYS = [
|
|
101870
|
+
"deviceIds",
|
|
101871
|
+
"deviceTypes",
|
|
101872
|
+
"nodeIds",
|
|
101873
|
+
"packageNames",
|
|
101874
|
+
"consumableLowPercent"
|
|
101875
|
+
];
|
|
101876
|
+
new Map(Object.entries({
|
|
101877
|
+
"device-online": "device-events",
|
|
101878
|
+
"device-offline": "device-events",
|
|
101879
|
+
"device-disabled": "device-events",
|
|
101880
|
+
"device-enabled": "device-events",
|
|
101881
|
+
"device-battery-low": "device-events",
|
|
101882
|
+
"device-battery-normal": "device-events",
|
|
101883
|
+
"device-consumable-low": "device-events",
|
|
101884
|
+
"device-consumable-normal": "device-events",
|
|
101885
|
+
"device-tank-low": "device-events",
|
|
101886
|
+
"device-tank-full": "device-events",
|
|
101887
|
+
"device-tank-normal": "device-events",
|
|
101888
|
+
"stream-online": "device-events",
|
|
101889
|
+
"stream-offline": "device-events",
|
|
101890
|
+
"detection-blind": "device-events",
|
|
101891
|
+
"node-online": "system",
|
|
101892
|
+
"node-offline": "system",
|
|
101893
|
+
"node-inference-unavailable": "system",
|
|
101894
|
+
"addon-crash-loop": "system",
|
|
101895
|
+
"addon-update-available": "system",
|
|
101896
|
+
"addon-updated": "system",
|
|
101897
|
+
"server-update-available": "system",
|
|
101898
|
+
"server-updated": "system",
|
|
101899
|
+
"wrapper-update-available": "system",
|
|
101900
|
+
"backup-completed": "system",
|
|
101901
|
+
"export-completed": "system",
|
|
101902
|
+
"alarm-triggered": "alarm",
|
|
101903
|
+
"alarm-armed": "alarm",
|
|
101904
|
+
"alarm-disarmed": "alarm",
|
|
101905
|
+
"alarm-arming": "alarm",
|
|
101906
|
+
"alarm-arm-refused": "alarm",
|
|
101907
|
+
"alarm-pending": "alarm",
|
|
101908
|
+
"alarm-rearmed": "alarm",
|
|
101909
|
+
"alarm-sensor-bypass": "alarm",
|
|
101910
|
+
"alarm-check-failed": "alarm",
|
|
101911
|
+
"camera-online": null,
|
|
101912
|
+
"camera-offline": null,
|
|
101913
|
+
"camera-disabled": null,
|
|
101914
|
+
"camera-enabled": null
|
|
101915
|
+
}));
|
|
101916
|
+
[...NC_SYSTEM_EVENT_FILTER_KEYS];
|
|
101008
101917
|
NC_AUDIO_DEFAULTS.hitPercent, NC_AUDIO_DEFAULTS.samplingSeconds;
|
|
101009
101918
|
/**
|
|
101919
|
+
* WHICH WAY A BOOLEAN SENSOR MOVED — read off the one slice a sensor event
|
|
101920
|
+
* persists (D640 "a sensor rule fires on the raise, the clear, or both").
|
|
101921
|
+
*
|
|
101922
|
+
* A device-event rule over a flood sensor notified on BOTH edges: water
|
|
101923
|
+
* detected, and back to dry. `NcConditions.sensorTransition` lets the rule
|
|
101924
|
+
* name one edge; this file answers, for one `SensorEvent`, which edge it is.
|
|
101925
|
+
*
|
|
101926
|
+
* NOT A SECOND TABLE. The "which boolean of this cap counts as HIGH" question
|
|
101927
|
+
* already has one answer, `SOURCE_CAP_ACTIVE_FIELD`
|
|
101928
|
+
* (`catalogs/sensor-active-state.ts`, the doorbell's and the recorder's edge
|
|
101929
|
+
* table), and a sensor event names its kind, not its cap. So the table here is
|
|
101930
|
+
* DERIVED: event kind → cap (`EVENT_KIND_BY_CAP`) → that cap's active field.
|
|
101931
|
+
* Only kinds whose taxonomy category is `sensor` take part — a switch has an
|
|
101932
|
+
* `on` boolean too, but "it triggers / it clears" is not a sentence about a
|
|
101933
|
+
* light, and offering it there would be a knob with a misleading label.
|
|
101934
|
+
* `presence` is a WORD (`home` / `not_home` / a zone), not a boolean, and is
|
|
101935
|
+
* absent from the source table, so it is absent here.
|
|
101936
|
+
*
|
|
101937
|
+
* `undefined` = never guessed: a kind with no raise (a doorbell press, a
|
|
101938
|
+
* button, a cover), or a slice whose field is missing or not a boolean. The
|
|
101939
|
+
* engine then does NOT filter — a flood alert must not go silent because one
|
|
101940
|
+
* read came back unreadable (the same stance as the alarm's `sensorEdgeOf`).
|
|
101941
|
+
*
|
|
101942
|
+
* Pure, RN-safe: the viewer imports the support rule from here.
|
|
101943
|
+
*/
|
|
101944
|
+
/** Event kind → the boolean field of its cap slice that means "raised". */
|
|
101945
|
+
var NC_SENSOR_RAISED_FIELD_BY_KIND = Object.freeze(Object.fromEntries(Object.entries(EVENT_KIND_BY_CAP).flatMap(([capName, descriptor]) => {
|
|
101946
|
+
const field = SOURCE_CAP_ACTIVE_FIELD[capName];
|
|
101947
|
+
if (field === void 0 || descriptor.category !== "sensor") return [];
|
|
101948
|
+
return [[descriptor.kind, field]];
|
|
101949
|
+
})));
|
|
101950
|
+
Object.freeze(Object.keys(NC_SENSOR_RAISED_FIELD_BY_KIND));
|
|
101951
|
+
var RULE_BODY_ONLY = ["body"];
|
|
101952
|
+
var RULE_TEXT = ["title", "body"];
|
|
101953
|
+
var BATTERY = ["device-battery-low", "device-battery-normal"];
|
|
101954
|
+
var CONSUMABLE = ["device-consumable-low", "device-consumable-normal"];
|
|
101955
|
+
var TANK = [
|
|
101956
|
+
"device-tank-low",
|
|
101957
|
+
"device-tank-full",
|
|
101958
|
+
"device-tank-normal"
|
|
101959
|
+
];
|
|
101960
|
+
var UPDATES = [
|
|
101961
|
+
"addon-update-available",
|
|
101962
|
+
"server-update-available",
|
|
101963
|
+
"wrapper-update-available"
|
|
101964
|
+
];
|
|
101965
|
+
var PACKAGE_UPDATES = ["addon-update-available", "server-update-available"];
|
|
101966
|
+
var ALARM = [
|
|
101967
|
+
"alarm-triggered",
|
|
101968
|
+
"alarm-armed",
|
|
101969
|
+
"alarm-disarmed",
|
|
101970
|
+
"alarm-arming",
|
|
101971
|
+
"alarm-arm-refused",
|
|
101972
|
+
"alarm-pending",
|
|
101973
|
+
"alarm-rearmed",
|
|
101974
|
+
"alarm-sensor-bypass",
|
|
101975
|
+
"alarm-check-failed"
|
|
101976
|
+
];
|
|
101977
|
+
var COMBINED_ALARM = ["alarm-triggered"];
|
|
101978
|
+
var DEVICE_EVENTS = [
|
|
101979
|
+
"device-online",
|
|
101980
|
+
"device-offline",
|
|
101981
|
+
"device-disabled",
|
|
101982
|
+
"device-enabled",
|
|
101983
|
+
...BATTERY,
|
|
101984
|
+
...CONSUMABLE,
|
|
101985
|
+
...TANK,
|
|
101986
|
+
"stream-online",
|
|
101987
|
+
"stream-offline",
|
|
101988
|
+
"detection-blind",
|
|
101989
|
+
...ALARM
|
|
101990
|
+
];
|
|
101991
|
+
[...DEVICE_EVENTS], [...RULE_TEXT], [...COMBINED_ALARM], [...RULE_TEXT], [...COMBINED_ALARM], [...RULE_TEXT], [...RULE_TEXT], [...COMBINED_ALARM], [...RULE_TEXT], [...PACKAGE_UPDATES], [...RULE_TEXT], [...RULE_TEXT], [...COMBINED_ALARM], [...RULE_TEXT], [...COMBINED_ALARM], [...RULE_TEXT], [...COMBINED_ALARM], [...RULE_TEXT], [...RULE_TEXT], [...RULE_TEXT], [...RULE_TEXT], [...RULE_TEXT], [...RULE_TEXT], [...RULE_TEXT], [...RULE_TEXT], [...RULE_BODY_ONLY], [...RULE_TEXT], [...RULE_TEXT], [...DEVICE_EVENTS], [...RULE_TEXT], [...DEVICE_EVENTS], [...RULE_TEXT], [...BATTERY, ...CONSUMABLE], [...RULE_TEXT], [...CONSUMABLE], [...RULE_TEXT], [...TANK], [...RULE_TEXT], [...RULE_TEXT], [...UPDATES], [...RULE_TEXT], [...UPDATES], [...RULE_TEXT], [...RULE_TEXT], [...RULE_TEXT], [...RULE_TEXT], [...UPDATES], [...RULE_TEXT], [...PACKAGE_UPDATES], [...RULE_TEXT], [...PACKAGE_UPDATES], [...RULE_TEXT], [...UPDATES], [...RULE_TEXT], [...UPDATES], [...RULE_TEXT], [...PACKAGE_UPDATES], [...RULE_TEXT], [...RULE_TEXT], [...RULE_TEXT], [...RULE_TEXT], [...RULE_TEXT], [...RULE_TEXT], [...RULE_TEXT], [...RULE_TEXT], [...RULE_TEXT], [...RULE_TEXT], [...RULE_TEXT], [...RULE_TEXT], [...RULE_TEXT], [...RULE_TEXT], [...RULE_TEXT], [...RULE_TEXT], [...RULE_TEXT], [...RULE_TEXT], [...RULE_TEXT], [...RULE_TEXT], [...RULE_TEXT], [...RULE_TEXT], [...RULE_TEXT], [...RULE_TEXT];
|
|
101992
|
+
/**
|
|
101010
101993
|
* TimelapseRule — the STANDALONE scheduled timelapse producer's rule model.
|
|
101011
101994
|
*
|
|
101012
101995
|
* Spec: `docs/superpowers/specs/2026-07-24-nc-occupancy-timelapse-design.md`
|
|
@@ -103258,10 +104241,13 @@ var DreameChildDevice = class extends BaseDevice {
|
|
|
103258
104241
|
try {
|
|
103259
104242
|
this.recomputeSlice();
|
|
103260
104243
|
} catch (err) {
|
|
103261
|
-
this.ctx.logger.warn("dreame child: stateChanged handler error", {
|
|
103262
|
-
|
|
103263
|
-
|
|
103264
|
-
|
|
104244
|
+
this.ctx.logger.warn("dreame child: stateChanged handler error", {
|
|
104245
|
+
tags: { deviceId: this.id },
|
|
104246
|
+
meta: {
|
|
104247
|
+
dreameDeviceId: this.dreameDeviceId,
|
|
104248
|
+
error: errMsg(err)
|
|
104249
|
+
}
|
|
104250
|
+
});
|
|
103265
104251
|
}
|
|
103266
104252
|
};
|
|
103267
104253
|
this.stateChangedUnsub = dreameFacades.onStateChanged(this.brokerId, this.dreameDeviceId, onState);
|
|
@@ -103706,6 +104692,40 @@ function clampPercent$1(value) {
|
|
|
103706
104692
|
return Math.max(0, Math.min(100, value));
|
|
103707
104693
|
}
|
|
103708
104694
|
/**
|
|
104695
|
+
* Where each {@link VacuumTankProps} field lives (siid/piid). Property ids and
|
|
104696
|
+
* enum values are the HA Dreame integration's (Tasshack `dreame-vacuum`
|
|
104697
|
+
* `types.py`, `DreameVacuumProperty` → siid/piid map; same on `dev` and
|
|
104698
|
+
* v2.0.0b25, checked 2026-09-26). 27/1 and 27/4 were already read here; 27/2 and
|
|
104699
|
+
* 27/3 are their neighbours on the same station service. The robot pushes them
|
|
104700
|
+
* on change; the consumables refresh also re-reads them from the shadow.
|
|
104701
|
+
*/
|
|
104702
|
+
var VACUUM_TANK_PROPS = {
|
|
104703
|
+
cleanWaterTankStatus: {
|
|
104704
|
+
siid: 27,
|
|
104705
|
+
piid: 1
|
|
104706
|
+
},
|
|
104707
|
+
waterTank: {
|
|
104708
|
+
siid: 4,
|
|
104709
|
+
piid: 6
|
|
104710
|
+
},
|
|
104711
|
+
detergentStatus: {
|
|
104712
|
+
siid: 27,
|
|
104713
|
+
piid: 4
|
|
104714
|
+
},
|
|
104715
|
+
dustCollection: {
|
|
104716
|
+
siid: 15,
|
|
104717
|
+
piid: 3
|
|
104718
|
+
},
|
|
104719
|
+
dirtyWaterTankStatus: {
|
|
104720
|
+
siid: 27,
|
|
104721
|
+
piid: 2
|
|
104722
|
+
},
|
|
104723
|
+
dustBagStatus: {
|
|
104724
|
+
siid: 27,
|
|
104725
|
+
piid: 3
|
|
104726
|
+
}
|
|
104727
|
+
};
|
|
104728
|
+
/**
|
|
103709
104729
|
* Clean-water tank `status`: prefer the richer clean-water-tank-status (27/1),
|
|
103710
104730
|
* fall back to the coarser water-tank presence (4/6). `null` when neither maps
|
|
103711
104731
|
* to a known good/low reading (NOT_INSTALLED / CHECKING / unknown). Pure.
|
|
@@ -103725,10 +104745,32 @@ function detergentTankStatus(value) {
|
|
|
103725
104745
|
if (value === 2) return "low";
|
|
103726
104746
|
return null;
|
|
103727
104747
|
}
|
|
103728
|
-
/**
|
|
103729
|
-
|
|
103730
|
-
|
|
103731
|
-
|
|
104748
|
+
/**
|
|
104749
|
+
* Dust-bin `status` from dust-collection (15/3); OVER_USE reads as `full`. A dust
|
|
104750
|
+
* bag that is missing or asks to be checked (27/3 NOT_INSTALLED / CHECK) is never
|
|
104751
|
+
* `ok` — the enum does not say "full" either, so it is unknown. Pure.
|
|
104752
|
+
*/
|
|
104753
|
+
function dustBinTankStatus(dustCollection, dustBag) {
|
|
104754
|
+
if (dustCollection === 2) return "full";
|
|
104755
|
+
if (dustBag !== null && dustBag !== 0) return null;
|
|
104756
|
+
if (dustCollection === 1) return "ok";
|
|
104757
|
+
return null;
|
|
104758
|
+
}
|
|
104759
|
+
/**
|
|
104760
|
+
* Dirty-water tank `status` from dirty-water-tank-status (27/2). The robot has
|
|
104761
|
+
* ONE value for "not installed or full" and cannot tell them apart; lifting the
|
|
104762
|
+
* tank out to EMPTY it reads exactly like a full one, so that value is unknown
|
|
104763
|
+
* (`null`) — never `full`, which would raise a false "tank full" at the moment
|
|
104764
|
+
* someone is fixing it (controller ruling 2026-09-26). Absent or any other
|
|
104765
|
+
* value is unknown too, never `ok`.
|
|
104766
|
+
*
|
|
104767
|
+
* The unambiguous "full" is the robot's own error `MiotError.DirtyWaterTankFull`
|
|
104768
|
+
* (86, the library's `errorCode`, siid 2 piid 2): while it is raised the tank is
|
|
104769
|
+
* `full` whatever 27/2 says; once it clears, 27/2 decides again. Pure.
|
|
104770
|
+
*/
|
|
104771
|
+
function dirtyWaterTankStatus(value, errorCode) {
|
|
104772
|
+
if (errorCode === MiotError.DirtyWaterTankFull) return "full";
|
|
104773
|
+
if (value === 0) return "ok";
|
|
103732
104774
|
return null;
|
|
103733
104775
|
}
|
|
103734
104776
|
/**
|
|
@@ -103752,16 +104794,16 @@ function dustBinTankStatus(value) {
|
|
|
103752
104794
|
* - detergent ← `hasDetergentReservoir`
|
|
103753
104795
|
* - dust-bin ← `canAutoEmpty` (an auto-empty robot has an onboard dust bin)
|
|
103754
104796
|
*/
|
|
103755
|
-
function decodeVacuumTanks(caps, props) {
|
|
104797
|
+
function decodeVacuumTanks(caps, props, errorCode = null) {
|
|
103756
104798
|
const tank = (present, status) => present ? {
|
|
103757
104799
|
level: null,
|
|
103758
104800
|
status
|
|
103759
104801
|
} : null;
|
|
103760
104802
|
return {
|
|
103761
104803
|
cleanWater: tank(Boolean(caps?.canMop), props ? cleanWaterTankStatus(props) : null),
|
|
103762
|
-
dirtyWater: tank(Boolean(caps?.canMopWash), null),
|
|
104804
|
+
dirtyWater: tank(Boolean(caps?.canMopWash), dirtyWaterTankStatus(props?.dirtyWaterTankStatus ?? null, errorCode)),
|
|
103763
104805
|
detergent: tank(Boolean(caps?.hasDetergentReservoir), props ? detergentTankStatus(props.detergentStatus) : null),
|
|
103764
|
-
dustBin: tank(Boolean(caps?.canAutoEmpty), props ? dustBinTankStatus(props.dustCollection) : null)
|
|
104806
|
+
dustBin: tank(Boolean(caps?.canAutoEmpty), props ? dustBinTankStatus(props.dustCollection, props.dustBagStatus) : null)
|
|
103765
104807
|
};
|
|
103766
104808
|
}
|
|
103767
104809
|
/**
|
|
@@ -103775,19 +104817,18 @@ function clampConsumablePct(value) {
|
|
|
103775
104817
|
return Math.max(0, Math.min(100, value));
|
|
103776
104818
|
}
|
|
103777
104819
|
/**
|
|
103778
|
-
*
|
|
103779
|
-
* handle. The returned object satisfies the cap's `runtimeState` schema
|
|
103780
|
-
* (`ConsumablesStatusSchema.extend({ lastFetchedAt: z.number() })`).
|
|
104820
|
+
* The `consumables` cap items for the consumables a vacuum REPORTS.
|
|
103781
104821
|
*
|
|
103782
104822
|
* PRESENCE-driven (mirrors HA): the library's `supportedConsumables` enumerates
|
|
103783
|
-
* ONLY the consumables this model reports (a present MIoT life property)
|
|
103784
|
-
*
|
|
103785
|
-
*
|
|
103786
|
-
*
|
|
103787
|
-
*
|
|
103788
|
-
|
|
103789
|
-
|
|
103790
|
-
|
|
104823
|
+
* ONLY the consumables this model reports (a present MIoT life property), each
|
|
104824
|
+
* with its remaining-life % and whether a reset action exists. No hardcoded
|
|
104825
|
+
* per-model list: a consumable the device does not expose never appears.
|
|
104826
|
+
*
|
|
104827
|
+
* `status` is `null` — the provider reports the level, the notification rule
|
|
104828
|
+
* decides what is low. The slice clocks are `nextConsumablesSlice`'s job. Pure.
|
|
104829
|
+
*/
|
|
104830
|
+
function buildVacuumConsumableItems(readings) {
|
|
104831
|
+
return readings.map((c) => ({
|
|
103791
104832
|
key: c.key,
|
|
103792
104833
|
label: c.label,
|
|
103793
104834
|
level: clampConsumablePct(c.leftPct),
|
|
@@ -103795,12 +104836,26 @@ function buildVacuumConsumables(vacuum) {
|
|
|
103795
104836
|
lastResetAt: null,
|
|
103796
104837
|
resettable: c.resettable
|
|
103797
104838
|
}));
|
|
103798
|
-
|
|
103799
|
-
|
|
103800
|
-
|
|
103801
|
-
|
|
103802
|
-
|
|
103803
|
-
|
|
104839
|
+
}
|
|
104840
|
+
/** Structural equality of two JSON-shaped values, key order ignored. */
|
|
104841
|
+
function jsonEqual(a, b) {
|
|
104842
|
+
if (a === b) return true;
|
|
104843
|
+
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]));
|
|
104844
|
+
if (a === null || b === null || typeof a !== "object" || typeof b !== "object") return false;
|
|
104845
|
+
const ka = Object.keys(a);
|
|
104846
|
+
const kb = Object.keys(b);
|
|
104847
|
+
return ka.length === kb.length && ka.every((k) => jsonEqual(Reflect.get(a, k), Reflect.get(b, k)));
|
|
104848
|
+
}
|
|
104849
|
+
/**
|
|
104850
|
+
* The `vacuum-control` slice to write, or `null` when only the clock moved.
|
|
104851
|
+
* `recomputeSlice` runs on EVERY push; without this each one was a write (and a
|
|
104852
|
+
* `device.state-changed`) even when nothing the robot reported had changed. Pure.
|
|
104853
|
+
*/
|
|
104854
|
+
function nextVacuumControlSlice(prev, next) {
|
|
104855
|
+
if (prev === void 0) return next;
|
|
104856
|
+
const { lastChangedAt: _prevAt, ...prevContent } = prev;
|
|
104857
|
+
const { lastChangedAt: _nextAt, ...nextContent } = next;
|
|
104858
|
+
return jsonEqual(prevContent, nextContent) ? null : next;
|
|
103804
104859
|
}
|
|
103805
104860
|
//#endregion
|
|
103806
104861
|
//#region src/devices/dreame-camera-stream.ts
|
|
@@ -150354,7 +151409,7 @@ var DreameCameraDevice = class extends DreameChildDevice {
|
|
|
150354
151409
|
});
|
|
150355
151410
|
const rfc = await startRfc4571Server({
|
|
150356
151411
|
stream,
|
|
150357
|
-
logger: this.ctx.logger,
|
|
151412
|
+
logger: this.ctx.logger.withTags({ deviceId: this.id }),
|
|
150358
151413
|
keyframeTimeoutMs: WAKE_TIMEOUT_MS,
|
|
150359
151414
|
onConsumerDisconnect: () => {
|
|
150360
151415
|
this.closeStream("idle");
|
|
@@ -150829,10 +151884,13 @@ var DreameMowerActionDevice = class extends DreameChildDevice {
|
|
|
150829
151884
|
try {
|
|
150830
151885
|
await mower.getMap();
|
|
150831
151886
|
} catch (err) {
|
|
150832
|
-
this.ctx.logger.debug("dreame mower action: map fetch failed (asleep?)", {
|
|
150833
|
-
|
|
150834
|
-
|
|
150835
|
-
|
|
151887
|
+
this.ctx.logger.debug("dreame mower action: map fetch failed (asleep?)", {
|
|
151888
|
+
tags: { deviceId: this.id },
|
|
151889
|
+
meta: {
|
|
151890
|
+
dreameDeviceId: this.dreameDeviceId,
|
|
151891
|
+
error: errMsg(err)
|
|
151892
|
+
}
|
|
151893
|
+
});
|
|
150836
151894
|
} finally {
|
|
150837
151895
|
this.#inflight = false;
|
|
150838
151896
|
this.#publishSlice();
|
|
@@ -150963,45 +152021,293 @@ var MOWER_CONSUMABLE_LABEL = {
|
|
|
150963
152021
|
function isMowerConsumableKey(key) {
|
|
150964
152022
|
return key === "blade" || key === "brush" || key === "maintenance";
|
|
150965
152023
|
}
|
|
150966
|
-
/** Below this remaining-life %, an item is flagged `replace` (mirrors the
|
|
150967
|
-
* donor's low-consumable warning threshold). */
|
|
150968
|
-
var MOWER_CONSUMABLE_REPLACE_PCT = 10;
|
|
150969
152024
|
/**
|
|
150970
152025
|
* Map the library's CMS consumable readings (blade/brush/maintenance, remaining
|
|
150971
152026
|
* %) to the `consumables` cap items. All three drive a real CMS reset, so every
|
|
150972
|
-
* item is `resettable`.
|
|
150973
|
-
*
|
|
152027
|
+
* item is `resettable`.
|
|
152028
|
+
*
|
|
152029
|
+
* `status` is `null`: the provider reports the LEVEL and gives no verdict. When
|
|
152030
|
+
* a level is "low" is the notification rule's threshold (operator, per rule);
|
|
152031
|
+
* a provider constant here used to flag `replace` at 10 % and override it.
|
|
150974
152032
|
*/
|
|
150975
152033
|
function buildMowerConsumables(readings) {
|
|
150976
152034
|
return readings.map((r) => ({
|
|
150977
152035
|
key: r.key,
|
|
150978
152036
|
label: MOWER_CONSUMABLE_LABEL[r.key],
|
|
150979
152037
|
level: r.remainingPercent,
|
|
150980
|
-
status:
|
|
152038
|
+
status: null,
|
|
150981
152039
|
lastResetAt: null,
|
|
150982
152040
|
resettable: true
|
|
150983
152041
|
}));
|
|
150984
152042
|
}
|
|
150985
152043
|
//#endregion
|
|
152044
|
+
//#region src/devices/dreame-consumables.ts
|
|
152045
|
+
/**
|
|
152046
|
+
* The `consumables` slice as both Dreame devices write it, and the one
|
|
152047
|
+
* refresher that re-reads it.
|
|
152048
|
+
*
|
|
152049
|
+
* - A slice is written only when something changed: an item (lastChangedAt), or
|
|
152050
|
+
* a refresh that LANDED (lastFetchedAt, which the cap declares volatile).
|
|
152051
|
+
* - An empty list is never written. "The read said nothing" is not "this device
|
|
152052
|
+
* has no consumables" — and the slice is `restored` across restarts (D642),
|
|
152053
|
+
* so an empty write would erase the last real observation.
|
|
152054
|
+
* - A refresh never overlaps itself, and a failed one changes nothing (D49): the
|
|
152055
|
+
* last good values stay, and the failure is logged with the device tag.
|
|
152056
|
+
*/
|
|
152057
|
+
var CONSUMABLES_CAP_NAME = "consumables";
|
|
152058
|
+
var CONSUMABLES_COLD_START = {
|
|
152059
|
+
items: [],
|
|
152060
|
+
lastChangedAt: 0,
|
|
152061
|
+
lastFetchedAt: 0
|
|
152062
|
+
};
|
|
152063
|
+
function itemEqual(a, b) {
|
|
152064
|
+
return a.key === b.key && a.label === b.label && a.level === b.level && a.status === b.status && a.lastResetAt === b.lastResetAt && a.resettable === b.resettable;
|
|
152065
|
+
}
|
|
152066
|
+
function itemsEqual(a, b) {
|
|
152067
|
+
return a.length === b.length && a.every((item, i) => {
|
|
152068
|
+
const other = b[i];
|
|
152069
|
+
return other !== void 0 && itemEqual(item, other);
|
|
152070
|
+
});
|
|
152071
|
+
}
|
|
152072
|
+
/**
|
|
152073
|
+
* The slice to write, or `null` for "write nothing".
|
|
152074
|
+
*
|
|
152075
|
+
* `landed` = these items come from a refresh that just answered (not from a
|
|
152076
|
+
* replay of the cache on a push), so `lastFetchedAt` may move.
|
|
152077
|
+
*/
|
|
152078
|
+
function nextConsumablesSlice(prev, items, now, landed) {
|
|
152079
|
+
if (items.length === 0) return null;
|
|
152080
|
+
if (prev !== void 0 && itemsEqual(prev.items, items)) return landed ? {
|
|
152081
|
+
...prev,
|
|
152082
|
+
items: [...prev.items],
|
|
152083
|
+
lastFetchedAt: now
|
|
152084
|
+
} : null;
|
|
152085
|
+
return {
|
|
152086
|
+
items: [...items],
|
|
152087
|
+
lastChangedAt: now,
|
|
152088
|
+
lastFetchedAt: landed ? now : prev?.lastFetchedAt ?? 0
|
|
152089
|
+
};
|
|
152090
|
+
}
|
|
152091
|
+
/** Write {@link nextConsumablesSlice}'s answer, if it has one. */
|
|
152092
|
+
function writeConsumables(store, items, now, landed) {
|
|
152093
|
+
const next = nextConsumablesSlice(store.getCapState(CONSUMABLES_CAP_NAME), items, now, landed);
|
|
152094
|
+
if (next !== null) store.setCapState(CONSUMABLES_CAP_NAME, next);
|
|
152095
|
+
}
|
|
152096
|
+
/** Seed the cold start ONLY when nothing was restored — never wipe the last reading. */
|
|
152097
|
+
function seedConsumablesColdStart(store) {
|
|
152098
|
+
if (store.getCapState("consumables") === void 0) store.setCapState(CONSUMABLES_CAP_NAME, CONSUMABLES_COLD_START);
|
|
152099
|
+
}
|
|
152100
|
+
/**
|
|
152101
|
+
* Narrow a cap-supplied reset key to one THIS device reports and can reset.
|
|
152102
|
+
* A real key of another model (e.g. `mop-pad` on a robot without one) would
|
|
152103
|
+
* otherwise send a reset action to a service the robot lacks.
|
|
152104
|
+
*/
|
|
152105
|
+
function resolveResettableKey(key, present) {
|
|
152106
|
+
const hit = present.find((c) => c.key === key);
|
|
152107
|
+
if (hit === void 0) throw new Error(`this device does not report "${key}"`);
|
|
152108
|
+
if (!hit.resettable) throw new Error(`"${key}" has no reset action on this device`);
|
|
152109
|
+
return hit.key;
|
|
152110
|
+
}
|
|
152111
|
+
var DEFAULT_FOLLOW_UP_MS = 6e4;
|
|
152112
|
+
var DreameConsumablesRefresher = class {
|
|
152113
|
+
#deps;
|
|
152114
|
+
#interval = null;
|
|
152115
|
+
#followUp = null;
|
|
152116
|
+
#running = null;
|
|
152117
|
+
/** At most ONE run waits behind the one in flight. */
|
|
152118
|
+
#queued = null;
|
|
152119
|
+
#stopped = false;
|
|
152120
|
+
constructor(deps) {
|
|
152121
|
+
this.#deps = deps;
|
|
152122
|
+
}
|
|
152123
|
+
/** False once stopped — the device is gone, nothing may be written for it. */
|
|
152124
|
+
live = () => !this.#stopped;
|
|
152125
|
+
/** Arm the periodic re-read (the caller triggers the first read itself). */
|
|
152126
|
+
start() {
|
|
152127
|
+
this.#stopped = false;
|
|
152128
|
+
const { intervalMs } = this.#deps;
|
|
152129
|
+
if (intervalMs === void 0 || this.#interval !== null) return;
|
|
152130
|
+
this.#interval = setInterval(() => {
|
|
152131
|
+
this.trigger("interval");
|
|
152132
|
+
}, intervalMs);
|
|
152133
|
+
}
|
|
152134
|
+
stop() {
|
|
152135
|
+
this.#stopped = true;
|
|
152136
|
+
if (this.#interval !== null) clearInterval(this.#interval);
|
|
152137
|
+
if (this.#followUp !== null) clearTimeout(this.#followUp);
|
|
152138
|
+
this.#interval = null;
|
|
152139
|
+
this.#followUp = null;
|
|
152140
|
+
this.#queued = null;
|
|
152141
|
+
}
|
|
152142
|
+
/**
|
|
152143
|
+
* Re-read now. Resolves `true` when the read landed. A trigger while one is in
|
|
152144
|
+
* flight does not start a second read: it queues one more run after it
|
|
152145
|
+
* (coalesced) and shares its promise.
|
|
152146
|
+
*/
|
|
152147
|
+
trigger(reason) {
|
|
152148
|
+
if (this.#stopped) return Promise.resolve(false);
|
|
152149
|
+
if (this.#running !== null) {
|
|
152150
|
+
this.#queued = reason;
|
|
152151
|
+
return this.#running;
|
|
152152
|
+
}
|
|
152153
|
+
const run = this.#drain(reason).finally(() => {
|
|
152154
|
+
this.#running = null;
|
|
152155
|
+
});
|
|
152156
|
+
this.#running = run;
|
|
152157
|
+
return run;
|
|
152158
|
+
}
|
|
152159
|
+
/** One re-read after `followUpMs`; a second call replaces the first. */
|
|
152160
|
+
scheduleFollowUp(reason) {
|
|
152161
|
+
if (this.#stopped) return;
|
|
152162
|
+
if (this.#followUp !== null) clearTimeout(this.#followUp);
|
|
152163
|
+
this.#followUp = setTimeout(() => {
|
|
152164
|
+
this.#followUp = null;
|
|
152165
|
+
if (!this.#stopped) this.trigger(reason);
|
|
152166
|
+
}, this.#deps.followUpMs ?? DEFAULT_FOLLOW_UP_MS);
|
|
152167
|
+
}
|
|
152168
|
+
async #drain(first) {
|
|
152169
|
+
let landed = await this.#once(first);
|
|
152170
|
+
while (this.#queued !== null && !this.#stopped) {
|
|
152171
|
+
const next = this.#queued;
|
|
152172
|
+
this.#queued = null;
|
|
152173
|
+
landed = await this.#once(next);
|
|
152174
|
+
}
|
|
152175
|
+
return landed;
|
|
152176
|
+
}
|
|
152177
|
+
async #once(reason) {
|
|
152178
|
+
const { deviceId, logger, subject, read } = this.#deps;
|
|
152179
|
+
try {
|
|
152180
|
+
await read(reason, this.live);
|
|
152181
|
+
if (this.#stopped) return false;
|
|
152182
|
+
logger.debug(`${subject}: consumables refreshed`, {
|
|
152183
|
+
tags: { deviceId },
|
|
152184
|
+
meta: { reason }
|
|
152185
|
+
});
|
|
152186
|
+
return true;
|
|
152187
|
+
} catch (err) {
|
|
152188
|
+
if (this.#stopped) return false;
|
|
152189
|
+
logger.warn(`${subject}: consumables refresh failed — keeping the last values`, {
|
|
152190
|
+
tags: { deviceId },
|
|
152191
|
+
meta: {
|
|
152192
|
+
reason,
|
|
152193
|
+
error: errMsg(err)
|
|
152194
|
+
}
|
|
152195
|
+
});
|
|
152196
|
+
return false;
|
|
152197
|
+
}
|
|
152198
|
+
}
|
|
152199
|
+
};
|
|
152200
|
+
//#endregion
|
|
152201
|
+
//#region src/devices/dreame-mower-consumables.ts
|
|
152202
|
+
/**
|
|
152203
|
+
* The mower's `consumables` slice (blade / brush / maintenance).
|
|
152204
|
+
*
|
|
152205
|
+
* The CMS counters are not cached MIoT props: they are read by a LIVE custom
|
|
152206
|
+
* action that answers only while the mower is awake — the instant it docks and
|
|
152207
|
+
* starts charging. A sleeping mower never answers, so there is no periodic
|
|
152208
|
+
* read: the triggers are activation (in case it is awake), the edge into
|
|
152209
|
+
* charging, and a reset.
|
|
152210
|
+
*
|
|
152211
|
+
* The slice is `restored` across restarts (D642), so the last reading is what
|
|
152212
|
+
* the panel shows while the mower sleeps. Nothing here may overwrite it with an
|
|
152213
|
+
* empty list: a failed, empty or missing answer is "nothing learned".
|
|
152214
|
+
*/
|
|
152215
|
+
/** Retries to ride the brief wake window when the mower transitions to charging. */
|
|
152216
|
+
var CONSUMABLES_WAKE_RETRIES = 2;
|
|
152217
|
+
/** Spacing between wake-window retries. */
|
|
152218
|
+
var CONSUMABLES_RETRY_DELAY_MS = 1500;
|
|
152219
|
+
var SUBJECT$1 = "dreame mower";
|
|
152220
|
+
var DreameMowerConsumables = class {
|
|
152221
|
+
#deps;
|
|
152222
|
+
#refresher;
|
|
152223
|
+
constructor(deps) {
|
|
152224
|
+
this.#deps = deps;
|
|
152225
|
+
this.#refresher = new DreameConsumablesRefresher({
|
|
152226
|
+
deviceId: deps.deviceId,
|
|
152227
|
+
logger: deps.logger,
|
|
152228
|
+
subject: SUBJECT$1,
|
|
152229
|
+
read: (_reason, live) => this.#read(live)
|
|
152230
|
+
});
|
|
152231
|
+
}
|
|
152232
|
+
/** Cold start only when nothing was restored. */
|
|
152233
|
+
seed() {
|
|
152234
|
+
seedConsumablesColdStart(this.#deps.store);
|
|
152235
|
+
}
|
|
152236
|
+
refresh(reason) {
|
|
152237
|
+
return this.#refresher.trigger(reason);
|
|
152238
|
+
}
|
|
152239
|
+
stop() {
|
|
152240
|
+
this.#refresher.stop();
|
|
152241
|
+
}
|
|
152242
|
+
/** Reset one counter, then re-read it; a failed re-read is logged by the refresher. */
|
|
152243
|
+
async reset(key) {
|
|
152244
|
+
const { deviceId, resolveMower } = this.#deps;
|
|
152245
|
+
if (!isMowerConsumableKey(key)) throw new Error(`${SUBJECT$1}: this device does not report "${key}" (device=${deviceId})`);
|
|
152246
|
+
const mower = resolveMower();
|
|
152247
|
+
if (mower === null) throw new Error(`${SUBJECT$1}: handle unavailable (device=${deviceId})`);
|
|
152248
|
+
await mower.resetConsumable(key);
|
|
152249
|
+
await this.#refresher.trigger("reset");
|
|
152250
|
+
}
|
|
152251
|
+
async #read(live) {
|
|
152252
|
+
const { resolveMower, store, now } = this.#deps;
|
|
152253
|
+
const retries = this.#deps.retries ?? CONSUMABLES_WAKE_RETRIES;
|
|
152254
|
+
const delayMs = this.#deps.retryDelayMs ?? CONSUMABLES_RETRY_DELAY_MS;
|
|
152255
|
+
let lastError = null;
|
|
152256
|
+
for (let attempt = 0; attempt <= retries; attempt++) {
|
|
152257
|
+
if (attempt > 0) await new Promise((resolve) => setTimeout(resolve, delayMs));
|
|
152258
|
+
if (!live()) return;
|
|
152259
|
+
const mower = resolveMower();
|
|
152260
|
+
if (mower === null) throw new Error("mower handle unavailable");
|
|
152261
|
+
try {
|
|
152262
|
+
const readings = await mower.getConsumables();
|
|
152263
|
+
if (!live()) return;
|
|
152264
|
+
const items = readings === null ? [] : buildMowerConsumables(readings);
|
|
152265
|
+
if (items.length === 0) {
|
|
152266
|
+
lastError = /* @__PURE__ */ new Error("the CMS action returned no reading");
|
|
152267
|
+
continue;
|
|
152268
|
+
}
|
|
152269
|
+
writeConsumables(store, items, (now ?? Date.now)(), true);
|
|
152270
|
+
return;
|
|
152271
|
+
} catch (err) {
|
|
152272
|
+
lastError = err;
|
|
152273
|
+
}
|
|
152274
|
+
}
|
|
152275
|
+
throw lastError instanceof Error ? lastError : new Error(String(lastError));
|
|
152276
|
+
}
|
|
152277
|
+
};
|
|
152278
|
+
/**
|
|
152279
|
+
* The edge INTO the awake-and-charging window — `status == Charging` OR
|
|
152280
|
+
* `chargingStatus == Charging`, either marks it (mirrors the donor
|
|
152281
|
+
* integration). The first observation after boot is not an edge: activation
|
|
152282
|
+
* already reads, and a mower found charging would otherwise be read twice.
|
|
152283
|
+
*/
|
|
152284
|
+
var MowerChargingEdge = class {
|
|
152285
|
+
#observed = false;
|
|
152286
|
+
#prevStatus = null;
|
|
152287
|
+
#prevCharging = null;
|
|
152288
|
+
observe(status, charging) {
|
|
152289
|
+
const edge = this.#observed && (status === MowerStatus.Charging && this.#prevStatus !== MowerStatus.Charging || charging === MowerChargingStatus.Charging && this.#prevCharging !== MowerChargingStatus.Charging);
|
|
152290
|
+
this.#observed = true;
|
|
152291
|
+
this.#prevStatus = status;
|
|
152292
|
+
this.#prevCharging = charging;
|
|
152293
|
+
return edge;
|
|
152294
|
+
}
|
|
152295
|
+
};
|
|
152296
|
+
//#endregion
|
|
150986
152297
|
//#region src/devices/dreame-mower-device.ts
|
|
150987
152298
|
var CAP_NAME$9 = "lawn-mower-control";
|
|
150988
|
-
var CONSUMABLES_CAP_NAME$1 = "consumables";
|
|
150989
152299
|
/**
|
|
150990
152300
|
* The CMS consumable custom-action only answers while the mower is momentarily
|
|
150991
152301
|
* awake and ACTIVELY charging — which it is the instant it docks with a depleted
|
|
150992
152302
|
* battery. We ride that push (mirrors the donor `dreame-mower`
|
|
150993
|
-
* `_handle_device_update`: STATUS==CHARGING → refresh consumables)
|
|
150994
|
-
*
|
|
150995
|
-
*
|
|
150996
|
-
*
|
|
150997
|
-
*
|
|
150998
|
-
*
|
|
150999
|
-
*
|
|
152303
|
+
* `_handle_device_update`: STATUS==CHARGING → refresh consumables). A
|
|
152304
|
+
* fully-charged / "charging complete" / sleeping mower at base does NOT answer
|
|
152305
|
+
* synchronous cloud commands (verified: every cloud action 80001s "device
|
|
152306
|
+
* offline", even with patient backoff), so a periodic poll never works — it is
|
|
152307
|
+
* intentionally NOT used here. We trigger on the transition into EITHER
|
|
152308
|
+
* `status == Charging` OR `chargingStatus == Charging`, since both mark the
|
|
152309
|
+
* awake-and-charging window, to maximise the capture chance.
|
|
151000
152310
|
*/
|
|
151001
|
-
/** Retries to ride the brief wake window when the mower transitions to charging. */
|
|
151002
|
-
var CONSUMABLES_WAKE_RETRIES = 2;
|
|
151003
|
-
/** Spacing between wake-window retries. */
|
|
151004
|
-
var CONSUMABLES_RETRY_DELAY_MS = 1500;
|
|
151005
152311
|
/**
|
|
151006
152312
|
* Cloud-shadow (no-wake) refresh cadence for the mower's battery / charging /
|
|
151007
152313
|
* status props. The shadow read (`refreshFromCache` → `getCachedProperties`)
|
|
@@ -151020,11 +152326,6 @@ var COLD_START = {
|
|
|
151020
152326
|
severity: "info",
|
|
151021
152327
|
lastChangedAt: 0
|
|
151022
152328
|
};
|
|
151023
|
-
var CONSUMABLES_COLD_START$1 = {
|
|
151024
|
-
items: [],
|
|
151025
|
-
lastChangedAt: 0,
|
|
151026
|
-
lastFetchedAt: 0
|
|
151027
|
-
};
|
|
151028
152329
|
/**
|
|
151029
152330
|
* Dreame robotic lawn-mower device. Registers the `lawn-mower-control` and
|
|
151030
152331
|
* `consumables` caps and maps the library `MowerDevice` getters → the cap slice
|
|
@@ -151038,20 +152339,26 @@ var CONSUMABLES_COLD_START$1 = {
|
|
|
151038
152339
|
* Consumables (blade / brush / maintenance) are NOT cached MIoT props — they are
|
|
151039
152340
|
* read via a LIVE CMS custom-action (`getConsumables()`) that only answers while
|
|
151040
152341
|
* the mower is awake. We fetch best-effort on activate, on the charging push
|
|
151041
|
-
* transition (the wake window — see {@link recomputeSlice}), and after a
|
|
151042
|
-
*
|
|
151043
|
-
* mower sleeps
|
|
152342
|
+
* transition (the wake window — see {@link recomputeSlice}), and after a reset.
|
|
152343
|
+
* The `consumables` slice is `restored` across restarts, so the last good
|
|
152344
|
+
* reading is shown while the mower sleeps; a failed or empty read never
|
|
152345
|
+
* overwrites it (see `DreameMowerConsumables`).
|
|
151044
152346
|
*
|
|
151045
152347
|
* TODO: map data (`getMap()`/`mapSvg()`) is deferred — there is no camstack map
|
|
151046
152348
|
* cap, and the mower `getMap()` even requires an injected batch fetcher.
|
|
151047
152349
|
*/
|
|
151048
152350
|
var DreameMowerDevice = class extends DreameChildDevice {
|
|
151049
|
-
/**
|
|
151050
|
-
#
|
|
151051
|
-
/** Last seen `chargingStatus` value — second awake-and-charging trigger. */
|
|
151052
|
-
#prevCharging = null;
|
|
152351
|
+
/** The edge into the charging wake window (the first push after boot is not one). */
|
|
152352
|
+
#chargingEdge = new MowerChargingEdge();
|
|
151053
152353
|
/** Periodic cloud-shadow refresh timer (battery / charging / status). */
|
|
151054
152354
|
#shadowTimer = null;
|
|
152355
|
+
/** CMS consumables: read on activate / charging edge / reset, never overlapping. */
|
|
152356
|
+
#consumables = new DreameMowerConsumables({
|
|
152357
|
+
deviceId: this.id,
|
|
152358
|
+
logger: this.ctx.logger,
|
|
152359
|
+
store: this.runtimeState,
|
|
152360
|
+
resolveMower: () => this.resolveMower()
|
|
152361
|
+
});
|
|
151055
152362
|
constructor(ctx) {
|
|
151056
152363
|
super(ctx);
|
|
151057
152364
|
}
|
|
@@ -151075,18 +152382,16 @@ var DreameMowerDevice = class extends DreameChildDevice {
|
|
|
151075
152382
|
});
|
|
151076
152383
|
this.runtimeState.setCapState(CAP_NAME$9, COLD_START);
|
|
151077
152384
|
this.ctx.registerNativeCap(consumablesCapability, {
|
|
151078
|
-
getStatus: async () => this.runtimeState.getCapState(
|
|
152385
|
+
getStatus: async () => this.runtimeState.getCapState("consumables") ?? CONSUMABLES_COLD_START,
|
|
151079
152386
|
reset: async ({ key }) => {
|
|
151080
|
-
|
|
151081
|
-
await this.requireMower().resetConsumable(key);
|
|
151082
|
-
await this.refreshConsumables();
|
|
152387
|
+
await this.#consumables.reset(key);
|
|
151083
152388
|
}
|
|
151084
152389
|
});
|
|
151085
|
-
|
|
152390
|
+
this.#consumables.seed();
|
|
151086
152391
|
}
|
|
151087
152392
|
async onActivate() {
|
|
151088
152393
|
await super.onActivate();
|
|
151089
|
-
this.
|
|
152394
|
+
this.#consumables.refresh("activate");
|
|
151090
152395
|
this.#refreshShadow();
|
|
151091
152396
|
this.#shadowTimer = setInterval(() => {
|
|
151092
152397
|
this.#refreshShadow();
|
|
@@ -151097,6 +152402,7 @@ var DreameMowerDevice = class extends DreameChildDevice {
|
|
|
151097
152402
|
clearInterval(this.#shadowTimer);
|
|
151098
152403
|
this.#shadowTimer = null;
|
|
151099
152404
|
}
|
|
152405
|
+
this.#consumables.stop();
|
|
151100
152406
|
await super.removeDevice();
|
|
151101
152407
|
}
|
|
151102
152408
|
/**
|
|
@@ -151111,10 +152417,13 @@ var DreameMowerDevice = class extends DreameChildDevice {
|
|
|
151111
152417
|
try {
|
|
151112
152418
|
await mower.refreshFromCache();
|
|
151113
152419
|
} catch (err) {
|
|
151114
|
-
this.ctx.logger.debug("dreame mower: cloud-shadow refresh failed", {
|
|
151115
|
-
|
|
151116
|
-
|
|
151117
|
-
|
|
152420
|
+
this.ctx.logger.debug("dreame mower: cloud-shadow refresh failed", {
|
|
152421
|
+
tags: { deviceId: this.id },
|
|
152422
|
+
meta: {
|
|
152423
|
+
dreameDeviceId: this.dreameDeviceId,
|
|
152424
|
+
error: errMsg(err)
|
|
152425
|
+
}
|
|
152426
|
+
});
|
|
151118
152427
|
}
|
|
151119
152428
|
}
|
|
151120
152429
|
recomputeSlice() {
|
|
@@ -151134,42 +152443,7 @@ var DreameMowerDevice = class extends DreameChildDevice {
|
|
|
151134
152443
|
lastChangedAt: Date.now()
|
|
151135
152444
|
};
|
|
151136
152445
|
this.runtimeState.setCapState(CAP_NAME$9, slice);
|
|
151137
|
-
|
|
151138
|
-
if (status === MowerStatus.Charging && this.#prevStatus !== MowerStatus.Charging || charging === MowerChargingStatus.Charging && this.#prevCharging !== MowerChargingStatus.Charging) this.refreshConsumables(CONSUMABLES_WAKE_RETRIES);
|
|
151139
|
-
this.#prevStatus = status;
|
|
151140
|
-
this.#prevCharging = charging;
|
|
151141
|
-
}
|
|
151142
|
-
/**
|
|
151143
|
-
* Live-read the CMS consumable counters and write the `consumables` slice.
|
|
151144
|
-
* Best-effort: a sleeping/unreachable mower (or a malformed response) leaves
|
|
151145
|
-
* the previous (persisted) slice untouched and logs at debug. Retries a few
|
|
151146
|
-
* times with a short delay to ride the brief charging-transition wake window.
|
|
151147
|
-
*/
|
|
151148
|
-
async refreshConsumables(retries = 0) {
|
|
151149
|
-
for (let attempt = 0; attempt <= retries; attempt++) {
|
|
151150
|
-
const mower = this.resolveMower();
|
|
151151
|
-
if (mower === null) return;
|
|
151152
|
-
try {
|
|
151153
|
-
const readings = await mower.getConsumables();
|
|
151154
|
-
if (readings === null) return;
|
|
151155
|
-
const slice = {
|
|
151156
|
-
items: [...buildMowerConsumables(readings)],
|
|
151157
|
-
lastChangedAt: Date.now(),
|
|
151158
|
-
lastFetchedAt: Date.now()
|
|
151159
|
-
};
|
|
151160
|
-
this.runtimeState.setCapState(CONSUMABLES_CAP_NAME$1, slice);
|
|
151161
|
-
return;
|
|
151162
|
-
} catch (err) {
|
|
151163
|
-
if (attempt === retries) {
|
|
151164
|
-
this.ctx.logger.debug("dreame mower: consumables fetch failed (asleep?)", { meta: {
|
|
151165
|
-
dreameDeviceId: this.dreameDeviceId,
|
|
151166
|
-
error: errMsg(err)
|
|
151167
|
-
} });
|
|
151168
|
-
return;
|
|
151169
|
-
}
|
|
151170
|
-
await new Promise((resolve) => setTimeout(resolve, CONSUMABLES_RETRY_DELAY_MS));
|
|
151171
|
-
}
|
|
151172
|
-
}
|
|
152446
|
+
if (this.#chargingEdge.observe(status, mower.charging)) this.#consumables.refresh("charging");
|
|
151173
152447
|
}
|
|
151174
152448
|
/** The live typed mower handle, or throw a clear error when disconnected. */
|
|
151175
152449
|
requireMower() {
|
|
@@ -151295,10 +152569,13 @@ var DreameMowerMapDevice = class extends DreameChildDevice {
|
|
|
151295
152569
|
const map = await mower.getMap();
|
|
151296
152570
|
this.#publishRender(map);
|
|
151297
152571
|
} catch (err) {
|
|
151298
|
-
this.ctx.logger.debug("dreame mower map: fetch/render failed", {
|
|
151299
|
-
|
|
151300
|
-
|
|
151301
|
-
|
|
152572
|
+
this.ctx.logger.debug("dreame mower map: fetch/render failed", {
|
|
152573
|
+
tags: { deviceId: this.id },
|
|
152574
|
+
meta: {
|
|
152575
|
+
dreameDeviceId: this.dreameDeviceId,
|
|
152576
|
+
error: errMsg(err)
|
|
152577
|
+
}
|
|
152578
|
+
});
|
|
151302
152579
|
} finally {
|
|
151303
152580
|
this.#inflight = false;
|
|
151304
152581
|
}
|
|
@@ -151365,10 +152642,13 @@ var DreameMowerMapSelectDevice = class extends DreameChildDevice {
|
|
|
151365
152642
|
try {
|
|
151366
152643
|
await mower.getMap();
|
|
151367
152644
|
} catch (err) {
|
|
151368
|
-
this.ctx.logger.debug("dreame mower map-select: map fetch failed", {
|
|
151369
|
-
|
|
151370
|
-
|
|
151371
|
-
|
|
152645
|
+
this.ctx.logger.debug("dreame mower map-select: map fetch failed", {
|
|
152646
|
+
tags: { deviceId: this.id },
|
|
152647
|
+
meta: {
|
|
152648
|
+
dreameDeviceId: this.dreameDeviceId,
|
|
152649
|
+
error: errMsg(err)
|
|
152650
|
+
}
|
|
152651
|
+
});
|
|
151372
152652
|
} finally {
|
|
151373
152653
|
this.#inflight = false;
|
|
151374
152654
|
this.#publishSlice();
|
|
@@ -151770,9 +153050,112 @@ var DreameSwitchDevice = class extends DreameChildDevice {
|
|
|
151770
153050
|
}
|
|
151771
153051
|
};
|
|
151772
153052
|
//#endregion
|
|
153053
|
+
//#region src/devices/dreame-vacuum-consumables.ts
|
|
153054
|
+
/** Hourly: a consumable moves by hours of cleaning, and the dock edge covers a run. */
|
|
153055
|
+
var CONSUMABLES_REFRESH_MS = 60 * 6e4;
|
|
153056
|
+
/** Time for the robot to report a reset to the cloud before the shadow is re-read. */
|
|
153057
|
+
var RESET_FOLLOW_UP_MS = 9e4;
|
|
153058
|
+
var SUBJECT = "dreame vacuum";
|
|
153059
|
+
/** The states a run ends from. `drying` is already on the dock. */
|
|
153060
|
+
var DOCK_EDGE_FROM = new Set([
|
|
153061
|
+
"cleaning",
|
|
153062
|
+
"returning",
|
|
153063
|
+
"paused"
|
|
153064
|
+
]);
|
|
153065
|
+
var DreameVacuumConsumables = class {
|
|
153066
|
+
#deps;
|
|
153067
|
+
#refresher;
|
|
153068
|
+
/** Last pushed lifecycle state; `null` until the first push after boot. */
|
|
153069
|
+
#prevState = null;
|
|
153070
|
+
constructor(deps) {
|
|
153071
|
+
this.#deps = deps;
|
|
153072
|
+
this.#refresher = new DreameConsumablesRefresher({
|
|
153073
|
+
deviceId: deps.deviceId,
|
|
153074
|
+
logger: deps.logger,
|
|
153075
|
+
subject: SUBJECT,
|
|
153076
|
+
read: (reason, live) => this.#read(reason, live),
|
|
153077
|
+
intervalMs: CONSUMABLES_REFRESH_MS,
|
|
153078
|
+
followUpMs: RESET_FOLLOW_UP_MS
|
|
153079
|
+
});
|
|
153080
|
+
}
|
|
153081
|
+
start() {
|
|
153082
|
+
this.#refresher.start();
|
|
153083
|
+
this.#refresher.trigger("activate");
|
|
153084
|
+
}
|
|
153085
|
+
stop() {
|
|
153086
|
+
this.#refresher.stop();
|
|
153087
|
+
}
|
|
153088
|
+
/**
|
|
153089
|
+
* Every `stateChanged` push: replay the cache into the slice (written only if
|
|
153090
|
+
* an item changed) and refresh on the edge into `docked`. The first push after
|
|
153091
|
+
* boot is not an edge — activation already reads — and neither is a hop
|
|
153092
|
+
* between station routines (`drying` → `docked`): only the end of a RUN is.
|
|
153093
|
+
*/
|
|
153094
|
+
onPush(state) {
|
|
153095
|
+
this.#apply(false);
|
|
153096
|
+
const enteredDock = this.#prevState !== null && DOCK_EDGE_FROM.has(this.#prevState) && state === "docked";
|
|
153097
|
+
this.#prevState = state;
|
|
153098
|
+
if (enteredDock) this.#refresher.trigger("docked");
|
|
153099
|
+
}
|
|
153100
|
+
async reset(key) {
|
|
153101
|
+
const { deviceId, logger, resolveVacuum } = this.#deps;
|
|
153102
|
+
const vacuum = resolveVacuum();
|
|
153103
|
+
if (vacuum === null) throw new Error(`${SUBJECT}: handle unavailable (device=${deviceId})`);
|
|
153104
|
+
const known = resolveResettableKey(key, vacuum.supportedConsumables);
|
|
153105
|
+
const result = await vacuum.resetConsumable(known);
|
|
153106
|
+
this.#apply(result.refreshedFrom !== null);
|
|
153107
|
+
logger.info(`${SUBJECT}: consumable reset`, {
|
|
153108
|
+
tags: { deviceId },
|
|
153109
|
+
meta: resetMeta(result)
|
|
153110
|
+
});
|
|
153111
|
+
if (result.refreshedFrom === "device") return;
|
|
153112
|
+
if (result.refreshedFrom === null) logger.warn(`${SUBJECT}: consumable reset, but its values could not be re-read`, {
|
|
153113
|
+
tags: { deviceId },
|
|
153114
|
+
meta: resetMeta(result)
|
|
153115
|
+
});
|
|
153116
|
+
this.#refresher.scheduleFollowUp("reset-follow-up");
|
|
153117
|
+
}
|
|
153118
|
+
async #read(reason, live) {
|
|
153119
|
+
const { deviceId, logger, resolveVacuum } = this.#deps;
|
|
153120
|
+
const vacuum = resolveVacuum();
|
|
153121
|
+
if (vacuum === null) throw new Error("vacuum handle unavailable");
|
|
153122
|
+
await vacuum.refreshConsumables();
|
|
153123
|
+
if (!live()) return;
|
|
153124
|
+
if (vacuum.supportedConsumables.length === 0) throw new Error("the shadow reported no consumable");
|
|
153125
|
+
this.#apply(true);
|
|
153126
|
+
try {
|
|
153127
|
+
await vacuum.refreshCachedProperties(Object.values(VACUUM_TANK_PROPS).map((p) => ({ ...p })));
|
|
153128
|
+
} catch (err) {
|
|
153129
|
+
if (!live()) return;
|
|
153130
|
+
logger.warn(`${SUBJECT}: tank-status refresh failed — keeping the last values`, {
|
|
153131
|
+
tags: { deviceId },
|
|
153132
|
+
meta: {
|
|
153133
|
+
reason,
|
|
153134
|
+
error: errMsg(err)
|
|
153135
|
+
}
|
|
153136
|
+
});
|
|
153137
|
+
}
|
|
153138
|
+
}
|
|
153139
|
+
#apply(landed) {
|
|
153140
|
+
if (!this.#refresher.live()) return;
|
|
153141
|
+
const { store, resolveVacuum, now } = this.#deps;
|
|
153142
|
+
const vacuum = resolveVacuum();
|
|
153143
|
+
if (vacuum === null) return;
|
|
153144
|
+
writeConsumables(store, buildVacuumConsumableItems(vacuum.supportedConsumables), (now ?? Date.now)(), landed);
|
|
153145
|
+
}
|
|
153146
|
+
};
|
|
153147
|
+
function resetMeta(result) {
|
|
153148
|
+
return {
|
|
153149
|
+
key: result.key,
|
|
153150
|
+
refreshedFrom: result.refreshedFrom,
|
|
153151
|
+
leftPct: result.leftPct,
|
|
153152
|
+
timeLeft: result.timeLeft,
|
|
153153
|
+
...result.refreshError !== null ? { refreshError: result.refreshError.message } : {}
|
|
153154
|
+
};
|
|
153155
|
+
}
|
|
153156
|
+
//#endregion
|
|
151773
153157
|
//#region src/devices/dreame-vacuum-device.ts
|
|
151774
153158
|
var CAP_NAME$1 = "vacuum-control";
|
|
151775
|
-
var CONSUMABLES_CAP_NAME = "consumables";
|
|
151776
153159
|
var CONTROL_COLD_START = {
|
|
151777
153160
|
state: "idle",
|
|
151778
153161
|
batteryLevel: null,
|
|
@@ -151787,11 +153170,6 @@ var CONTROL_COLD_START = {
|
|
|
151787
153170
|
errorLabel: null,
|
|
151788
153171
|
lastChangedAt: 0
|
|
151789
153172
|
};
|
|
151790
|
-
var CONSUMABLES_COLD_START = {
|
|
151791
|
-
items: [],
|
|
151792
|
-
lastChangedAt: 0,
|
|
151793
|
-
lastFetchedAt: 0
|
|
151794
|
-
};
|
|
151795
153173
|
/**
|
|
151796
153174
|
* Dreame robot-vacuum device. Registers the `vacuum-control` and `consumables`
|
|
151797
153175
|
* caps and maps the library `VacuumDevice` getters → the cap slices on every
|
|
@@ -151803,17 +153181,16 @@ var CONSUMABLES_COLD_START = {
|
|
|
151803
153181
|
* suction levels).
|
|
151804
153182
|
*
|
|
151805
153183
|
* Tank state (cleanWater/dirtyWater/detergent/dustBin): the library exposes NO
|
|
151806
|
-
* tank fill
|
|
151807
|
-
* `
|
|
151808
|
-
*
|
|
151809
|
-
*
|
|
151810
|
-
*
|
|
151811
|
-
*
|
|
151812
|
-
*
|
|
151813
|
-
*
|
|
151814
|
-
*
|
|
151815
|
-
*
|
|
151816
|
-
* no-op that throws.
|
|
153184
|
+
* tank fill LEVEL, so `level` is always null. PRESENCE comes from the model
|
|
153185
|
+
* capability flags and a discrete `status` from the station's tank-status enum
|
|
153186
|
+
* props (`decodeVacuumTanks`, ids in `VACUUM_TANK_PROPS`); an absent tank is
|
|
153187
|
+
* `null`, an unknown status is `null` — never `ok`.
|
|
153188
|
+
*
|
|
153189
|
+
* Consumables: whatever the model REPORTS (the library's presence-driven
|
|
153190
|
+
* `supportedConsumables`), re-read from the cloud shadow on activate, hourly and
|
|
153191
|
+
* on the edge into docked. `reset()` accepts only a key this robot reports and
|
|
153192
|
+
* can reset, drives its MIoT reset action, and rebuilds the slice from the
|
|
153193
|
+
* library's post-reset re-read (see `DreameVacuumConsumables`).
|
|
151817
153194
|
*
|
|
151818
153195
|
* Map data is NOT on this device: the cleaning map is surfaced as a separate
|
|
151819
153196
|
* `image` child (`DreameVacuumMapDevice`) grouped under the same Container, per
|
|
@@ -151831,25 +153208,33 @@ var DreameVacuumDevice = class extends DreameChildDevice {
|
|
|
151831
153208
|
const vacuum = this.resolveVacuum();
|
|
151832
153209
|
if (vacuum !== null) {
|
|
151833
153210
|
vacuum.refreshFromCache().catch((err) => {
|
|
151834
|
-
this.ctx.logger.debug("dreame vacuum: state seed failed", {
|
|
151835
|
-
|
|
151836
|
-
|
|
151837
|
-
|
|
153211
|
+
this.ctx.logger.debug("dreame vacuum: state seed failed", {
|
|
153212
|
+
tags: { deviceId: this.id },
|
|
153213
|
+
meta: {
|
|
153214
|
+
dreameDeviceId: this.dreameDeviceId,
|
|
153215
|
+
error: errMsg(err)
|
|
153216
|
+
}
|
|
153217
|
+
});
|
|
151838
153218
|
});
|
|
151839
153219
|
vacuum.refreshAiDetection().catch((err) => {
|
|
151840
|
-
this.ctx.logger.debug("dreame vacuum: AI_DETECTION seed failed", {
|
|
151841
|
-
|
|
151842
|
-
|
|
151843
|
-
|
|
151844
|
-
|
|
151845
|
-
|
|
151846
|
-
|
|
151847
|
-
dreameDeviceId: this.dreameDeviceId,
|
|
151848
|
-
error: errMsg(err)
|
|
151849
|
-
} });
|
|
153220
|
+
this.ctx.logger.debug("dreame vacuum: AI_DETECTION seed failed", {
|
|
153221
|
+
tags: { deviceId: this.id },
|
|
153222
|
+
meta: {
|
|
153223
|
+
dreameDeviceId: this.dreameDeviceId,
|
|
153224
|
+
error: errMsg(err)
|
|
153225
|
+
}
|
|
153226
|
+
});
|
|
151850
153227
|
});
|
|
151851
153228
|
}
|
|
153229
|
+
this.consumables.start();
|
|
151852
153230
|
}
|
|
153231
|
+
/** Consumables refresh + reset — see `DreameVacuumConsumables`. */
|
|
153232
|
+
consumables = new DreameVacuumConsumables({
|
|
153233
|
+
deviceId: this.id,
|
|
153234
|
+
logger: this.ctx.logger,
|
|
153235
|
+
store: this.runtimeState,
|
|
153236
|
+
resolveVacuum: () => this.resolveVacuum()
|
|
153237
|
+
});
|
|
151853
153238
|
/** Narrow the generic handle to the library's typed {@link DreameVacuum}, or null. */
|
|
151854
153239
|
/** The robot's own link — one reader shared with the camera child. */
|
|
151855
153240
|
networkLink = new DreameNetworkLinkMonitor({
|
|
@@ -151868,6 +153253,7 @@ var DreameVacuumDevice = class extends DreameChildDevice {
|
|
|
151868
153253
|
}
|
|
151869
153254
|
async removeDevice() {
|
|
151870
153255
|
this.networkLink.stop();
|
|
153256
|
+
this.consumables.stop();
|
|
151871
153257
|
await super.removeDevice();
|
|
151872
153258
|
}
|
|
151873
153259
|
resolveVacuum() {
|
|
@@ -151906,30 +153292,23 @@ var DreameVacuumDevice = class extends DreameChildDevice {
|
|
|
151906
153292
|
/**
|
|
151907
153293
|
* Register the `consumables` cap. The provider surfaces remaining life % for
|
|
151908
153294
|
* every consumable the model REPORTS (presence-driven via the library's
|
|
151909
|
-
* `supportedConsumables`)
|
|
151910
|
-
* action
|
|
151911
|
-
*
|
|
153295
|
+
* `supportedConsumables`); `reset()` drives the consumable's real MIoT reset
|
|
153296
|
+
* action. Items whose model has no reset action report `resettable: false`
|
|
153297
|
+
* (the UI suppresses the button, and `reset()` refuses them).
|
|
151912
153298
|
*/
|
|
151913
153299
|
registerConsumablesCap() {
|
|
151914
153300
|
this.ctx.registerNativeCap(consumablesCapability, {
|
|
151915
|
-
getStatus: async () => this.runtimeState.getCapState(
|
|
153301
|
+
getStatus: async () => this.runtimeState.getCapState("consumables") ?? CONSUMABLES_COLD_START,
|
|
151916
153302
|
reset: async ({ key }) => {
|
|
151917
|
-
|
|
151918
|
-
await this.requireVacuum().resetConsumable(key);
|
|
151919
|
-
this.recomputeSlice();
|
|
153303
|
+
await this.consumables.reset(key);
|
|
151920
153304
|
}
|
|
151921
153305
|
});
|
|
151922
|
-
this.runtimeState
|
|
153306
|
+
seedConsumablesColdStart(this.runtimeState);
|
|
151923
153307
|
}
|
|
151924
|
-
/**
|
|
151925
|
-
* `registerControlCap()` (called by `super.onActivate()`) already registers
|
|
151926
|
-
* the consumables cap before the parent seeds the first slice via
|
|
151927
|
-
* `recomputeSlice()`. No additional work needed here; the override is removed.
|
|
151928
|
-
*/
|
|
151929
153308
|
recomputeSlice() {
|
|
151930
153309
|
const vacuum = this.resolveVacuum();
|
|
151931
153310
|
if (vacuum === null) return;
|
|
151932
|
-
const tanks = decodeVacuumTanks(vacuum.vacuumCapabilities, this.readTankProps());
|
|
153311
|
+
const tanks = decodeVacuumTanks(vacuum.vacuumCapabilities, this.readTankProps(), vacuum.errorCode);
|
|
151933
153312
|
const controlSlice = {
|
|
151934
153313
|
state: mapMiotStateToVacuumState(vacuum.status),
|
|
151935
153314
|
batteryLevel: clampBatteryLevel$1(vacuum.battery),
|
|
@@ -151944,27 +153323,29 @@ var DreameVacuumDevice = class extends DreameChildDevice {
|
|
|
151944
153323
|
errorLabel: vacuum.errorCode ? `Error ${vacuum.errorCode}` : null,
|
|
151945
153324
|
lastChangedAt: Date.now()
|
|
151946
153325
|
};
|
|
151947
|
-
this.runtimeState.
|
|
151948
|
-
|
|
151949
|
-
|
|
153326
|
+
const toWrite = nextVacuumControlSlice(this.runtimeState.getCapState(CAP_NAME$1), controlSlice);
|
|
153327
|
+
if (toWrite !== null) this.runtimeState.setCapState(CAP_NAME$1, toWrite);
|
|
153328
|
+
this.consumables.onPush(controlSlice.state);
|
|
151950
153329
|
}
|
|
151951
153330
|
/**
|
|
151952
|
-
* Read the
|
|
151953
|
-
* {@link decodeVacuumTanks}. Each is `null` when the device
|
|
151954
|
-
* property (or a non-numeric value), so a model without these
|
|
151955
|
-
* falls back to presence-only tanks.
|
|
153331
|
+
* Read the tank-status MIoT enum props ({@link VACUUM_TANK_PROPS}) from the
|
|
153332
|
+
* live handle for {@link decodeVacuumTanks}. Each is `null` when the device
|
|
153333
|
+
* reports no such property (or a non-numeric value), so a model without these
|
|
153334
|
+
* props simply falls back to presence-only tanks.
|
|
151956
153335
|
*/
|
|
151957
153336
|
readTankProps() {
|
|
151958
153337
|
const vacuum = this.resolveVacuum();
|
|
151959
|
-
const readEnum = (
|
|
151960
|
-
const value = vacuum?.getProperty(siid, piid)?.value;
|
|
153338
|
+
const readEnum = (prop) => {
|
|
153339
|
+
const value = vacuum?.getProperty(prop.siid, prop.piid)?.value;
|
|
151961
153340
|
return typeof value === "number" ? value : null;
|
|
151962
153341
|
};
|
|
151963
153342
|
return {
|
|
151964
|
-
cleanWaterTankStatus: readEnum(
|
|
151965
|
-
waterTank: readEnum(
|
|
151966
|
-
detergentStatus: readEnum(
|
|
151967
|
-
dustCollection: readEnum(
|
|
153343
|
+
cleanWaterTankStatus: readEnum(VACUUM_TANK_PROPS.cleanWaterTankStatus),
|
|
153344
|
+
waterTank: readEnum(VACUUM_TANK_PROPS.waterTank),
|
|
153345
|
+
detergentStatus: readEnum(VACUUM_TANK_PROPS.detergentStatus),
|
|
153346
|
+
dustCollection: readEnum(VACUUM_TANK_PROPS.dustCollection),
|
|
153347
|
+
dirtyWaterTankStatus: readEnum(VACUUM_TANK_PROPS.dirtyWaterTankStatus),
|
|
153348
|
+
dustBagStatus: readEnum(VACUUM_TANK_PROPS.dustBagStatus)
|
|
151968
153349
|
};
|
|
151969
153350
|
}
|
|
151970
153351
|
/** The live typed vacuum handle, or throw a clear error when disconnected. */
|
|
@@ -152013,10 +153394,13 @@ var DreameVacuumMapDevice = class extends DreameChildDevice {
|
|
|
152013
153394
|
await super.onActivate();
|
|
152014
153395
|
const vacuum = this.resolveVacuum();
|
|
152015
153396
|
if (vacuum !== null) vacuum.refreshSavedMapFilename().catch((err) => {
|
|
152016
|
-
this.ctx.logger.debug("dreame map: saved-map seed failed", {
|
|
152017
|
-
|
|
152018
|
-
|
|
152019
|
-
|
|
153397
|
+
this.ctx.logger.debug("dreame map: saved-map seed failed", {
|
|
153398
|
+
tags: { deviceId: this.id },
|
|
153399
|
+
meta: {
|
|
153400
|
+
dreameDeviceId: this.dreameDeviceId,
|
|
153401
|
+
error: errMsg(err)
|
|
153402
|
+
}
|
|
153403
|
+
});
|
|
152020
153404
|
});
|
|
152021
153405
|
}
|
|
152022
153406
|
/** Narrow the generic handle to the library's typed {@link DreameVacuum}, or null. */
|
|
@@ -152045,10 +153429,13 @@ var DreameVacuumMapDevice = class extends DreameChildDevice {
|
|
|
152045
153429
|
if (map === null) return;
|
|
152046
153430
|
this.#publishRender(map);
|
|
152047
153431
|
} catch (err) {
|
|
152048
|
-
this.ctx.logger.warn("dreame map: fetch/render failed", {
|
|
152049
|
-
|
|
152050
|
-
|
|
152051
|
-
|
|
153432
|
+
this.ctx.logger.warn("dreame map: fetch/render failed", {
|
|
153433
|
+
tags: { deviceId: this.id },
|
|
153434
|
+
meta: {
|
|
153435
|
+
dreameDeviceId: this.dreameDeviceId,
|
|
153436
|
+
error: errMsg(err)
|
|
153437
|
+
}
|
|
153438
|
+
});
|
|
152052
153439
|
} finally {
|
|
152053
153440
|
this.#inflight = false;
|
|
152054
153441
|
}
|
|
@@ -152406,10 +153793,13 @@ var DreameContainerDevice = class extends BaseDevice {
|
|
|
152406
153793
|
getAccessoryChildren() {
|
|
152407
153794
|
const kind = classifyDreameModel(this.model);
|
|
152408
153795
|
if (kind === "unsupported") {
|
|
152409
|
-
this.ctx.logger.warn("DreameContainerDevice: unsupported model; no children", {
|
|
152410
|
-
|
|
152411
|
-
|
|
152412
|
-
|
|
153796
|
+
this.ctx.logger.warn("DreameContainerDevice: unsupported model; no children", {
|
|
153797
|
+
tags: { deviceId: this.id },
|
|
153798
|
+
meta: {
|
|
153799
|
+
model: this.model,
|
|
153800
|
+
dreameDeviceId: this.dreameDeviceId
|
|
153801
|
+
}
|
|
153802
|
+
});
|
|
152413
153803
|
return [];
|
|
152414
153804
|
}
|
|
152415
153805
|
const meta = {
|
|
@@ -152872,10 +154262,13 @@ var DreameContainerDevice = class extends BaseDevice {
|
|
|
152872
154262
|
childLayout
|
|
152873
154263
|
});
|
|
152874
154264
|
} catch (err) {
|
|
152875
|
-
this.ctx.logger.warn("dreame container: failed to persist childLayout", {
|
|
152876
|
-
|
|
152877
|
-
|
|
152878
|
-
|
|
154265
|
+
this.ctx.logger.warn("dreame container: failed to persist childLayout", {
|
|
154266
|
+
tags: { deviceId: this.id },
|
|
154267
|
+
meta: {
|
|
154268
|
+
dreameDeviceId: this.dreameDeviceId,
|
|
154269
|
+
error: errMsg(err)
|
|
154270
|
+
}
|
|
154271
|
+
});
|
|
152879
154272
|
}
|
|
152880
154273
|
}
|
|
152881
154274
|
/** The AI-obstacle features the live model exposes — empty when the handle is
|