@camstack/addon-provider-reolink 1.2.166 → 1.2.167

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 +248 -194
  2. package/dist/addon.mjs +248 -194
  3. package/package.json +1 -1
package/dist/addon.mjs CHANGED
@@ -25,7 +25,7 @@ import { mkdir as mkdir$1 } from "fs/promises";
25
25
  import { spawn as spawn$1 } from "node:child_process";
26
26
  import os, { tmpdir } from "node:os";
27
27
  import { createReadStream } from "node:fs";
28
- //#region ../types/dist/event-category-C5xZWqz6.mjs
28
+ //#region ../types/dist/event-category-BGmz_oxW.mjs
29
29
  var EventCategory = /* @__PURE__ */ function(EventCategory) {
30
30
  EventCategory["SystemBoot"] = "system.boot";
31
31
  EventCategory["SystemAddonsReady"] = "system.addons-ready";
@@ -558,6 +558,15 @@ var EventCategory = /* @__PURE__ */ function(EventCategory) {
558
558
  */
559
559
  EventCategory["PipelineAudioInferenceResult"] = "pipeline.audio-inference-result";
560
560
  EventCategory["DetectionPhaseTransition"] = "detection.phase-transition";
561
+ /**
562
+ * A camera provider's RAW onboard motion push (D673): the `MotionOnMotionChanged`
563
+ * data shape without `triggeredBy`, built by `providerMotionEvent`. Its one
564
+ * consumer is the orchestrator's camera-sensors bridge, which re-emits the
565
+ * camera's EFFECTIVE onboard motion as `MotionOnMotionChanged` (a pass-through
566
+ * for a camera with no attached sensor). Providers must not emit
567
+ * `MotionOnMotionChanged` themselves; `scripts/check-onboard-motion-emitters.ts`
568
+ * keeps it so (camera-grid is a named, deferred exception).
569
+ */
561
570
  EventCategory["ProviderMotion"] = "provider.motion";
562
571
  EventCategory["ProviderDetection"] = "provider.detection";
563
572
  EventCategory["EnrichmentEmbeddingStored"] = "enrichment.embedding.stored";
@@ -5396,7 +5405,7 @@ var ZodIssueCode = {
5396
5405
  var ZodFirstPartyTypeKind;
5397
5406
  ZodFirstPartyTypeKind || (ZodFirstPartyTypeKind = {});
5398
5407
  //#endregion
5399
- //#region ../types/dist/sleep-gU6hqv4_.mjs
5408
+ //#region ../types/dist/sleep-CDCONfkt.mjs
5400
5409
  /**
5401
5410
  * The audio chunk plane's byte format, and the ONE expansion from a coded
5402
5411
  * window to float samples (D455).
@@ -7487,7 +7496,7 @@ var DeviceRole = /* @__PURE__ */ function(DeviceRole) {
7487
7496
  return DeviceRole;
7488
7497
  }({});
7489
7498
  //#endregion
7490
- //#region ../types/dist/composition-Dq42XWp0.mjs
7499
+ //#region ../types/dist/composition-43aqsZQj.mjs
7491
7500
  /**
7492
7501
  * The ffmpeg build every CamStack node downloads, per platform and arch.
7493
7502
  *
@@ -7544,13 +7553,12 @@ portable("linux64"), portable("linuxarm64"), portable("macarm64"), portable("mac
7544
7553
  * LA tabella "quale booleano di questo tipo di device conta come ALTO", e il
7545
7554
  * valutatore puro del suo FRONTE.
7546
7555
  *
7547
- * Viveva dentro il builtin virtual-doorbell
7548
- * (`@camstack/system` — `builtins/doorbell/trigger-engine.ts`) e i suoi
7549
- * predicati erano privati al modulo. Il recorder ne ha bisogno per il trigger
7550
- * `RecordingTriggers.sensorDeviceIds`: copiarla avrebbe creato la SECONDA
7551
- * tabella, che diverge alla prima cap aggiunta e il cui sintomo — "il sensore
7552
- * fa suonare il campanello ma non registra" — è esattamente D62. Quindi si
7553
- * SPOSTA qui e il doorbell la ri-esporta.
7556
+ * UNA tabella per tutti quelli che chiedono "questo device è ALTO?": il
7557
+ * trigger `RecordingTriggers.sensorDeviceIds` del recorder, i marker dei
7558
+ * sensori della post-analysis, il bridge camera-sensors dell'orchestratore e
7559
+ * le sorgenti di un campanello composto (`doorbellFeatureSources`, D671).
7560
+ * Una seconda copia divergerebbe alla prima cap aggiunta, e il sintomo — "il
7561
+ * sensore fa suonare il campanello ma non registra" — è esattamente D62.
7554
7562
  *
7555
7563
  * ⚠ NON è `DEVICE_STATE_READERS` (`catalogs/device-state-vocabulary.ts`), e le
7556
7564
  * due non vanno unificate: quella risponde a "qual è la PAROLA di stato per una
@@ -7576,8 +7584,6 @@ var SOURCE_CAP_ACTIVE_FIELD = {
7576
7584
  vibration: "detected",
7577
7585
  tamper: "tampered"
7578
7586
  };
7579
- /** Cap names whose presence in a device's bindings qualify it as a source. */
7580
- var SOURCE_CAPS = Object.keys(SOURCE_CAP_ACTIVE_FIELD);
7581
7587
  /**
7582
7588
  * Error types for the safe expression engine. Two distinct classes so callers
7583
7589
  * can tell a compile-time (grammar) failure from a runtime (evaluation)
@@ -9307,7 +9313,7 @@ var RecordingTriggersSchema = object({
9307
9313
  *
9308
9314
  * Cosa vuol dire "alto" dipende dal TIPO di device e non è deciso qui:
9309
9315
  * `SOURCE_CAP_ACTIVE_FIELD` (`catalogs/sensor-active-state.ts`) è LA tabella,
9310
- * la stessa che il virtual-doorbell usa dal 2026-08-05. Ed è il FRONTE, non
9316
+ * la stessa del campanello composto (D671). Ed è il FRONTE, non
9311
9317
  * il livello: un contatto trovato già aperto al riavvio del runner non fa
9312
9318
  * registrare.
9313
9319
  */
@@ -16328,109 +16334,10 @@ method(object({
16328
16334
  deviceIds: array(number()).readonly(),
16329
16335
  caps: array(string()).readonly().optional()
16330
16336
  }), record(string(), record(string(), unknown().nullable())));
16331
- /**
16332
- * Doorbell button cap. Two kinds of providers coexist behind this cap
16333
- * name (same pattern as `snapshot`):
16334
- *
16335
- * - **Native** providers: registered per-device by device-driver
16336
- * addons via `ctx.registerNativeCap` — either on a
16337
- * `DeviceType.Button` accessory with `role: DeviceRole.Doorbell`,
16338
- * or directly on the camera (Reolink registers at camera level).
16339
- * Emits an `onPressed` event every time the firmware pushes a
16340
- * ring; status tracks the last press and a pressCount since start
16341
- * (diagnostic).
16342
- *
16343
- * - **Wrapper** provider: the `virtual-doorbell` system builtin
16344
- * (`@camstack/system/builtins/doorbell`). Turns ANY binary-ish
16345
- * device (contact / switch / event-emitter …) into a doorbell for
16346
- * a camera. `defaultActive: false` — the operator explicitly binds
16347
- * it per camera in the device-bindings UI, then picks the source
16348
- * device + trigger in the per-device settings.
16349
- *
16350
- * The DeviceEventPropagator re-emits `onPressed` on the camera parent
16351
- * — subscribers listening at the camera level receive ring events
16352
- * with `via[]` populated. No code on the parent needed.
16353
- */
16354
- var DoorbellStatusSchema = object({
16355
- /** Ms epoch of the last press. null = never observed since this provider started. */
16356
- lastPressedAt: number().nullable(),
16357
- /** Counter since provider start. Resets on reboot. Useful for metrics/debug. */
16358
- pressCountSinceStart: number()
16359
- });
16360
- var DoorbellPressEventSchema = object({
16361
- deviceId: number(),
16362
- timestamp: number()
16363
- });
16364
- /**
16365
- * The `doorbell-source` extension slot's editable value (D648): the camera's
16366
- * virtual-doorbell trigger devices, exactly what the wrapper's
16367
- * `doorbellSources` persists (as `{ deviceId }` records).
16368
- */
16369
- var DoorbellSourceSlotValueSchema = object({ deviceIds: array(number().int().positive()) });
16370
- /**
16371
- * The `doorbell-source` slot — which devices ring this camera's VIRTUAL
16372
- * doorbell, owned by the `virtual-doorbell` wrapper over its
16373
- * `doorbellSources` store (D648). A source rings when one of its
16374
- * `SOURCE_CAPS` rises. Whether the wrapper is bound to the camera at all is a
16375
- * different authority (`deviceManager.setWrapperActive`, D62) — the slot
16376
- * reports it, it never writes it.
16377
- */
16378
- var DOORBELL_SOURCE_SLOT = {
16379
- id: "doorbell-source",
16380
- label: "Doorbell trigger devices",
16381
- description: "Any of these becoming active rings this camera's virtual doorbell: a binary sensor when it goes active, a switch when it turns on.",
16382
- shape: "association",
16383
- hostDeviceTypes: [DeviceType.Camera],
16384
- sourceCaps: SOURCE_CAPS,
16385
- cardinality: "many",
16386
- value: DoorbellSourceSlotValueSchema
16387
- };
16388
- var doorbellCapability = {
16389
- name: "doorbell",
16390
- scope: "device",
16391
- deviceNative: true,
16392
- mode: "singleton",
16393
- kind: "wrapper",
16394
- defaultActive: false,
16395
- deviceTypes: [DeviceType.Button, DeviceType.Camera],
16396
- extensionSlots: [DOORBELL_SOURCE_SLOT],
16397
- methods: {},
16398
- events: {
16399
- /**
16400
- * Fires once per physical press. Reolink delivers via Baichuan
16401
- * push (`ReolinkSimpleEvent.type === 'doorbell'`). There is no
16402
- * release/duration — it's a pulse.
16403
- */
16404
- onPressed: {
16405
- data: DoorbellPressEventSchema,
16406
- fromState: {
16407
- field: "lastPressedAt",
16408
- on: "advance"
16409
- }
16410
- } },
16411
- status: {
16412
- schema: DoorbellStatusSchema,
16413
- kind: "push"
16414
- },
16415
- /**
16416
- * Runtime-state slice — last press timestamp + lifetime press count.
16417
- * Mirrored by the kernel and readable via
16418
- * `device.state.doorbell.value`. UIs can show "last ring 5m ago"
16419
- * without subscribing.
16420
- */
16421
- runtimeState: DoorbellStatusSchema,
16422
- /**
16423
- * Runtime-state durability: **restored** — a monotonic accumulator: the slice IS the record. `lastPressedAt` is content here, not a clock, so it is deliberately NOT volatile.
16424
- *
16425
- * See `RuntimeStateDurability`. Enforced by
16426
- * `scripts/check-runtime-state-durability.ts`.
16427
- */
16428
- durability: "restored"
16429
- };
16430
- var ExtensionSlotIdSchema = _enum(["doorbell-source", "linked-devices"]);
16431
- DeviceType.Camera, DeviceType.Camera;
16337
+ var ExtensionSlotIdSchema = _enum(["linked-devices"]);
16338
+ DeviceType.Camera;
16432
16339
  /** A slot's editable value, discriminated on `slot`. */
16433
- var ExtensionSlotValueSchema = discriminatedUnion("slot", [DoorbellSourceSlotValueSchema.extend({ slot: literal("doorbell-source") }), LinkedDevicesSlotValueSchema.extend({ slot: literal("linked-devices") })]);
16340
+ var ExtensionSlotValueSchema = discriminatedUnion("slot", [LinkedDevicesSlotValueSchema.extend({ slot: literal("linked-devices") })]);
16434
16341
  DeviceType.Camera;
16435
16342
  var ExtensionSourceAvailabilitySchema = discriminatedUnion("state", [object({ state: literal("ok") }), object({
16436
16343
  state: literal("unreachable"),
@@ -16467,10 +16374,11 @@ var ExtensionSlotSourceSchema = object({
16467
16374
  });
16468
16375
  /**
16469
16376
  * A DIFFERENT authority that gates whether the slot's sources do anything —
16470
- * reported read-only, named, never written through this contract (D62). The
16471
- * virtual doorbell's sources ring only while the wrapper is bound to the
16472
- * camera, and that binding belongs to `deviceManager.setWrapperActive`.
16473
- * `unknown` means the gate could not be read; it is not "inactive" (D315).
16377
+ * reported read-only, named, never written through this contract (D62) — e.g.
16378
+ * a slot whose sources act only while a wrapper is bound to the host, a
16379
+ * binding that belongs to `deviceManager.setWrapperActive`. No slot reports
16380
+ * one today (`linked-devices` answers `null`). `unknown` means the gate could
16381
+ * not be read; it is not "inactive" (D315).
16474
16382
  */
16475
16383
  var ExtensionSlotActivationSchema = object({
16476
16384
  state: _enum([
@@ -16486,7 +16394,7 @@ var ExtensionSlotRowSchema = discriminatedUnion("state", [object({
16486
16394
  /** `<source>:<slotId>` — the routing key for `listCandidates` / `setSlot`. */
16487
16395
  slotRef: string(),
16488
16396
  slot: ExtensionSlotIdSchema,
16489
- /** Names the store written (D62), e.g. `virtual-doorbell:doorbellSources`. */
16397
+ /** Names the store written (D62), e.g. `device-manager:_linkedDeviceIds`. */
16490
16398
  authority: string(),
16491
16399
  activation: ExtensionSlotActivationSchema.nullable(),
16492
16400
  value: ExtensionSlotValueSchema,
@@ -21648,12 +21556,26 @@ var CameraSensorRefSchema = object({
21648
21556
  function cameraSensorRefKey(ref) {
21649
21557
  return `${ref.addonId}/${ref.stableId}`;
21650
21558
  }
21651
- /** CS-1 accepts exactly one mode; CS-2 (D673) widens it to confirm / replace. */
21652
- var CameraSensorsModeSchema = _enum(["in-addition"]);
21653
- var CameraSensorsConfigSchema = object({
21654
- mode: CameraSensorsModeSchema,
21655
- sensors: array(CameraSensorRefSchema).min(1).max(8)
21656
- }).superRefine((value, ctx) => {
21559
+ /**
21560
+ * A sensor's mode (D673). `in-addition`: its motion is the camera's motion too.
21561
+ * `replace`: its motion is the camera's, and while one is READY the camera's
21562
+ * own motion no longer counts on its own. `confirm`: while one is READY the
21563
+ * camera's own motion counts only while a confirm sensor is active — with or
21564
+ * without a replace sensor (spec C2, the controller's ruling on Q1).
21565
+ */
21566
+ var CameraSensorModeSchema = _enum([
21567
+ "in-addition",
21568
+ "confirm",
21569
+ "replace"
21570
+ ]);
21571
+ /** A stored sensor with no mode — every CS-1 row — is in addition. */
21572
+ var DEFAULT_CAMERA_SENSOR_MODE = "in-addition";
21573
+ /**
21574
+ * A camera's attached sensors. A CS-1 row carried an envelope
21575
+ * `mode: 'in-addition'`: `z.object` drops it on parse, and the PARSED value is
21576
+ * what the write stores (D672 rule 3), so it is never written again.
21577
+ */
21578
+ var CameraSensorsConfigSchema = object({ sensors: array(CameraSensorRefSchema.extend({ mode: CameraSensorModeSchema.default(DEFAULT_CAMERA_SENSOR_MODE) })).min(1).max(8) }).superRefine((value, ctx) => {
21657
21579
  const seen = /* @__PURE__ */ new Set();
21658
21580
  value.sensors.forEach((ref, index) => {
21659
21581
  const key = cameraSensorRefKey(ref);
@@ -21665,39 +21587,41 @@ var CameraSensorsConfigSchema = object({
21665
21587
  seen.add(key);
21666
21588
  });
21667
21589
  });
21668
- var RefFields = {
21590
+ /** Every attachment names its mode; an older orchestrator that does not is in addition. */
21591
+ var AttachmentFields = {
21669
21592
  addonId: string(),
21670
- stableId: string()
21593
+ stableId: string(),
21594
+ mode: CameraSensorModeSchema.default(DEFAULT_CAMERA_SENSOR_MODE)
21671
21595
  };
21672
21596
  var CameraSensorAttachmentSchema = discriminatedUnion("state", [
21673
21597
  object({
21674
- ...RefFields,
21598
+ ...AttachmentFields,
21675
21599
  state: literal("ready"),
21676
21600
  deviceId: number(),
21677
21601
  name: string(),
21678
21602
  caps: array(string()).readonly()
21679
21603
  }),
21680
21604
  object({
21681
- ...RefFields,
21605
+ ...AttachmentFields,
21682
21606
  state: literal("offline"),
21683
21607
  deviceId: number(),
21684
21608
  name: string(),
21685
21609
  reason: _enum(["offline", "disabled"])
21686
21610
  }),
21687
21611
  object({
21688
- ...RefFields,
21612
+ ...AttachmentFields,
21689
21613
  state: literal("missing"),
21690
21614
  reason: string()
21691
21615
  }),
21692
21616
  object({
21693
- ...RefFields,
21617
+ ...AttachmentFields,
21694
21618
  state: literal("unsupported"),
21695
21619
  deviceId: number(),
21696
21620
  name: string(),
21697
21621
  reason: string()
21698
21622
  }),
21699
21623
  object({
21700
- ...RefFields,
21624
+ ...AttachmentFields,
21701
21625
  state: literal("unreadable"),
21702
21626
  reason: string()
21703
21627
  })
@@ -21710,17 +21634,49 @@ var CameraSensorAttachmentSchema = discriminatedUnion("state", [
21710
21634
  * say.
21711
21635
  */
21712
21636
  var CameraSensorsNotForwardedSchema = _enum(["onboard-not-listed", "no-active-detection"]);
21637
+ /** Whether the camera's OWN motion counts on its own (D673 C2), from its READY sensors (C3). */
21638
+ var CameraOwnMotionSchema = _enum([
21639
+ "always",
21640
+ "when-confirmed",
21641
+ "never"
21642
+ ]);
21643
+ /** Confirm / Replace cannot suppress this camera's motion: its provider still emits it directly (D673 C13). */
21644
+ var CameraSensorsNotEnforcedSchema = _enum(["provider-not-split"]);
21645
+ /**
21646
+ * The camera's OWN onboard motion (D696): the raw input the bridge combines,
21647
+ * never the effective value. The camera's `motion` slice is the effective one,
21648
+ * so a view of the camera's own row reads this.
21649
+ * - `atMs`: when the bridge received the camera's last own edge.
21650
+ * - `detected`: evaluated WHEN THE STATUS IS ASKED — the last edge said `true`
21651
+ * AND it is younger than the window the bridge counts the camera's own motion
21652
+ * for (`motionCooldownMs`, floored at two sustain ticks). A camera that never
21653
+ * sends a clear (heartbeats only) therefore reads `false` once no push came
21654
+ * within the window; a status read is a snapshot, so a live view re-asks.
21655
+ * Absent = this orchestrator has not received the camera's own motion since it
21656
+ * started (unknown, never "quiet"), or an orchestrator older than D696.
21657
+ */
21658
+ var CameraRawLevelSchema = object({
21659
+ detected: boolean(),
21660
+ atMs: number()
21661
+ });
21713
21662
  var CameraSensorsStatusSchema = discriminatedUnion("state", [
21714
- object({ state: literal("not-configured") }),
21663
+ object({
21664
+ state: literal("not-configured"),
21665
+ cameraLevel: CameraRawLevelSchema.optional()
21666
+ }),
21715
21667
  object({
21716
21668
  state: literal("unreadable"),
21717
- reason: string()
21669
+ reason: string(),
21670
+ cameraLevel: CameraRawLevelSchema.optional()
21718
21671
  }),
21719
21672
  object({
21720
21673
  state: literal("configured"),
21721
- mode: CameraSensorsModeSchema,
21674
+ cameraLevel: CameraRawLevelSchema.optional(),
21722
21675
  sensors: array(CameraSensorAttachmentSchema).readonly(),
21723
- notForwarded: CameraSensorsNotForwardedSchema.optional()
21676
+ /** Absent = an orchestrator older than CS-2: it does not say (D315). */
21677
+ cameraMotion: CameraOwnMotionSchema.optional(),
21678
+ notForwarded: CameraSensorsNotForwardedSchema.optional(),
21679
+ notEnforced: CameraSensorsNotEnforcedSchema.optional()
21724
21680
  })
21725
21681
  ]);
21726
21682
  Object.freeze({
@@ -24951,8 +24907,10 @@ var RunnerCameraConfigSchema = object({
24951
24907
  * - `analyzer`: ML/frame-diff motion detection (motion-wasm or other
24952
24908
  * analyzer providers) running on decoded frames in this runner.
24953
24909
  * - `onboard`: camera-native motion pushes (Reolink Baichuan, ONVIF
24954
- * analytics). The provider emits ProviderMotion
24955
- * events; the orchestrator forwards to `reportMotion`.
24910
+ * analytics). The provider emits its raw push as `ProviderMotion`
24911
+ * (D673); the orchestrator's camera-sensors bridge re-emits the camera's
24912
+ * effective motion as `MotionOnMotionChanged`, which is what reaches
24913
+ * `reportMotion`.
24956
24914
  */
24957
24915
  motionSources: MotionSourcesSchema.default(["analyzer"]),
24958
24916
  /**
@@ -25129,7 +25087,15 @@ var RunnerLocalLoadSchema = object({
25129
25087
  hasGpu: boolean(),
25130
25088
  inferenceBackend: string().optional(),
25131
25089
  cpuCores: number().optional()
25132
- })
25090
+ }),
25091
+ /**
25092
+ * Who writes a camera's `motion` runtime slice (D696). `'orchestrator'`: this
25093
+ * runner never writes it. ABSENT = a runner older than D696, which still
25094
+ * writes the slice from its phase machine; the orchestrator does not write
25095
+ * while any runner node is absent-or-unanswered, so one slice never has two
25096
+ * writers.
25097
+ */
25098
+ motionSliceWriter: literal("orchestrator").optional()
25133
25099
  });
25134
25100
  /**
25135
25101
  * Aggregate runtime metrics for the runner's whole local pool. Mirrors the
@@ -31172,6 +31138,70 @@ onStatusChanged: { data: object({
31172
31138
  * whether persisting is worth a SQLite commit. */
31173
31139
  volatileStateFields: ["lastChangedAt"]
31174
31140
  };
31141
+ var DoorbellStatusSchema = object({
31142
+ /** Ms epoch of the last press. null = never observed since this provider started. */
31143
+ lastPressedAt: number().nullable(),
31144
+ /** Counter since provider start. Resets on reboot. Useful for metrics/debug. */
31145
+ pressCountSinceStart: number()
31146
+ });
31147
+ var DoorbellPressEventSchema = object({
31148
+ deviceId: number(),
31149
+ timestamp: number()
31150
+ });
31151
+ /**
31152
+ * Doorbell button cap. Two kinds of provider serve it, and to every consumer
31153
+ * they are the same cap:
31154
+ *
31155
+ * - **Native** providers, registered per device by a driver addon through
31156
+ * `ctx.registerNativeCap` — a `DeviceType.Button` accessory with
31157
+ * `role: DeviceRole.Doorbell`, or the camera itself (Reolink registers at
31158
+ * camera level). They emit `onPressed` on every firmware ring.
31159
+ * - **A composed doorbell** (D670, D671): a camera's customization gives it
31160
+ * the cap, its `lastPressedAt` rising from the sources the operator
31161
+ * assigned in Customize → Doorbell, and the composer emits `onPressed`
31162
+ * from that field's advance (`fromState`).
31163
+ *
31164
+ * The DeviceEventPropagator re-emits `onPressed` on the camera parent.
31165
+ */
31166
+ var doorbellCapability = {
31167
+ name: "doorbell",
31168
+ scope: "device",
31169
+ deviceNative: true,
31170
+ mode: "singleton",
31171
+ deviceTypes: [DeviceType.Button, DeviceType.Camera],
31172
+ methods: {},
31173
+ events: {
31174
+ /**
31175
+ * Fires once per physical press. Reolink delivers via Baichuan
31176
+ * push (`ReolinkSimpleEvent.type === 'doorbell'`). There is no
31177
+ * release/duration — it's a pulse.
31178
+ */
31179
+ onPressed: {
31180
+ data: DoorbellPressEventSchema,
31181
+ fromState: {
31182
+ field: "lastPressedAt",
31183
+ on: "advance"
31184
+ }
31185
+ } },
31186
+ status: {
31187
+ schema: DoorbellStatusSchema,
31188
+ kind: "push"
31189
+ },
31190
+ /**
31191
+ * Runtime-state slice — last press timestamp + lifetime press count.
31192
+ * Mirrored by the kernel and readable via
31193
+ * `device.state.doorbell.value`. UIs can show "last ring 5m ago"
31194
+ * without subscribing.
31195
+ */
31196
+ runtimeState: DoorbellStatusSchema,
31197
+ /**
31198
+ * Runtime-state durability: **restored** — a monotonic accumulator: the slice IS the record. `lastPressedAt` is content here, not a clock, so it is deliberately NOT volatile.
31199
+ *
31200
+ * See `RuntimeStateDurability`. Enforced by
31201
+ * `scripts/check-runtime-state-durability.ts`.
31202
+ */
31203
+ durability: "restored"
31204
+ };
31175
31205
  /**
31176
31206
  * Enum-state sensor — a string value picked from a finite option set.
31177
31207
  * Drives HA `sensor` entries with `state_class: enum` (HVAC action
@@ -33316,9 +33346,9 @@ var MotionStatusSchema = object({
33316
33346
  /** Ms epoch of the last detected-true observation. Null if never detected. */
33317
33347
  lastDetectedAt: number().nullable(),
33318
33348
  /**
33319
- * `MOTION_CLOSE_AFTER_MS` while `detected: true` on a `Camera` device,
33320
- * `null` while false and on every `Sensor` device (D475) — see that
33321
- * constant's doc for the one-authority rule.
33349
+ * `MOTION_CLOSE_AFTER_MS` while `detected: true` on a `Camera` device (the
33350
+ * orchestrator writes it, D696), `null` while false and on every `Sensor`
33351
+ * device (D475) — see that constant's doc for the one-authority rule.
33322
33352
  *
33323
33353
  * ## Reading this field still arms nothing
33324
33354
  *
@@ -33336,34 +33366,31 @@ var MotionStatusSchema = object({
33336
33366
  * `addon-export-alexa/src/motion-clear-hold.ts` and
33337
33367
  * `addon-export-hap`'s `RESET_DEBOUNCE_MS` both do.
33338
33368
  *
33339
- * ## Who writes it
33340
- *
33341
- * - **Cameras** — the runner's phase machine, `active → watching` on
33342
- * `cooldown_expired`, which then writes this slice with
33343
- * `detected: false` (`handlePhaseChanged` in
33344
- * `pipeline-runner/index.ts`). It produces the FALLING edge, which
33345
- * matters most for the sources that only ever push a rising one:
33346
- * Reolink emits `MotionOnMotionChanged { detected: true }` and never
33347
- * a false.
33348
- * - **Sensors** (Home Assistant binary sensors, Homematic) — the
33349
- * provider pushes the false itself, from the upstream system's own
33350
- * state change. No phase machine is involved.
33351
- *
33352
- * ### The phase machine is CANONICAL, not sole — and that is a defect
33353
- *
33354
- * An earlier revision of this docblock (mine, 2026-09-12) claimed the
33355
- * phase machine is the sole writer for a camera. It is not.
33356
- * `hikvision-camera.ts:3464` and `amcrest-camera.ts:445` both call
33357
- * `setCapSlice(motionCapability, …)` on their own rising edge, and
33358
- * Hikvision's comment says why: it read THIS docblock, agreed the
33359
- * runner is canonical, and wrote anyway to avoid per-tick churn. So
33360
- * two authorities can disagree about one slice, which this repo
33361
- * forbids, and the doc said otherwise — which is worse than saying
33362
- * nothing, because it reads as verification.
33363
- *
33364
- * This predates D475 and is not fixed there: the fix touches every
33365
- * camera provider. Recorded in D475's Consequences. Do not restore the
33366
- * "sole writer" wording without also removing the other writers.
33369
+ * ## Who writes it (D696)
33370
+ *
33371
+ * - **Cameras** — the orchestrator, and nothing else. It writes the camera's
33372
+ * EFFECTIVE motion (`addon-pipeline-orchestrator`
33373
+ * `camera-sensors/motion-slice.ts`), alongside the effective
33374
+ * `MotionOnMotionChanged` it emits or receives: the camera-sensors bridge
33375
+ * for a split camera, an unsplit emitter's direct event, the analyzer and
33376
+ * device activity, as the camera's `motionSources` counts them. A source
33377
+ * that never sends a clear falls one window after its last `true`.
33378
+ * `lastDetectedAt` moves only on an effective rise. No camera provider
33379
+ * writes this slice (not even a boot seed), and neither does the runner's
33380
+ * phase machine: its phase is "a session is attached" for every on-motion
33381
+ * session, not motion, and it now has its own `DetectionPhaseTransition`.
33382
+ * A provider's `isDetected` answers from the hub's slice.
33383
+ * - **Sensors** (Home Assistant binary sensors, Homematic, Matter, Alexa
33384
+ * imports) — the provider pushes both edges itself, from the upstream
33385
+ * system's own state change. That is the sensor's own fact.
33386
+ *
33387
+ * `scripts/check-onboard-motion-emitters.ts` refuses any other writer of a
33388
+ * `motion` slice; each sensor writer is a named exemption.
33389
+ *
33390
+ * This replaces the D475 §4 rule ("the phase machine is the sole writer").
33391
+ * That rule was never true: Hikvision and Amcrest wrote the slice raw beside
33392
+ * it, every provider seeded it at boot, and every session left it
33393
+ * `detected: true` after its detach.
33367
33394
  */
33368
33395
  autoClearAfterMs: number().nullable()
33369
33396
  });
@@ -33435,11 +33462,12 @@ onMotionChanged: { data: MotionOnMotionChangedDataSchema } },
33435
33462
  */
33436
33463
  runtimeState: MotionStatusSchema,
33437
33464
  /**
33438
- * Runtime-state durability: **session** — every writer of this slice
33465
+ * Runtime-state durability: **session** — the writer of this slice
33439
33466
  * writes only on an EDGE, so a restored `detected: true` would stay
33440
33467
  * frozen until the next one instead of being corrected. The next edge
33441
- * re-publishes the real state. (On who the writers are, and why there
33442
- * is more than one, see `autoClearAfterMs` above.)
33468
+ * re-publishes the real state, and the orchestrator re-publishes every
33469
+ * camera when it starts writing (D696; on who writes it, see
33470
+ * `autoClearAfterMs` above).
33443
33471
  *
33444
33472
  * See `RuntimeStateDurability`. Enforced by
33445
33473
  * `scripts/check-runtime-state-durability.ts`.
@@ -35954,8 +35982,7 @@ var ptzAutotrackCapability = {
35954
35982
  deviceNative: true,
35955
35983
  mode: "singleton",
35956
35984
  /**
35957
- * Two kinds of provider behind one name, the same shape `snapshot` and
35958
- * `doorbell` use:
35985
+ * Two kinds of provider behind one name, the same shape `snapshot` uses:
35959
35986
  *
35960
35987
  * - **Native**: the camera's own firmware autotrack, registered per device by
35961
35988
  * the driver when the probe says it exists (Reolink: yes; Hikvision:
@@ -35963,7 +35990,7 @@ var ptzAutotrackCapability = {
35963
35990
  * - **Wrapper**: the emulated implementation (`@camstack/system`'s `autotrack` builtin),
35964
35991
  * which follows a subject with `ptz.move` pulses driven by the detection
35965
35992
  * pipeline. `defaultActive: false` — the operator binds it per camera in
35966
- * the bindings UI, exactly as `virtual-doorbell` is bound.
35993
+ * the bindings UI.
35967
35994
  *
35968
35995
  * The binding chooses WHICH implementation answers; `setEnabled` stays the
35969
35996
  * one switch for the function either way. A camera never has both driving its
@@ -53491,6 +53518,25 @@ TimelapseRuleInputSchema.extend({
53491
53518
  createdAt: number(),
53492
53519
  updatedAt: number()
53493
53520
  });
53521
+ async function readHubMotionDetected(deviceState, deviceId) {
53522
+ let slice;
53523
+ try {
53524
+ slice = await deviceState.getCapSlice.query({
53525
+ deviceId,
53526
+ capName: "motion"
53527
+ });
53528
+ } catch (err) {
53529
+ const reason = err instanceof Error ? err.message : String(err);
53530
+ throw new Error(`motion state of device ${deviceId} is unreadable: ${reason}`, { cause: err });
53531
+ }
53532
+ const detected = slice?.["detected"];
53533
+ if (typeof detected !== "boolean") throw new Error(`motion state of device ${deviceId} is not published yet (unknown)`);
53534
+ return detected;
53535
+ }
53536
+ MotionOnMotionChangedDataSchema.omit({ triggeredBy: true });
53537
+ function providerMotionEvent(source, data) {
53538
+ return createEvent(EventCategory.ProviderMotion, source, data);
53539
+ }
53494
53540
  object({
53495
53541
  /**
53496
53542
  * Fraction of the box's own size added on EACH side before cutting.
@@ -165046,6 +165092,22 @@ var ReolinkCamera = class ReolinkCamera extends BaseDevice {
165046
165092
  * writers are {@link refreshNetworkLinkFromApi}. Seeded unknown so the
165047
165093
  * restored slice (if any) stays valid and a fresh device draws no link.
165048
165094
  */
165095
+ /**
165096
+ * The native `motion` cap. This provider writes NO `motion` slice (D696):
165097
+ * `handleSimpleEvent` emits its raw push as `ProviderMotion` (`source:
165098
+ * 'onboard'`, D673), the orchestrator's camera-sensors bridge turns it into
165099
+ * the effective `MotionOnMotionChanged`, and the orchestrator is the slice's
165100
+ * only writer, with that effective value, on the hub. `isDetected`
165101
+ * therefore answers from the HUB's slice; an unreadable hub, or a slice
165102
+ * nobody has published since the hub started, fails the call. It never
165103
+ * answers `false` for "unknown" (D49, D393).
165104
+ */
165105
+ registerMotionCap() {
165106
+ this.ctx.registerNativeCap(motionCapability, { isDetected: async ({ deviceId }) => {
165107
+ if (deviceId !== this.id) throw new Error(`ReolinkCamera: deviceId mismatch, expected ${this.id}, got ${deviceId}`);
165108
+ return readHubMotionDetected(this.ctx.api.deviceState, this.id);
165109
+ } });
165110
+ }
165049
165111
  registerNetworkLink() {
165050
165112
  this.ctx.registerNativeCap(networkLinkCapability, { getStatus: async ({ deviceId }) => {
165051
165113
  if (deviceId !== this.id) throw new Error(`ReolinkCamera: deviceId mismatch, expected ${this.id}, got ${deviceId}`);
@@ -165774,15 +165836,7 @@ var ReolinkCamera = class ReolinkCamera extends BaseDevice {
165774
165836
  };
165775
165837
  } });
165776
165838
  this.registerNetworkLink();
165777
- this.ctx.registerNativeCap(motionCapability, { isDetected: async ({ deviceId }) => {
165778
- if (deviceId !== this.id) throw new Error(`ReolinkCamera: deviceId mismatch, expected ${this.id}, got ${deviceId}`);
165779
- return this.state.motion.detected ?? false;
165780
- } });
165781
- this.setCapSlice(motionCapability, {
165782
- detected: false,
165783
- lastDetectedAt: null,
165784
- autoClearAfterMs: null
165785
- });
165839
+ this.registerMotionCap();
165786
165840
  this.registerDoorbellIfSupported();
165787
165841
  this.registerPtzIfSupported();
165788
165842
  this.registerPtzAutotrackIfSupported();
@@ -171781,11 +171835,11 @@ var ReolinkCamera = class ReolinkCamera extends BaseDevice {
171781
171835
  * axes, each driven exclusively by their own firmware push events.
171782
171836
  *
171783
171837
  * - 'motion' + AI types (people, vehicle, animal, face, package):
171784
- * emit `EventCategory.MotionOnMotionChanged` (`source: 'onboard'`)
171785
- * so the runner's phase machine reacts via `reportMotion`. The
171786
- * runner is the sole writer of the `motion` runtime-state slice
171787
- * (no direct slice writes here — keeps onboard symmetric with the
171788
- * analyzer path). AI subtype also lands on
171838
+ * emit the RAW push as `EventCategory.ProviderMotion`
171839
+ * (`source: 'onboard'`, D673). The orchestrator's camera-sensors bridge
171840
+ * re-emits it as the effective `MotionOnMotionChanged`, and the
171841
+ * orchestrator is the only writer of the camera's `motion` runtime-state
171842
+ * slice (D696; no slice write here). AI subtype also lands on
171789
171843
  * `EventCategory.DetectionCameraNative`.
171790
171844
  * - 'awake' / 'sleeping': emit DeviceAwake / DeviceSleeping. Battery
171791
171845
  * power-state only — does NOT touch online state.
@@ -171838,7 +171892,7 @@ var ReolinkCamera = class ReolinkCamera extends BaseDevice {
171838
171892
  } });
171839
171893
  if (event.type === "motion") {
171840
171894
  const now = Date.now();
171841
- this.ctx.eventBus.emit(createEvent(EventCategory.MotionOnMotionChanged, eventSource, {
171895
+ this.ctx.eventBus.emit(providerMotionEvent(eventSource, {
171842
171896
  deviceId: this.id,
171843
171897
  detected: true,
171844
171898
  timestamp: now,
@@ -171866,7 +171920,7 @@ var ReolinkCamera = class ReolinkCamera extends BaseDevice {
171866
171920
  }]
171867
171921
  }));
171868
171922
  } else this.noteNativeDetectionSuppressed(aiClass);
171869
- this.ctx.eventBus.emit(createEvent(EventCategory.MotionOnMotionChanged, eventSource, {
171923
+ this.ctx.eventBus.emit(providerMotionEvent(eventSource, {
171870
171924
  deviceId: this.id,
171871
171925
  detected: true,
171872
171926
  timestamp: now,
@@ -173715,7 +173769,7 @@ var AutodetectCache = class {
173715
173769
  * silently dropped.
173716
173770
  *
173717
173771
  * Returns a LIST rather than a single type because of `doorbell`. The
173718
- * camera's `handleSimpleEvent` emits `MotionOnMotionChanged` for `motion`
173772
+ * camera's `handleSimpleEvent` emits `ProviderMotion` for `motion`
173719
173773
  * and for every AI class, but the `doorbell` branch emits ONLY
173720
173774
  * `DoorbellOnPressed` and returns. An email is the sole signal a sleeping
173721
173775
  * battery camera can send, so a doorbell-classified email mapped to