@camstack/addon-provider-hikvision 1.2.105 → 1.2.106

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 +167 -13
  2. package/dist/addon.mjs +167 -13
  3. package/package.json +1 -1
package/dist/addon.js CHANGED
@@ -7298,6 +7298,23 @@ method(object({ deviceId: number() }), array(StreamSourceEntrySchema)), method(o
7298
7298
  action: string().min(1),
7299
7299
  input: unknown()
7300
7300
  }), unknown(), { kind: "mutation" }), method(object({ deviceId: number() }), _void(), { kind: "mutation" }), method(object({ deviceId: number() }), unknown().nullable()), method(object({ deviceId: number() }), RawStateResultSchema.nullable(), { auth: "protected" });
7301
+ new Set([
7302
+ "track",
7303
+ "summary",
7304
+ "face",
7305
+ "identity",
7306
+ "plate",
7307
+ "vehicle",
7308
+ "scene",
7309
+ "motion",
7310
+ "object",
7311
+ "audio"
7312
+ ]);
7313
+ new Set([
7314
+ "motion",
7315
+ "object",
7316
+ "audio"
7317
+ ]);
7301
7318
  var EncodeProfileSchema = object({
7302
7319
  video: object({
7303
7320
  codec: _enum([
@@ -12791,8 +12808,17 @@ method(object({
12791
12808
  }), array(SettingsRecordSchema).readonly()), method(object({
12792
12809
  namespace: string().optional(),
12793
12810
  collection: string(),
12794
- record: SettingsRecordSchema
12795
- }), _void(), { kind: "mutation" }), method(object({
12811
+ record: object({
12812
+ id: string().optional(),
12813
+ data: record(string(), unknown())
12814
+ })
12815
+ }), object({
12816
+ /**
12817
+ * The id the row ACTUALLY got (D473): the one supplied, the UUID minted
12818
+ * for an absent one, or the ROWID SQLite assigned on an `INTEGER`
12819
+ * primary key — which is the only place an auto key is knowable.
12820
+ */
12821
+ id: union([string(), number()]) }), { kind: "mutation" }), method(object({
12796
12822
  namespace: string().optional(),
12797
12823
  collection: string(),
12798
12824
  records: array(BulkRecordSchema).readonly()
@@ -12901,8 +12927,17 @@ method(_void(), EngineInfoSchema, { auth: "admin" }), method(object({
12901
12927
  }), array(SettingsRecordSchema).readonly(), { auth: "admin" }), method(object({
12902
12928
  namespace: string().optional(),
12903
12929
  collection: string(),
12904
- record: SettingsRecordSchema
12905
- }), _void(), {
12930
+ record: object({
12931
+ id: string().optional(),
12932
+ data: record(string(), unknown())
12933
+ })
12934
+ }), object({
12935
+ /**
12936
+ * The id the row ACTUALLY got (D473): the one supplied, the UUID minted
12937
+ * for an absent one, or the ROWID SQLite assigned on an `INTEGER`
12938
+ * primary key — which is the only place an auto key is knowable.
12939
+ */
12940
+ id: union([string(), number()]) }), {
12906
12941
  kind: "mutation",
12907
12942
  auth: "admin"
12908
12943
  }), method(object({
@@ -19829,7 +19864,20 @@ var TrackSchema = object({
19829
19864
  ...TrackRetrainFields
19830
19865
  });
19831
19866
  var BaseEventFields = {
19832
- id: string(),
19867
+ /**
19868
+ * A SQLite ROWID, assigned by the database (D474).
19869
+ *
19870
+ * Was a 36-character UUID and cost 263 MB of a 1 117 MB database — paid
19871
+ * TWICE per row, in the row and in the primary-key index, across 2.1 million
19872
+ * motion, audio and object events. An `INTEGER PRIMARY KEY` in SQLite **is**
19873
+ * the rowid: the table itself is that B-tree, so the index stops existing
19874
+ * rather than getting smaller. No shorter string does that.
19875
+ *
19876
+ * Defined once here for all three event kinds, which is why they move
19877
+ * together: a per-table migration would have forked this and
19878
+ * `COMMON_BASE_COLUMNS` and reunited them two stages later.
19879
+ */
19880
+ id: number().int(),
19833
19881
  deviceId: number(),
19834
19882
  timestamp: number()
19835
19883
  };
@@ -19848,7 +19896,34 @@ var MotionEventSchema = object({
19848
19896
  /** Omitted in slim projection. */
19849
19897
  frameHeight: number().optional(),
19850
19898
  /** Populated by B5 (recording playback URL for this event). */
19851
- mediaUrl: string().optional()
19899
+ mediaUrl: string().optional(),
19900
+ /**
19901
+ * One row per motion EPISODE, not one per push (D475). `null` while the
19902
+ * episode is still open — a further rising edge extends it in place rather
19903
+ * than inserting a new row. Set once, at close, to `lastOnAt - startedAt`
19904
+ * (the span from the first rising edge to the LAST one, deliberately NOT
19905
+ * `closedAt - startedAt` — the close delay is a quiet CONFIRMATION, not
19906
+ * movement, and folding it in would report `MOTION_CLOSE_AFTER_MS` of
19907
+ * motion for an instantaneous trigger).
19908
+ *
19909
+ * **Absent** (not merely `null`) on a row written before D475 — that means
19910
+ * "closed the old way, before this column existed", never "still open".
19911
+ * Nothing in this codebase may read an absent `durationMs` as an open
19912
+ * episode; only `null` means open.
19913
+ */
19914
+ durationMs: number().nullable().optional(),
19915
+ /**
19916
+ * Ms offsets from `timestamp` (the episode's own first rising edge, so the
19917
+ * first entry is always `0`) of every genuine off→on transition the
19918
+ * episode saw — "ogni evento on si deve salvare" (D475). NOT one entry per
19919
+ * push: a firmware source that keepalives at ~1 Hz for the whole burst
19920
+ * (Reolink, Hikvision) produces exactly one edge; a source that reports an
19921
+ * explicit `false` mid-episode and then resumes before the quiet window
19922
+ * elapses produces another. Stored compactly — see `motion-edge-codec.ts`
19923
+ * — and decoded back to this shape on read. Absent/empty on a legacy row,
19924
+ * which must never be read as "no episode happened here".
19925
+ */
19926
+ edges: array(number()).readonly().optional()
19852
19927
  });
19853
19928
  /**
19854
19929
  * Which detection SOURCE produced an object event. `pipeline` = the ML
@@ -19903,6 +19978,23 @@ var ObjectEventSchema = object({
19903
19978
  * includes it (it is light). Absent on rows written before this field.
19904
19979
  */
19905
19980
  frameId: string().optional(),
19981
+ /**
19982
+ * A PRODUCER-chosen key that makes a synthetic event's emission idempotent
19983
+ * (D474).
19984
+ *
19985
+ * Only the package detector writes it, and it exists because the event id
19986
+ * stopped being choosable: the delivery and pick-up rows used to BE their
19987
+ * dedupe key (`pa-pkg-<entryId>-delivered`), which is how "never emit a
19988
+ * second delivery for this entry" survived a restart. An `INTEGER` rowid is
19989
+ * assigned by SQLite, so that key had to move off the primary key rather
19990
+ * than be dropped — a detector that cannot recognise its own row re-delivers
19991
+ * every parcel on every boot.
19992
+ *
19993
+ * Absent on every other object event, and on every row written before this
19994
+ * field. Never a substitute for `id`: it is unique per (producer, occasion),
19995
+ * not per row, and nothing addresses a row by it.
19996
+ */
19997
+ idempotencyKey: string().optional(),
19906
19998
  /** Omitted in slim projection. */
19907
19999
  trackId: string().optional(),
19908
20000
  className: string(),
@@ -28268,14 +28360,72 @@ authKey: string().optional() }), object({
28268
28360
  * (alert-center, advanced-notifier) can subscribe once and receive
28269
28361
  * motion from every camera.
28270
28362
  */
28363
+ /**
28364
+ * How long after a camera's last rising edge `pipeline-analytics` closes the
28365
+ * motion EPISODE row it kept open for it (D475) — "il delay della cap", in
28366
+ * the operator's words. One authority for the whole system: every provider
28367
+ * that seeds `autoClearAfterMs` on a `Camera` device writes this SAME
28368
+ * constant while `detected: true` (never its own number — Hikvision used to
28369
+ * seed its internal 3 s inactivity timer here, which is a different clock for
28370
+ * a different purpose), and `MotionEpisodeTracker` imports it directly rather
28371
+ * than reading the live cap value on the hot path. A `Sensor` device (HA
28372
+ * binary sensor, Homematic) gets a genuine push both ways and has no episode
28373
+ * to close on a timer, so it writes `null`, never this constant.
28374
+ */
28375
+ var MOTION_CLOSE_AFTER_MS = 15e3;
28271
28376
  var MotionStatusSchema = object({
28272
28377
  detected: boolean(),
28273
28378
  /** Ms epoch of the last detected-true observation. Null if never detected. */
28274
28379
  lastDetectedAt: number().nullable(),
28275
28380
  /**
28276
- * Ms after which `detected` auto-reverts to false if no fresh push
28277
- * arrives. Null means the provider leaves detected state until a
28278
- * native "clear" event.
28381
+ * `MOTION_CLOSE_AFTER_MS` while `detected: true` on a `Camera` device,
28382
+ * `null` while false and on every `Sensor` device (D475) — see that
28383
+ * constant's doc for the one-authority rule.
28384
+ *
28385
+ * ## Reading this field still arms nothing
28386
+ *
28387
+ * It reads like an instruction to the consumer ("revert after N ms if
28388
+ * no fresh push arrives") and it is not one: nothing in this repo reads
28389
+ * the LIVE cap value to drive a timer. `pipeline-analytics`'s motion-episode
28390
+ * close DOES now use the same number — `MOTION_CLOSE_AFTER_MS` — but as an
28391
+ * imported constant, not as a read of `device.state.motion.value`, so this
28392
+ * field stays what it always was: DESCRIPTIVE output, mirroring an answer
28393
+ * computed elsewhere. Building a self-clear timer out of a READ of this
28394
+ * field would add a second falling-edge authority beside whichever one
28395
+ * already owns the device, and two that can disagree are worse than one.
28396
+ * Consumers that need a falling edge SHAPED differently — held open across
28397
+ * a flapping source — debounce on their own side and say so, as
28398
+ * `addon-export-alexa/src/motion-clear-hold.ts` and
28399
+ * `addon-export-hap`'s `RESET_DEBOUNCE_MS` both do.
28400
+ *
28401
+ * ## Who writes it
28402
+ *
28403
+ * - **Cameras** — the runner's phase machine, `active → watching` on
28404
+ * `cooldown_expired`, which then writes this slice with
28405
+ * `detected: false` (`handlePhaseChanged` in
28406
+ * `pipeline-runner/index.ts`). It produces the FALLING edge, which
28407
+ * matters most for the sources that only ever push a rising one:
28408
+ * Reolink emits `MotionOnMotionChanged { detected: true }` and never
28409
+ * a false.
28410
+ * - **Sensors** (Home Assistant binary sensors, Homematic) — the
28411
+ * provider pushes the false itself, from the upstream system's own
28412
+ * state change. No phase machine is involved.
28413
+ *
28414
+ * ### The phase machine is CANONICAL, not sole — and that is a defect
28415
+ *
28416
+ * An earlier revision of this docblock (mine, 2026-09-12) claimed the
28417
+ * phase machine is the sole writer for a camera. It is not.
28418
+ * `hikvision-camera.ts:3464` and `amcrest-camera.ts:445` both call
28419
+ * `setCapSlice(motionCapability, …)` on their own rising edge, and
28420
+ * Hikvision's comment says why: it read THIS docblock, agreed the
28421
+ * runner is canonical, and wrote anyway to avoid per-tick churn. So
28422
+ * two authorities can disagree about one slice, which this repo
28423
+ * forbids, and the doc said otherwise — which is worse than saying
28424
+ * nothing, because it reads as verification.
28425
+ *
28426
+ * This predates D475 and is not fixed there: the fix touches every
28427
+ * camera provider. Recorded in D475's Consequences. Do not restore the
28428
+ * "sole writer" wording without also removing the other writers.
28279
28429
  */
28280
28430
  autoClearAfterMs: number().nullable()
28281
28431
  });
@@ -28345,7 +28495,11 @@ onMotionChanged: { data: MotionOnMotionChangedDataSchema } },
28345
28495
  */
28346
28496
  runtimeState: MotionStatusSchema,
28347
28497
  /**
28348
- * 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.
28498
+ * Runtime-state durability: **session** — every writer of this slice
28499
+ * writes only on an EDGE, so a restored `detected: true` would stay
28500
+ * frozen until the next one instead of being corrected. The next edge
28501
+ * re-publishes the real state. (On who the writers are, and why there
28502
+ * is more than one, see `autoClearAfterMs` above.)
28349
28503
  *
28350
28504
  * See `RuntimeStateDurability`. Enforced by
28351
28505
  * `scripts/check-runtime-state-durability.ts`.
@@ -49261,7 +49415,7 @@ var HikvisionCamera = class HikvisionCamera extends BaseDevice {
49261
49415
  this.setCapSlice(motionCapability, {
49262
49416
  detected: false,
49263
49417
  lastDetectedAt: null,
49264
- autoClearAfterMs: HikvisionCamera.MOTION_INACTIVE_AFTER_MS
49418
+ autoClearAfterMs: null
49265
49419
  });
49266
49420
  this.registerIntercomIfSupported();
49267
49421
  this.registerOsdCap(1);
@@ -51507,7 +51661,7 @@ var HikvisionCamera = class HikvisionCamera extends BaseDevice {
51507
51661
  this.setCapSlice(motionCapability, {
51508
51662
  detected: true,
51509
51663
  lastDetectedAt: now,
51510
- autoClearAfterMs: HikvisionCamera.MOTION_INACTIVE_AFTER_MS
51664
+ autoClearAfterMs: MOTION_CLOSE_AFTER_MS
51511
51665
  });
51512
51666
  } catch {}
51513
51667
  const existing = this.motionInactiveTimers.get(camStreamId);
@@ -51527,7 +51681,7 @@ var HikvisionCamera = class HikvisionCamera extends BaseDevice {
51527
51681
  this.setCapSlice(motionCapability, {
51528
51682
  detected: false,
51529
51683
  lastDetectedAt,
51530
- autoClearAfterMs: HikvisionCamera.MOTION_INACTIVE_AFTER_MS
51684
+ autoClearAfterMs: null
51531
51685
  });
51532
51686
  } catch {}
51533
51687
  }, HikvisionCamera.MOTION_INACTIVE_AFTER_MS);
package/dist/addon.mjs CHANGED
@@ -7299,6 +7299,23 @@ method(object({ deviceId: number() }), array(StreamSourceEntrySchema)), method(o
7299
7299
  action: string().min(1),
7300
7300
  input: unknown()
7301
7301
  }), unknown(), { kind: "mutation" }), method(object({ deviceId: number() }), _void(), { kind: "mutation" }), method(object({ deviceId: number() }), unknown().nullable()), method(object({ deviceId: number() }), RawStateResultSchema.nullable(), { auth: "protected" });
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([
@@ -12792,8 +12809,17 @@ method(object({
12792
12809
  }), array(SettingsRecordSchema).readonly()), method(object({
12793
12810
  namespace: string().optional(),
12794
12811
  collection: string(),
12795
- record: SettingsRecordSchema
12796
- }), _void(), { kind: "mutation" }), method(object({
12812
+ record: object({
12813
+ id: string().optional(),
12814
+ data: record(string(), unknown())
12815
+ })
12816
+ }), object({
12817
+ /**
12818
+ * The id the row ACTUALLY got (D473): the one supplied, the UUID minted
12819
+ * for an absent one, or the ROWID SQLite assigned on an `INTEGER`
12820
+ * primary key — which is the only place an auto key is knowable.
12821
+ */
12822
+ id: union([string(), number()]) }), { kind: "mutation" }), method(object({
12797
12823
  namespace: string().optional(),
12798
12824
  collection: string(),
12799
12825
  records: array(BulkRecordSchema).readonly()
@@ -12902,8 +12928,17 @@ method(_void(), EngineInfoSchema, { auth: "admin" }), method(object({
12902
12928
  }), array(SettingsRecordSchema).readonly(), { auth: "admin" }), method(object({
12903
12929
  namespace: string().optional(),
12904
12930
  collection: string(),
12905
- record: SettingsRecordSchema
12906
- }), _void(), {
12931
+ record: object({
12932
+ id: string().optional(),
12933
+ data: record(string(), unknown())
12934
+ })
12935
+ }), object({
12936
+ /**
12937
+ * The id the row ACTUALLY got (D473): the one supplied, the UUID minted
12938
+ * for an absent one, or the ROWID SQLite assigned on an `INTEGER`
12939
+ * primary key — which is the only place an auto key is knowable.
12940
+ */
12941
+ id: union([string(), number()]) }), {
12907
12942
  kind: "mutation",
12908
12943
  auth: "admin"
12909
12944
  }), method(object({
@@ -19830,7 +19865,20 @@ var TrackSchema = object({
19830
19865
  ...TrackRetrainFields
19831
19866
  });
19832
19867
  var BaseEventFields = {
19833
- id: string(),
19868
+ /**
19869
+ * A SQLite ROWID, assigned by the database (D474).
19870
+ *
19871
+ * Was a 36-character UUID and cost 263 MB of a 1 117 MB database — paid
19872
+ * TWICE per row, in the row and in the primary-key index, across 2.1 million
19873
+ * motion, audio and object events. An `INTEGER PRIMARY KEY` in SQLite **is**
19874
+ * the rowid: the table itself is that B-tree, so the index stops existing
19875
+ * rather than getting smaller. No shorter string does that.
19876
+ *
19877
+ * Defined once here for all three event kinds, which is why they move
19878
+ * together: a per-table migration would have forked this and
19879
+ * `COMMON_BASE_COLUMNS` and reunited them two stages later.
19880
+ */
19881
+ id: number().int(),
19834
19882
  deviceId: number(),
19835
19883
  timestamp: number()
19836
19884
  };
@@ -19849,7 +19897,34 @@ var MotionEventSchema = object({
19849
19897
  /** Omitted in slim projection. */
19850
19898
  frameHeight: number().optional(),
19851
19899
  /** Populated by B5 (recording playback URL for this event). */
19852
- mediaUrl: string().optional()
19900
+ mediaUrl: string().optional(),
19901
+ /**
19902
+ * One row per motion EPISODE, not one per push (D475). `null` while the
19903
+ * episode is still open — a further rising edge extends it in place rather
19904
+ * than inserting a new row. Set once, at close, to `lastOnAt - startedAt`
19905
+ * (the span from the first rising edge to the LAST one, deliberately NOT
19906
+ * `closedAt - startedAt` — the close delay is a quiet CONFIRMATION, not
19907
+ * movement, and folding it in would report `MOTION_CLOSE_AFTER_MS` of
19908
+ * motion for an instantaneous trigger).
19909
+ *
19910
+ * **Absent** (not merely `null`) on a row written before D475 — that means
19911
+ * "closed the old way, before this column existed", never "still open".
19912
+ * Nothing in this codebase may read an absent `durationMs` as an open
19913
+ * episode; only `null` means open.
19914
+ */
19915
+ durationMs: number().nullable().optional(),
19916
+ /**
19917
+ * Ms offsets from `timestamp` (the episode's own first rising edge, so the
19918
+ * first entry is always `0`) of every genuine off→on transition the
19919
+ * episode saw — "ogni evento on si deve salvare" (D475). NOT one entry per
19920
+ * push: a firmware source that keepalives at ~1 Hz for the whole burst
19921
+ * (Reolink, Hikvision) produces exactly one edge; a source that reports an
19922
+ * explicit `false` mid-episode and then resumes before the quiet window
19923
+ * elapses produces another. Stored compactly — see `motion-edge-codec.ts`
19924
+ * — and decoded back to this shape on read. Absent/empty on a legacy row,
19925
+ * which must never be read as "no episode happened here".
19926
+ */
19927
+ edges: array(number()).readonly().optional()
19853
19928
  });
19854
19929
  /**
19855
19930
  * Which detection SOURCE produced an object event. `pipeline` = the ML
@@ -19904,6 +19979,23 @@ var ObjectEventSchema = object({
19904
19979
  * includes it (it is light). Absent on rows written before this field.
19905
19980
  */
19906
19981
  frameId: string().optional(),
19982
+ /**
19983
+ * A PRODUCER-chosen key that makes a synthetic event's emission idempotent
19984
+ * (D474).
19985
+ *
19986
+ * Only the package detector writes it, and it exists because the event id
19987
+ * stopped being choosable: the delivery and pick-up rows used to BE their
19988
+ * dedupe key (`pa-pkg-<entryId>-delivered`), which is how "never emit a
19989
+ * second delivery for this entry" survived a restart. An `INTEGER` rowid is
19990
+ * assigned by SQLite, so that key had to move off the primary key rather
19991
+ * than be dropped — a detector that cannot recognise its own row re-delivers
19992
+ * every parcel on every boot.
19993
+ *
19994
+ * Absent on every other object event, and on every row written before this
19995
+ * field. Never a substitute for `id`: it is unique per (producer, occasion),
19996
+ * not per row, and nothing addresses a row by it.
19997
+ */
19998
+ idempotencyKey: string().optional(),
19907
19999
  /** Omitted in slim projection. */
19908
20000
  trackId: string().optional(),
19909
20001
  className: string(),
@@ -28269,14 +28361,72 @@ authKey: string().optional() }), object({
28269
28361
  * (alert-center, advanced-notifier) can subscribe once and receive
28270
28362
  * motion from every camera.
28271
28363
  */
28364
+ /**
28365
+ * How long after a camera's last rising edge `pipeline-analytics` closes the
28366
+ * motion EPISODE row it kept open for it (D475) — "il delay della cap", in
28367
+ * the operator's words. One authority for the whole system: every provider
28368
+ * that seeds `autoClearAfterMs` on a `Camera` device writes this SAME
28369
+ * constant while `detected: true` (never its own number — Hikvision used to
28370
+ * seed its internal 3 s inactivity timer here, which is a different clock for
28371
+ * a different purpose), and `MotionEpisodeTracker` imports it directly rather
28372
+ * than reading the live cap value on the hot path. A `Sensor` device (HA
28373
+ * binary sensor, Homematic) gets a genuine push both ways and has no episode
28374
+ * to close on a timer, so it writes `null`, never this constant.
28375
+ */
28376
+ var MOTION_CLOSE_AFTER_MS = 15e3;
28272
28377
  var MotionStatusSchema = object({
28273
28378
  detected: boolean(),
28274
28379
  /** Ms epoch of the last detected-true observation. Null if never detected. */
28275
28380
  lastDetectedAt: number().nullable(),
28276
28381
  /**
28277
- * Ms after which `detected` auto-reverts to false if no fresh push
28278
- * arrives. Null means the provider leaves detected state until a
28279
- * native "clear" event.
28382
+ * `MOTION_CLOSE_AFTER_MS` while `detected: true` on a `Camera` device,
28383
+ * `null` while false and on every `Sensor` device (D475) — see that
28384
+ * constant's doc for the one-authority rule.
28385
+ *
28386
+ * ## Reading this field still arms nothing
28387
+ *
28388
+ * It reads like an instruction to the consumer ("revert after N ms if
28389
+ * no fresh push arrives") and it is not one: nothing in this repo reads
28390
+ * the LIVE cap value to drive a timer. `pipeline-analytics`'s motion-episode
28391
+ * close DOES now use the same number — `MOTION_CLOSE_AFTER_MS` — but as an
28392
+ * imported constant, not as a read of `device.state.motion.value`, so this
28393
+ * field stays what it always was: DESCRIPTIVE output, mirroring an answer
28394
+ * computed elsewhere. Building a self-clear timer out of a READ of this
28395
+ * field would add a second falling-edge authority beside whichever one
28396
+ * already owns the device, and two that can disagree are worse than one.
28397
+ * Consumers that need a falling edge SHAPED differently — held open across
28398
+ * a flapping source — debounce on their own side and say so, as
28399
+ * `addon-export-alexa/src/motion-clear-hold.ts` and
28400
+ * `addon-export-hap`'s `RESET_DEBOUNCE_MS` both do.
28401
+ *
28402
+ * ## Who writes it
28403
+ *
28404
+ * - **Cameras** — the runner's phase machine, `active → watching` on
28405
+ * `cooldown_expired`, which then writes this slice with
28406
+ * `detected: false` (`handlePhaseChanged` in
28407
+ * `pipeline-runner/index.ts`). It produces the FALLING edge, which
28408
+ * matters most for the sources that only ever push a rising one:
28409
+ * Reolink emits `MotionOnMotionChanged { detected: true }` and never
28410
+ * a false.
28411
+ * - **Sensors** (Home Assistant binary sensors, Homematic) — the
28412
+ * provider pushes the false itself, from the upstream system's own
28413
+ * state change. No phase machine is involved.
28414
+ *
28415
+ * ### The phase machine is CANONICAL, not sole — and that is a defect
28416
+ *
28417
+ * An earlier revision of this docblock (mine, 2026-09-12) claimed the
28418
+ * phase machine is the sole writer for a camera. It is not.
28419
+ * `hikvision-camera.ts:3464` and `amcrest-camera.ts:445` both call
28420
+ * `setCapSlice(motionCapability, …)` on their own rising edge, and
28421
+ * Hikvision's comment says why: it read THIS docblock, agreed the
28422
+ * runner is canonical, and wrote anyway to avoid per-tick churn. So
28423
+ * two authorities can disagree about one slice, which this repo
28424
+ * forbids, and the doc said otherwise — which is worse than saying
28425
+ * nothing, because it reads as verification.
28426
+ *
28427
+ * This predates D475 and is not fixed there: the fix touches every
28428
+ * camera provider. Recorded in D475's Consequences. Do not restore the
28429
+ * "sole writer" wording without also removing the other writers.
28280
28430
  */
28281
28431
  autoClearAfterMs: number().nullable()
28282
28432
  });
@@ -28346,7 +28496,11 @@ onMotionChanged: { data: MotionOnMotionChangedDataSchema } },
28346
28496
  */
28347
28497
  runtimeState: MotionStatusSchema,
28348
28498
  /**
28349
- * 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.
28499
+ * Runtime-state durability: **session** — every writer of this slice
28500
+ * writes only on an EDGE, so a restored `detected: true` would stay
28501
+ * frozen until the next one instead of being corrected. The next edge
28502
+ * re-publishes the real state. (On who the writers are, and why there
28503
+ * is more than one, see `autoClearAfterMs` above.)
28350
28504
  *
28351
28505
  * See `RuntimeStateDurability`. Enforced by
28352
28506
  * `scripts/check-runtime-state-durability.ts`.
@@ -49262,7 +49416,7 @@ var HikvisionCamera = class HikvisionCamera extends BaseDevice {
49262
49416
  this.setCapSlice(motionCapability, {
49263
49417
  detected: false,
49264
49418
  lastDetectedAt: null,
49265
- autoClearAfterMs: HikvisionCamera.MOTION_INACTIVE_AFTER_MS
49419
+ autoClearAfterMs: null
49266
49420
  });
49267
49421
  this.registerIntercomIfSupported();
49268
49422
  this.registerOsdCap(1);
@@ -51508,7 +51662,7 @@ var HikvisionCamera = class HikvisionCamera extends BaseDevice {
51508
51662
  this.setCapSlice(motionCapability, {
51509
51663
  detected: true,
51510
51664
  lastDetectedAt: now,
51511
- autoClearAfterMs: HikvisionCamera.MOTION_INACTIVE_AFTER_MS
51665
+ autoClearAfterMs: MOTION_CLOSE_AFTER_MS
51512
51666
  });
51513
51667
  } catch {}
51514
51668
  const existing = this.motionInactiveTimers.get(camStreamId);
@@ -51528,7 +51682,7 @@ var HikvisionCamera = class HikvisionCamera extends BaseDevice {
51528
51682
  this.setCapSlice(motionCapability, {
51529
51683
  detected: false,
51530
51684
  lastDetectedAt,
51531
- autoClearAfterMs: HikvisionCamera.MOTION_INACTIVE_AFTER_MS
51685
+ autoClearAfterMs: null
51532
51686
  });
51533
51687
  } catch {}
51534
51688
  }, HikvisionCamera.MOTION_INACTIVE_AFTER_MS);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@camstack/addon-provider-hikvision",
3
- "version": "1.2.105",
3
+ "version": "1.2.106",
4
4
  "description": "Hikvision camera device provider addon for CamStack — ISAPI over HTTP(S) with digest auth (snapshot, alarm stream, RTSP discovery)",
5
5
  "keywords": [
6
6
  "camstack",