@camstack/addon-provider-dreame 0.2.75 → 0.2.77

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 +143 -38
  2. package/dist/addon.mjs +143 -38
  3. package/package.json +1 -1
package/dist/addon.js CHANGED
@@ -60522,17 +60522,29 @@ var HHMM = /^([01]\d|2[0-3]):[0-5]\d$/;
60522
60522
  /**
60523
60523
  * DERIVED per-camera storage summary — the single field cheap consumers read
60524
60524
  * (the viewer's status dot, the camera list) instead of walking `bands`:
60525
- * - `off` — no band covers the camera (or it is disabled).
60526
- * - `events` — every band records around triggers only.
60527
- * - `continuous` — at least one band records continuously.
60528
- *
60529
- * NEVER authored: the recorder stamps it from the authoritative `bands` on
60530
- * every save (`activeModeForConfig`). Writing it has no effect.
60525
+ * - `off` — no band covers the camera (or it is disabled).
60526
+ * - `events` — every band records around triggers only.
60527
+ * - `continuous` — at least one band records continuously.
60528
+ * - `on-device-decision`— the DEVICE decides: recording runs for as long as the
60529
+ * camera raises its own `recording-signal` level (a robot that cleans). There
60530
+ * is no schedule to author, because there is no hour to program — see
60531
+ * {@link RecordingConfigSchema}`.deviceDecision`.
60532
+ *
60533
+ * NEVER authored: the recorder stamps it from the authoritative intent
60534
+ * (`bands` + `deviceDecision`) on every save (`activeModeForConfig`). Writing it
60535
+ * has no effect.
60536
+ *
60537
+ * `on-device-decision` is named for WHO decides, not for how the recording is
60538
+ * requested. `on-demand` was rejected: in this repo's vocabulary a "demand" is
60539
+ * something the operator makes (the live gate is the recorder's own "demand
60540
+ * window"), and a knob whose name suggests the operator starts it while the
60541
+ * device actually does is the D62 shape — a control nobody can predict.
60531
60542
  */
60532
60543
  var RecordingStorageModeSchema = _enum([
60533
60544
  "off",
60534
60545
  "events",
60535
- "continuous"
60546
+ "continuous",
60547
+ "on-device-decision"
60536
60548
  ]);
60537
60549
  /**
60538
60550
  * Le macro classi che possono aprire una finestra di registrazione — le stesse
@@ -60582,22 +60594,7 @@ var RecordingTriggersSchema = object({
60582
60594
  * il livello: un contatto trovato già aperto al riavvio del runner non fa
60583
60595
  * registrare.
60584
60596
  */
60585
- sensorDeviceIds: array(number().int().positive()).min(1).max(16).refine(noDuplicates, { message: "sensorDeviceIds must not repeat a device" }).optional(),
60586
- /**
60587
- * La camera registra per tutta la durata del SEGNALE che lei stessa
60588
- * pubblica — la cap `recording-signal` (`active: true` mentre il device è in
60589
- * funzione: un robot che pulisce). È un LIVELLO, non un fronte: la finestra
60590
- * resta aperta finché il segnale è alto e si chiude `postBufferSec` dopo la
60591
- * caduta, così una pulizia di quaranta minuti è UNA clip.
60592
- *
60593
- * Non nomina un device: il segnale è della camera stessa, tenuto fresco dal
60594
- * suo provider (D224); il recorder lo ascolta su `DeviceStateChanged` e lo
60595
- * riconcilia leggendo la slice via RPC, con un tetto oltre il quale un
60596
- * segnale mai abbassato (un evento perso) non può tenere aperta una
60597
- * registrazione (D11). Vedi `recorder/addon/band-decision.ts`
60598
- * (`holdTrigger` / `releaseTrigger`).
60599
- */
60600
- deviceSignal: boolean().optional()
60597
+ sensorDeviceIds: array(number().int().positive()).min(1).max(16).refine(noDuplicates, { message: "sensorDeviceIds must not repeat a device" }).optional()
60601
60598
  });
60602
60599
  /**
60603
60600
  * Mode of a single recording band — the recorder per-band vocabulary.
@@ -60627,13 +60624,28 @@ var RecordingBandSchema = object({
60627
60624
  end: string().regex(HHMM),
60628
60625
  mode: RecordingBandModeSchema,
60629
60626
  triggers: RecordingBandTriggersSchema.optional(),
60627
+ /**
60628
+ * RETIRED (D381). A band no longer carries a pre-buffer of its own.
60629
+ *
60630
+ * It was the retroactive KEEP bound — which already-written segments survive
60631
+ * a trigger — and it had to be at least as wide as the widest pre-roll the
60632
+ * broker could serve, or the gate would delete the seconds the ring had just
60633
+ * handed the writer. That "at least as wide as" is the tell: it was a second
60634
+ * expression of the broker's retention, in different units, in a different
60635
+ * addon, with an invariant the operator had to maintain by hand. The keep
60636
+ * bound is now derived from the one prebuffer ceiling and there is nothing
60637
+ * left to configure here.
60638
+ *
60639
+ * The key survives in the SHAPE so a config written before that record still
60640
+ * parses and can be migrated deliberately — `RecordingBandSchema` is not
60641
+ * strict, so simply deleting it would strip an operator's value in silence on
60642
+ * the next save (the failure D380 came one commit from shipping). Nothing
60643
+ * reads it: {@link stripRetiredBandPreBufferSec} removes it on load.
60644
+ */
60630
60645
  preBufferSec: number().min(0).optional(),
60631
60646
  postBufferSec: number().min(0).optional()
60632
60647
  });
60633
- ({
60634
- preBufferSec: 15,
60635
- postBufferSec: 30
60636
- }).postBufferSec * 1e3;
60648
+ ({ postBufferSec: 30 }).postBufferSec * 1e3;
60637
60649
  /**
60638
60650
  * Per-device retention overrides. Every field is optional; an unset or `0`
60639
60651
  * value inherits the node-wide recorder default. Only footage-lifetime limits
@@ -60681,8 +60693,29 @@ var RecordingConfigSchema = object({
60681
60693
  * "off" is the absence of a covering band, never a band value.
60682
60694
  */
60683
60695
  bands: array(RecordingBandSchema).default([]),
60696
+ /**
60697
+ * THE device-decided intent: record for as long as the camera itself raises
60698
+ * `recording-signal` (`active: true` while the device is in function — a
60699
+ * robot that cleans). AUTHORED, unlike `mode`, and the only authored
60700
+ * recording intent that is not a band.
60701
+ *
60702
+ * It carries NO schedule on purpose. A band is an HOUR, and this feature has
60703
+ * no hour to program: the device decides. What it costs the operator is one
60704
+ * flag — "this camera can record on its own" — and what it buys is the same
60705
+ * hold/release the recorder already implements (D371, `holdTrigger` /
60706
+ * `releaseTrigger`): the window opens on the rise, stays open for the whole
60707
+ * job however long it is, and closes one pad after the fall.
60708
+ *
60709
+ * EXCLUSIVE with `bands` (below): two authorities deciding when the same
60710
+ * camera records is the D62 failure. `off` remains `enabled: false`, so an
60711
+ * operator switching this camera off is REPORTED off, never as broken.
60712
+ */
60713
+ deviceDecision: boolean().optional(),
60684
60714
  retention: RecordingRetentionSchema.optional()
60685
- }).strict();
60715
+ }).strict().refine((config) => config.deviceDecision !== true || config.bands.length === 0, {
60716
+ message: "deviceDecision is a whole-config mode and carries no schedule: it cannot be combined with bands",
60717
+ path: ["bands"]
60718
+ });
60686
60719
  /**
60687
60720
  * Entity-relocation job state (storage entity-routing spec, Phase 4).
60688
60721
  *
@@ -73715,11 +73748,7 @@ var CameraAudioStatusSchema = object({
73715
73748
  });
73716
73749
  /** Recording block — null when no recording cap is active for this device. */
73717
73750
  var CameraRecordingStatusSchema = object({
73718
- mode: _enum([
73719
- "off",
73720
- "continuous",
73721
- "events"
73722
- ]),
73751
+ mode: RecordingStorageModeSchema,
73723
73752
  active: boolean(),
73724
73753
  storageBytes: number()
73725
73754
  });
@@ -82146,11 +82175,10 @@ DeviceType.Camera, DeviceType.Sensor, DeviceType.Switch, method(object({ deviceI
82146
82175
  var RecordingStatusSchema = object({
82147
82176
  deviceId: number(),
82148
82177
  enabled: boolean(),
82149
- activeMode: _enum([
82150
- "off",
82151
- "continuous",
82152
- "events"
82153
- ]),
82178
+ /** THE derived storage mode, from the one definition
82179
+ * (`deriveRecordingMode`) — never a second enum. A duplicated list is how
82180
+ * `on-device-decision` could have reached the recorder and not the status. */
82181
+ activeMode: RecordingStorageModeSchema,
82154
82182
  nodeId: string(),
82155
82183
  storageBytes: number()
82156
82184
  });
@@ -96799,6 +96827,16 @@ var dreameChildSchema = object({
96799
96827
  navigationDisabledEntryIds: array(string()).optional(),
96800
96828
  /** Privacy PIN set at pairing — required to wake a coded camera (camera child only). */
96801
96829
  navigationAccessCode: string().optional(),
96830
+ /**
96831
+ * ms-epoch of the ONE-SHOT recording seed (camera child only, D380).
96832
+ *
96833
+ * A MARK, never the value: it says this camera was seeded, which stays true
96834
+ * after the operator switches recording off — and an operator's `off` is
96835
+ * byte-identical to a camera that was never seeded (`enabled: false,
96836
+ * bands: []`), so nothing but this field can tell the two apart. The
96837
+ * authority for what is recorded remains `RecordingConfig` (D62).
96838
+ */
96839
+ recordingSeededAt: number().optional(),
96802
96840
  mapShowNoGo: boolean().optional(),
96803
96841
  mapShowVirtualWalls: boolean().optional(),
96804
96842
  mapShowObstacles: boolean().optional(),
@@ -97849,6 +97887,51 @@ var DreameNetworkLinkMonitor = class {
97849
97887
  }
97850
97888
  };
97851
97889
  //#endregion
97890
+ //#region src/devices/dreame-recording-seed.ts
97891
+ /**
97892
+ * THE config the provider seeds: recording ON, decided by the device.
97893
+ *
97894
+ * No bands (the mode carries none, D380) and no profiles/retention: those are
97895
+ * cluster-wide defaults the recorder already resolves, and pinning them here
97896
+ * would be the provider deciding storage policy for a camera it does not own.
97897
+ */
97898
+ var DEVICE_DECISION_SEED_CONFIG = Object.freeze({
97899
+ enabled: true,
97900
+ deviceDecision: true,
97901
+ bands: []
97902
+ });
97903
+ /**
97904
+ * Seed `on-device-decision` recording for this camera, once, ever.
97905
+ *
97906
+ * Never throws: it runs on the activation path and a camera that fails to be
97907
+ * seeded must still come up. Every branch that does not write says why — a
97908
+ * silent skip here is indistinguishable from the feature not existing.
97909
+ */
97910
+ async function seedDeviceDecisionRecording(deps) {
97911
+ const { deviceId, logger } = deps;
97912
+ if (deps.alreadySeeded) return "already-seeded";
97913
+ try {
97914
+ await deps.writeConfig(DEVICE_DECISION_SEED_CONFIG);
97915
+ } catch (err) {
97916
+ logger.warn("robot camera: could not seed on-device-decision recording — retried at the next start", {
97917
+ tags: { deviceId },
97918
+ meta: { error: errMsg(err) }
97919
+ });
97920
+ return "failed";
97921
+ }
97922
+ try {
97923
+ await deps.markSeeded();
97924
+ } catch (err) {
97925
+ logger.warn("robot camera: recording seeded but the one-shot mark could not be persisted — a restart may seed it again and override an operator who switched it off", {
97926
+ tags: { deviceId },
97927
+ meta: { error: errMsg(err) }
97928
+ });
97929
+ return "seeded";
97930
+ }
97931
+ logger.info("robot camera: seeded on-device-decision recording (once, at first start)", { tags: { deviceId } });
97932
+ return "seeded";
97933
+ }
97934
+ //#endregion
97852
97935
  //#region src/devices/dreame-recording-signal.ts
97853
97936
  /** Vacuum lifecycle states during which the camera asks to be recorded. */
97854
97937
  var ACTIVE_STATES = new Set(["cleaning", "returning"]);
@@ -143563,6 +143646,28 @@ var DreameCameraDevice = class extends DreameChildDevice {
143563
143646
  this.networkLink.start();
143564
143647
  this.registerStreamCatalogCap();
143565
143648
  this.registerRecordingSignalCap();
143649
+ this.seedRecordingConfig();
143650
+ }
143651
+ /**
143652
+ * Seed `on-device-decision` recording the FIRST time this camera comes up.
143653
+ *
143654
+ * Gated on the durable mark and NOT on the current config: an operator who
143655
+ * switched recording off leaves a config that is byte-identical to one that
143656
+ * was never seeded, so reading the value would re-enable it at every restart.
143657
+ * The value stays the recorder's (D62) — this writes a default through the
143658
+ * same cap method an operator uses and keeps no copy of it.
143659
+ */
143660
+ async seedRecordingConfig() {
143661
+ await seedDeviceDecisionRecording({
143662
+ deviceId: this.id,
143663
+ alreadySeeded: this.config.get("recordingSeededAt") !== void 0,
143664
+ writeConfig: (config) => this.ctx.api.recording.setDeviceConfig.mutate({
143665
+ deviceId: this.id,
143666
+ config
143667
+ }),
143668
+ markSeeded: () => this.config.set("recordingSeededAt", Date.now()),
143669
+ logger: this.ctx.logger
143670
+ });
143566
143671
  }
143567
143672
  /**
143568
143673
  * The camera's own "record me" LEVEL, derived from the vacuum lifecycle
package/dist/addon.mjs CHANGED
@@ -60519,17 +60519,29 @@ var HHMM = /^([01]\d|2[0-3]):[0-5]\d$/;
60519
60519
  /**
60520
60520
  * DERIVED per-camera storage summary — the single field cheap consumers read
60521
60521
  * (the viewer's status dot, the camera list) instead of walking `bands`:
60522
- * - `off` — no band covers the camera (or it is disabled).
60523
- * - `events` — every band records around triggers only.
60524
- * - `continuous` — at least one band records continuously.
60525
- *
60526
- * NEVER authored: the recorder stamps it from the authoritative `bands` on
60527
- * every save (`activeModeForConfig`). Writing it has no effect.
60522
+ * - `off` — no band covers the camera (or it is disabled).
60523
+ * - `events` — every band records around triggers only.
60524
+ * - `continuous` — at least one band records continuously.
60525
+ * - `on-device-decision`— the DEVICE decides: recording runs for as long as the
60526
+ * camera raises its own `recording-signal` level (a robot that cleans). There
60527
+ * is no schedule to author, because there is no hour to program — see
60528
+ * {@link RecordingConfigSchema}`.deviceDecision`.
60529
+ *
60530
+ * NEVER authored: the recorder stamps it from the authoritative intent
60531
+ * (`bands` + `deviceDecision`) on every save (`activeModeForConfig`). Writing it
60532
+ * has no effect.
60533
+ *
60534
+ * `on-device-decision` is named for WHO decides, not for how the recording is
60535
+ * requested. `on-demand` was rejected: in this repo's vocabulary a "demand" is
60536
+ * something the operator makes (the live gate is the recorder's own "demand
60537
+ * window"), and a knob whose name suggests the operator starts it while the
60538
+ * device actually does is the D62 shape — a control nobody can predict.
60528
60539
  */
60529
60540
  var RecordingStorageModeSchema = _enum([
60530
60541
  "off",
60531
60542
  "events",
60532
- "continuous"
60543
+ "continuous",
60544
+ "on-device-decision"
60533
60545
  ]);
60534
60546
  /**
60535
60547
  * Le macro classi che possono aprire una finestra di registrazione — le stesse
@@ -60579,22 +60591,7 @@ var RecordingTriggersSchema = object({
60579
60591
  * il livello: un contatto trovato già aperto al riavvio del runner non fa
60580
60592
  * registrare.
60581
60593
  */
60582
- sensorDeviceIds: array(number().int().positive()).min(1).max(16).refine(noDuplicates, { message: "sensorDeviceIds must not repeat a device" }).optional(),
60583
- /**
60584
- * La camera registra per tutta la durata del SEGNALE che lei stessa
60585
- * pubblica — la cap `recording-signal` (`active: true` mentre il device è in
60586
- * funzione: un robot che pulisce). È un LIVELLO, non un fronte: la finestra
60587
- * resta aperta finché il segnale è alto e si chiude `postBufferSec` dopo la
60588
- * caduta, così una pulizia di quaranta minuti è UNA clip.
60589
- *
60590
- * Non nomina un device: il segnale è della camera stessa, tenuto fresco dal
60591
- * suo provider (D224); il recorder lo ascolta su `DeviceStateChanged` e lo
60592
- * riconcilia leggendo la slice via RPC, con un tetto oltre il quale un
60593
- * segnale mai abbassato (un evento perso) non può tenere aperta una
60594
- * registrazione (D11). Vedi `recorder/addon/band-decision.ts`
60595
- * (`holdTrigger` / `releaseTrigger`).
60596
- */
60597
- deviceSignal: boolean().optional()
60594
+ sensorDeviceIds: array(number().int().positive()).min(1).max(16).refine(noDuplicates, { message: "sensorDeviceIds must not repeat a device" }).optional()
60598
60595
  });
60599
60596
  /**
60600
60597
  * Mode of a single recording band — the recorder per-band vocabulary.
@@ -60624,13 +60621,28 @@ var RecordingBandSchema = object({
60624
60621
  end: string().regex(HHMM),
60625
60622
  mode: RecordingBandModeSchema,
60626
60623
  triggers: RecordingBandTriggersSchema.optional(),
60624
+ /**
60625
+ * RETIRED (D381). A band no longer carries a pre-buffer of its own.
60626
+ *
60627
+ * It was the retroactive KEEP bound — which already-written segments survive
60628
+ * a trigger — and it had to be at least as wide as the widest pre-roll the
60629
+ * broker could serve, or the gate would delete the seconds the ring had just
60630
+ * handed the writer. That "at least as wide as" is the tell: it was a second
60631
+ * expression of the broker's retention, in different units, in a different
60632
+ * addon, with an invariant the operator had to maintain by hand. The keep
60633
+ * bound is now derived from the one prebuffer ceiling and there is nothing
60634
+ * left to configure here.
60635
+ *
60636
+ * The key survives in the SHAPE so a config written before that record still
60637
+ * parses and can be migrated deliberately — `RecordingBandSchema` is not
60638
+ * strict, so simply deleting it would strip an operator's value in silence on
60639
+ * the next save (the failure D380 came one commit from shipping). Nothing
60640
+ * reads it: {@link stripRetiredBandPreBufferSec} removes it on load.
60641
+ */
60627
60642
  preBufferSec: number().min(0).optional(),
60628
60643
  postBufferSec: number().min(0).optional()
60629
60644
  });
60630
- ({
60631
- preBufferSec: 15,
60632
- postBufferSec: 30
60633
- }).postBufferSec * 1e3;
60645
+ ({ postBufferSec: 30 }).postBufferSec * 1e3;
60634
60646
  /**
60635
60647
  * Per-device retention overrides. Every field is optional; an unset or `0`
60636
60648
  * value inherits the node-wide recorder default. Only footage-lifetime limits
@@ -60678,8 +60690,29 @@ var RecordingConfigSchema = object({
60678
60690
  * "off" is the absence of a covering band, never a band value.
60679
60691
  */
60680
60692
  bands: array(RecordingBandSchema).default([]),
60693
+ /**
60694
+ * THE device-decided intent: record for as long as the camera itself raises
60695
+ * `recording-signal` (`active: true` while the device is in function — a
60696
+ * robot that cleans). AUTHORED, unlike `mode`, and the only authored
60697
+ * recording intent that is not a band.
60698
+ *
60699
+ * It carries NO schedule on purpose. A band is an HOUR, and this feature has
60700
+ * no hour to program: the device decides. What it costs the operator is one
60701
+ * flag — "this camera can record on its own" — and what it buys is the same
60702
+ * hold/release the recorder already implements (D371, `holdTrigger` /
60703
+ * `releaseTrigger`): the window opens on the rise, stays open for the whole
60704
+ * job however long it is, and closes one pad after the fall.
60705
+ *
60706
+ * EXCLUSIVE with `bands` (below): two authorities deciding when the same
60707
+ * camera records is the D62 failure. `off` remains `enabled: false`, so an
60708
+ * operator switching this camera off is REPORTED off, never as broken.
60709
+ */
60710
+ deviceDecision: boolean().optional(),
60681
60711
  retention: RecordingRetentionSchema.optional()
60682
- }).strict();
60712
+ }).strict().refine((config) => config.deviceDecision !== true || config.bands.length === 0, {
60713
+ message: "deviceDecision is a whole-config mode and carries no schedule: it cannot be combined with bands",
60714
+ path: ["bands"]
60715
+ });
60683
60716
  /**
60684
60717
  * Entity-relocation job state (storage entity-routing spec, Phase 4).
60685
60718
  *
@@ -73712,11 +73745,7 @@ var CameraAudioStatusSchema = object({
73712
73745
  });
73713
73746
  /** Recording block — null when no recording cap is active for this device. */
73714
73747
  var CameraRecordingStatusSchema = object({
73715
- mode: _enum([
73716
- "off",
73717
- "continuous",
73718
- "events"
73719
- ]),
73748
+ mode: RecordingStorageModeSchema,
73720
73749
  active: boolean(),
73721
73750
  storageBytes: number()
73722
73751
  });
@@ -82143,11 +82172,10 @@ DeviceType.Camera, DeviceType.Sensor, DeviceType.Switch, method(object({ deviceI
82143
82172
  var RecordingStatusSchema = object({
82144
82173
  deviceId: number(),
82145
82174
  enabled: boolean(),
82146
- activeMode: _enum([
82147
- "off",
82148
- "continuous",
82149
- "events"
82150
- ]),
82175
+ /** THE derived storage mode, from the one definition
82176
+ * (`deriveRecordingMode`) — never a second enum. A duplicated list is how
82177
+ * `on-device-decision` could have reached the recorder and not the status. */
82178
+ activeMode: RecordingStorageModeSchema,
82151
82179
  nodeId: string(),
82152
82180
  storageBytes: number()
82153
82181
  });
@@ -96796,6 +96824,16 @@ var dreameChildSchema = object({
96796
96824
  navigationDisabledEntryIds: array(string()).optional(),
96797
96825
  /** Privacy PIN set at pairing — required to wake a coded camera (camera child only). */
96798
96826
  navigationAccessCode: string().optional(),
96827
+ /**
96828
+ * ms-epoch of the ONE-SHOT recording seed (camera child only, D380).
96829
+ *
96830
+ * A MARK, never the value: it says this camera was seeded, which stays true
96831
+ * after the operator switches recording off — and an operator's `off` is
96832
+ * byte-identical to a camera that was never seeded (`enabled: false,
96833
+ * bands: []`), so nothing but this field can tell the two apart. The
96834
+ * authority for what is recorded remains `RecordingConfig` (D62).
96835
+ */
96836
+ recordingSeededAt: number().optional(),
96799
96837
  mapShowNoGo: boolean().optional(),
96800
96838
  mapShowVirtualWalls: boolean().optional(),
96801
96839
  mapShowObstacles: boolean().optional(),
@@ -97846,6 +97884,51 @@ var DreameNetworkLinkMonitor = class {
97846
97884
  }
97847
97885
  };
97848
97886
  //#endregion
97887
+ //#region src/devices/dreame-recording-seed.ts
97888
+ /**
97889
+ * THE config the provider seeds: recording ON, decided by the device.
97890
+ *
97891
+ * No bands (the mode carries none, D380) and no profiles/retention: those are
97892
+ * cluster-wide defaults the recorder already resolves, and pinning them here
97893
+ * would be the provider deciding storage policy for a camera it does not own.
97894
+ */
97895
+ var DEVICE_DECISION_SEED_CONFIG = Object.freeze({
97896
+ enabled: true,
97897
+ deviceDecision: true,
97898
+ bands: []
97899
+ });
97900
+ /**
97901
+ * Seed `on-device-decision` recording for this camera, once, ever.
97902
+ *
97903
+ * Never throws: it runs on the activation path and a camera that fails to be
97904
+ * seeded must still come up. Every branch that does not write says why — a
97905
+ * silent skip here is indistinguishable from the feature not existing.
97906
+ */
97907
+ async function seedDeviceDecisionRecording(deps) {
97908
+ const { deviceId, logger } = deps;
97909
+ if (deps.alreadySeeded) return "already-seeded";
97910
+ try {
97911
+ await deps.writeConfig(DEVICE_DECISION_SEED_CONFIG);
97912
+ } catch (err) {
97913
+ logger.warn("robot camera: could not seed on-device-decision recording — retried at the next start", {
97914
+ tags: { deviceId },
97915
+ meta: { error: errMsg(err) }
97916
+ });
97917
+ return "failed";
97918
+ }
97919
+ try {
97920
+ await deps.markSeeded();
97921
+ } catch (err) {
97922
+ logger.warn("robot camera: recording seeded but the one-shot mark could not be persisted — a restart may seed it again and override an operator who switched it off", {
97923
+ tags: { deviceId },
97924
+ meta: { error: errMsg(err) }
97925
+ });
97926
+ return "seeded";
97927
+ }
97928
+ logger.info("robot camera: seeded on-device-decision recording (once, at first start)", { tags: { deviceId } });
97929
+ return "seeded";
97930
+ }
97931
+ //#endregion
97849
97932
  //#region src/devices/dreame-recording-signal.ts
97850
97933
  /** Vacuum lifecycle states during which the camera asks to be recorded. */
97851
97934
  var ACTIVE_STATES = new Set(["cleaning", "returning"]);
@@ -143560,6 +143643,28 @@ var DreameCameraDevice = class extends DreameChildDevice {
143560
143643
  this.networkLink.start();
143561
143644
  this.registerStreamCatalogCap();
143562
143645
  this.registerRecordingSignalCap();
143646
+ this.seedRecordingConfig();
143647
+ }
143648
+ /**
143649
+ * Seed `on-device-decision` recording the FIRST time this camera comes up.
143650
+ *
143651
+ * Gated on the durable mark and NOT on the current config: an operator who
143652
+ * switched recording off leaves a config that is byte-identical to one that
143653
+ * was never seeded, so reading the value would re-enable it at every restart.
143654
+ * The value stays the recorder's (D62) — this writes a default through the
143655
+ * same cap method an operator uses and keeps no copy of it.
143656
+ */
143657
+ async seedRecordingConfig() {
143658
+ await seedDeviceDecisionRecording({
143659
+ deviceId: this.id,
143660
+ alreadySeeded: this.config.get("recordingSeededAt") !== void 0,
143661
+ writeConfig: (config) => this.ctx.api.recording.setDeviceConfig.mutate({
143662
+ deviceId: this.id,
143663
+ config
143664
+ }),
143665
+ markSeeded: () => this.config.set("recordingSeededAt", Date.now()),
143666
+ logger: this.ctx.logger
143667
+ });
143563
143668
  }
143564
143669
  /**
143565
143670
  * The camera's own "record me" LEVEL, derived from the vacuum lifecycle
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@camstack/addon-provider-dreame",
3
- "version": "0.2.75",
3
+ "version": "0.2.77",
4
4
  "description": "Dreame robot-vacuum / lawn-mower device-provider addon for CamStack — wraps the @apocaliss92/nodedreame Dreamehome cloud client",
5
5
  "keywords": [
6
6
  "camstack",