@camstack/addon-notifiers 1.2.101 → 1.2.103

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 +146 -11
  2. package/dist/addon.mjs +146 -11
  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(),
@@ -26482,9 +26572,54 @@ object({
26482
26572
  /** Ms epoch of the last detected-true observation. Null if never detected. */
26483
26573
  lastDetectedAt: number().nullable(),
26484
26574
  /**
26485
- * Ms after which `detected` auto-reverts to false if no fresh push
26486
- * arrives. Null means the provider leaves detected state until a
26487
- * 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.
26488
26623
  */
26489
26624
  autoClearAfterMs: number().nullable()
26490
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(),
@@ -26455,9 +26545,54 @@ object({
26455
26545
  /** Ms epoch of the last detected-true observation. Null if never detected. */
26456
26546
  lastDetectedAt: number().nullable(),
26457
26547
  /**
26458
- * Ms after which `detected` auto-reverts to false if no fresh push
26459
- * arrives. Null means the provider leaves detected state until a
26460
- * 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.
26461
26596
  */
26462
26597
  autoClearAfterMs: number().nullable()
26463
26598
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@camstack/addon-notifiers",
3
- "version": "1.2.101",
3
+ "version": "1.2.103",
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",