@camstack/addon-provider-petkit 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
@@ -8405,6 +8405,23 @@ function errMsg(err) {
8405
8405
  if (typeof err === "string") return err;
8406
8406
  return String(err);
8407
8407
  }
8408
+ new Set([
8409
+ "track",
8410
+ "summary",
8411
+ "face",
8412
+ "identity",
8413
+ "plate",
8414
+ "vehicle",
8415
+ "scene",
8416
+ "motion",
8417
+ "object",
8418
+ "audio"
8419
+ ]);
8420
+ new Set([
8421
+ "motion",
8422
+ "object",
8423
+ "audio"
8424
+ ]);
8408
8425
  var EncodeProfileSchema = object({
8409
8426
  video: object({
8410
8427
  codec: _enum([
@@ -13605,8 +13622,17 @@ method(object({
13605
13622
  }), array(SettingsRecordSchema).readonly()), method(object({
13606
13623
  namespace: string().optional(),
13607
13624
  collection: string(),
13608
- record: SettingsRecordSchema
13609
- }), _void(), { kind: "mutation" }), method(object({
13625
+ record: object({
13626
+ id: string().optional(),
13627
+ data: record(string(), unknown())
13628
+ })
13629
+ }), object({
13630
+ /**
13631
+ * The id the row ACTUALLY got (D473): the one supplied, the UUID minted
13632
+ * for an absent one, or the ROWID SQLite assigned on an `INTEGER`
13633
+ * primary key — which is the only place an auto key is knowable.
13634
+ */
13635
+ id: union([string(), number()]) }), { kind: "mutation" }), method(object({
13610
13636
  namespace: string().optional(),
13611
13637
  collection: string(),
13612
13638
  records: array(BulkRecordSchema).readonly()
@@ -13715,8 +13741,17 @@ method(_void(), EngineInfoSchema, { auth: "admin" }), method(object({
13715
13741
  }), array(SettingsRecordSchema).readonly(), { auth: "admin" }), method(object({
13716
13742
  namespace: string().optional(),
13717
13743
  collection: string(),
13718
- record: SettingsRecordSchema
13719
- }), _void(), {
13744
+ record: object({
13745
+ id: string().optional(),
13746
+ data: record(string(), unknown())
13747
+ })
13748
+ }), object({
13749
+ /**
13750
+ * The id the row ACTUALLY got (D473): the one supplied, the UUID minted
13751
+ * for an absent one, or the ROWID SQLite assigned on an `INTEGER`
13752
+ * primary key — which is the only place an auto key is knowable.
13753
+ */
13754
+ id: union([string(), number()]) }), {
13720
13755
  kind: "mutation",
13721
13756
  auth: "admin"
13722
13757
  }), method(object({
@@ -20641,7 +20676,20 @@ var TrackSchema = object({
20641
20676
  ...TrackRetrainFields
20642
20677
  });
20643
20678
  var BaseEventFields = {
20644
- id: string(),
20679
+ /**
20680
+ * A SQLite ROWID, assigned by the database (D474).
20681
+ *
20682
+ * Was a 36-character UUID and cost 263 MB of a 1 117 MB database — paid
20683
+ * TWICE per row, in the row and in the primary-key index, across 2.1 million
20684
+ * motion, audio and object events. An `INTEGER PRIMARY KEY` in SQLite **is**
20685
+ * the rowid: the table itself is that B-tree, so the index stops existing
20686
+ * rather than getting smaller. No shorter string does that.
20687
+ *
20688
+ * Defined once here for all three event kinds, which is why they move
20689
+ * together: a per-table migration would have forked this and
20690
+ * `COMMON_BASE_COLUMNS` and reunited them two stages later.
20691
+ */
20692
+ id: number().int(),
20645
20693
  deviceId: number(),
20646
20694
  timestamp: number()
20647
20695
  };
@@ -20660,7 +20708,34 @@ var MotionEventSchema = object({
20660
20708
  /** Omitted in slim projection. */
20661
20709
  frameHeight: number().optional(),
20662
20710
  /** Populated by B5 (recording playback URL for this event). */
20663
- mediaUrl: string().optional()
20711
+ mediaUrl: string().optional(),
20712
+ /**
20713
+ * One row per motion EPISODE, not one per push (D475). `null` while the
20714
+ * episode is still open — a further rising edge extends it in place rather
20715
+ * than inserting a new row. Set once, at close, to `lastOnAt - startedAt`
20716
+ * (the span from the first rising edge to the LAST one, deliberately NOT
20717
+ * `closedAt - startedAt` — the close delay is a quiet CONFIRMATION, not
20718
+ * movement, and folding it in would report `MOTION_CLOSE_AFTER_MS` of
20719
+ * motion for an instantaneous trigger).
20720
+ *
20721
+ * **Absent** (not merely `null`) on a row written before D475 — that means
20722
+ * "closed the old way, before this column existed", never "still open".
20723
+ * Nothing in this codebase may read an absent `durationMs` as an open
20724
+ * episode; only `null` means open.
20725
+ */
20726
+ durationMs: number().nullable().optional(),
20727
+ /**
20728
+ * Ms offsets from `timestamp` (the episode's own first rising edge, so the
20729
+ * first entry is always `0`) of every genuine off→on transition the
20730
+ * episode saw — "ogni evento on si deve salvare" (D475). NOT one entry per
20731
+ * push: a firmware source that keepalives at ~1 Hz for the whole burst
20732
+ * (Reolink, Hikvision) produces exactly one edge; a source that reports an
20733
+ * explicit `false` mid-episode and then resumes before the quiet window
20734
+ * elapses produces another. Stored compactly — see `motion-edge-codec.ts`
20735
+ * — and decoded back to this shape on read. Absent/empty on a legacy row,
20736
+ * which must never be read as "no episode happened here".
20737
+ */
20738
+ edges: array(number()).readonly().optional()
20664
20739
  });
20665
20740
  /**
20666
20741
  * Which detection SOURCE produced an object event. `pipeline` = the ML
@@ -20715,6 +20790,23 @@ var ObjectEventSchema = object({
20715
20790
  * includes it (it is light). Absent on rows written before this field.
20716
20791
  */
20717
20792
  frameId: string().optional(),
20793
+ /**
20794
+ * A PRODUCER-chosen key that makes a synthetic event's emission idempotent
20795
+ * (D474).
20796
+ *
20797
+ * Only the package detector writes it, and it exists because the event id
20798
+ * stopped being choosable: the delivery and pick-up rows used to BE their
20799
+ * dedupe key (`pa-pkg-<entryId>-delivered`), which is how "never emit a
20800
+ * second delivery for this entry" survived a restart. An `INTEGER` rowid is
20801
+ * assigned by SQLite, so that key had to move off the primary key rather
20802
+ * than be dropped — a detector that cannot recognise its own row re-delivers
20803
+ * every parcel on every boot.
20804
+ *
20805
+ * Absent on every other object event, and on every row written before this
20806
+ * field. Never a substitute for `id`: it is unique per (producer, occasion),
20807
+ * not per row, and nothing addresses a row by it.
20808
+ */
20809
+ idempotencyKey: string().optional(),
20718
20810
  /** Omitted in slim projection. */
20719
20811
  trackId: string().optional(),
20720
20812
  className: string(),
@@ -21817,6 +21909,14 @@ var NativeCropResultSchema = object({
21817
21909
  * set `encodeJpeg: true`; `bytes` is then absent.
21818
21910
  */
21819
21911
  jpeg: string().optional(),
21912
+ /**
21913
+ * The SAME compressed JPEG as `jpeg`, as bytes (D462). Present instead of
21914
+ * `jpeg` when the request set `acceptJpegBytes`; a request that did not gets
21915
+ * `jpeg` exactly as before. MsgPack and the mesh leg both carry binary —
21916
+ * `bytes` above has crossed this boundary as a `Uint8Array` all along — so
21917
+ * base64 was buying nothing but a multi-megabyte string in the relay's heap.
21918
+ */
21919
+ jpegBytes: _instanceof(Uint8Array).optional(),
21820
21920
  width: number().int().positive(),
21821
21921
  height: number().int().positive(),
21822
21922
  /**
@@ -21883,7 +21983,14 @@ var ParkTrackFrameResultSchema = discriminatedUnion("parked", [object({
21883
21983
  })]);
21884
21984
  /** A retrieved parcel — the runner's own JPEG, base64 for the wire. */
21885
21985
  var ParkedTrackFrameSchema = object({
21886
- jpeg: string(),
21986
+ /**
21987
+ * Base64 JPEG — the pre-D462 wire. OPTIONAL since D462: a request that set
21988
+ * `acceptJpegBytes` is answered in `jpegBytes` and this is then absent.
21989
+ * Exactly one of the two is present.
21990
+ */
21991
+ jpeg: string().optional(),
21992
+ /** The same JPEG as bytes, for a caller that declared it reads them (D462). */
21993
+ jpegBytes: _instanceof(Uint8Array).optional(),
21887
21994
  width: number().int().positive(),
21888
21995
  height: number().int().positive(),
21889
21996
  /** The frame instant the parcel shows (the caller's clock, echoed back). */
@@ -22470,6 +22577,13 @@ method(RunnerCameraConfigSchema, object({ success: literal(true) }), { kind: "mu
22470
22577
  bbox: NativeCropBboxSchema,
22471
22578
  maxWidth: number().int().positive().optional(),
22472
22579
  /**
22580
+ * The caller reads a `Uint8Array` (D462). When set, a JPEG answer comes
22581
+ * back in `jpegBytes` instead of base64 `jpeg`. Absent means the old
22582
+ * wire — never assume consent: a pre-D462 caller parses the field as
22583
+ * base64 and bytes would decode to garbage rather than fail.
22584
+ */
22585
+ acceptJpegBytes: boolean().optional(),
22586
+ /**
22473
22587
  * When `true`, the runner encodes the resolved crop to JPEG ON THE
22474
22588
  * OWNING NODE and returns it in `jpeg` (base64) INSTEAD of raw `bytes`.
22475
22589
  * Callers set this for CROSS-NODE fetches (`handle.nodeId` is a remote
@@ -22537,7 +22651,14 @@ method(RunnerCameraConfigSchema, object({ success: literal(true) }), { kind: "mu
22537
22651
  }), ParkTrackFrameResultSchema, { kind: "mutation" }), method(object({
22538
22652
  deviceId: number(),
22539
22653
  trackId: string(),
22540
- kind: ParkedFrameKindSchema
22654
+ kind: ParkedFrameKindSchema,
22655
+ /**
22656
+ * The caller reads a `Uint8Array` (D462). When set, a JPEG answer comes
22657
+ * back in `jpegBytes` instead of base64 `jpeg`. Absent means the old
22658
+ * wire — never assume consent: a pre-D462 caller parses the field as
22659
+ * base64 and bytes would decode to garbage rather than fail.
22660
+ */
22661
+ acceptJpegBytes: boolean().optional()
22541
22662
  }), ParkedTrackFrameSchema.nullable()), method(object({
22542
22663
  deviceId: number(),
22543
22664
  trackId: string()
@@ -28943,26 +29064,59 @@ authKey: string().optional() }), object({
28943
29064
  /** Human-readable error when `ok: false`. */
28944
29065
  error: string().optional()
28945
29066
  }), { kind: "mutation" });
28946
- /**
28947
- * Hardware / firmware motion sensor cap — binary detected state plus
28948
- * a timestamp of the last observation. Distinct from
28949
- * `motion-detection.cap.ts` which owns the LOCAL ML motion pipeline;
28950
- * `motion` is the lightweight readout from on-camera motion (Reolink
28951
- * `GetMdState`, Baichuan push `type: motion`, ONVIF analytics).
28952
- *
28953
- * Native-motion providers also fan out to `detection.camera-native`
28954
- * with `source: 'onboard'` so cross-cutting system services
28955
- * (alert-center, advanced-notifier) can subscribe once and receive
28956
- * motion from every camera.
28957
- */
28958
29067
  var MotionStatusSchema = object({
28959
29068
  detected: boolean(),
28960
29069
  /** Ms epoch of the last detected-true observation. Null if never detected. */
28961
29070
  lastDetectedAt: number().nullable(),
28962
29071
  /**
28963
- * Ms after which `detected` auto-reverts to false if no fresh push
28964
- * arrives. Null means the provider leaves detected state until a
28965
- * native "clear" event.
29072
+ * `MOTION_CLOSE_AFTER_MS` while `detected: true` on a `Camera` device,
29073
+ * `null` while false and on every `Sensor` device (D475) — see that
29074
+ * constant's doc for the one-authority rule.
29075
+ *
29076
+ * ## Reading this field still arms nothing
29077
+ *
29078
+ * It reads like an instruction to the consumer ("revert after N ms if
29079
+ * no fresh push arrives") and it is not one: nothing in this repo reads
29080
+ * the LIVE cap value to drive a timer. `pipeline-analytics`'s motion-episode
29081
+ * close DOES now use the same number — `MOTION_CLOSE_AFTER_MS` — but as an
29082
+ * imported constant, not as a read of `device.state.motion.value`, so this
29083
+ * field stays what it always was: DESCRIPTIVE output, mirroring an answer
29084
+ * computed elsewhere. Building a self-clear timer out of a READ of this
29085
+ * field would add a second falling-edge authority beside whichever one
29086
+ * already owns the device, and two that can disagree are worse than one.
29087
+ * Consumers that need a falling edge SHAPED differently — held open across
29088
+ * a flapping source — debounce on their own side and say so, as
29089
+ * `addon-export-alexa/src/motion-clear-hold.ts` and
29090
+ * `addon-export-hap`'s `RESET_DEBOUNCE_MS` both do.
29091
+ *
29092
+ * ## Who writes it
29093
+ *
29094
+ * - **Cameras** — the runner's phase machine, `active → watching` on
29095
+ * `cooldown_expired`, which then writes this slice with
29096
+ * `detected: false` (`handlePhaseChanged` in
29097
+ * `pipeline-runner/index.ts`). It produces the FALLING edge, which
29098
+ * matters most for the sources that only ever push a rising one:
29099
+ * Reolink emits `MotionOnMotionChanged { detected: true }` and never
29100
+ * a false.
29101
+ * - **Sensors** (Home Assistant binary sensors, Homematic) — the
29102
+ * provider pushes the false itself, from the upstream system's own
29103
+ * state change. No phase machine is involved.
29104
+ *
29105
+ * ### The phase machine is CANONICAL, not sole — and that is a defect
29106
+ *
29107
+ * An earlier revision of this docblock (mine, 2026-09-12) claimed the
29108
+ * phase machine is the sole writer for a camera. It is not.
29109
+ * `hikvision-camera.ts:3464` and `amcrest-camera.ts:445` both call
29110
+ * `setCapSlice(motionCapability, …)` on their own rising edge, and
29111
+ * Hikvision's comment says why: it read THIS docblock, agreed the
29112
+ * runner is canonical, and wrote anyway to avoid per-tick churn. So
29113
+ * two authorities can disagree about one slice, which this repo
29114
+ * forbids, and the doc said otherwise — which is worse than saying
29115
+ * nothing, because it reads as verification.
29116
+ *
29117
+ * This predates D475 and is not fixed there: the fix touches every
29118
+ * camera provider. Recorded in D475's Consequences. Do not restore the
29119
+ * "sole writer" wording without also removing the other writers.
28966
29120
  */
28967
29121
  autoClearAfterMs: number().nullable()
28968
29122
  });
@@ -29032,7 +29186,11 @@ onMotionChanged: { data: MotionOnMotionChangedDataSchema } },
29032
29186
  */
29033
29187
  runtimeState: MotionStatusSchema,
29034
29188
  /**
29035
- * 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.
29189
+ * Runtime-state durability: **session** — every writer of this slice
29190
+ * writes only on an EDGE, so a restored `detected: true` would stay
29191
+ * frozen until the next one instead of being corrected. The next edge
29192
+ * re-publishes the real state. (On who the writers are, and why there
29193
+ * is more than one, see `autoClearAfterMs` above.)
29036
29194
  *
29037
29195
  * See `RuntimeStateDurability`. Enforced by
29038
29196
  * `scripts/check-runtime-state-durability.ts`.
package/dist/addon.mjs CHANGED
@@ -8404,6 +8404,23 @@ function errMsg(err) {
8404
8404
  if (typeof err === "string") return err;
8405
8405
  return String(err);
8406
8406
  }
8407
+ new Set([
8408
+ "track",
8409
+ "summary",
8410
+ "face",
8411
+ "identity",
8412
+ "plate",
8413
+ "vehicle",
8414
+ "scene",
8415
+ "motion",
8416
+ "object",
8417
+ "audio"
8418
+ ]);
8419
+ new Set([
8420
+ "motion",
8421
+ "object",
8422
+ "audio"
8423
+ ]);
8407
8424
  var EncodeProfileSchema = object({
8408
8425
  video: object({
8409
8426
  codec: _enum([
@@ -13604,8 +13621,17 @@ method(object({
13604
13621
  }), array(SettingsRecordSchema).readonly()), method(object({
13605
13622
  namespace: string().optional(),
13606
13623
  collection: string(),
13607
- record: SettingsRecordSchema
13608
- }), _void(), { kind: "mutation" }), method(object({
13624
+ record: object({
13625
+ id: string().optional(),
13626
+ data: record(string(), unknown())
13627
+ })
13628
+ }), object({
13629
+ /**
13630
+ * The id the row ACTUALLY got (D473): the one supplied, the UUID minted
13631
+ * for an absent one, or the ROWID SQLite assigned on an `INTEGER`
13632
+ * primary key — which is the only place an auto key is knowable.
13633
+ */
13634
+ id: union([string(), number()]) }), { kind: "mutation" }), method(object({
13609
13635
  namespace: string().optional(),
13610
13636
  collection: string(),
13611
13637
  records: array(BulkRecordSchema).readonly()
@@ -13714,8 +13740,17 @@ method(_void(), EngineInfoSchema, { auth: "admin" }), method(object({
13714
13740
  }), array(SettingsRecordSchema).readonly(), { auth: "admin" }), method(object({
13715
13741
  namespace: string().optional(),
13716
13742
  collection: string(),
13717
- record: SettingsRecordSchema
13718
- }), _void(), {
13743
+ record: object({
13744
+ id: string().optional(),
13745
+ data: record(string(), unknown())
13746
+ })
13747
+ }), object({
13748
+ /**
13749
+ * The id the row ACTUALLY got (D473): the one supplied, the UUID minted
13750
+ * for an absent one, or the ROWID SQLite assigned on an `INTEGER`
13751
+ * primary key — which is the only place an auto key is knowable.
13752
+ */
13753
+ id: union([string(), number()]) }), {
13719
13754
  kind: "mutation",
13720
13755
  auth: "admin"
13721
13756
  }), method(object({
@@ -20640,7 +20675,20 @@ var TrackSchema = object({
20640
20675
  ...TrackRetrainFields
20641
20676
  });
20642
20677
  var BaseEventFields = {
20643
- id: string(),
20678
+ /**
20679
+ * A SQLite ROWID, assigned by the database (D474).
20680
+ *
20681
+ * Was a 36-character UUID and cost 263 MB of a 1 117 MB database — paid
20682
+ * TWICE per row, in the row and in the primary-key index, across 2.1 million
20683
+ * motion, audio and object events. An `INTEGER PRIMARY KEY` in SQLite **is**
20684
+ * the rowid: the table itself is that B-tree, so the index stops existing
20685
+ * rather than getting smaller. No shorter string does that.
20686
+ *
20687
+ * Defined once here for all three event kinds, which is why they move
20688
+ * together: a per-table migration would have forked this and
20689
+ * `COMMON_BASE_COLUMNS` and reunited them two stages later.
20690
+ */
20691
+ id: number().int(),
20644
20692
  deviceId: number(),
20645
20693
  timestamp: number()
20646
20694
  };
@@ -20659,7 +20707,34 @@ var MotionEventSchema = object({
20659
20707
  /** Omitted in slim projection. */
20660
20708
  frameHeight: number().optional(),
20661
20709
  /** Populated by B5 (recording playback URL for this event). */
20662
- mediaUrl: string().optional()
20710
+ mediaUrl: string().optional(),
20711
+ /**
20712
+ * One row per motion EPISODE, not one per push (D475). `null` while the
20713
+ * episode is still open — a further rising edge extends it in place rather
20714
+ * than inserting a new row. Set once, at close, to `lastOnAt - startedAt`
20715
+ * (the span from the first rising edge to the LAST one, deliberately NOT
20716
+ * `closedAt - startedAt` — the close delay is a quiet CONFIRMATION, not
20717
+ * movement, and folding it in would report `MOTION_CLOSE_AFTER_MS` of
20718
+ * motion for an instantaneous trigger).
20719
+ *
20720
+ * **Absent** (not merely `null`) on a row written before D475 — that means
20721
+ * "closed the old way, before this column existed", never "still open".
20722
+ * Nothing in this codebase may read an absent `durationMs` as an open
20723
+ * episode; only `null` means open.
20724
+ */
20725
+ durationMs: number().nullable().optional(),
20726
+ /**
20727
+ * Ms offsets from `timestamp` (the episode's own first rising edge, so the
20728
+ * first entry is always `0`) of every genuine off→on transition the
20729
+ * episode saw — "ogni evento on si deve salvare" (D475). NOT one entry per
20730
+ * push: a firmware source that keepalives at ~1 Hz for the whole burst
20731
+ * (Reolink, Hikvision) produces exactly one edge; a source that reports an
20732
+ * explicit `false` mid-episode and then resumes before the quiet window
20733
+ * elapses produces another. Stored compactly — see `motion-edge-codec.ts`
20734
+ * — and decoded back to this shape on read. Absent/empty on a legacy row,
20735
+ * which must never be read as "no episode happened here".
20736
+ */
20737
+ edges: array(number()).readonly().optional()
20663
20738
  });
20664
20739
  /**
20665
20740
  * Which detection SOURCE produced an object event. `pipeline` = the ML
@@ -20714,6 +20789,23 @@ var ObjectEventSchema = object({
20714
20789
  * includes it (it is light). Absent on rows written before this field.
20715
20790
  */
20716
20791
  frameId: string().optional(),
20792
+ /**
20793
+ * A PRODUCER-chosen key that makes a synthetic event's emission idempotent
20794
+ * (D474).
20795
+ *
20796
+ * Only the package detector writes it, and it exists because the event id
20797
+ * stopped being choosable: the delivery and pick-up rows used to BE their
20798
+ * dedupe key (`pa-pkg-<entryId>-delivered`), which is how "never emit a
20799
+ * second delivery for this entry" survived a restart. An `INTEGER` rowid is
20800
+ * assigned by SQLite, so that key had to move off the primary key rather
20801
+ * than be dropped — a detector that cannot recognise its own row re-delivers
20802
+ * every parcel on every boot.
20803
+ *
20804
+ * Absent on every other object event, and on every row written before this
20805
+ * field. Never a substitute for `id`: it is unique per (producer, occasion),
20806
+ * not per row, and nothing addresses a row by it.
20807
+ */
20808
+ idempotencyKey: string().optional(),
20717
20809
  /** Omitted in slim projection. */
20718
20810
  trackId: string().optional(),
20719
20811
  className: string(),
@@ -21816,6 +21908,14 @@ var NativeCropResultSchema = object({
21816
21908
  * set `encodeJpeg: true`; `bytes` is then absent.
21817
21909
  */
21818
21910
  jpeg: string().optional(),
21911
+ /**
21912
+ * The SAME compressed JPEG as `jpeg`, as bytes (D462). Present instead of
21913
+ * `jpeg` when the request set `acceptJpegBytes`; a request that did not gets
21914
+ * `jpeg` exactly as before. MsgPack and the mesh leg both carry binary —
21915
+ * `bytes` above has crossed this boundary as a `Uint8Array` all along — so
21916
+ * base64 was buying nothing but a multi-megabyte string in the relay's heap.
21917
+ */
21918
+ jpegBytes: _instanceof(Uint8Array).optional(),
21819
21919
  width: number().int().positive(),
21820
21920
  height: number().int().positive(),
21821
21921
  /**
@@ -21882,7 +21982,14 @@ var ParkTrackFrameResultSchema = discriminatedUnion("parked", [object({
21882
21982
  })]);
21883
21983
  /** A retrieved parcel — the runner's own JPEG, base64 for the wire. */
21884
21984
  var ParkedTrackFrameSchema = object({
21885
- jpeg: string(),
21985
+ /**
21986
+ * Base64 JPEG — the pre-D462 wire. OPTIONAL since D462: a request that set
21987
+ * `acceptJpegBytes` is answered in `jpegBytes` and this is then absent.
21988
+ * Exactly one of the two is present.
21989
+ */
21990
+ jpeg: string().optional(),
21991
+ /** The same JPEG as bytes, for a caller that declared it reads them (D462). */
21992
+ jpegBytes: _instanceof(Uint8Array).optional(),
21886
21993
  width: number().int().positive(),
21887
21994
  height: number().int().positive(),
21888
21995
  /** The frame instant the parcel shows (the caller's clock, echoed back). */
@@ -22469,6 +22576,13 @@ method(RunnerCameraConfigSchema, object({ success: literal(true) }), { kind: "mu
22469
22576
  bbox: NativeCropBboxSchema,
22470
22577
  maxWidth: number().int().positive().optional(),
22471
22578
  /**
22579
+ * The caller reads a `Uint8Array` (D462). When set, a JPEG answer comes
22580
+ * back in `jpegBytes` instead of base64 `jpeg`. Absent means the old
22581
+ * wire — never assume consent: a pre-D462 caller parses the field as
22582
+ * base64 and bytes would decode to garbage rather than fail.
22583
+ */
22584
+ acceptJpegBytes: boolean().optional(),
22585
+ /**
22472
22586
  * When `true`, the runner encodes the resolved crop to JPEG ON THE
22473
22587
  * OWNING NODE and returns it in `jpeg` (base64) INSTEAD of raw `bytes`.
22474
22588
  * Callers set this for CROSS-NODE fetches (`handle.nodeId` is a remote
@@ -22536,7 +22650,14 @@ method(RunnerCameraConfigSchema, object({ success: literal(true) }), { kind: "mu
22536
22650
  }), ParkTrackFrameResultSchema, { kind: "mutation" }), method(object({
22537
22651
  deviceId: number(),
22538
22652
  trackId: string(),
22539
- kind: ParkedFrameKindSchema
22653
+ kind: ParkedFrameKindSchema,
22654
+ /**
22655
+ * The caller reads a `Uint8Array` (D462). When set, a JPEG answer comes
22656
+ * back in `jpegBytes` instead of base64 `jpeg`. Absent means the old
22657
+ * wire — never assume consent: a pre-D462 caller parses the field as
22658
+ * base64 and bytes would decode to garbage rather than fail.
22659
+ */
22660
+ acceptJpegBytes: boolean().optional()
22540
22661
  }), ParkedTrackFrameSchema.nullable()), method(object({
22541
22662
  deviceId: number(),
22542
22663
  trackId: string()
@@ -28942,26 +29063,59 @@ authKey: string().optional() }), object({
28942
29063
  /** Human-readable error when `ok: false`. */
28943
29064
  error: string().optional()
28944
29065
  }), { kind: "mutation" });
28945
- /**
28946
- * Hardware / firmware motion sensor cap — binary detected state plus
28947
- * a timestamp of the last observation. Distinct from
28948
- * `motion-detection.cap.ts` which owns the LOCAL ML motion pipeline;
28949
- * `motion` is the lightweight readout from on-camera motion (Reolink
28950
- * `GetMdState`, Baichuan push `type: motion`, ONVIF analytics).
28951
- *
28952
- * Native-motion providers also fan out to `detection.camera-native`
28953
- * with `source: 'onboard'` so cross-cutting system services
28954
- * (alert-center, advanced-notifier) can subscribe once and receive
28955
- * motion from every camera.
28956
- */
28957
29066
  var MotionStatusSchema = object({
28958
29067
  detected: boolean(),
28959
29068
  /** Ms epoch of the last detected-true observation. Null if never detected. */
28960
29069
  lastDetectedAt: number().nullable(),
28961
29070
  /**
28962
- * Ms after which `detected` auto-reverts to false if no fresh push
28963
- * arrives. Null means the provider leaves detected state until a
28964
- * native "clear" event.
29071
+ * `MOTION_CLOSE_AFTER_MS` while `detected: true` on a `Camera` device,
29072
+ * `null` while false and on every `Sensor` device (D475) — see that
29073
+ * constant's doc for the one-authority rule.
29074
+ *
29075
+ * ## Reading this field still arms nothing
29076
+ *
29077
+ * It reads like an instruction to the consumer ("revert after N ms if
29078
+ * no fresh push arrives") and it is not one: nothing in this repo reads
29079
+ * the LIVE cap value to drive a timer. `pipeline-analytics`'s motion-episode
29080
+ * close DOES now use the same number — `MOTION_CLOSE_AFTER_MS` — but as an
29081
+ * imported constant, not as a read of `device.state.motion.value`, so this
29082
+ * field stays what it always was: DESCRIPTIVE output, mirroring an answer
29083
+ * computed elsewhere. Building a self-clear timer out of a READ of this
29084
+ * field would add a second falling-edge authority beside whichever one
29085
+ * already owns the device, and two that can disagree are worse than one.
29086
+ * Consumers that need a falling edge SHAPED differently — held open across
29087
+ * a flapping source — debounce on their own side and say so, as
29088
+ * `addon-export-alexa/src/motion-clear-hold.ts` and
29089
+ * `addon-export-hap`'s `RESET_DEBOUNCE_MS` both do.
29090
+ *
29091
+ * ## Who writes it
29092
+ *
29093
+ * - **Cameras** — the runner's phase machine, `active → watching` on
29094
+ * `cooldown_expired`, which then writes this slice with
29095
+ * `detected: false` (`handlePhaseChanged` in
29096
+ * `pipeline-runner/index.ts`). It produces the FALLING edge, which
29097
+ * matters most for the sources that only ever push a rising one:
29098
+ * Reolink emits `MotionOnMotionChanged { detected: true }` and never
29099
+ * a false.
29100
+ * - **Sensors** (Home Assistant binary sensors, Homematic) — the
29101
+ * provider pushes the false itself, from the upstream system's own
29102
+ * state change. No phase machine is involved.
29103
+ *
29104
+ * ### The phase machine is CANONICAL, not sole — and that is a defect
29105
+ *
29106
+ * An earlier revision of this docblock (mine, 2026-09-12) claimed the
29107
+ * phase machine is the sole writer for a camera. It is not.
29108
+ * `hikvision-camera.ts:3464` and `amcrest-camera.ts:445` both call
29109
+ * `setCapSlice(motionCapability, …)` on their own rising edge, and
29110
+ * Hikvision's comment says why: it read THIS docblock, agreed the
29111
+ * runner is canonical, and wrote anyway to avoid per-tick churn. So
29112
+ * two authorities can disagree about one slice, which this repo
29113
+ * forbids, and the doc said otherwise — which is worse than saying
29114
+ * nothing, because it reads as verification.
29115
+ *
29116
+ * This predates D475 and is not fixed there: the fix touches every
29117
+ * camera provider. Recorded in D475's Consequences. Do not restore the
29118
+ * "sole writer" wording without also removing the other writers.
28965
29119
  */
28966
29120
  autoClearAfterMs: number().nullable()
28967
29121
  });
@@ -29031,7 +29185,11 @@ onMotionChanged: { data: MotionOnMotionChangedDataSchema } },
29031
29185
  */
29032
29186
  runtimeState: MotionStatusSchema,
29033
29187
  /**
29034
- * 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.
29188
+ * Runtime-state durability: **session** — every writer of this slice
29189
+ * writes only on an EDGE, so a restored `detected: true` would stay
29190
+ * frozen until the next one instead of being corrected. The next edge
29191
+ * re-publishes the real state. (On who the writers are, and why there
29192
+ * is more than one, see `autoClearAfterMs` above.)
29035
29193
  *
29036
29194
  * See `RuntimeStateDurability`. Enforced by
29037
29195
  * `scripts/check-runtime-state-durability.ts`.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@camstack/addon-provider-petkit",
3
- "version": "0.2.96",
3
+ "version": "0.2.98",
4
4
  "description": "PetKit smart-feeder device-provider addon for CamStack — wraps the @apocaliss92/nodepetkit PetKit cloud client",
5
5
  "keywords": [
6
6
  "camstack",