@camstack/addon-export-hap 1.2.108 → 1.2.109

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.
@@ -7941,6 +7941,23 @@ function errMsg$15(err) {
7941
7941
  if (typeof err === "string") return err;
7942
7942
  return String(err);
7943
7943
  }
7944
+ new Set([
7945
+ "track",
7946
+ "summary",
7947
+ "face",
7948
+ "identity",
7949
+ "plate",
7950
+ "vehicle",
7951
+ "scene",
7952
+ "motion",
7953
+ "object",
7954
+ "audio"
7955
+ ]);
7956
+ new Set([
7957
+ "motion",
7958
+ "object",
7959
+ "audio"
7960
+ ]);
7944
7961
  var EncodeProfileSchema = object({
7945
7962
  video: object({
7946
7963
  codec: _enum([
@@ -13121,8 +13138,17 @@ method(object({
13121
13138
  }), array(SettingsRecordSchema).readonly()), method(object({
13122
13139
  namespace: string().optional(),
13123
13140
  collection: string(),
13124
- record: SettingsRecordSchema
13125
- }), _void(), { kind: "mutation" }), method(object({
13141
+ record: object({
13142
+ id: string().optional(),
13143
+ data: record(string(), unknown())
13144
+ })
13145
+ }), object({
13146
+ /**
13147
+ * The id the row ACTUALLY got (D473): the one supplied, the UUID minted
13148
+ * for an absent one, or the ROWID SQLite assigned on an `INTEGER`
13149
+ * primary key — which is the only place an auto key is knowable.
13150
+ */
13151
+ id: union([string(), number()]) }), { kind: "mutation" }), method(object({
13126
13152
  namespace: string().optional(),
13127
13153
  collection: string(),
13128
13154
  records: array(BulkRecordSchema).readonly()
@@ -13231,8 +13257,17 @@ method(_void(), EngineInfoSchema, { auth: "admin" }), method(object({
13231
13257
  }), array(SettingsRecordSchema).readonly(), { auth: "admin" }), method(object({
13232
13258
  namespace: string().optional(),
13233
13259
  collection: string(),
13234
- record: SettingsRecordSchema
13235
- }), _void(), {
13260
+ record: object({
13261
+ id: string().optional(),
13262
+ data: record(string(), unknown())
13263
+ })
13264
+ }), object({
13265
+ /**
13266
+ * The id the row ACTUALLY got (D473): the one supplied, the UUID minted
13267
+ * for an absent one, or the ROWID SQLite assigned on an `INTEGER`
13268
+ * primary key — which is the only place an auto key is knowable.
13269
+ */
13270
+ id: union([string(), number()]) }), {
13236
13271
  kind: "mutation",
13237
13272
  auth: "admin"
13238
13273
  }), method(object({
@@ -19955,7 +19990,20 @@ var TrackSchema = object({
19955
19990
  ...TrackRetrainFields
19956
19991
  });
19957
19992
  var BaseEventFields = {
19958
- id: string(),
19993
+ /**
19994
+ * A SQLite ROWID, assigned by the database (D474).
19995
+ *
19996
+ * Was a 36-character UUID and cost 263 MB of a 1 117 MB database — paid
19997
+ * TWICE per row, in the row and in the primary-key index, across 2.1 million
19998
+ * motion, audio and object events. An `INTEGER PRIMARY KEY` in SQLite **is**
19999
+ * the rowid: the table itself is that B-tree, so the index stops existing
20000
+ * rather than getting smaller. No shorter string does that.
20001
+ *
20002
+ * Defined once here for all three event kinds, which is why they move
20003
+ * together: a per-table migration would have forked this and
20004
+ * `COMMON_BASE_COLUMNS` and reunited them two stages later.
20005
+ */
20006
+ id: number().int(),
19959
20007
  deviceId: number(),
19960
20008
  timestamp: number()
19961
20009
  };
@@ -19974,7 +20022,34 @@ var MotionEventSchema = object({
19974
20022
  /** Omitted in slim projection. */
19975
20023
  frameHeight: number().optional(),
19976
20024
  /** Populated by B5 (recording playback URL for this event). */
19977
- mediaUrl: string().optional()
20025
+ mediaUrl: string().optional(),
20026
+ /**
20027
+ * One row per motion EPISODE, not one per push (D475). `null` while the
20028
+ * episode is still open — a further rising edge extends it in place rather
20029
+ * than inserting a new row. Set once, at close, to `lastOnAt - startedAt`
20030
+ * (the span from the first rising edge to the LAST one, deliberately NOT
20031
+ * `closedAt - startedAt` — the close delay is a quiet CONFIRMATION, not
20032
+ * movement, and folding it in would report `MOTION_CLOSE_AFTER_MS` of
20033
+ * motion for an instantaneous trigger).
20034
+ *
20035
+ * **Absent** (not merely `null`) on a row written before D475 — that means
20036
+ * "closed the old way, before this column existed", never "still open".
20037
+ * Nothing in this codebase may read an absent `durationMs` as an open
20038
+ * episode; only `null` means open.
20039
+ */
20040
+ durationMs: number().nullable().optional(),
20041
+ /**
20042
+ * Ms offsets from `timestamp` (the episode's own first rising edge, so the
20043
+ * first entry is always `0`) of every genuine off→on transition the
20044
+ * episode saw — "ogni evento on si deve salvare" (D475). NOT one entry per
20045
+ * push: a firmware source that keepalives at ~1 Hz for the whole burst
20046
+ * (Reolink, Hikvision) produces exactly one edge; a source that reports an
20047
+ * explicit `false` mid-episode and then resumes before the quiet window
20048
+ * elapses produces another. Stored compactly — see `motion-edge-codec.ts`
20049
+ * — and decoded back to this shape on read. Absent/empty on a legacy row,
20050
+ * which must never be read as "no episode happened here".
20051
+ */
20052
+ edges: array(number()).readonly().optional()
19978
20053
  });
19979
20054
  /**
19980
20055
  * Which detection SOURCE produced an object event. `pipeline` = the ML
@@ -20029,6 +20104,23 @@ var ObjectEventSchema = object({
20029
20104
  * includes it (it is light). Absent on rows written before this field.
20030
20105
  */
20031
20106
  frameId: string().optional(),
20107
+ /**
20108
+ * A PRODUCER-chosen key that makes a synthetic event's emission idempotent
20109
+ * (D474).
20110
+ *
20111
+ * Only the package detector writes it, and it exists because the event id
20112
+ * stopped being choosable: the delivery and pick-up rows used to BE their
20113
+ * dedupe key (`pa-pkg-<entryId>-delivered`), which is how "never emit a
20114
+ * second delivery for this entry" survived a restart. An `INTEGER` rowid is
20115
+ * assigned by SQLite, so that key had to move off the primary key rather
20116
+ * than be dropped — a detector that cannot recognise its own row re-delivers
20117
+ * every parcel on every boot.
20118
+ *
20119
+ * Absent on every other object event, and on every row written before this
20120
+ * field. Never a substitute for `id`: it is unique per (producer, occasion),
20121
+ * not per row, and nothing addresses a row by it.
20122
+ */
20123
+ idempotencyKey: string().optional(),
20032
20124
  /** Omitted in slim projection. */
20033
20125
  trackId: string().optional(),
20034
20126
  className: string(),
@@ -27126,26 +27218,59 @@ authKey: string().optional() }), object({
27126
27218
  /** Human-readable error when `ok: false`. */
27127
27219
  error: string().optional()
27128
27220
  }), { kind: "mutation" });
27129
- /**
27130
- * Hardware / firmware motion sensor cap — binary detected state plus
27131
- * a timestamp of the last observation. Distinct from
27132
- * `motion-detection.cap.ts` which owns the LOCAL ML motion pipeline;
27133
- * `motion` is the lightweight readout from on-camera motion (Reolink
27134
- * `GetMdState`, Baichuan push `type: motion`, ONVIF analytics).
27135
- *
27136
- * Native-motion providers also fan out to `detection.camera-native`
27137
- * with `source: 'onboard'` so cross-cutting system services
27138
- * (alert-center, advanced-notifier) can subscribe once and receive
27139
- * motion from every camera.
27140
- */
27141
27221
  var MotionStatusSchema = object({
27142
27222
  detected: boolean(),
27143
27223
  /** Ms epoch of the last detected-true observation. Null if never detected. */
27144
27224
  lastDetectedAt: number().nullable(),
27145
27225
  /**
27146
- * Ms after which `detected` auto-reverts to false if no fresh push
27147
- * arrives. Null means the provider leaves detected state until a
27148
- * native "clear" event.
27226
+ * `MOTION_CLOSE_AFTER_MS` while `detected: true` on a `Camera` device,
27227
+ * `null` while false and on every `Sensor` device (D475) — see that
27228
+ * constant's doc for the one-authority rule.
27229
+ *
27230
+ * ## Reading this field still arms nothing
27231
+ *
27232
+ * It reads like an instruction to the consumer ("revert after N ms if
27233
+ * no fresh push arrives") and it is not one: nothing in this repo reads
27234
+ * the LIVE cap value to drive a timer. `pipeline-analytics`'s motion-episode
27235
+ * close DOES now use the same number — `MOTION_CLOSE_AFTER_MS` — but as an
27236
+ * imported constant, not as a read of `device.state.motion.value`, so this
27237
+ * field stays what it always was: DESCRIPTIVE output, mirroring an answer
27238
+ * computed elsewhere. Building a self-clear timer out of a READ of this
27239
+ * field would add a second falling-edge authority beside whichever one
27240
+ * already owns the device, and two that can disagree are worse than one.
27241
+ * Consumers that need a falling edge SHAPED differently — held open across
27242
+ * a flapping source — debounce on their own side and say so, as
27243
+ * `addon-export-alexa/src/motion-clear-hold.ts` and
27244
+ * `addon-export-hap`'s `RESET_DEBOUNCE_MS` both do.
27245
+ *
27246
+ * ## Who writes it
27247
+ *
27248
+ * - **Cameras** — the runner's phase machine, `active → watching` on
27249
+ * `cooldown_expired`, which then writes this slice with
27250
+ * `detected: false` (`handlePhaseChanged` in
27251
+ * `pipeline-runner/index.ts`). It produces the FALLING edge, which
27252
+ * matters most for the sources that only ever push a rising one:
27253
+ * Reolink emits `MotionOnMotionChanged { detected: true }` and never
27254
+ * a false.
27255
+ * - **Sensors** (Home Assistant binary sensors, Homematic) — the
27256
+ * provider pushes the false itself, from the upstream system's own
27257
+ * state change. No phase machine is involved.
27258
+ *
27259
+ * ### The phase machine is CANONICAL, not sole — and that is a defect
27260
+ *
27261
+ * An earlier revision of this docblock (mine, 2026-09-12) claimed the
27262
+ * phase machine is the sole writer for a camera. It is not.
27263
+ * `hikvision-camera.ts:3464` and `amcrest-camera.ts:445` both call
27264
+ * `setCapSlice(motionCapability, …)` on their own rising edge, and
27265
+ * Hikvision's comment says why: it read THIS docblock, agreed the
27266
+ * runner is canonical, and wrote anyway to avoid per-tick churn. So
27267
+ * two authorities can disagree about one slice, which this repo
27268
+ * forbids, and the doc said otherwise — which is worse than saying
27269
+ * nothing, because it reads as verification.
27270
+ *
27271
+ * This predates D475 and is not fixed there: the fix touches every
27272
+ * camera provider. Recorded in D475's Consequences. Do not restore the
27273
+ * "sole writer" wording without also removing the other writers.
27149
27274
  */
27150
27275
  autoClearAfterMs: number().nullable()
27151
27276
  });
@@ -7929,6 +7929,23 @@ function errMsg$15(err) {
7929
7929
  if (typeof err === "string") return err;
7930
7930
  return String(err);
7931
7931
  }
7932
+ new Set([
7933
+ "track",
7934
+ "summary",
7935
+ "face",
7936
+ "identity",
7937
+ "plate",
7938
+ "vehicle",
7939
+ "scene",
7940
+ "motion",
7941
+ "object",
7942
+ "audio"
7943
+ ]);
7944
+ new Set([
7945
+ "motion",
7946
+ "object",
7947
+ "audio"
7948
+ ]);
7932
7949
  var EncodeProfileSchema = object({
7933
7950
  video: object({
7934
7951
  codec: _enum([
@@ -13109,8 +13126,17 @@ method(object({
13109
13126
  }), array(SettingsRecordSchema).readonly()), method(object({
13110
13127
  namespace: string().optional(),
13111
13128
  collection: string(),
13112
- record: SettingsRecordSchema
13113
- }), _void(), { kind: "mutation" }), method(object({
13129
+ record: object({
13130
+ id: string().optional(),
13131
+ data: record(string(), unknown())
13132
+ })
13133
+ }), object({
13134
+ /**
13135
+ * The id the row ACTUALLY got (D473): the one supplied, the UUID minted
13136
+ * for an absent one, or the ROWID SQLite assigned on an `INTEGER`
13137
+ * primary key — which is the only place an auto key is knowable.
13138
+ */
13139
+ id: union([string(), number()]) }), { kind: "mutation" }), method(object({
13114
13140
  namespace: string().optional(),
13115
13141
  collection: string(),
13116
13142
  records: array(BulkRecordSchema).readonly()
@@ -13219,8 +13245,17 @@ method(_void(), EngineInfoSchema, { auth: "admin" }), method(object({
13219
13245
  }), array(SettingsRecordSchema).readonly(), { auth: "admin" }), method(object({
13220
13246
  namespace: string().optional(),
13221
13247
  collection: string(),
13222
- record: SettingsRecordSchema
13223
- }), _void(), {
13248
+ record: object({
13249
+ id: string().optional(),
13250
+ data: record(string(), unknown())
13251
+ })
13252
+ }), object({
13253
+ /**
13254
+ * The id the row ACTUALLY got (D473): the one supplied, the UUID minted
13255
+ * for an absent one, or the ROWID SQLite assigned on an `INTEGER`
13256
+ * primary key — which is the only place an auto key is knowable.
13257
+ */
13258
+ id: union([string(), number()]) }), {
13224
13259
  kind: "mutation",
13225
13260
  auth: "admin"
13226
13261
  }), method(object({
@@ -19943,7 +19978,20 @@ var TrackSchema = object({
19943
19978
  ...TrackRetrainFields
19944
19979
  });
19945
19980
  var BaseEventFields = {
19946
- id: string(),
19981
+ /**
19982
+ * A SQLite ROWID, assigned by the database (D474).
19983
+ *
19984
+ * Was a 36-character UUID and cost 263 MB of a 1 117 MB database — paid
19985
+ * TWICE per row, in the row and in the primary-key index, across 2.1 million
19986
+ * motion, audio and object events. An `INTEGER PRIMARY KEY` in SQLite **is**
19987
+ * the rowid: the table itself is that B-tree, so the index stops existing
19988
+ * rather than getting smaller. No shorter string does that.
19989
+ *
19990
+ * Defined once here for all three event kinds, which is why they move
19991
+ * together: a per-table migration would have forked this and
19992
+ * `COMMON_BASE_COLUMNS` and reunited them two stages later.
19993
+ */
19994
+ id: number().int(),
19947
19995
  deviceId: number(),
19948
19996
  timestamp: number()
19949
19997
  };
@@ -19962,7 +20010,34 @@ var MotionEventSchema = object({
19962
20010
  /** Omitted in slim projection. */
19963
20011
  frameHeight: number().optional(),
19964
20012
  /** Populated by B5 (recording playback URL for this event). */
19965
- mediaUrl: string().optional()
20013
+ mediaUrl: string().optional(),
20014
+ /**
20015
+ * One row per motion EPISODE, not one per push (D475). `null` while the
20016
+ * episode is still open — a further rising edge extends it in place rather
20017
+ * than inserting a new row. Set once, at close, to `lastOnAt - startedAt`
20018
+ * (the span from the first rising edge to the LAST one, deliberately NOT
20019
+ * `closedAt - startedAt` — the close delay is a quiet CONFIRMATION, not
20020
+ * movement, and folding it in would report `MOTION_CLOSE_AFTER_MS` of
20021
+ * motion for an instantaneous trigger).
20022
+ *
20023
+ * **Absent** (not merely `null`) on a row written before D475 — that means
20024
+ * "closed the old way, before this column existed", never "still open".
20025
+ * Nothing in this codebase may read an absent `durationMs` as an open
20026
+ * episode; only `null` means open.
20027
+ */
20028
+ durationMs: number().nullable().optional(),
20029
+ /**
20030
+ * Ms offsets from `timestamp` (the episode's own first rising edge, so the
20031
+ * first entry is always `0`) of every genuine off→on transition the
20032
+ * episode saw — "ogni evento on si deve salvare" (D475). NOT one entry per
20033
+ * push: a firmware source that keepalives at ~1 Hz for the whole burst
20034
+ * (Reolink, Hikvision) produces exactly one edge; a source that reports an
20035
+ * explicit `false` mid-episode and then resumes before the quiet window
20036
+ * elapses produces another. Stored compactly — see `motion-edge-codec.ts`
20037
+ * — and decoded back to this shape on read. Absent/empty on a legacy row,
20038
+ * which must never be read as "no episode happened here".
20039
+ */
20040
+ edges: array(number()).readonly().optional()
19966
20041
  });
19967
20042
  /**
19968
20043
  * Which detection SOURCE produced an object event. `pipeline` = the ML
@@ -20017,6 +20092,23 @@ var ObjectEventSchema = object({
20017
20092
  * includes it (it is light). Absent on rows written before this field.
20018
20093
  */
20019
20094
  frameId: string().optional(),
20095
+ /**
20096
+ * A PRODUCER-chosen key that makes a synthetic event's emission idempotent
20097
+ * (D474).
20098
+ *
20099
+ * Only the package detector writes it, and it exists because the event id
20100
+ * stopped being choosable: the delivery and pick-up rows used to BE their
20101
+ * dedupe key (`pa-pkg-<entryId>-delivered`), which is how "never emit a
20102
+ * second delivery for this entry" survived a restart. An `INTEGER` rowid is
20103
+ * assigned by SQLite, so that key had to move off the primary key rather
20104
+ * than be dropped — a detector that cannot recognise its own row re-delivers
20105
+ * every parcel on every boot.
20106
+ *
20107
+ * Absent on every other object event, and on every row written before this
20108
+ * field. Never a substitute for `id`: it is unique per (producer, occasion),
20109
+ * not per row, and nothing addresses a row by it.
20110
+ */
20111
+ idempotencyKey: string().optional(),
20020
20112
  /** Omitted in slim projection. */
20021
20113
  trackId: string().optional(),
20022
20114
  className: string(),
@@ -27114,26 +27206,59 @@ authKey: string().optional() }), object({
27114
27206
  /** Human-readable error when `ok: false`. */
27115
27207
  error: string().optional()
27116
27208
  }), { kind: "mutation" });
27117
- /**
27118
- * Hardware / firmware motion sensor cap — binary detected state plus
27119
- * a timestamp of the last observation. Distinct from
27120
- * `motion-detection.cap.ts` which owns the LOCAL ML motion pipeline;
27121
- * `motion` is the lightweight readout from on-camera motion (Reolink
27122
- * `GetMdState`, Baichuan push `type: motion`, ONVIF analytics).
27123
- *
27124
- * Native-motion providers also fan out to `detection.camera-native`
27125
- * with `source: 'onboard'` so cross-cutting system services
27126
- * (alert-center, advanced-notifier) can subscribe once and receive
27127
- * motion from every camera.
27128
- */
27129
27209
  var MotionStatusSchema = object({
27130
27210
  detected: boolean(),
27131
27211
  /** Ms epoch of the last detected-true observation. Null if never detected. */
27132
27212
  lastDetectedAt: number().nullable(),
27133
27213
  /**
27134
- * Ms after which `detected` auto-reverts to false if no fresh push
27135
- * arrives. Null means the provider leaves detected state until a
27136
- * native "clear" event.
27214
+ * `MOTION_CLOSE_AFTER_MS` while `detected: true` on a `Camera` device,
27215
+ * `null` while false and on every `Sensor` device (D475) — see that
27216
+ * constant's doc for the one-authority rule.
27217
+ *
27218
+ * ## Reading this field still arms nothing
27219
+ *
27220
+ * It reads like an instruction to the consumer ("revert after N ms if
27221
+ * no fresh push arrives") and it is not one: nothing in this repo reads
27222
+ * the LIVE cap value to drive a timer. `pipeline-analytics`'s motion-episode
27223
+ * close DOES now use the same number — `MOTION_CLOSE_AFTER_MS` — but as an
27224
+ * imported constant, not as a read of `device.state.motion.value`, so this
27225
+ * field stays what it always was: DESCRIPTIVE output, mirroring an answer
27226
+ * computed elsewhere. Building a self-clear timer out of a READ of this
27227
+ * field would add a second falling-edge authority beside whichever one
27228
+ * already owns the device, and two that can disagree are worse than one.
27229
+ * Consumers that need a falling edge SHAPED differently — held open across
27230
+ * a flapping source — debounce on their own side and say so, as
27231
+ * `addon-export-alexa/src/motion-clear-hold.ts` and
27232
+ * `addon-export-hap`'s `RESET_DEBOUNCE_MS` both do.
27233
+ *
27234
+ * ## Who writes it
27235
+ *
27236
+ * - **Cameras** — the runner's phase machine, `active → watching` on
27237
+ * `cooldown_expired`, which then writes this slice with
27238
+ * `detected: false` (`handlePhaseChanged` in
27239
+ * `pipeline-runner/index.ts`). It produces the FALLING edge, which
27240
+ * matters most for the sources that only ever push a rising one:
27241
+ * Reolink emits `MotionOnMotionChanged { detected: true }` and never
27242
+ * a false.
27243
+ * - **Sensors** (Home Assistant binary sensors, Homematic) — the
27244
+ * provider pushes the false itself, from the upstream system's own
27245
+ * state change. No phase machine is involved.
27246
+ *
27247
+ * ### The phase machine is CANONICAL, not sole — and that is a defect
27248
+ *
27249
+ * An earlier revision of this docblock (mine, 2026-09-12) claimed the
27250
+ * phase machine is the sole writer for a camera. It is not.
27251
+ * `hikvision-camera.ts:3464` and `amcrest-camera.ts:445` both call
27252
+ * `setCapSlice(motionCapability, …)` on their own rising edge, and
27253
+ * Hikvision's comment says why: it read THIS docblock, agreed the
27254
+ * runner is canonical, and wrote anyway to avoid per-tick churn. So
27255
+ * two authorities can disagree about one slice, which this repo
27256
+ * forbids, and the doc said otherwise — which is worse than saying
27257
+ * nothing, because it reads as verification.
27258
+ *
27259
+ * This predates D475 and is not fixed there: the fix touches every
27260
+ * camera provider. Recorded in D475's Consequences. Do not restore the
27261
+ * "sole writer" wording without also removing the other writers.
27137
27262
  */
27138
27263
  autoClearAfterMs: number().nullable()
27139
27264
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@camstack/addon-export-hap",
3
- "version": "1.2.108",
3
+ "version": "1.2.109",
4
4
  "description": "HomeKit (HAP) exporter for CamStack devices. Publishes each exposed device as its own HomeKit accessory: cameras and doorbells with SRTP streaming, HomeKit Secure Video, motion, two-way audio, PTZ and battery; switches, lights, locks and sensors through a capability→service table.",
5
5
  "keywords": [
6
6
  "camstack",