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