@camstack/addon-decoder-nodeav 1.2.97 → 1.2.99

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -7303,6 +7303,23 @@ function errMsg(err) {
7303
7303
  if (typeof err === "string") return err;
7304
7304
  return String(err);
7305
7305
  }
7306
+ new Set([
7307
+ "track",
7308
+ "summary",
7309
+ "face",
7310
+ "identity",
7311
+ "plate",
7312
+ "vehicle",
7313
+ "scene",
7314
+ "motion",
7315
+ "object",
7316
+ "audio"
7317
+ ]);
7318
+ new Set([
7319
+ "motion",
7320
+ "object",
7321
+ "audio"
7322
+ ]);
7306
7323
  var EncodeProfileSchema = object({
7307
7324
  video: object({
7308
7325
  codec: _enum([
@@ -12438,8 +12455,17 @@ method(object({
12438
12455
  }), array(SettingsRecordSchema).readonly()), method(object({
12439
12456
  namespace: string().optional(),
12440
12457
  collection: string(),
12441
- record: SettingsRecordSchema
12442
- }), _void(), { kind: "mutation" }), method(object({
12458
+ record: object({
12459
+ id: string().optional(),
12460
+ data: record(string(), unknown())
12461
+ })
12462
+ }), object({
12463
+ /**
12464
+ * The id the row ACTUALLY got (D473): the one supplied, the UUID minted
12465
+ * for an absent one, or the ROWID SQLite assigned on an `INTEGER`
12466
+ * primary key — which is the only place an auto key is knowable.
12467
+ */
12468
+ id: union([string(), number()]) }), { kind: "mutation" }), method(object({
12443
12469
  namespace: string().optional(),
12444
12470
  collection: string(),
12445
12471
  records: array(BulkRecordSchema).readonly()
@@ -12548,8 +12574,17 @@ method(_void(), EngineInfoSchema, { auth: "admin" }), method(object({
12548
12574
  }), array(SettingsRecordSchema).readonly(), { auth: "admin" }), method(object({
12549
12575
  namespace: string().optional(),
12550
12576
  collection: string(),
12551
- record: SettingsRecordSchema
12552
- }), _void(), {
12577
+ record: object({
12578
+ id: string().optional(),
12579
+ data: record(string(), unknown())
12580
+ })
12581
+ }), object({
12582
+ /**
12583
+ * The id the row ACTUALLY got (D473): the one supplied, the UUID minted
12584
+ * for an absent one, or the ROWID SQLite assigned on an `INTEGER`
12585
+ * primary key — which is the only place an auto key is knowable.
12586
+ */
12587
+ id: union([string(), number()]) }), {
12553
12588
  kind: "mutation",
12554
12589
  auth: "admin"
12555
12590
  }), method(object({
@@ -19284,7 +19319,20 @@ var TrackSchema = object({
19284
19319
  ...TrackRetrainFields
19285
19320
  });
19286
19321
  var BaseEventFields = {
19287
- id: string(),
19322
+ /**
19323
+ * A SQLite ROWID, assigned by the database (D474).
19324
+ *
19325
+ * Was a 36-character UUID and cost 263 MB of a 1 117 MB database — paid
19326
+ * TWICE per row, in the row and in the primary-key index, across 2.1 million
19327
+ * motion, audio and object events. An `INTEGER PRIMARY KEY` in SQLite **is**
19328
+ * the rowid: the table itself is that B-tree, so the index stops existing
19329
+ * rather than getting smaller. No shorter string does that.
19330
+ *
19331
+ * Defined once here for all three event kinds, which is why they move
19332
+ * together: a per-table migration would have forked this and
19333
+ * `COMMON_BASE_COLUMNS` and reunited them two stages later.
19334
+ */
19335
+ id: number().int(),
19288
19336
  deviceId: number(),
19289
19337
  timestamp: number()
19290
19338
  };
@@ -19303,7 +19351,34 @@ var MotionEventSchema = object({
19303
19351
  /** Omitted in slim projection. */
19304
19352
  frameHeight: number().optional(),
19305
19353
  /** Populated by B5 (recording playback URL for this event). */
19306
- mediaUrl: string().optional()
19354
+ mediaUrl: string().optional(),
19355
+ /**
19356
+ * One row per motion EPISODE, not one per push (D475). `null` while the
19357
+ * episode is still open — a further rising edge extends it in place rather
19358
+ * than inserting a new row. Set once, at close, to `lastOnAt - startedAt`
19359
+ * (the span from the first rising edge to the LAST one, deliberately NOT
19360
+ * `closedAt - startedAt` — the close delay is a quiet CONFIRMATION, not
19361
+ * movement, and folding it in would report `MOTION_CLOSE_AFTER_MS` of
19362
+ * motion for an instantaneous trigger).
19363
+ *
19364
+ * **Absent** (not merely `null`) on a row written before D475 — that means
19365
+ * "closed the old way, before this column existed", never "still open".
19366
+ * Nothing in this codebase may read an absent `durationMs` as an open
19367
+ * episode; only `null` means open.
19368
+ */
19369
+ durationMs: number().nullable().optional(),
19370
+ /**
19371
+ * Ms offsets from `timestamp` (the episode's own first rising edge, so the
19372
+ * first entry is always `0`) of every genuine off→on transition the
19373
+ * episode saw — "ogni evento on si deve salvare" (D475). NOT one entry per
19374
+ * push: a firmware source that keepalives at ~1 Hz for the whole burst
19375
+ * (Reolink, Hikvision) produces exactly one edge; a source that reports an
19376
+ * explicit `false` mid-episode and then resumes before the quiet window
19377
+ * elapses produces another. Stored compactly — see `motion-edge-codec.ts`
19378
+ * — and decoded back to this shape on read. Absent/empty on a legacy row,
19379
+ * which must never be read as "no episode happened here".
19380
+ */
19381
+ edges: array(number()).readonly().optional()
19307
19382
  });
19308
19383
  /**
19309
19384
  * Which detection SOURCE produced an object event. `pipeline` = the ML
@@ -19358,6 +19433,23 @@ var ObjectEventSchema = object({
19358
19433
  * includes it (it is light). Absent on rows written before this field.
19359
19434
  */
19360
19435
  frameId: string().optional(),
19436
+ /**
19437
+ * A PRODUCER-chosen key that makes a synthetic event's emission idempotent
19438
+ * (D474).
19439
+ *
19440
+ * Only the package detector writes it, and it exists because the event id
19441
+ * stopped being choosable: the delivery and pick-up rows used to BE their
19442
+ * dedupe key (`pa-pkg-<entryId>-delivered`), which is how "never emit a
19443
+ * second delivery for this entry" survived a restart. An `INTEGER` rowid is
19444
+ * assigned by SQLite, so that key had to move off the primary key rather
19445
+ * than be dropped — a detector that cannot recognise its own row re-delivers
19446
+ * every parcel on every boot.
19447
+ *
19448
+ * Absent on every other object event, and on every row written before this
19449
+ * field. Never a substitute for `id`: it is unique per (producer, occasion),
19450
+ * not per row, and nothing addresses a row by it.
19451
+ */
19452
+ idempotencyKey: string().optional(),
19361
19453
  /** Omitted in slim projection. */
19362
19454
  trackId: string().optional(),
19363
19455
  className: string(),
@@ -26424,9 +26516,54 @@ object({
26424
26516
  /** Ms epoch of the last detected-true observation. Null if never detected. */
26425
26517
  lastDetectedAt: number().nullable(),
26426
26518
  /**
26427
- * Ms after which `detected` auto-reverts to false if no fresh push
26428
- * arrives. Null means the provider leaves detected state until a
26429
- * native "clear" event.
26519
+ * `MOTION_CLOSE_AFTER_MS` while `detected: true` on a `Camera` device,
26520
+ * `null` while false and on every `Sensor` device (D475) — see that
26521
+ * constant's doc for the one-authority rule.
26522
+ *
26523
+ * ## Reading this field still arms nothing
26524
+ *
26525
+ * It reads like an instruction to the consumer ("revert after N ms if
26526
+ * no fresh push arrives") and it is not one: nothing in this repo reads
26527
+ * the LIVE cap value to drive a timer. `pipeline-analytics`'s motion-episode
26528
+ * close DOES now use the same number — `MOTION_CLOSE_AFTER_MS` — but as an
26529
+ * imported constant, not as a read of `device.state.motion.value`, so this
26530
+ * field stays what it always was: DESCRIPTIVE output, mirroring an answer
26531
+ * computed elsewhere. Building a self-clear timer out of a READ of this
26532
+ * field would add a second falling-edge authority beside whichever one
26533
+ * already owns the device, and two that can disagree are worse than one.
26534
+ * Consumers that need a falling edge SHAPED differently — held open across
26535
+ * a flapping source — debounce on their own side and say so, as
26536
+ * `addon-export-alexa/src/motion-clear-hold.ts` and
26537
+ * `addon-export-hap`'s `RESET_DEBOUNCE_MS` both do.
26538
+ *
26539
+ * ## Who writes it
26540
+ *
26541
+ * - **Cameras** — the runner's phase machine, `active → watching` on
26542
+ * `cooldown_expired`, which then writes this slice with
26543
+ * `detected: false` (`handlePhaseChanged` in
26544
+ * `pipeline-runner/index.ts`). It produces the FALLING edge, which
26545
+ * matters most for the sources that only ever push a rising one:
26546
+ * Reolink emits `MotionOnMotionChanged { detected: true }` and never
26547
+ * a false.
26548
+ * - **Sensors** (Home Assistant binary sensors, Homematic) — the
26549
+ * provider pushes the false itself, from the upstream system's own
26550
+ * state change. No phase machine is involved.
26551
+ *
26552
+ * ### The phase machine is CANONICAL, not sole — and that is a defect
26553
+ *
26554
+ * An earlier revision of this docblock (mine, 2026-09-12) claimed the
26555
+ * phase machine is the sole writer for a camera. It is not.
26556
+ * `hikvision-camera.ts:3464` and `amcrest-camera.ts:445` both call
26557
+ * `setCapSlice(motionCapability, …)` on their own rising edge, and
26558
+ * Hikvision's comment says why: it read THIS docblock, agreed the
26559
+ * runner is canonical, and wrote anyway to avoid per-tick churn. So
26560
+ * two authorities can disagree about one slice, which this repo
26561
+ * forbids, and the doc said otherwise — which is worse than saying
26562
+ * nothing, because it reads as verification.
26563
+ *
26564
+ * This predates D475 and is not fixed there: the fix touches every
26565
+ * camera provider. Recorded in D475's Consequences. Do not restore the
26566
+ * "sole writer" wording without also removing the other writers.
26430
26567
  */
26431
26568
  autoClearAfterMs: number().nullable()
26432
26569
  });
package/dist/index.mjs CHANGED
@@ -7299,6 +7299,23 @@ function errMsg(err) {
7299
7299
  if (typeof err === "string") return err;
7300
7300
  return String(err);
7301
7301
  }
7302
+ new Set([
7303
+ "track",
7304
+ "summary",
7305
+ "face",
7306
+ "identity",
7307
+ "plate",
7308
+ "vehicle",
7309
+ "scene",
7310
+ "motion",
7311
+ "object",
7312
+ "audio"
7313
+ ]);
7314
+ new Set([
7315
+ "motion",
7316
+ "object",
7317
+ "audio"
7318
+ ]);
7302
7319
  var EncodeProfileSchema = object({
7303
7320
  video: object({
7304
7321
  codec: _enum([
@@ -12434,8 +12451,17 @@ method(object({
12434
12451
  }), array(SettingsRecordSchema).readonly()), method(object({
12435
12452
  namespace: string().optional(),
12436
12453
  collection: string(),
12437
- record: SettingsRecordSchema
12438
- }), _void(), { kind: "mutation" }), method(object({
12454
+ record: object({
12455
+ id: string().optional(),
12456
+ data: record(string(), unknown())
12457
+ })
12458
+ }), object({
12459
+ /**
12460
+ * The id the row ACTUALLY got (D473): the one supplied, the UUID minted
12461
+ * for an absent one, or the ROWID SQLite assigned on an `INTEGER`
12462
+ * primary key — which is the only place an auto key is knowable.
12463
+ */
12464
+ id: union([string(), number()]) }), { kind: "mutation" }), method(object({
12439
12465
  namespace: string().optional(),
12440
12466
  collection: string(),
12441
12467
  records: array(BulkRecordSchema).readonly()
@@ -12544,8 +12570,17 @@ method(_void(), EngineInfoSchema, { auth: "admin" }), method(object({
12544
12570
  }), array(SettingsRecordSchema).readonly(), { auth: "admin" }), method(object({
12545
12571
  namespace: string().optional(),
12546
12572
  collection: string(),
12547
- record: SettingsRecordSchema
12548
- }), _void(), {
12573
+ record: object({
12574
+ id: string().optional(),
12575
+ data: record(string(), unknown())
12576
+ })
12577
+ }), object({
12578
+ /**
12579
+ * The id the row ACTUALLY got (D473): the one supplied, the UUID minted
12580
+ * for an absent one, or the ROWID SQLite assigned on an `INTEGER`
12581
+ * primary key — which is the only place an auto key is knowable.
12582
+ */
12583
+ id: union([string(), number()]) }), {
12549
12584
  kind: "mutation",
12550
12585
  auth: "admin"
12551
12586
  }), method(object({
@@ -19280,7 +19315,20 @@ var TrackSchema = object({
19280
19315
  ...TrackRetrainFields
19281
19316
  });
19282
19317
  var BaseEventFields = {
19283
- id: string(),
19318
+ /**
19319
+ * A SQLite ROWID, assigned by the database (D474).
19320
+ *
19321
+ * Was a 36-character UUID and cost 263 MB of a 1 117 MB database — paid
19322
+ * TWICE per row, in the row and in the primary-key index, across 2.1 million
19323
+ * motion, audio and object events. An `INTEGER PRIMARY KEY` in SQLite **is**
19324
+ * the rowid: the table itself is that B-tree, so the index stops existing
19325
+ * rather than getting smaller. No shorter string does that.
19326
+ *
19327
+ * Defined once here for all three event kinds, which is why they move
19328
+ * together: a per-table migration would have forked this and
19329
+ * `COMMON_BASE_COLUMNS` and reunited them two stages later.
19330
+ */
19331
+ id: number().int(),
19284
19332
  deviceId: number(),
19285
19333
  timestamp: number()
19286
19334
  };
@@ -19299,7 +19347,34 @@ var MotionEventSchema = object({
19299
19347
  /** Omitted in slim projection. */
19300
19348
  frameHeight: number().optional(),
19301
19349
  /** Populated by B5 (recording playback URL for this event). */
19302
- mediaUrl: string().optional()
19350
+ mediaUrl: string().optional(),
19351
+ /**
19352
+ * One row per motion EPISODE, not one per push (D475). `null` while the
19353
+ * episode is still open — a further rising edge extends it in place rather
19354
+ * than inserting a new row. Set once, at close, to `lastOnAt - startedAt`
19355
+ * (the span from the first rising edge to the LAST one, deliberately NOT
19356
+ * `closedAt - startedAt` — the close delay is a quiet CONFIRMATION, not
19357
+ * movement, and folding it in would report `MOTION_CLOSE_AFTER_MS` of
19358
+ * motion for an instantaneous trigger).
19359
+ *
19360
+ * **Absent** (not merely `null`) on a row written before D475 — that means
19361
+ * "closed the old way, before this column existed", never "still open".
19362
+ * Nothing in this codebase may read an absent `durationMs` as an open
19363
+ * episode; only `null` means open.
19364
+ */
19365
+ durationMs: number().nullable().optional(),
19366
+ /**
19367
+ * Ms offsets from `timestamp` (the episode's own first rising edge, so the
19368
+ * first entry is always `0`) of every genuine off→on transition the
19369
+ * episode saw — "ogni evento on si deve salvare" (D475). NOT one entry per
19370
+ * push: a firmware source that keepalives at ~1 Hz for the whole burst
19371
+ * (Reolink, Hikvision) produces exactly one edge; a source that reports an
19372
+ * explicit `false` mid-episode and then resumes before the quiet window
19373
+ * elapses produces another. Stored compactly — see `motion-edge-codec.ts`
19374
+ * — and decoded back to this shape on read. Absent/empty on a legacy row,
19375
+ * which must never be read as "no episode happened here".
19376
+ */
19377
+ edges: array(number()).readonly().optional()
19303
19378
  });
19304
19379
  /**
19305
19380
  * Which detection SOURCE produced an object event. `pipeline` = the ML
@@ -19354,6 +19429,23 @@ var ObjectEventSchema = object({
19354
19429
  * includes it (it is light). Absent on rows written before this field.
19355
19430
  */
19356
19431
  frameId: string().optional(),
19432
+ /**
19433
+ * A PRODUCER-chosen key that makes a synthetic event's emission idempotent
19434
+ * (D474).
19435
+ *
19436
+ * Only the package detector writes it, and it exists because the event id
19437
+ * stopped being choosable: the delivery and pick-up rows used to BE their
19438
+ * dedupe key (`pa-pkg-<entryId>-delivered`), which is how "never emit a
19439
+ * second delivery for this entry" survived a restart. An `INTEGER` rowid is
19440
+ * assigned by SQLite, so that key had to move off the primary key rather
19441
+ * than be dropped — a detector that cannot recognise its own row re-delivers
19442
+ * every parcel on every boot.
19443
+ *
19444
+ * Absent on every other object event, and on every row written before this
19445
+ * field. Never a substitute for `id`: it is unique per (producer, occasion),
19446
+ * not per row, and nothing addresses a row by it.
19447
+ */
19448
+ idempotencyKey: string().optional(),
19357
19449
  /** Omitted in slim projection. */
19358
19450
  trackId: string().optional(),
19359
19451
  className: string(),
@@ -26420,9 +26512,54 @@ object({
26420
26512
  /** Ms epoch of the last detected-true observation. Null if never detected. */
26421
26513
  lastDetectedAt: number().nullable(),
26422
26514
  /**
26423
- * Ms after which `detected` auto-reverts to false if no fresh push
26424
- * arrives. Null means the provider leaves detected state until a
26425
- * native "clear" event.
26515
+ * `MOTION_CLOSE_AFTER_MS` while `detected: true` on a `Camera` device,
26516
+ * `null` while false and on every `Sensor` device (D475) — see that
26517
+ * constant's doc for the one-authority rule.
26518
+ *
26519
+ * ## Reading this field still arms nothing
26520
+ *
26521
+ * It reads like an instruction to the consumer ("revert after N ms if
26522
+ * no fresh push arrives") and it is not one: nothing in this repo reads
26523
+ * the LIVE cap value to drive a timer. `pipeline-analytics`'s motion-episode
26524
+ * close DOES now use the same number — `MOTION_CLOSE_AFTER_MS` — but as an
26525
+ * imported constant, not as a read of `device.state.motion.value`, so this
26526
+ * field stays what it always was: DESCRIPTIVE output, mirroring an answer
26527
+ * computed elsewhere. Building a self-clear timer out of a READ of this
26528
+ * field would add a second falling-edge authority beside whichever one
26529
+ * already owns the device, and two that can disagree are worse than one.
26530
+ * Consumers that need a falling edge SHAPED differently — held open across
26531
+ * a flapping source — debounce on their own side and say so, as
26532
+ * `addon-export-alexa/src/motion-clear-hold.ts` and
26533
+ * `addon-export-hap`'s `RESET_DEBOUNCE_MS` both do.
26534
+ *
26535
+ * ## Who writes it
26536
+ *
26537
+ * - **Cameras** — the runner's phase machine, `active → watching` on
26538
+ * `cooldown_expired`, which then writes this slice with
26539
+ * `detected: false` (`handlePhaseChanged` in
26540
+ * `pipeline-runner/index.ts`). It produces the FALLING edge, which
26541
+ * matters most for the sources that only ever push a rising one:
26542
+ * Reolink emits `MotionOnMotionChanged { detected: true }` and never
26543
+ * a false.
26544
+ * - **Sensors** (Home Assistant binary sensors, Homematic) — the
26545
+ * provider pushes the false itself, from the upstream system's own
26546
+ * state change. No phase machine is involved.
26547
+ *
26548
+ * ### The phase machine is CANONICAL, not sole — and that is a defect
26549
+ *
26550
+ * An earlier revision of this docblock (mine, 2026-09-12) claimed the
26551
+ * phase machine is the sole writer for a camera. It is not.
26552
+ * `hikvision-camera.ts:3464` and `amcrest-camera.ts:445` both call
26553
+ * `setCapSlice(motionCapability, …)` on their own rising edge, and
26554
+ * Hikvision's comment says why: it read THIS docblock, agreed the
26555
+ * runner is canonical, and wrote anyway to avoid per-tick churn. So
26556
+ * two authorities can disagree about one slice, which this repo
26557
+ * forbids, and the doc said otherwise — which is worse than saying
26558
+ * nothing, because it reads as verification.
26559
+ *
26560
+ * This predates D475 and is not fixed there: the fix touches every
26561
+ * camera provider. Recorded in D475's Consequences. Do not restore the
26562
+ * "sole writer" wording without also removing the other writers.
26426
26563
  */
26427
26564
  autoClearAfterMs: number().nullable()
26428
26565
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@camstack/addon-decoder-nodeav",
3
- "version": "1.2.97",
3
+ "version": "1.2.99",
4
4
  "description": "Standalone in-process node-av decoder addon for CamStack",
5
5
  "keywords": [
6
6
  "camstack",