@camstack/addon-provider-dreo 0.2.96 → 0.2.98

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 +182 -24
  2. package/dist/addon.mjs +182 -24
  3. package/package.json +1 -1
package/dist/addon.js CHANGED
@@ -7339,6 +7339,23 @@ function errMsg(err) {
7339
7339
  if (typeof err === "string") return err;
7340
7340
  return String(err);
7341
7341
  }
7342
+ new Set([
7343
+ "track",
7344
+ "summary",
7345
+ "face",
7346
+ "identity",
7347
+ "plate",
7348
+ "vehicle",
7349
+ "scene",
7350
+ "motion",
7351
+ "object",
7352
+ "audio"
7353
+ ]);
7354
+ new Set([
7355
+ "motion",
7356
+ "object",
7357
+ "audio"
7358
+ ]);
7342
7359
  var EncodeProfileSchema = object({
7343
7360
  video: object({
7344
7361
  codec: _enum([
@@ -12539,8 +12556,17 @@ method(object({
12539
12556
  }), array(SettingsRecordSchema).readonly()), method(object({
12540
12557
  namespace: string().optional(),
12541
12558
  collection: string(),
12542
- record: SettingsRecordSchema
12543
- }), _void(), { kind: "mutation" }), method(object({
12559
+ record: object({
12560
+ id: string().optional(),
12561
+ data: record(string(), unknown())
12562
+ })
12563
+ }), object({
12564
+ /**
12565
+ * The id the row ACTUALLY got (D473): the one supplied, the UUID minted
12566
+ * for an absent one, or the ROWID SQLite assigned on an `INTEGER`
12567
+ * primary key — which is the only place an auto key is knowable.
12568
+ */
12569
+ id: union([string(), number()]) }), { kind: "mutation" }), method(object({
12544
12570
  namespace: string().optional(),
12545
12571
  collection: string(),
12546
12572
  records: array(BulkRecordSchema).readonly()
@@ -12649,8 +12675,17 @@ method(_void(), EngineInfoSchema, { auth: "admin" }), method(object({
12649
12675
  }), array(SettingsRecordSchema).readonly(), { auth: "admin" }), method(object({
12650
12676
  namespace: string().optional(),
12651
12677
  collection: string(),
12652
- record: SettingsRecordSchema
12653
- }), _void(), {
12678
+ record: object({
12679
+ id: string().optional(),
12680
+ data: record(string(), unknown())
12681
+ })
12682
+ }), object({
12683
+ /**
12684
+ * The id the row ACTUALLY got (D473): the one supplied, the UUID minted
12685
+ * for an absent one, or the ROWID SQLite assigned on an `INTEGER`
12686
+ * primary key — which is the only place an auto key is knowable.
12687
+ */
12688
+ id: union([string(), number()]) }), {
12654
12689
  kind: "mutation",
12655
12690
  auth: "admin"
12656
12691
  }), method(object({
@@ -19575,7 +19610,20 @@ var TrackSchema = object({
19575
19610
  ...TrackRetrainFields
19576
19611
  });
19577
19612
  var BaseEventFields = {
19578
- id: string(),
19613
+ /**
19614
+ * A SQLite ROWID, assigned by the database (D474).
19615
+ *
19616
+ * Was a 36-character UUID and cost 263 MB of a 1 117 MB database — paid
19617
+ * TWICE per row, in the row and in the primary-key index, across 2.1 million
19618
+ * motion, audio and object events. An `INTEGER PRIMARY KEY` in SQLite **is**
19619
+ * the rowid: the table itself is that B-tree, so the index stops existing
19620
+ * rather than getting smaller. No shorter string does that.
19621
+ *
19622
+ * Defined once here for all three event kinds, which is why they move
19623
+ * together: a per-table migration would have forked this and
19624
+ * `COMMON_BASE_COLUMNS` and reunited them two stages later.
19625
+ */
19626
+ id: number().int(),
19579
19627
  deviceId: number(),
19580
19628
  timestamp: number()
19581
19629
  };
@@ -19594,7 +19642,34 @@ var MotionEventSchema = object({
19594
19642
  /** Omitted in slim projection. */
19595
19643
  frameHeight: number().optional(),
19596
19644
  /** Populated by B5 (recording playback URL for this event). */
19597
- mediaUrl: string().optional()
19645
+ mediaUrl: string().optional(),
19646
+ /**
19647
+ * One row per motion EPISODE, not one per push (D475). `null` while the
19648
+ * episode is still open — a further rising edge extends it in place rather
19649
+ * than inserting a new row. Set once, at close, to `lastOnAt - startedAt`
19650
+ * (the span from the first rising edge to the LAST one, deliberately NOT
19651
+ * `closedAt - startedAt` — the close delay is a quiet CONFIRMATION, not
19652
+ * movement, and folding it in would report `MOTION_CLOSE_AFTER_MS` of
19653
+ * motion for an instantaneous trigger).
19654
+ *
19655
+ * **Absent** (not merely `null`) on a row written before D475 — that means
19656
+ * "closed the old way, before this column existed", never "still open".
19657
+ * Nothing in this codebase may read an absent `durationMs` as an open
19658
+ * episode; only `null` means open.
19659
+ */
19660
+ durationMs: number().nullable().optional(),
19661
+ /**
19662
+ * Ms offsets from `timestamp` (the episode's own first rising edge, so the
19663
+ * first entry is always `0`) of every genuine off→on transition the
19664
+ * episode saw — "ogni evento on si deve salvare" (D475). NOT one entry per
19665
+ * push: a firmware source that keepalives at ~1 Hz for the whole burst
19666
+ * (Reolink, Hikvision) produces exactly one edge; a source that reports an
19667
+ * explicit `false` mid-episode and then resumes before the quiet window
19668
+ * elapses produces another. Stored compactly — see `motion-edge-codec.ts`
19669
+ * — and decoded back to this shape on read. Absent/empty on a legacy row,
19670
+ * which must never be read as "no episode happened here".
19671
+ */
19672
+ edges: array(number()).readonly().optional()
19598
19673
  });
19599
19674
  /**
19600
19675
  * Which detection SOURCE produced an object event. `pipeline` = the ML
@@ -19649,6 +19724,23 @@ var ObjectEventSchema = object({
19649
19724
  * includes it (it is light). Absent on rows written before this field.
19650
19725
  */
19651
19726
  frameId: string().optional(),
19727
+ /**
19728
+ * A PRODUCER-chosen key that makes a synthetic event's emission idempotent
19729
+ * (D474).
19730
+ *
19731
+ * Only the package detector writes it, and it exists because the event id
19732
+ * stopped being choosable: the delivery and pick-up rows used to BE their
19733
+ * dedupe key (`pa-pkg-<entryId>-delivered`), which is how "never emit a
19734
+ * second delivery for this entry" survived a restart. An `INTEGER` rowid is
19735
+ * assigned by SQLite, so that key had to move off the primary key rather
19736
+ * than be dropped — a detector that cannot recognise its own row re-delivers
19737
+ * every parcel on every boot.
19738
+ *
19739
+ * Absent on every other object event, and on every row written before this
19740
+ * field. Never a substitute for `id`: it is unique per (producer, occasion),
19741
+ * not per row, and nothing addresses a row by it.
19742
+ */
19743
+ idempotencyKey: string().optional(),
19652
19744
  /** Omitted in slim projection. */
19653
19745
  trackId: string().optional(),
19654
19746
  className: string(),
@@ -20751,6 +20843,14 @@ var NativeCropResultSchema = object({
20751
20843
  * set `encodeJpeg: true`; `bytes` is then absent.
20752
20844
  */
20753
20845
  jpeg: string().optional(),
20846
+ /**
20847
+ * The SAME compressed JPEG as `jpeg`, as bytes (D462). Present instead of
20848
+ * `jpeg` when the request set `acceptJpegBytes`; a request that did not gets
20849
+ * `jpeg` exactly as before. MsgPack and the mesh leg both carry binary —
20850
+ * `bytes` above has crossed this boundary as a `Uint8Array` all along — so
20851
+ * base64 was buying nothing but a multi-megabyte string in the relay's heap.
20852
+ */
20853
+ jpegBytes: _instanceof(Uint8Array).optional(),
20754
20854
  width: number().int().positive(),
20755
20855
  height: number().int().positive(),
20756
20856
  /**
@@ -20817,7 +20917,14 @@ var ParkTrackFrameResultSchema = discriminatedUnion("parked", [object({
20817
20917
  })]);
20818
20918
  /** A retrieved parcel — the runner's own JPEG, base64 for the wire. */
20819
20919
  var ParkedTrackFrameSchema = object({
20820
- jpeg: string(),
20920
+ /**
20921
+ * Base64 JPEG — the pre-D462 wire. OPTIONAL since D462: a request that set
20922
+ * `acceptJpegBytes` is answered in `jpegBytes` and this is then absent.
20923
+ * Exactly one of the two is present.
20924
+ */
20925
+ jpeg: string().optional(),
20926
+ /** The same JPEG as bytes, for a caller that declared it reads them (D462). */
20927
+ jpegBytes: _instanceof(Uint8Array).optional(),
20821
20928
  width: number().int().positive(),
20822
20929
  height: number().int().positive(),
20823
20930
  /** The frame instant the parcel shows (the caller's clock, echoed back). */
@@ -21404,6 +21511,13 @@ method(RunnerCameraConfigSchema, object({ success: literal(true) }), { kind: "mu
21404
21511
  bbox: NativeCropBboxSchema,
21405
21512
  maxWidth: number().int().positive().optional(),
21406
21513
  /**
21514
+ * The caller reads a `Uint8Array` (D462). When set, a JPEG answer comes
21515
+ * back in `jpegBytes` instead of base64 `jpeg`. Absent means the old
21516
+ * wire — never assume consent: a pre-D462 caller parses the field as
21517
+ * base64 and bytes would decode to garbage rather than fail.
21518
+ */
21519
+ acceptJpegBytes: boolean().optional(),
21520
+ /**
21407
21521
  * When `true`, the runner encodes the resolved crop to JPEG ON THE
21408
21522
  * OWNING NODE and returns it in `jpeg` (base64) INSTEAD of raw `bytes`.
21409
21523
  * Callers set this for CROSS-NODE fetches (`handle.nodeId` is a remote
@@ -21471,7 +21585,14 @@ method(RunnerCameraConfigSchema, object({ success: literal(true) }), { kind: "mu
21471
21585
  }), ParkTrackFrameResultSchema, { kind: "mutation" }), method(object({
21472
21586
  deviceId: number(),
21473
21587
  trackId: string(),
21474
- kind: ParkedFrameKindSchema
21588
+ kind: ParkedFrameKindSchema,
21589
+ /**
21590
+ * The caller reads a `Uint8Array` (D462). When set, a JPEG answer comes
21591
+ * back in `jpegBytes` instead of base64 `jpeg`. Absent means the old
21592
+ * wire — never assume consent: a pre-D462 caller parses the field as
21593
+ * base64 and bytes would decode to garbage rather than fail.
21594
+ */
21595
+ acceptJpegBytes: boolean().optional()
21475
21596
  }), ParkedTrackFrameSchema.nullable()), method(object({
21476
21597
  deviceId: number(),
21477
21598
  trackId: string()
@@ -27877,26 +27998,59 @@ authKey: string().optional() }), object({
27877
27998
  /** Human-readable error when `ok: false`. */
27878
27999
  error: string().optional()
27879
28000
  }), { kind: "mutation" });
27880
- /**
27881
- * Hardware / firmware motion sensor cap — binary detected state plus
27882
- * a timestamp of the last observation. Distinct from
27883
- * `motion-detection.cap.ts` which owns the LOCAL ML motion pipeline;
27884
- * `motion` is the lightweight readout from on-camera motion (Reolink
27885
- * `GetMdState`, Baichuan push `type: motion`, ONVIF analytics).
27886
- *
27887
- * Native-motion providers also fan out to `detection.camera-native`
27888
- * with `source: 'onboard'` so cross-cutting system services
27889
- * (alert-center, advanced-notifier) can subscribe once and receive
27890
- * motion from every camera.
27891
- */
27892
28001
  var MotionStatusSchema = object({
27893
28002
  detected: boolean(),
27894
28003
  /** Ms epoch of the last detected-true observation. Null if never detected. */
27895
28004
  lastDetectedAt: number().nullable(),
27896
28005
  /**
27897
- * Ms after which `detected` auto-reverts to false if no fresh push
27898
- * arrives. Null means the provider leaves detected state until a
27899
- * native "clear" event.
28006
+ * `MOTION_CLOSE_AFTER_MS` while `detected: true` on a `Camera` device,
28007
+ * `null` while false and on every `Sensor` device (D475) — see that
28008
+ * constant's doc for the one-authority rule.
28009
+ *
28010
+ * ## Reading this field still arms nothing
28011
+ *
28012
+ * It reads like an instruction to the consumer ("revert after N ms if
28013
+ * no fresh push arrives") and it is not one: nothing in this repo reads
28014
+ * the LIVE cap value to drive a timer. `pipeline-analytics`'s motion-episode
28015
+ * close DOES now use the same number — `MOTION_CLOSE_AFTER_MS` — but as an
28016
+ * imported constant, not as a read of `device.state.motion.value`, so this
28017
+ * field stays what it always was: DESCRIPTIVE output, mirroring an answer
28018
+ * computed elsewhere. Building a self-clear timer out of a READ of this
28019
+ * field would add a second falling-edge authority beside whichever one
28020
+ * already owns the device, and two that can disagree are worse than one.
28021
+ * Consumers that need a falling edge SHAPED differently — held open across
28022
+ * a flapping source — debounce on their own side and say so, as
28023
+ * `addon-export-alexa/src/motion-clear-hold.ts` and
28024
+ * `addon-export-hap`'s `RESET_DEBOUNCE_MS` both do.
28025
+ *
28026
+ * ## Who writes it
28027
+ *
28028
+ * - **Cameras** — the runner's phase machine, `active → watching` on
28029
+ * `cooldown_expired`, which then writes this slice with
28030
+ * `detected: false` (`handlePhaseChanged` in
28031
+ * `pipeline-runner/index.ts`). It produces the FALLING edge, which
28032
+ * matters most for the sources that only ever push a rising one:
28033
+ * Reolink emits `MotionOnMotionChanged { detected: true }` and never
28034
+ * a false.
28035
+ * - **Sensors** (Home Assistant binary sensors, Homematic) — the
28036
+ * provider pushes the false itself, from the upstream system's own
28037
+ * state change. No phase machine is involved.
28038
+ *
28039
+ * ### The phase machine is CANONICAL, not sole — and that is a defect
28040
+ *
28041
+ * An earlier revision of this docblock (mine, 2026-09-12) claimed the
28042
+ * phase machine is the sole writer for a camera. It is not.
28043
+ * `hikvision-camera.ts:3464` and `amcrest-camera.ts:445` both call
28044
+ * `setCapSlice(motionCapability, …)` on their own rising edge, and
28045
+ * Hikvision's comment says why: it read THIS docblock, agreed the
28046
+ * runner is canonical, and wrote anyway to avoid per-tick churn. So
28047
+ * two authorities can disagree about one slice, which this repo
28048
+ * forbids, and the doc said otherwise — which is worse than saying
28049
+ * nothing, because it reads as verification.
28050
+ *
28051
+ * This predates D475 and is not fixed there: the fix touches every
28052
+ * camera provider. Recorded in D475's Consequences. Do not restore the
28053
+ * "sole writer" wording without also removing the other writers.
27900
28054
  */
27901
28055
  autoClearAfterMs: number().nullable()
27902
28056
  });
@@ -27966,7 +28120,11 @@ onMotionChanged: { data: MotionOnMotionChangedDataSchema } },
27966
28120
  */
27967
28121
  runtimeState: MotionStatusSchema,
27968
28122
  /**
27969
- * Runtime-state durability: **session** — self-clearing by construction (`autoClearAfterMs`); a restored `detected: true` is a frozen event, and the next frame re-publishes the real one.
28123
+ * Runtime-state durability: **session** — every writer of this slice
28124
+ * writes only on an EDGE, so a restored `detected: true` would stay
28125
+ * frozen until the next one instead of being corrected. The next edge
28126
+ * re-publishes the real state. (On who the writers are, and why there
28127
+ * is more than one, see `autoClearAfterMs` above.)
27970
28128
  *
27971
28129
  * See `RuntimeStateDurability`. Enforced by
27972
28130
  * `scripts/check-runtime-state-durability.ts`.
package/dist/addon.mjs CHANGED
@@ -7340,6 +7340,23 @@ function errMsg(err) {
7340
7340
  if (typeof err === "string") return err;
7341
7341
  return String(err);
7342
7342
  }
7343
+ new Set([
7344
+ "track",
7345
+ "summary",
7346
+ "face",
7347
+ "identity",
7348
+ "plate",
7349
+ "vehicle",
7350
+ "scene",
7351
+ "motion",
7352
+ "object",
7353
+ "audio"
7354
+ ]);
7355
+ new Set([
7356
+ "motion",
7357
+ "object",
7358
+ "audio"
7359
+ ]);
7343
7360
  var EncodeProfileSchema = object({
7344
7361
  video: object({
7345
7362
  codec: _enum([
@@ -12540,8 +12557,17 @@ method(object({
12540
12557
  }), array(SettingsRecordSchema).readonly()), method(object({
12541
12558
  namespace: string().optional(),
12542
12559
  collection: string(),
12543
- record: SettingsRecordSchema
12544
- }), _void(), { kind: "mutation" }), method(object({
12560
+ record: object({
12561
+ id: string().optional(),
12562
+ data: record(string(), unknown())
12563
+ })
12564
+ }), object({
12565
+ /**
12566
+ * The id the row ACTUALLY got (D473): the one supplied, the UUID minted
12567
+ * for an absent one, or the ROWID SQLite assigned on an `INTEGER`
12568
+ * primary key — which is the only place an auto key is knowable.
12569
+ */
12570
+ id: union([string(), number()]) }), { kind: "mutation" }), method(object({
12545
12571
  namespace: string().optional(),
12546
12572
  collection: string(),
12547
12573
  records: array(BulkRecordSchema).readonly()
@@ -12650,8 +12676,17 @@ method(_void(), EngineInfoSchema, { auth: "admin" }), method(object({
12650
12676
  }), array(SettingsRecordSchema).readonly(), { auth: "admin" }), method(object({
12651
12677
  namespace: string().optional(),
12652
12678
  collection: string(),
12653
- record: SettingsRecordSchema
12654
- }), _void(), {
12679
+ record: object({
12680
+ id: string().optional(),
12681
+ data: record(string(), unknown())
12682
+ })
12683
+ }), object({
12684
+ /**
12685
+ * The id the row ACTUALLY got (D473): the one supplied, the UUID minted
12686
+ * for an absent one, or the ROWID SQLite assigned on an `INTEGER`
12687
+ * primary key — which is the only place an auto key is knowable.
12688
+ */
12689
+ id: union([string(), number()]) }), {
12655
12690
  kind: "mutation",
12656
12691
  auth: "admin"
12657
12692
  }), method(object({
@@ -19576,7 +19611,20 @@ var TrackSchema = object({
19576
19611
  ...TrackRetrainFields
19577
19612
  });
19578
19613
  var BaseEventFields = {
19579
- id: string(),
19614
+ /**
19615
+ * A SQLite ROWID, assigned by the database (D474).
19616
+ *
19617
+ * Was a 36-character UUID and cost 263 MB of a 1 117 MB database — paid
19618
+ * TWICE per row, in the row and in the primary-key index, across 2.1 million
19619
+ * motion, audio and object events. An `INTEGER PRIMARY KEY` in SQLite **is**
19620
+ * the rowid: the table itself is that B-tree, so the index stops existing
19621
+ * rather than getting smaller. No shorter string does that.
19622
+ *
19623
+ * Defined once here for all three event kinds, which is why they move
19624
+ * together: a per-table migration would have forked this and
19625
+ * `COMMON_BASE_COLUMNS` and reunited them two stages later.
19626
+ */
19627
+ id: number().int(),
19580
19628
  deviceId: number(),
19581
19629
  timestamp: number()
19582
19630
  };
@@ -19595,7 +19643,34 @@ var MotionEventSchema = object({
19595
19643
  /** Omitted in slim projection. */
19596
19644
  frameHeight: number().optional(),
19597
19645
  /** Populated by B5 (recording playback URL for this event). */
19598
- mediaUrl: string().optional()
19646
+ mediaUrl: string().optional(),
19647
+ /**
19648
+ * One row per motion EPISODE, not one per push (D475). `null` while the
19649
+ * episode is still open — a further rising edge extends it in place rather
19650
+ * than inserting a new row. Set once, at close, to `lastOnAt - startedAt`
19651
+ * (the span from the first rising edge to the LAST one, deliberately NOT
19652
+ * `closedAt - startedAt` — the close delay is a quiet CONFIRMATION, not
19653
+ * movement, and folding it in would report `MOTION_CLOSE_AFTER_MS` of
19654
+ * motion for an instantaneous trigger).
19655
+ *
19656
+ * **Absent** (not merely `null`) on a row written before D475 — that means
19657
+ * "closed the old way, before this column existed", never "still open".
19658
+ * Nothing in this codebase may read an absent `durationMs` as an open
19659
+ * episode; only `null` means open.
19660
+ */
19661
+ durationMs: number().nullable().optional(),
19662
+ /**
19663
+ * Ms offsets from `timestamp` (the episode's own first rising edge, so the
19664
+ * first entry is always `0`) of every genuine off→on transition the
19665
+ * episode saw — "ogni evento on si deve salvare" (D475). NOT one entry per
19666
+ * push: a firmware source that keepalives at ~1 Hz for the whole burst
19667
+ * (Reolink, Hikvision) produces exactly one edge; a source that reports an
19668
+ * explicit `false` mid-episode and then resumes before the quiet window
19669
+ * elapses produces another. Stored compactly — see `motion-edge-codec.ts`
19670
+ * — and decoded back to this shape on read. Absent/empty on a legacy row,
19671
+ * which must never be read as "no episode happened here".
19672
+ */
19673
+ edges: array(number()).readonly().optional()
19599
19674
  });
19600
19675
  /**
19601
19676
  * Which detection SOURCE produced an object event. `pipeline` = the ML
@@ -19650,6 +19725,23 @@ var ObjectEventSchema = object({
19650
19725
  * includes it (it is light). Absent on rows written before this field.
19651
19726
  */
19652
19727
  frameId: string().optional(),
19728
+ /**
19729
+ * A PRODUCER-chosen key that makes a synthetic event's emission idempotent
19730
+ * (D474).
19731
+ *
19732
+ * Only the package detector writes it, and it exists because the event id
19733
+ * stopped being choosable: the delivery and pick-up rows used to BE their
19734
+ * dedupe key (`pa-pkg-<entryId>-delivered`), which is how "never emit a
19735
+ * second delivery for this entry" survived a restart. An `INTEGER` rowid is
19736
+ * assigned by SQLite, so that key had to move off the primary key rather
19737
+ * than be dropped — a detector that cannot recognise its own row re-delivers
19738
+ * every parcel on every boot.
19739
+ *
19740
+ * Absent on every other object event, and on every row written before this
19741
+ * field. Never a substitute for `id`: it is unique per (producer, occasion),
19742
+ * not per row, and nothing addresses a row by it.
19743
+ */
19744
+ idempotencyKey: string().optional(),
19653
19745
  /** Omitted in slim projection. */
19654
19746
  trackId: string().optional(),
19655
19747
  className: string(),
@@ -20752,6 +20844,14 @@ var NativeCropResultSchema = object({
20752
20844
  * set `encodeJpeg: true`; `bytes` is then absent.
20753
20845
  */
20754
20846
  jpeg: string().optional(),
20847
+ /**
20848
+ * The SAME compressed JPEG as `jpeg`, as bytes (D462). Present instead of
20849
+ * `jpeg` when the request set `acceptJpegBytes`; a request that did not gets
20850
+ * `jpeg` exactly as before. MsgPack and the mesh leg both carry binary —
20851
+ * `bytes` above has crossed this boundary as a `Uint8Array` all along — so
20852
+ * base64 was buying nothing but a multi-megabyte string in the relay's heap.
20853
+ */
20854
+ jpegBytes: _instanceof(Uint8Array).optional(),
20755
20855
  width: number().int().positive(),
20756
20856
  height: number().int().positive(),
20757
20857
  /**
@@ -20818,7 +20918,14 @@ var ParkTrackFrameResultSchema = discriminatedUnion("parked", [object({
20818
20918
  })]);
20819
20919
  /** A retrieved parcel — the runner's own JPEG, base64 for the wire. */
20820
20920
  var ParkedTrackFrameSchema = object({
20821
- jpeg: string(),
20921
+ /**
20922
+ * Base64 JPEG — the pre-D462 wire. OPTIONAL since D462: a request that set
20923
+ * `acceptJpegBytes` is answered in `jpegBytes` and this is then absent.
20924
+ * Exactly one of the two is present.
20925
+ */
20926
+ jpeg: string().optional(),
20927
+ /** The same JPEG as bytes, for a caller that declared it reads them (D462). */
20928
+ jpegBytes: _instanceof(Uint8Array).optional(),
20822
20929
  width: number().int().positive(),
20823
20930
  height: number().int().positive(),
20824
20931
  /** The frame instant the parcel shows (the caller's clock, echoed back). */
@@ -21405,6 +21512,13 @@ method(RunnerCameraConfigSchema, object({ success: literal(true) }), { kind: "mu
21405
21512
  bbox: NativeCropBboxSchema,
21406
21513
  maxWidth: number().int().positive().optional(),
21407
21514
  /**
21515
+ * The caller reads a `Uint8Array` (D462). When set, a JPEG answer comes
21516
+ * back in `jpegBytes` instead of base64 `jpeg`. Absent means the old
21517
+ * wire — never assume consent: a pre-D462 caller parses the field as
21518
+ * base64 and bytes would decode to garbage rather than fail.
21519
+ */
21520
+ acceptJpegBytes: boolean().optional(),
21521
+ /**
21408
21522
  * When `true`, the runner encodes the resolved crop to JPEG ON THE
21409
21523
  * OWNING NODE and returns it in `jpeg` (base64) INSTEAD of raw `bytes`.
21410
21524
  * Callers set this for CROSS-NODE fetches (`handle.nodeId` is a remote
@@ -21472,7 +21586,14 @@ method(RunnerCameraConfigSchema, object({ success: literal(true) }), { kind: "mu
21472
21586
  }), ParkTrackFrameResultSchema, { kind: "mutation" }), method(object({
21473
21587
  deviceId: number(),
21474
21588
  trackId: string(),
21475
- kind: ParkedFrameKindSchema
21589
+ kind: ParkedFrameKindSchema,
21590
+ /**
21591
+ * The caller reads a `Uint8Array` (D462). When set, a JPEG answer comes
21592
+ * back in `jpegBytes` instead of base64 `jpeg`. Absent means the old
21593
+ * wire — never assume consent: a pre-D462 caller parses the field as
21594
+ * base64 and bytes would decode to garbage rather than fail.
21595
+ */
21596
+ acceptJpegBytes: boolean().optional()
21476
21597
  }), ParkedTrackFrameSchema.nullable()), method(object({
21477
21598
  deviceId: number(),
21478
21599
  trackId: string()
@@ -27878,26 +27999,59 @@ authKey: string().optional() }), object({
27878
27999
  /** Human-readable error when `ok: false`. */
27879
28000
  error: string().optional()
27880
28001
  }), { kind: "mutation" });
27881
- /**
27882
- * Hardware / firmware motion sensor cap — binary detected state plus
27883
- * a timestamp of the last observation. Distinct from
27884
- * `motion-detection.cap.ts` which owns the LOCAL ML motion pipeline;
27885
- * `motion` is the lightweight readout from on-camera motion (Reolink
27886
- * `GetMdState`, Baichuan push `type: motion`, ONVIF analytics).
27887
- *
27888
- * Native-motion providers also fan out to `detection.camera-native`
27889
- * with `source: 'onboard'` so cross-cutting system services
27890
- * (alert-center, advanced-notifier) can subscribe once and receive
27891
- * motion from every camera.
27892
- */
27893
28002
  var MotionStatusSchema = object({
27894
28003
  detected: boolean(),
27895
28004
  /** Ms epoch of the last detected-true observation. Null if never detected. */
27896
28005
  lastDetectedAt: number().nullable(),
27897
28006
  /**
27898
- * Ms after which `detected` auto-reverts to false if no fresh push
27899
- * arrives. Null means the provider leaves detected state until a
27900
- * native "clear" event.
28007
+ * `MOTION_CLOSE_AFTER_MS` while `detected: true` on a `Camera` device,
28008
+ * `null` while false and on every `Sensor` device (D475) — see that
28009
+ * constant's doc for the one-authority rule.
28010
+ *
28011
+ * ## Reading this field still arms nothing
28012
+ *
28013
+ * It reads like an instruction to the consumer ("revert after N ms if
28014
+ * no fresh push arrives") and it is not one: nothing in this repo reads
28015
+ * the LIVE cap value to drive a timer. `pipeline-analytics`'s motion-episode
28016
+ * close DOES now use the same number — `MOTION_CLOSE_AFTER_MS` — but as an
28017
+ * imported constant, not as a read of `device.state.motion.value`, so this
28018
+ * field stays what it always was: DESCRIPTIVE output, mirroring an answer
28019
+ * computed elsewhere. Building a self-clear timer out of a READ of this
28020
+ * field would add a second falling-edge authority beside whichever one
28021
+ * already owns the device, and two that can disagree are worse than one.
28022
+ * Consumers that need a falling edge SHAPED differently — held open across
28023
+ * a flapping source — debounce on their own side and say so, as
28024
+ * `addon-export-alexa/src/motion-clear-hold.ts` and
28025
+ * `addon-export-hap`'s `RESET_DEBOUNCE_MS` both do.
28026
+ *
28027
+ * ## Who writes it
28028
+ *
28029
+ * - **Cameras** — the runner's phase machine, `active → watching` on
28030
+ * `cooldown_expired`, which then writes this slice with
28031
+ * `detected: false` (`handlePhaseChanged` in
28032
+ * `pipeline-runner/index.ts`). It produces the FALLING edge, which
28033
+ * matters most for the sources that only ever push a rising one:
28034
+ * Reolink emits `MotionOnMotionChanged { detected: true }` and never
28035
+ * a false.
28036
+ * - **Sensors** (Home Assistant binary sensors, Homematic) — the
28037
+ * provider pushes the false itself, from the upstream system's own
28038
+ * state change. No phase machine is involved.
28039
+ *
28040
+ * ### The phase machine is CANONICAL, not sole — and that is a defect
28041
+ *
28042
+ * An earlier revision of this docblock (mine, 2026-09-12) claimed the
28043
+ * phase machine is the sole writer for a camera. It is not.
28044
+ * `hikvision-camera.ts:3464` and `amcrest-camera.ts:445` both call
28045
+ * `setCapSlice(motionCapability, …)` on their own rising edge, and
28046
+ * Hikvision's comment says why: it read THIS docblock, agreed the
28047
+ * runner is canonical, and wrote anyway to avoid per-tick churn. So
28048
+ * two authorities can disagree about one slice, which this repo
28049
+ * forbids, and the doc said otherwise — which is worse than saying
28050
+ * nothing, because it reads as verification.
28051
+ *
28052
+ * This predates D475 and is not fixed there: the fix touches every
28053
+ * camera provider. Recorded in D475's Consequences. Do not restore the
28054
+ * "sole writer" wording without also removing the other writers.
27901
28055
  */
27902
28056
  autoClearAfterMs: number().nullable()
27903
28057
  });
@@ -27967,7 +28121,11 @@ onMotionChanged: { data: MotionOnMotionChangedDataSchema } },
27967
28121
  */
27968
28122
  runtimeState: MotionStatusSchema,
27969
28123
  /**
27970
- * Runtime-state durability: **session** — self-clearing by construction (`autoClearAfterMs`); a restored `detected: true` is a frozen event, and the next frame re-publishes the real one.
28124
+ * Runtime-state durability: **session** — every writer of this slice
28125
+ * writes only on an EDGE, so a restored `detected: true` would stay
28126
+ * frozen until the next one instead of being corrected. The next edge
28127
+ * re-publishes the real state. (On who the writers are, and why there
28128
+ * is more than one, see `autoClearAfterMs` above.)
27971
28129
  *
27972
28130
  * See `RuntimeStateDurability`. Enforced by
27973
28131
  * `scripts/check-runtime-state-durability.ts`.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@camstack/addon-provider-dreo",
3
- "version": "0.2.96",
3
+ "version": "0.2.98",
4
4
  "description": "Dreo smart-device (fan / air-circulator / purifier / heater / humidifier) device-provider addon for CamStack — wraps the @apocaliss92/nodedreo Dreo cloud client (REST + WebSocket)",
5
5
  "keywords": [
6
6
  "camstack",