@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.
- package/dist/addon.js +143 -38
- package/dist/addon.mjs +143 -38
- 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`
|
|
60526
|
-
* - `events`
|
|
60527
|
-
* - `continuous`
|
|
60528
|
-
*
|
|
60529
|
-
*
|
|
60530
|
-
*
|
|
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:
|
|
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
|
-
|
|
82150
|
-
|
|
82151
|
-
|
|
82152
|
-
|
|
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`
|
|
60523
|
-
* - `events`
|
|
60524
|
-
* - `continuous`
|
|
60525
|
-
*
|
|
60526
|
-
*
|
|
60527
|
-
*
|
|
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:
|
|
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
|
-
|
|
82147
|
-
|
|
82148
|
-
|
|
82149
|
-
|
|
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.
|
|
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",
|