@camstack/addon-notifiers 1.2.100 → 1.2.102

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 +177 -13
  2. package/dist/addon.mjs +177 -13
  3. package/package.json +1 -1
package/dist/addon.js CHANGED
@@ -7317,8 +7317,23 @@ method(object({ deviceId: number() }), array(StreamSourceEntrySchema)), method(o
7317
7317
  action: string().min(1),
7318
7318
  input: unknown()
7319
7319
  }), unknown(), { kind: "mutation" }), method(object({ deviceId: number() }), _void(), { kind: "mutation" }), method(object({ deviceId: number() }), unknown().nullable()), method(object({ deviceId: number() }), RawStateResultSchema.nullable(), { auth: "protected" });
7320
- //#endregion
7321
- //#region ../types/dist/index.mjs
7320
+ new Set([
7321
+ "track",
7322
+ "summary",
7323
+ "face",
7324
+ "identity",
7325
+ "plate",
7326
+ "vehicle",
7327
+ "scene",
7328
+ "motion",
7329
+ "object",
7330
+ "audio"
7331
+ ]);
7332
+ new Set([
7333
+ "motion",
7334
+ "object",
7335
+ "audio"
7336
+ ]);
7322
7337
  /**
7323
7338
  * Build an `IAddonRouteProvider` from a list of routes. Implements
7324
7339
  * both the operator-facing `getRoutes` (returning route descriptors
@@ -12575,8 +12590,17 @@ method(object({
12575
12590
  }), array(SettingsRecordSchema).readonly()), method(object({
12576
12591
  namespace: string().optional(),
12577
12592
  collection: string(),
12578
- record: SettingsRecordSchema
12579
- }), _void(), { kind: "mutation" }), method(object({
12593
+ record: object({
12594
+ id: string().optional(),
12595
+ data: record(string(), unknown())
12596
+ })
12597
+ }), object({
12598
+ /**
12599
+ * The id the row ACTUALLY got (D473): the one supplied, the UUID minted
12600
+ * for an absent one, or the ROWID SQLite assigned on an `INTEGER`
12601
+ * primary key — which is the only place an auto key is knowable.
12602
+ */
12603
+ id: union([string(), number()]) }), { kind: "mutation" }), method(object({
12580
12604
  namespace: string().optional(),
12581
12605
  collection: string(),
12582
12606
  records: array(BulkRecordSchema).readonly()
@@ -12685,8 +12709,17 @@ method(_void(), EngineInfoSchema, { auth: "admin" }), method(object({
12685
12709
  }), array(SettingsRecordSchema).readonly(), { auth: "admin" }), method(object({
12686
12710
  namespace: string().optional(),
12687
12711
  collection: string(),
12688
- record: SettingsRecordSchema
12689
- }), _void(), {
12712
+ record: object({
12713
+ id: string().optional(),
12714
+ data: record(string(), unknown())
12715
+ })
12716
+ }), object({
12717
+ /**
12718
+ * The id the row ACTUALLY got (D473): the one supplied, the UUID minted
12719
+ * for an absent one, or the ROWID SQLite assigned on an `INTEGER`
12720
+ * primary key — which is the only place an auto key is knowable.
12721
+ */
12722
+ id: union([string(), number()]) }), {
12690
12723
  kind: "mutation",
12691
12724
  auth: "admin"
12692
12725
  }), method(object({
@@ -19342,7 +19375,20 @@ var TrackSchema = object({
19342
19375
  ...TrackRetrainFields
19343
19376
  });
19344
19377
  var BaseEventFields = {
19345
- id: string(),
19378
+ /**
19379
+ * A SQLite ROWID, assigned by the database (D474).
19380
+ *
19381
+ * Was a 36-character UUID and cost 263 MB of a 1 117 MB database — paid
19382
+ * TWICE per row, in the row and in the primary-key index, across 2.1 million
19383
+ * motion, audio and object events. An `INTEGER PRIMARY KEY` in SQLite **is**
19384
+ * the rowid: the table itself is that B-tree, so the index stops existing
19385
+ * rather than getting smaller. No shorter string does that.
19386
+ *
19387
+ * Defined once here for all three event kinds, which is why they move
19388
+ * together: a per-table migration would have forked this and
19389
+ * `COMMON_BASE_COLUMNS` and reunited them two stages later.
19390
+ */
19391
+ id: number().int(),
19346
19392
  deviceId: number(),
19347
19393
  timestamp: number()
19348
19394
  };
@@ -19361,7 +19407,34 @@ var MotionEventSchema = object({
19361
19407
  /** Omitted in slim projection. */
19362
19408
  frameHeight: number().optional(),
19363
19409
  /** Populated by B5 (recording playback URL for this event). */
19364
- mediaUrl: string().optional()
19410
+ mediaUrl: string().optional(),
19411
+ /**
19412
+ * One row per motion EPISODE, not one per push (D475). `null` while the
19413
+ * episode is still open — a further rising edge extends it in place rather
19414
+ * than inserting a new row. Set once, at close, to `lastOnAt - startedAt`
19415
+ * (the span from the first rising edge to the LAST one, deliberately NOT
19416
+ * `closedAt - startedAt` — the close delay is a quiet CONFIRMATION, not
19417
+ * movement, and folding it in would report `MOTION_CLOSE_AFTER_MS` of
19418
+ * motion for an instantaneous trigger).
19419
+ *
19420
+ * **Absent** (not merely `null`) on a row written before D475 — that means
19421
+ * "closed the old way, before this column existed", never "still open".
19422
+ * Nothing in this codebase may read an absent `durationMs` as an open
19423
+ * episode; only `null` means open.
19424
+ */
19425
+ durationMs: number().nullable().optional(),
19426
+ /**
19427
+ * Ms offsets from `timestamp` (the episode's own first rising edge, so the
19428
+ * first entry is always `0`) of every genuine off→on transition the
19429
+ * episode saw — "ogni evento on si deve salvare" (D475). NOT one entry per
19430
+ * push: a firmware source that keepalives at ~1 Hz for the whole burst
19431
+ * (Reolink, Hikvision) produces exactly one edge; a source that reports an
19432
+ * explicit `false` mid-episode and then resumes before the quiet window
19433
+ * elapses produces another. Stored compactly — see `motion-edge-codec.ts`
19434
+ * — and decoded back to this shape on read. Absent/empty on a legacy row,
19435
+ * which must never be read as "no episode happened here".
19436
+ */
19437
+ edges: array(number()).readonly().optional()
19365
19438
  });
19366
19439
  /**
19367
19440
  * Which detection SOURCE produced an object event. `pipeline` = the ML
@@ -19416,6 +19489,23 @@ var ObjectEventSchema = object({
19416
19489
  * includes it (it is light). Absent on rows written before this field.
19417
19490
  */
19418
19491
  frameId: string().optional(),
19492
+ /**
19493
+ * A PRODUCER-chosen key that makes a synthetic event's emission idempotent
19494
+ * (D474).
19495
+ *
19496
+ * Only the package detector writes it, and it exists because the event id
19497
+ * stopped being choosable: the delivery and pick-up rows used to BE their
19498
+ * dedupe key (`pa-pkg-<entryId>-delivered`), which is how "never emit a
19499
+ * second delivery for this entry" survived a restart. An `INTEGER` rowid is
19500
+ * assigned by SQLite, so that key had to move off the primary key rather
19501
+ * than be dropped — a detector that cannot recognise its own row re-delivers
19502
+ * every parcel on every boot.
19503
+ *
19504
+ * Absent on every other object event, and on every row written before this
19505
+ * field. Never a substitute for `id`: it is unique per (producer, occasion),
19506
+ * not per row, and nothing addresses a row by it.
19507
+ */
19508
+ idempotencyKey: string().optional(),
19419
19509
  /** Omitted in slim projection. */
19420
19510
  trackId: string().optional(),
19421
19511
  className: string(),
@@ -20518,6 +20608,14 @@ var NativeCropResultSchema = object({
20518
20608
  * set `encodeJpeg: true`; `bytes` is then absent.
20519
20609
  */
20520
20610
  jpeg: string().optional(),
20611
+ /**
20612
+ * The SAME compressed JPEG as `jpeg`, as bytes (D462). Present instead of
20613
+ * `jpeg` when the request set `acceptJpegBytes`; a request that did not gets
20614
+ * `jpeg` exactly as before. MsgPack and the mesh leg both carry binary —
20615
+ * `bytes` above has crossed this boundary as a `Uint8Array` all along — so
20616
+ * base64 was buying nothing but a multi-megabyte string in the relay's heap.
20617
+ */
20618
+ jpegBytes: _instanceof(Uint8Array).optional(),
20521
20619
  width: number().int().positive(),
20522
20620
  height: number().int().positive(),
20523
20621
  /**
@@ -20584,7 +20682,14 @@ var ParkTrackFrameResultSchema = discriminatedUnion("parked", [object({
20584
20682
  })]);
20585
20683
  /** A retrieved parcel — the runner's own JPEG, base64 for the wire. */
20586
20684
  var ParkedTrackFrameSchema = object({
20587
- jpeg: string(),
20685
+ /**
20686
+ * Base64 JPEG — the pre-D462 wire. OPTIONAL since D462: a request that set
20687
+ * `acceptJpegBytes` is answered in `jpegBytes` and this is then absent.
20688
+ * Exactly one of the two is present.
20689
+ */
20690
+ jpeg: string().optional(),
20691
+ /** The same JPEG as bytes, for a caller that declared it reads them (D462). */
20692
+ jpegBytes: _instanceof(Uint8Array).optional(),
20588
20693
  width: number().int().positive(),
20589
20694
  height: number().int().positive(),
20590
20695
  /** The frame instant the parcel shows (the caller's clock, echoed back). */
@@ -21171,6 +21276,13 @@ method(RunnerCameraConfigSchema, object({ success: literal(true) }), { kind: "mu
21171
21276
  bbox: NativeCropBboxSchema,
21172
21277
  maxWidth: number().int().positive().optional(),
21173
21278
  /**
21279
+ * The caller reads a `Uint8Array` (D462). When set, a JPEG answer comes
21280
+ * back in `jpegBytes` instead of base64 `jpeg`. Absent means the old
21281
+ * wire — never assume consent: a pre-D462 caller parses the field as
21282
+ * base64 and bytes would decode to garbage rather than fail.
21283
+ */
21284
+ acceptJpegBytes: boolean().optional(),
21285
+ /**
21174
21286
  * When `true`, the runner encodes the resolved crop to JPEG ON THE
21175
21287
  * OWNING NODE and returns it in `jpeg` (base64) INSTEAD of raw `bytes`.
21176
21288
  * Callers set this for CROSS-NODE fetches (`handle.nodeId` is a remote
@@ -21238,7 +21350,14 @@ method(RunnerCameraConfigSchema, object({ success: literal(true) }), { kind: "mu
21238
21350
  }), ParkTrackFrameResultSchema, { kind: "mutation" }), method(object({
21239
21351
  deviceId: number(),
21240
21352
  trackId: string(),
21241
- kind: ParkedFrameKindSchema
21353
+ kind: ParkedFrameKindSchema,
21354
+ /**
21355
+ * The caller reads a `Uint8Array` (D462). When set, a JPEG answer comes
21356
+ * back in `jpegBytes` instead of base64 `jpeg`. Absent means the old
21357
+ * wire — never assume consent: a pre-D462 caller parses the field as
21358
+ * base64 and bytes would decode to garbage rather than fail.
21359
+ */
21360
+ acceptJpegBytes: boolean().optional()
21242
21361
  }), ParkedTrackFrameSchema.nullable()), method(object({
21243
21362
  deviceId: number(),
21244
21363
  trackId: string()
@@ -26453,9 +26572,54 @@ object({
26453
26572
  /** Ms epoch of the last detected-true observation. Null if never detected. */
26454
26573
  lastDetectedAt: number().nullable(),
26455
26574
  /**
26456
- * Ms after which `detected` auto-reverts to false if no fresh push
26457
- * arrives. Null means the provider leaves detected state until a
26458
- * native "clear" event.
26575
+ * `MOTION_CLOSE_AFTER_MS` while `detected: true` on a `Camera` device,
26576
+ * `null` while false and on every `Sensor` device (D475) — see that
26577
+ * constant's doc for the one-authority rule.
26578
+ *
26579
+ * ## Reading this field still arms nothing
26580
+ *
26581
+ * It reads like an instruction to the consumer ("revert after N ms if
26582
+ * no fresh push arrives") and it is not one: nothing in this repo reads
26583
+ * the LIVE cap value to drive a timer. `pipeline-analytics`'s motion-episode
26584
+ * close DOES now use the same number — `MOTION_CLOSE_AFTER_MS` — but as an
26585
+ * imported constant, not as a read of `device.state.motion.value`, so this
26586
+ * field stays what it always was: DESCRIPTIVE output, mirroring an answer
26587
+ * computed elsewhere. Building a self-clear timer out of a READ of this
26588
+ * field would add a second falling-edge authority beside whichever one
26589
+ * already owns the device, and two that can disagree are worse than one.
26590
+ * Consumers that need a falling edge SHAPED differently — held open across
26591
+ * a flapping source — debounce on their own side and say so, as
26592
+ * `addon-export-alexa/src/motion-clear-hold.ts` and
26593
+ * `addon-export-hap`'s `RESET_DEBOUNCE_MS` both do.
26594
+ *
26595
+ * ## Who writes it
26596
+ *
26597
+ * - **Cameras** — the runner's phase machine, `active → watching` on
26598
+ * `cooldown_expired`, which then writes this slice with
26599
+ * `detected: false` (`handlePhaseChanged` in
26600
+ * `pipeline-runner/index.ts`). It produces the FALLING edge, which
26601
+ * matters most for the sources that only ever push a rising one:
26602
+ * Reolink emits `MotionOnMotionChanged { detected: true }` and never
26603
+ * a false.
26604
+ * - **Sensors** (Home Assistant binary sensors, Homematic) — the
26605
+ * provider pushes the false itself, from the upstream system's own
26606
+ * state change. No phase machine is involved.
26607
+ *
26608
+ * ### The phase machine is CANONICAL, not sole — and that is a defect
26609
+ *
26610
+ * An earlier revision of this docblock (mine, 2026-09-12) claimed the
26611
+ * phase machine is the sole writer for a camera. It is not.
26612
+ * `hikvision-camera.ts:3464` and `amcrest-camera.ts:445` both call
26613
+ * `setCapSlice(motionCapability, …)` on their own rising edge, and
26614
+ * Hikvision's comment says why: it read THIS docblock, agreed the
26615
+ * runner is canonical, and wrote anyway to avoid per-tick churn. So
26616
+ * two authorities can disagree about one slice, which this repo
26617
+ * forbids, and the doc said otherwise — which is worse than saying
26618
+ * nothing, because it reads as verification.
26619
+ *
26620
+ * This predates D475 and is not fixed there: the fix touches every
26621
+ * camera provider. Recorded in D475's Consequences. Do not restore the
26622
+ * "sole writer" wording without also removing the other writers.
26459
26623
  */
26460
26624
  autoClearAfterMs: number().nullable()
26461
26625
  });
package/dist/addon.mjs CHANGED
@@ -7290,8 +7290,23 @@ method(object({ deviceId: number() }), array(StreamSourceEntrySchema)), method(o
7290
7290
  action: string().min(1),
7291
7291
  input: unknown()
7292
7292
  }), unknown(), { kind: "mutation" }), method(object({ deviceId: number() }), _void(), { kind: "mutation" }), method(object({ deviceId: number() }), unknown().nullable()), method(object({ deviceId: number() }), RawStateResultSchema.nullable(), { auth: "protected" });
7293
- //#endregion
7294
- //#region ../types/dist/index.mjs
7293
+ new Set([
7294
+ "track",
7295
+ "summary",
7296
+ "face",
7297
+ "identity",
7298
+ "plate",
7299
+ "vehicle",
7300
+ "scene",
7301
+ "motion",
7302
+ "object",
7303
+ "audio"
7304
+ ]);
7305
+ new Set([
7306
+ "motion",
7307
+ "object",
7308
+ "audio"
7309
+ ]);
7295
7310
  /**
7296
7311
  * Build an `IAddonRouteProvider` from a list of routes. Implements
7297
7312
  * both the operator-facing `getRoutes` (returning route descriptors
@@ -12548,8 +12563,17 @@ method(object({
12548
12563
  }), array(SettingsRecordSchema).readonly()), method(object({
12549
12564
  namespace: string().optional(),
12550
12565
  collection: string(),
12551
- record: SettingsRecordSchema
12552
- }), _void(), { kind: "mutation" }), method(object({
12566
+ record: object({
12567
+ id: string().optional(),
12568
+ data: record(string(), unknown())
12569
+ })
12570
+ }), object({
12571
+ /**
12572
+ * The id the row ACTUALLY got (D473): the one supplied, the UUID minted
12573
+ * for an absent one, or the ROWID SQLite assigned on an `INTEGER`
12574
+ * primary key — which is the only place an auto key is knowable.
12575
+ */
12576
+ id: union([string(), number()]) }), { kind: "mutation" }), method(object({
12553
12577
  namespace: string().optional(),
12554
12578
  collection: string(),
12555
12579
  records: array(BulkRecordSchema).readonly()
@@ -12658,8 +12682,17 @@ method(_void(), EngineInfoSchema, { auth: "admin" }), method(object({
12658
12682
  }), array(SettingsRecordSchema).readonly(), { auth: "admin" }), method(object({
12659
12683
  namespace: string().optional(),
12660
12684
  collection: string(),
12661
- record: SettingsRecordSchema
12662
- }), _void(), {
12685
+ record: object({
12686
+ id: string().optional(),
12687
+ data: record(string(), unknown())
12688
+ })
12689
+ }), object({
12690
+ /**
12691
+ * The id the row ACTUALLY got (D473): the one supplied, the UUID minted
12692
+ * for an absent one, or the ROWID SQLite assigned on an `INTEGER`
12693
+ * primary key — which is the only place an auto key is knowable.
12694
+ */
12695
+ id: union([string(), number()]) }), {
12663
12696
  kind: "mutation",
12664
12697
  auth: "admin"
12665
12698
  }), method(object({
@@ -19315,7 +19348,20 @@ var TrackSchema = object({
19315
19348
  ...TrackRetrainFields
19316
19349
  });
19317
19350
  var BaseEventFields = {
19318
- id: string(),
19351
+ /**
19352
+ * A SQLite ROWID, assigned by the database (D474).
19353
+ *
19354
+ * Was a 36-character UUID and cost 263 MB of a 1 117 MB database — paid
19355
+ * TWICE per row, in the row and in the primary-key index, across 2.1 million
19356
+ * motion, audio and object events. An `INTEGER PRIMARY KEY` in SQLite **is**
19357
+ * the rowid: the table itself is that B-tree, so the index stops existing
19358
+ * rather than getting smaller. No shorter string does that.
19359
+ *
19360
+ * Defined once here for all three event kinds, which is why they move
19361
+ * together: a per-table migration would have forked this and
19362
+ * `COMMON_BASE_COLUMNS` and reunited them two stages later.
19363
+ */
19364
+ id: number().int(),
19319
19365
  deviceId: number(),
19320
19366
  timestamp: number()
19321
19367
  };
@@ -19334,7 +19380,34 @@ var MotionEventSchema = object({
19334
19380
  /** Omitted in slim projection. */
19335
19381
  frameHeight: number().optional(),
19336
19382
  /** Populated by B5 (recording playback URL for this event). */
19337
- mediaUrl: string().optional()
19383
+ mediaUrl: string().optional(),
19384
+ /**
19385
+ * One row per motion EPISODE, not one per push (D475). `null` while the
19386
+ * episode is still open — a further rising edge extends it in place rather
19387
+ * than inserting a new row. Set once, at close, to `lastOnAt - startedAt`
19388
+ * (the span from the first rising edge to the LAST one, deliberately NOT
19389
+ * `closedAt - startedAt` — the close delay is a quiet CONFIRMATION, not
19390
+ * movement, and folding it in would report `MOTION_CLOSE_AFTER_MS` of
19391
+ * motion for an instantaneous trigger).
19392
+ *
19393
+ * **Absent** (not merely `null`) on a row written before D475 — that means
19394
+ * "closed the old way, before this column existed", never "still open".
19395
+ * Nothing in this codebase may read an absent `durationMs` as an open
19396
+ * episode; only `null` means open.
19397
+ */
19398
+ durationMs: number().nullable().optional(),
19399
+ /**
19400
+ * Ms offsets from `timestamp` (the episode's own first rising edge, so the
19401
+ * first entry is always `0`) of every genuine off→on transition the
19402
+ * episode saw — "ogni evento on si deve salvare" (D475). NOT one entry per
19403
+ * push: a firmware source that keepalives at ~1 Hz for the whole burst
19404
+ * (Reolink, Hikvision) produces exactly one edge; a source that reports an
19405
+ * explicit `false` mid-episode and then resumes before the quiet window
19406
+ * elapses produces another. Stored compactly — see `motion-edge-codec.ts`
19407
+ * — and decoded back to this shape on read. Absent/empty on a legacy row,
19408
+ * which must never be read as "no episode happened here".
19409
+ */
19410
+ edges: array(number()).readonly().optional()
19338
19411
  });
19339
19412
  /**
19340
19413
  * Which detection SOURCE produced an object event. `pipeline` = the ML
@@ -19389,6 +19462,23 @@ var ObjectEventSchema = object({
19389
19462
  * includes it (it is light). Absent on rows written before this field.
19390
19463
  */
19391
19464
  frameId: string().optional(),
19465
+ /**
19466
+ * A PRODUCER-chosen key that makes a synthetic event's emission idempotent
19467
+ * (D474).
19468
+ *
19469
+ * Only the package detector writes it, and it exists because the event id
19470
+ * stopped being choosable: the delivery and pick-up rows used to BE their
19471
+ * dedupe key (`pa-pkg-<entryId>-delivered`), which is how "never emit a
19472
+ * second delivery for this entry" survived a restart. An `INTEGER` rowid is
19473
+ * assigned by SQLite, so that key had to move off the primary key rather
19474
+ * than be dropped — a detector that cannot recognise its own row re-delivers
19475
+ * every parcel on every boot.
19476
+ *
19477
+ * Absent on every other object event, and on every row written before this
19478
+ * field. Never a substitute for `id`: it is unique per (producer, occasion),
19479
+ * not per row, and nothing addresses a row by it.
19480
+ */
19481
+ idempotencyKey: string().optional(),
19392
19482
  /** Omitted in slim projection. */
19393
19483
  trackId: string().optional(),
19394
19484
  className: string(),
@@ -20491,6 +20581,14 @@ var NativeCropResultSchema = object({
20491
20581
  * set `encodeJpeg: true`; `bytes` is then absent.
20492
20582
  */
20493
20583
  jpeg: string().optional(),
20584
+ /**
20585
+ * The SAME compressed JPEG as `jpeg`, as bytes (D462). Present instead of
20586
+ * `jpeg` when the request set `acceptJpegBytes`; a request that did not gets
20587
+ * `jpeg` exactly as before. MsgPack and the mesh leg both carry binary —
20588
+ * `bytes` above has crossed this boundary as a `Uint8Array` all along — so
20589
+ * base64 was buying nothing but a multi-megabyte string in the relay's heap.
20590
+ */
20591
+ jpegBytes: _instanceof(Uint8Array).optional(),
20494
20592
  width: number().int().positive(),
20495
20593
  height: number().int().positive(),
20496
20594
  /**
@@ -20557,7 +20655,14 @@ var ParkTrackFrameResultSchema = discriminatedUnion("parked", [object({
20557
20655
  })]);
20558
20656
  /** A retrieved parcel — the runner's own JPEG, base64 for the wire. */
20559
20657
  var ParkedTrackFrameSchema = object({
20560
- jpeg: string(),
20658
+ /**
20659
+ * Base64 JPEG — the pre-D462 wire. OPTIONAL since D462: a request that set
20660
+ * `acceptJpegBytes` is answered in `jpegBytes` and this is then absent.
20661
+ * Exactly one of the two is present.
20662
+ */
20663
+ jpeg: string().optional(),
20664
+ /** The same JPEG as bytes, for a caller that declared it reads them (D462). */
20665
+ jpegBytes: _instanceof(Uint8Array).optional(),
20561
20666
  width: number().int().positive(),
20562
20667
  height: number().int().positive(),
20563
20668
  /** The frame instant the parcel shows (the caller's clock, echoed back). */
@@ -21144,6 +21249,13 @@ method(RunnerCameraConfigSchema, object({ success: literal(true) }), { kind: "mu
21144
21249
  bbox: NativeCropBboxSchema,
21145
21250
  maxWidth: number().int().positive().optional(),
21146
21251
  /**
21252
+ * The caller reads a `Uint8Array` (D462). When set, a JPEG answer comes
21253
+ * back in `jpegBytes` instead of base64 `jpeg`. Absent means the old
21254
+ * wire — never assume consent: a pre-D462 caller parses the field as
21255
+ * base64 and bytes would decode to garbage rather than fail.
21256
+ */
21257
+ acceptJpegBytes: boolean().optional(),
21258
+ /**
21147
21259
  * When `true`, the runner encodes the resolved crop to JPEG ON THE
21148
21260
  * OWNING NODE and returns it in `jpeg` (base64) INSTEAD of raw `bytes`.
21149
21261
  * Callers set this for CROSS-NODE fetches (`handle.nodeId` is a remote
@@ -21211,7 +21323,14 @@ method(RunnerCameraConfigSchema, object({ success: literal(true) }), { kind: "mu
21211
21323
  }), ParkTrackFrameResultSchema, { kind: "mutation" }), method(object({
21212
21324
  deviceId: number(),
21213
21325
  trackId: string(),
21214
- kind: ParkedFrameKindSchema
21326
+ kind: ParkedFrameKindSchema,
21327
+ /**
21328
+ * The caller reads a `Uint8Array` (D462). When set, a JPEG answer comes
21329
+ * back in `jpegBytes` instead of base64 `jpeg`. Absent means the old
21330
+ * wire — never assume consent: a pre-D462 caller parses the field as
21331
+ * base64 and bytes would decode to garbage rather than fail.
21332
+ */
21333
+ acceptJpegBytes: boolean().optional()
21215
21334
  }), ParkedTrackFrameSchema.nullable()), method(object({
21216
21335
  deviceId: number(),
21217
21336
  trackId: string()
@@ -26426,9 +26545,54 @@ object({
26426
26545
  /** Ms epoch of the last detected-true observation. Null if never detected. */
26427
26546
  lastDetectedAt: number().nullable(),
26428
26547
  /**
26429
- * Ms after which `detected` auto-reverts to false if no fresh push
26430
- * arrives. Null means the provider leaves detected state until a
26431
- * native "clear" event.
26548
+ * `MOTION_CLOSE_AFTER_MS` while `detected: true` on a `Camera` device,
26549
+ * `null` while false and on every `Sensor` device (D475) — see that
26550
+ * constant's doc for the one-authority rule.
26551
+ *
26552
+ * ## Reading this field still arms nothing
26553
+ *
26554
+ * It reads like an instruction to the consumer ("revert after N ms if
26555
+ * no fresh push arrives") and it is not one: nothing in this repo reads
26556
+ * the LIVE cap value to drive a timer. `pipeline-analytics`'s motion-episode
26557
+ * close DOES now use the same number — `MOTION_CLOSE_AFTER_MS` — but as an
26558
+ * imported constant, not as a read of `device.state.motion.value`, so this
26559
+ * field stays what it always was: DESCRIPTIVE output, mirroring an answer
26560
+ * computed elsewhere. Building a self-clear timer out of a READ of this
26561
+ * field would add a second falling-edge authority beside whichever one
26562
+ * already owns the device, and two that can disagree are worse than one.
26563
+ * Consumers that need a falling edge SHAPED differently — held open across
26564
+ * a flapping source — debounce on their own side and say so, as
26565
+ * `addon-export-alexa/src/motion-clear-hold.ts` and
26566
+ * `addon-export-hap`'s `RESET_DEBOUNCE_MS` both do.
26567
+ *
26568
+ * ## Who writes it
26569
+ *
26570
+ * - **Cameras** — the runner's phase machine, `active → watching` on
26571
+ * `cooldown_expired`, which then writes this slice with
26572
+ * `detected: false` (`handlePhaseChanged` in
26573
+ * `pipeline-runner/index.ts`). It produces the FALLING edge, which
26574
+ * matters most for the sources that only ever push a rising one:
26575
+ * Reolink emits `MotionOnMotionChanged { detected: true }` and never
26576
+ * a false.
26577
+ * - **Sensors** (Home Assistant binary sensors, Homematic) — the
26578
+ * provider pushes the false itself, from the upstream system's own
26579
+ * state change. No phase machine is involved.
26580
+ *
26581
+ * ### The phase machine is CANONICAL, not sole — and that is a defect
26582
+ *
26583
+ * An earlier revision of this docblock (mine, 2026-09-12) claimed the
26584
+ * phase machine is the sole writer for a camera. It is not.
26585
+ * `hikvision-camera.ts:3464` and `amcrest-camera.ts:445` both call
26586
+ * `setCapSlice(motionCapability, …)` on their own rising edge, and
26587
+ * Hikvision's comment says why: it read THIS docblock, agreed the
26588
+ * runner is canonical, and wrote anyway to avoid per-tick churn. So
26589
+ * two authorities can disagree about one slice, which this repo
26590
+ * forbids, and the doc said otherwise — which is worse than saying
26591
+ * nothing, because it reads as verification.
26592
+ *
26593
+ * This predates D475 and is not fixed there: the fix touches every
26594
+ * camera provider. Recorded in D475's Consequences. Do not restore the
26595
+ * "sole writer" wording without also removing the other writers.
26432
26596
  */
26433
26597
  autoClearAfterMs: number().nullable()
26434
26598
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@camstack/addon-notifiers",
3
- "version": "1.2.100",
3
+ "version": "1.2.102",
4
4
  "description": "System notifiers addon for CamStack — a `notification-output` collection provider hosting per-kind notifier adapters (ntfy, pushover, gotify, telegram, discord, webhook, zentik).",
5
5
  "keywords": [
6
6
  "camstack",