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