@camstack/addon-provider-gree 0.2.95 → 0.2.97

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
@@ -7306,6 +7306,23 @@ function errMsg(err) {
7306
7306
  if (typeof err === "string") return err;
7307
7307
  return String(err);
7308
7308
  }
7309
+ new Set([
7310
+ "track",
7311
+ "summary",
7312
+ "face",
7313
+ "identity",
7314
+ "plate",
7315
+ "vehicle",
7316
+ "scene",
7317
+ "motion",
7318
+ "object",
7319
+ "audio"
7320
+ ]);
7321
+ new Set([
7322
+ "motion",
7323
+ "object",
7324
+ "audio"
7325
+ ]);
7309
7326
  var EncodeProfileSchema = object({
7310
7327
  video: object({
7311
7328
  codec: _enum([
@@ -12506,8 +12523,17 @@ method(object({
12506
12523
  }), array(SettingsRecordSchema).readonly()), method(object({
12507
12524
  namespace: string().optional(),
12508
12525
  collection: string(),
12509
- record: SettingsRecordSchema
12510
- }), _void(), { kind: "mutation" }), method(object({
12526
+ record: object({
12527
+ id: string().optional(),
12528
+ data: record(string(), unknown())
12529
+ })
12530
+ }), object({
12531
+ /**
12532
+ * The id the row ACTUALLY got (D473): the one supplied, the UUID minted
12533
+ * for an absent one, or the ROWID SQLite assigned on an `INTEGER`
12534
+ * primary key — which is the only place an auto key is knowable.
12535
+ */
12536
+ id: union([string(), number()]) }), { kind: "mutation" }), method(object({
12511
12537
  namespace: string().optional(),
12512
12538
  collection: string(),
12513
12539
  records: array(BulkRecordSchema).readonly()
@@ -12616,8 +12642,17 @@ method(_void(), EngineInfoSchema, { auth: "admin" }), method(object({
12616
12642
  }), array(SettingsRecordSchema).readonly(), { auth: "admin" }), method(object({
12617
12643
  namespace: string().optional(),
12618
12644
  collection: string(),
12619
- record: SettingsRecordSchema
12620
- }), _void(), {
12645
+ record: object({
12646
+ id: string().optional(),
12647
+ data: record(string(), unknown())
12648
+ })
12649
+ }), object({
12650
+ /**
12651
+ * The id the row ACTUALLY got (D473): the one supplied, the UUID minted
12652
+ * for an absent one, or the ROWID SQLite assigned on an `INTEGER`
12653
+ * primary key — which is the only place an auto key is knowable.
12654
+ */
12655
+ id: union([string(), number()]) }), {
12621
12656
  kind: "mutation",
12622
12657
  auth: "admin"
12623
12658
  }), method(object({
@@ -19525,7 +19560,20 @@ var TrackSchema = object({
19525
19560
  ...TrackRetrainFields
19526
19561
  });
19527
19562
  var BaseEventFields = {
19528
- id: string(),
19563
+ /**
19564
+ * A SQLite ROWID, assigned by the database (D474).
19565
+ *
19566
+ * Was a 36-character UUID and cost 263 MB of a 1 117 MB database — paid
19567
+ * TWICE per row, in the row and in the primary-key index, across 2.1 million
19568
+ * motion, audio and object events. An `INTEGER PRIMARY KEY` in SQLite **is**
19569
+ * the rowid: the table itself is that B-tree, so the index stops existing
19570
+ * rather than getting smaller. No shorter string does that.
19571
+ *
19572
+ * Defined once here for all three event kinds, which is why they move
19573
+ * together: a per-table migration would have forked this and
19574
+ * `COMMON_BASE_COLUMNS` and reunited them two stages later.
19575
+ */
19576
+ id: number().int(),
19529
19577
  deviceId: number(),
19530
19578
  timestamp: number()
19531
19579
  };
@@ -19544,7 +19592,34 @@ var MotionEventSchema = object({
19544
19592
  /** Omitted in slim projection. */
19545
19593
  frameHeight: number().optional(),
19546
19594
  /** Populated by B5 (recording playback URL for this event). */
19547
- mediaUrl: string().optional()
19595
+ mediaUrl: string().optional(),
19596
+ /**
19597
+ * One row per motion EPISODE, not one per push (D475). `null` while the
19598
+ * episode is still open — a further rising edge extends it in place rather
19599
+ * than inserting a new row. Set once, at close, to `lastOnAt - startedAt`
19600
+ * (the span from the first rising edge to the LAST one, deliberately NOT
19601
+ * `closedAt - startedAt` — the close delay is a quiet CONFIRMATION, not
19602
+ * movement, and folding it in would report `MOTION_CLOSE_AFTER_MS` of
19603
+ * motion for an instantaneous trigger).
19604
+ *
19605
+ * **Absent** (not merely `null`) on a row written before D475 — that means
19606
+ * "closed the old way, before this column existed", never "still open".
19607
+ * Nothing in this codebase may read an absent `durationMs` as an open
19608
+ * episode; only `null` means open.
19609
+ */
19610
+ durationMs: number().nullable().optional(),
19611
+ /**
19612
+ * Ms offsets from `timestamp` (the episode's own first rising edge, so the
19613
+ * first entry is always `0`) of every genuine off→on transition the
19614
+ * episode saw — "ogni evento on si deve salvare" (D475). NOT one entry per
19615
+ * push: a firmware source that keepalives at ~1 Hz for the whole burst
19616
+ * (Reolink, Hikvision) produces exactly one edge; a source that reports an
19617
+ * explicit `false` mid-episode and then resumes before the quiet window
19618
+ * elapses produces another. Stored compactly — see `motion-edge-codec.ts`
19619
+ * — and decoded back to this shape on read. Absent/empty on a legacy row,
19620
+ * which must never be read as "no episode happened here".
19621
+ */
19622
+ edges: array(number()).readonly().optional()
19548
19623
  });
19549
19624
  /**
19550
19625
  * Which detection SOURCE produced an object event. `pipeline` = the ML
@@ -19599,6 +19674,23 @@ var ObjectEventSchema = object({
19599
19674
  * includes it (it is light). Absent on rows written before this field.
19600
19675
  */
19601
19676
  frameId: string().optional(),
19677
+ /**
19678
+ * A PRODUCER-chosen key that makes a synthetic event's emission idempotent
19679
+ * (D474).
19680
+ *
19681
+ * Only the package detector writes it, and it exists because the event id
19682
+ * stopped being choosable: the delivery and pick-up rows used to BE their
19683
+ * dedupe key (`pa-pkg-<entryId>-delivered`), which is how "never emit a
19684
+ * second delivery for this entry" survived a restart. An `INTEGER` rowid is
19685
+ * assigned by SQLite, so that key had to move off the primary key rather
19686
+ * than be dropped — a detector that cannot recognise its own row re-delivers
19687
+ * every parcel on every boot.
19688
+ *
19689
+ * Absent on every other object event, and on every row written before this
19690
+ * field. Never a substitute for `id`: it is unique per (producer, occasion),
19691
+ * not per row, and nothing addresses a row by it.
19692
+ */
19693
+ idempotencyKey: string().optional(),
19602
19694
  /** Omitted in slim projection. */
19603
19695
  trackId: string().optional(),
19604
19696
  className: string(),
@@ -20701,6 +20793,14 @@ var NativeCropResultSchema = object({
20701
20793
  * set `encodeJpeg: true`; `bytes` is then absent.
20702
20794
  */
20703
20795
  jpeg: string().optional(),
20796
+ /**
20797
+ * The SAME compressed JPEG as `jpeg`, as bytes (D462). Present instead of
20798
+ * `jpeg` when the request set `acceptJpegBytes`; a request that did not gets
20799
+ * `jpeg` exactly as before. MsgPack and the mesh leg both carry binary —
20800
+ * `bytes` above has crossed this boundary as a `Uint8Array` all along — so
20801
+ * base64 was buying nothing but a multi-megabyte string in the relay's heap.
20802
+ */
20803
+ jpegBytes: _instanceof(Uint8Array).optional(),
20704
20804
  width: number().int().positive(),
20705
20805
  height: number().int().positive(),
20706
20806
  /**
@@ -20767,7 +20867,14 @@ var ParkTrackFrameResultSchema = discriminatedUnion("parked", [object({
20767
20867
  })]);
20768
20868
  /** A retrieved parcel — the runner's own JPEG, base64 for the wire. */
20769
20869
  var ParkedTrackFrameSchema = object({
20770
- jpeg: string(),
20870
+ /**
20871
+ * Base64 JPEG — the pre-D462 wire. OPTIONAL since D462: a request that set
20872
+ * `acceptJpegBytes` is answered in `jpegBytes` and this is then absent.
20873
+ * Exactly one of the two is present.
20874
+ */
20875
+ jpeg: string().optional(),
20876
+ /** The same JPEG as bytes, for a caller that declared it reads them (D462). */
20877
+ jpegBytes: _instanceof(Uint8Array).optional(),
20771
20878
  width: number().int().positive(),
20772
20879
  height: number().int().positive(),
20773
20880
  /** The frame instant the parcel shows (the caller's clock, echoed back). */
@@ -21354,6 +21461,13 @@ method(RunnerCameraConfigSchema, object({ success: literal(true) }), { kind: "mu
21354
21461
  bbox: NativeCropBboxSchema,
21355
21462
  maxWidth: number().int().positive().optional(),
21356
21463
  /**
21464
+ * The caller reads a `Uint8Array` (D462). When set, a JPEG answer comes
21465
+ * back in `jpegBytes` instead of base64 `jpeg`. Absent means the old
21466
+ * wire — never assume consent: a pre-D462 caller parses the field as
21467
+ * base64 and bytes would decode to garbage rather than fail.
21468
+ */
21469
+ acceptJpegBytes: boolean().optional(),
21470
+ /**
21357
21471
  * When `true`, the runner encodes the resolved crop to JPEG ON THE
21358
21472
  * OWNING NODE and returns it in `jpeg` (base64) INSTEAD of raw `bytes`.
21359
21473
  * Callers set this for CROSS-NODE fetches (`handle.nodeId` is a remote
@@ -21421,7 +21535,14 @@ method(RunnerCameraConfigSchema, object({ success: literal(true) }), { kind: "mu
21421
21535
  }), ParkTrackFrameResultSchema, { kind: "mutation" }), method(object({
21422
21536
  deviceId: number(),
21423
21537
  trackId: string(),
21424
- kind: ParkedFrameKindSchema
21538
+ kind: ParkedFrameKindSchema,
21539
+ /**
21540
+ * The caller reads a `Uint8Array` (D462). When set, a JPEG answer comes
21541
+ * back in `jpegBytes` instead of base64 `jpeg`. Absent means the old
21542
+ * wire — never assume consent: a pre-D462 caller parses the field as
21543
+ * base64 and bytes would decode to garbage rather than fail.
21544
+ */
21545
+ acceptJpegBytes: boolean().optional()
21425
21546
  }), ParkedTrackFrameSchema.nullable()), method(object({
21426
21547
  deviceId: number(),
21427
21548
  trackId: string()
@@ -27819,26 +27940,59 @@ authKey: string().optional() }), object({
27819
27940
  /** Human-readable error when `ok: false`. */
27820
27941
  error: string().optional()
27821
27942
  }), { kind: "mutation" });
27822
- /**
27823
- * Hardware / firmware motion sensor cap — binary detected state plus
27824
- * a timestamp of the last observation. Distinct from
27825
- * `motion-detection.cap.ts` which owns the LOCAL ML motion pipeline;
27826
- * `motion` is the lightweight readout from on-camera motion (Reolink
27827
- * `GetMdState`, Baichuan push `type: motion`, ONVIF analytics).
27828
- *
27829
- * Native-motion providers also fan out to `detection.camera-native`
27830
- * with `source: 'onboard'` so cross-cutting system services
27831
- * (alert-center, advanced-notifier) can subscribe once and receive
27832
- * motion from every camera.
27833
- */
27834
27943
  var MotionStatusSchema = object({
27835
27944
  detected: boolean(),
27836
27945
  /** Ms epoch of the last detected-true observation. Null if never detected. */
27837
27946
  lastDetectedAt: number().nullable(),
27838
27947
  /**
27839
- * Ms after which `detected` auto-reverts to false if no fresh push
27840
- * arrives. Null means the provider leaves detected state until a
27841
- * native "clear" event.
27948
+ * `MOTION_CLOSE_AFTER_MS` while `detected: true` on a `Camera` device,
27949
+ * `null` while false and on every `Sensor` device (D475) — see that
27950
+ * constant's doc for the one-authority rule.
27951
+ *
27952
+ * ## Reading this field still arms nothing
27953
+ *
27954
+ * It reads like an instruction to the consumer ("revert after N ms if
27955
+ * no fresh push arrives") and it is not one: nothing in this repo reads
27956
+ * the LIVE cap value to drive a timer. `pipeline-analytics`'s motion-episode
27957
+ * close DOES now use the same number — `MOTION_CLOSE_AFTER_MS` — but as an
27958
+ * imported constant, not as a read of `device.state.motion.value`, so this
27959
+ * field stays what it always was: DESCRIPTIVE output, mirroring an answer
27960
+ * computed elsewhere. Building a self-clear timer out of a READ of this
27961
+ * field would add a second falling-edge authority beside whichever one
27962
+ * already owns the device, and two that can disagree are worse than one.
27963
+ * Consumers that need a falling edge SHAPED differently — held open across
27964
+ * a flapping source — debounce on their own side and say so, as
27965
+ * `addon-export-alexa/src/motion-clear-hold.ts` and
27966
+ * `addon-export-hap`'s `RESET_DEBOUNCE_MS` both do.
27967
+ *
27968
+ * ## Who writes it
27969
+ *
27970
+ * - **Cameras** — the runner's phase machine, `active → watching` on
27971
+ * `cooldown_expired`, which then writes this slice with
27972
+ * `detected: false` (`handlePhaseChanged` in
27973
+ * `pipeline-runner/index.ts`). It produces the FALLING edge, which
27974
+ * matters most for the sources that only ever push a rising one:
27975
+ * Reolink emits `MotionOnMotionChanged { detected: true }` and never
27976
+ * a false.
27977
+ * - **Sensors** (Home Assistant binary sensors, Homematic) — the
27978
+ * provider pushes the false itself, from the upstream system's own
27979
+ * state change. No phase machine is involved.
27980
+ *
27981
+ * ### The phase machine is CANONICAL, not sole — and that is a defect
27982
+ *
27983
+ * An earlier revision of this docblock (mine, 2026-09-12) claimed the
27984
+ * phase machine is the sole writer for a camera. It is not.
27985
+ * `hikvision-camera.ts:3464` and `amcrest-camera.ts:445` both call
27986
+ * `setCapSlice(motionCapability, …)` on their own rising edge, and
27987
+ * Hikvision's comment says why: it read THIS docblock, agreed the
27988
+ * runner is canonical, and wrote anyway to avoid per-tick churn. So
27989
+ * two authorities can disagree about one slice, which this repo
27990
+ * forbids, and the doc said otherwise — which is worse than saying
27991
+ * nothing, because it reads as verification.
27992
+ *
27993
+ * This predates D475 and is not fixed there: the fix touches every
27994
+ * camera provider. Recorded in D475's Consequences. Do not restore the
27995
+ * "sole writer" wording without also removing the other writers.
27842
27996
  */
27843
27997
  autoClearAfterMs: number().nullable()
27844
27998
  });
@@ -27908,7 +28062,11 @@ onMotionChanged: { data: MotionOnMotionChangedDataSchema } },
27908
28062
  */
27909
28063
  runtimeState: MotionStatusSchema,
27910
28064
  /**
27911
- * 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.
28065
+ * Runtime-state durability: **session** — every writer of this slice
28066
+ * writes only on an EDGE, so a restored `detected: true` would stay
28067
+ * frozen until the next one instead of being corrected. The next edge
28068
+ * re-publishes the real state. (On who the writers are, and why there
28069
+ * is more than one, see `autoClearAfterMs` above.)
27912
28070
  *
27913
28071
  * See `RuntimeStateDurability`. Enforced by
27914
28072
  * `scripts/check-runtime-state-durability.ts`.
package/dist/addon.mjs CHANGED
@@ -7305,6 +7305,23 @@ function errMsg(err) {
7305
7305
  if (typeof err === "string") return err;
7306
7306
  return String(err);
7307
7307
  }
7308
+ new Set([
7309
+ "track",
7310
+ "summary",
7311
+ "face",
7312
+ "identity",
7313
+ "plate",
7314
+ "vehicle",
7315
+ "scene",
7316
+ "motion",
7317
+ "object",
7318
+ "audio"
7319
+ ]);
7320
+ new Set([
7321
+ "motion",
7322
+ "object",
7323
+ "audio"
7324
+ ]);
7308
7325
  var EncodeProfileSchema = object({
7309
7326
  video: object({
7310
7327
  codec: _enum([
@@ -12505,8 +12522,17 @@ method(object({
12505
12522
  }), array(SettingsRecordSchema).readonly()), method(object({
12506
12523
  namespace: string().optional(),
12507
12524
  collection: string(),
12508
- record: SettingsRecordSchema
12509
- }), _void(), { kind: "mutation" }), method(object({
12525
+ record: object({
12526
+ id: string().optional(),
12527
+ data: record(string(), unknown())
12528
+ })
12529
+ }), object({
12530
+ /**
12531
+ * The id the row ACTUALLY got (D473): the one supplied, the UUID minted
12532
+ * for an absent one, or the ROWID SQLite assigned on an `INTEGER`
12533
+ * primary key — which is the only place an auto key is knowable.
12534
+ */
12535
+ id: union([string(), number()]) }), { kind: "mutation" }), method(object({
12510
12536
  namespace: string().optional(),
12511
12537
  collection: string(),
12512
12538
  records: array(BulkRecordSchema).readonly()
@@ -12615,8 +12641,17 @@ method(_void(), EngineInfoSchema, { auth: "admin" }), method(object({
12615
12641
  }), array(SettingsRecordSchema).readonly(), { auth: "admin" }), method(object({
12616
12642
  namespace: string().optional(),
12617
12643
  collection: string(),
12618
- record: SettingsRecordSchema
12619
- }), _void(), {
12644
+ record: object({
12645
+ id: string().optional(),
12646
+ data: record(string(), unknown())
12647
+ })
12648
+ }), object({
12649
+ /**
12650
+ * The id the row ACTUALLY got (D473): the one supplied, the UUID minted
12651
+ * for an absent one, or the ROWID SQLite assigned on an `INTEGER`
12652
+ * primary key — which is the only place an auto key is knowable.
12653
+ */
12654
+ id: union([string(), number()]) }), {
12620
12655
  kind: "mutation",
12621
12656
  auth: "admin"
12622
12657
  }), method(object({
@@ -19524,7 +19559,20 @@ var TrackSchema = object({
19524
19559
  ...TrackRetrainFields
19525
19560
  });
19526
19561
  var BaseEventFields = {
19527
- id: string(),
19562
+ /**
19563
+ * A SQLite ROWID, assigned by the database (D474).
19564
+ *
19565
+ * Was a 36-character UUID and cost 263 MB of a 1 117 MB database — paid
19566
+ * TWICE per row, in the row and in the primary-key index, across 2.1 million
19567
+ * motion, audio and object events. An `INTEGER PRIMARY KEY` in SQLite **is**
19568
+ * the rowid: the table itself is that B-tree, so the index stops existing
19569
+ * rather than getting smaller. No shorter string does that.
19570
+ *
19571
+ * Defined once here for all three event kinds, which is why they move
19572
+ * together: a per-table migration would have forked this and
19573
+ * `COMMON_BASE_COLUMNS` and reunited them two stages later.
19574
+ */
19575
+ id: number().int(),
19528
19576
  deviceId: number(),
19529
19577
  timestamp: number()
19530
19578
  };
@@ -19543,7 +19591,34 @@ var MotionEventSchema = object({
19543
19591
  /** Omitted in slim projection. */
19544
19592
  frameHeight: number().optional(),
19545
19593
  /** Populated by B5 (recording playback URL for this event). */
19546
- mediaUrl: string().optional()
19594
+ mediaUrl: string().optional(),
19595
+ /**
19596
+ * One row per motion EPISODE, not one per push (D475). `null` while the
19597
+ * episode is still open — a further rising edge extends it in place rather
19598
+ * than inserting a new row. Set once, at close, to `lastOnAt - startedAt`
19599
+ * (the span from the first rising edge to the LAST one, deliberately NOT
19600
+ * `closedAt - startedAt` — the close delay is a quiet CONFIRMATION, not
19601
+ * movement, and folding it in would report `MOTION_CLOSE_AFTER_MS` of
19602
+ * motion for an instantaneous trigger).
19603
+ *
19604
+ * **Absent** (not merely `null`) on a row written before D475 — that means
19605
+ * "closed the old way, before this column existed", never "still open".
19606
+ * Nothing in this codebase may read an absent `durationMs` as an open
19607
+ * episode; only `null` means open.
19608
+ */
19609
+ durationMs: number().nullable().optional(),
19610
+ /**
19611
+ * Ms offsets from `timestamp` (the episode's own first rising edge, so the
19612
+ * first entry is always `0`) of every genuine off→on transition the
19613
+ * episode saw — "ogni evento on si deve salvare" (D475). NOT one entry per
19614
+ * push: a firmware source that keepalives at ~1 Hz for the whole burst
19615
+ * (Reolink, Hikvision) produces exactly one edge; a source that reports an
19616
+ * explicit `false` mid-episode and then resumes before the quiet window
19617
+ * elapses produces another. Stored compactly — see `motion-edge-codec.ts`
19618
+ * — and decoded back to this shape on read. Absent/empty on a legacy row,
19619
+ * which must never be read as "no episode happened here".
19620
+ */
19621
+ edges: array(number()).readonly().optional()
19547
19622
  });
19548
19623
  /**
19549
19624
  * Which detection SOURCE produced an object event. `pipeline` = the ML
@@ -19598,6 +19673,23 @@ var ObjectEventSchema = object({
19598
19673
  * includes it (it is light). Absent on rows written before this field.
19599
19674
  */
19600
19675
  frameId: string().optional(),
19676
+ /**
19677
+ * A PRODUCER-chosen key that makes a synthetic event's emission idempotent
19678
+ * (D474).
19679
+ *
19680
+ * Only the package detector writes it, and it exists because the event id
19681
+ * stopped being choosable: the delivery and pick-up rows used to BE their
19682
+ * dedupe key (`pa-pkg-<entryId>-delivered`), which is how "never emit a
19683
+ * second delivery for this entry" survived a restart. An `INTEGER` rowid is
19684
+ * assigned by SQLite, so that key had to move off the primary key rather
19685
+ * than be dropped — a detector that cannot recognise its own row re-delivers
19686
+ * every parcel on every boot.
19687
+ *
19688
+ * Absent on every other object event, and on every row written before this
19689
+ * field. Never a substitute for `id`: it is unique per (producer, occasion),
19690
+ * not per row, and nothing addresses a row by it.
19691
+ */
19692
+ idempotencyKey: string().optional(),
19601
19693
  /** Omitted in slim projection. */
19602
19694
  trackId: string().optional(),
19603
19695
  className: string(),
@@ -20700,6 +20792,14 @@ var NativeCropResultSchema = object({
20700
20792
  * set `encodeJpeg: true`; `bytes` is then absent.
20701
20793
  */
20702
20794
  jpeg: string().optional(),
20795
+ /**
20796
+ * The SAME compressed JPEG as `jpeg`, as bytes (D462). Present instead of
20797
+ * `jpeg` when the request set `acceptJpegBytes`; a request that did not gets
20798
+ * `jpeg` exactly as before. MsgPack and the mesh leg both carry binary —
20799
+ * `bytes` above has crossed this boundary as a `Uint8Array` all along — so
20800
+ * base64 was buying nothing but a multi-megabyte string in the relay's heap.
20801
+ */
20802
+ jpegBytes: _instanceof(Uint8Array).optional(),
20703
20803
  width: number().int().positive(),
20704
20804
  height: number().int().positive(),
20705
20805
  /**
@@ -20766,7 +20866,14 @@ var ParkTrackFrameResultSchema = discriminatedUnion("parked", [object({
20766
20866
  })]);
20767
20867
  /** A retrieved parcel — the runner's own JPEG, base64 for the wire. */
20768
20868
  var ParkedTrackFrameSchema = object({
20769
- jpeg: string(),
20869
+ /**
20870
+ * Base64 JPEG — the pre-D462 wire. OPTIONAL since D462: a request that set
20871
+ * `acceptJpegBytes` is answered in `jpegBytes` and this is then absent.
20872
+ * Exactly one of the two is present.
20873
+ */
20874
+ jpeg: string().optional(),
20875
+ /** The same JPEG as bytes, for a caller that declared it reads them (D462). */
20876
+ jpegBytes: _instanceof(Uint8Array).optional(),
20770
20877
  width: number().int().positive(),
20771
20878
  height: number().int().positive(),
20772
20879
  /** The frame instant the parcel shows (the caller's clock, echoed back). */
@@ -21353,6 +21460,13 @@ method(RunnerCameraConfigSchema, object({ success: literal(true) }), { kind: "mu
21353
21460
  bbox: NativeCropBboxSchema,
21354
21461
  maxWidth: number().int().positive().optional(),
21355
21462
  /**
21463
+ * The caller reads a `Uint8Array` (D462). When set, a JPEG answer comes
21464
+ * back in `jpegBytes` instead of base64 `jpeg`. Absent means the old
21465
+ * wire — never assume consent: a pre-D462 caller parses the field as
21466
+ * base64 and bytes would decode to garbage rather than fail.
21467
+ */
21468
+ acceptJpegBytes: boolean().optional(),
21469
+ /**
21356
21470
  * When `true`, the runner encodes the resolved crop to JPEG ON THE
21357
21471
  * OWNING NODE and returns it in `jpeg` (base64) INSTEAD of raw `bytes`.
21358
21472
  * Callers set this for CROSS-NODE fetches (`handle.nodeId` is a remote
@@ -21420,7 +21534,14 @@ method(RunnerCameraConfigSchema, object({ success: literal(true) }), { kind: "mu
21420
21534
  }), ParkTrackFrameResultSchema, { kind: "mutation" }), method(object({
21421
21535
  deviceId: number(),
21422
21536
  trackId: string(),
21423
- kind: ParkedFrameKindSchema
21537
+ kind: ParkedFrameKindSchema,
21538
+ /**
21539
+ * The caller reads a `Uint8Array` (D462). When set, a JPEG answer comes
21540
+ * back in `jpegBytes` instead of base64 `jpeg`. Absent means the old
21541
+ * wire — never assume consent: a pre-D462 caller parses the field as
21542
+ * base64 and bytes would decode to garbage rather than fail.
21543
+ */
21544
+ acceptJpegBytes: boolean().optional()
21424
21545
  }), ParkedTrackFrameSchema.nullable()), method(object({
21425
21546
  deviceId: number(),
21426
21547
  trackId: string()
@@ -27818,26 +27939,59 @@ authKey: string().optional() }), object({
27818
27939
  /** Human-readable error when `ok: false`. */
27819
27940
  error: string().optional()
27820
27941
  }), { kind: "mutation" });
27821
- /**
27822
- * Hardware / firmware motion sensor cap — binary detected state plus
27823
- * a timestamp of the last observation. Distinct from
27824
- * `motion-detection.cap.ts` which owns the LOCAL ML motion pipeline;
27825
- * `motion` is the lightweight readout from on-camera motion (Reolink
27826
- * `GetMdState`, Baichuan push `type: motion`, ONVIF analytics).
27827
- *
27828
- * Native-motion providers also fan out to `detection.camera-native`
27829
- * with `source: 'onboard'` so cross-cutting system services
27830
- * (alert-center, advanced-notifier) can subscribe once and receive
27831
- * motion from every camera.
27832
- */
27833
27942
  var MotionStatusSchema = object({
27834
27943
  detected: boolean(),
27835
27944
  /** Ms epoch of the last detected-true observation. Null if never detected. */
27836
27945
  lastDetectedAt: number().nullable(),
27837
27946
  /**
27838
- * Ms after which `detected` auto-reverts to false if no fresh push
27839
- * arrives. Null means the provider leaves detected state until a
27840
- * native "clear" event.
27947
+ * `MOTION_CLOSE_AFTER_MS` while `detected: true` on a `Camera` device,
27948
+ * `null` while false and on every `Sensor` device (D475) — see that
27949
+ * constant's doc for the one-authority rule.
27950
+ *
27951
+ * ## Reading this field still arms nothing
27952
+ *
27953
+ * It reads like an instruction to the consumer ("revert after N ms if
27954
+ * no fresh push arrives") and it is not one: nothing in this repo reads
27955
+ * the LIVE cap value to drive a timer. `pipeline-analytics`'s motion-episode
27956
+ * close DOES now use the same number — `MOTION_CLOSE_AFTER_MS` — but as an
27957
+ * imported constant, not as a read of `device.state.motion.value`, so this
27958
+ * field stays what it always was: DESCRIPTIVE output, mirroring an answer
27959
+ * computed elsewhere. Building a self-clear timer out of a READ of this
27960
+ * field would add a second falling-edge authority beside whichever one
27961
+ * already owns the device, and two that can disagree are worse than one.
27962
+ * Consumers that need a falling edge SHAPED differently — held open across
27963
+ * a flapping source — debounce on their own side and say so, as
27964
+ * `addon-export-alexa/src/motion-clear-hold.ts` and
27965
+ * `addon-export-hap`'s `RESET_DEBOUNCE_MS` both do.
27966
+ *
27967
+ * ## Who writes it
27968
+ *
27969
+ * - **Cameras** — the runner's phase machine, `active → watching` on
27970
+ * `cooldown_expired`, which then writes this slice with
27971
+ * `detected: false` (`handlePhaseChanged` in
27972
+ * `pipeline-runner/index.ts`). It produces the FALLING edge, which
27973
+ * matters most for the sources that only ever push a rising one:
27974
+ * Reolink emits `MotionOnMotionChanged { detected: true }` and never
27975
+ * a false.
27976
+ * - **Sensors** (Home Assistant binary sensors, Homematic) — the
27977
+ * provider pushes the false itself, from the upstream system's own
27978
+ * state change. No phase machine is involved.
27979
+ *
27980
+ * ### The phase machine is CANONICAL, not sole — and that is a defect
27981
+ *
27982
+ * An earlier revision of this docblock (mine, 2026-09-12) claimed the
27983
+ * phase machine is the sole writer for a camera. It is not.
27984
+ * `hikvision-camera.ts:3464` and `amcrest-camera.ts:445` both call
27985
+ * `setCapSlice(motionCapability, …)` on their own rising edge, and
27986
+ * Hikvision's comment says why: it read THIS docblock, agreed the
27987
+ * runner is canonical, and wrote anyway to avoid per-tick churn. So
27988
+ * two authorities can disagree about one slice, which this repo
27989
+ * forbids, and the doc said otherwise — which is worse than saying
27990
+ * nothing, because it reads as verification.
27991
+ *
27992
+ * This predates D475 and is not fixed there: the fix touches every
27993
+ * camera provider. Recorded in D475's Consequences. Do not restore the
27994
+ * "sole writer" wording without also removing the other writers.
27841
27995
  */
27842
27996
  autoClearAfterMs: number().nullable()
27843
27997
  });
@@ -27907,7 +28061,11 @@ onMotionChanged: { data: MotionOnMotionChangedDataSchema } },
27907
28061
  */
27908
28062
  runtimeState: MotionStatusSchema,
27909
28063
  /**
27910
- * 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.
28064
+ * Runtime-state durability: **session** — every writer of this slice
28065
+ * writes only on an EDGE, so a restored `detected: true` would stay
28066
+ * frozen until the next one instead of being corrected. The next edge
28067
+ * re-publishes the real state. (On who the writers are, and why there
28068
+ * is more than one, see `autoClearAfterMs` above.)
27911
28069
  *
27912
28070
  * See `RuntimeStateDurability`. Enforced by
27913
28071
  * `scripts/check-runtime-state-durability.ts`.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@camstack/addon-provider-gree",
3
- "version": "0.2.95",
3
+ "version": "0.2.97",
4
4
  "description": "Gree air-conditioner device-provider addon for CamStack — wraps the @apocaliss92/nodegree local-UDP client (LAN discovery + AES control), exposing climate-control and fan-control",
5
5
  "keywords": [
6
6
  "camstack",