@camstack/addon-provider-amcrest 0.2.124 → 0.2.126
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/addon.js +328 -241
- package/dist/addon.mjs +328 -241
- package/package.json +1 -1
package/dist/addon.mjs
CHANGED
|
@@ -13,7 +13,7 @@ import { networkInterfaces } from "node:os";
|
|
|
13
13
|
var __commonJSMin = (cb, mod) => () => (mod || (cb((mod = { exports: {} }).exports, mod), cb = null), mod.exports);
|
|
14
14
|
var __require = /* @__PURE__ */ createRequire(import.meta.url);
|
|
15
15
|
//#endregion
|
|
16
|
-
//#region ../types/dist/event-category-
|
|
16
|
+
//#region ../types/dist/event-category-BGmz_oxW.mjs
|
|
17
17
|
var EventCategory = /* @__PURE__ */ function(EventCategory) {
|
|
18
18
|
EventCategory["SystemBoot"] = "system.boot";
|
|
19
19
|
EventCategory["SystemAddonsReady"] = "system.addons-ready";
|
|
@@ -546,6 +546,15 @@ var EventCategory = /* @__PURE__ */ function(EventCategory) {
|
|
|
546
546
|
*/
|
|
547
547
|
EventCategory["PipelineAudioInferenceResult"] = "pipeline.audio-inference-result";
|
|
548
548
|
EventCategory["DetectionPhaseTransition"] = "detection.phase-transition";
|
|
549
|
+
/**
|
|
550
|
+
* A camera provider's RAW onboard motion push (D673): the `MotionOnMotionChanged`
|
|
551
|
+
* data shape without `triggeredBy`, built by `providerMotionEvent`. Its one
|
|
552
|
+
* consumer is the orchestrator's camera-sensors bridge, which re-emits the
|
|
553
|
+
* camera's EFFECTIVE onboard motion as `MotionOnMotionChanged` (a pass-through
|
|
554
|
+
* for a camera with no attached sensor). Providers must not emit
|
|
555
|
+
* `MotionOnMotionChanged` themselves; `scripts/check-onboard-motion-emitters.ts`
|
|
556
|
+
* keeps it so (camera-grid is a named, deferred exception).
|
|
557
|
+
*/
|
|
549
558
|
EventCategory["ProviderMotion"] = "provider.motion";
|
|
550
559
|
EventCategory["ProviderDetection"] = "provider.detection";
|
|
551
560
|
EventCategory["EnrichmentEmbeddingStored"] = "enrichment.embedding.stored";
|
|
@@ -5384,7 +5393,7 @@ var ZodIssueCode = {
|
|
|
5384
5393
|
var ZodFirstPartyTypeKind;
|
|
5385
5394
|
ZodFirstPartyTypeKind || (ZodFirstPartyTypeKind = {});
|
|
5386
5395
|
//#endregion
|
|
5387
|
-
//#region ../types/dist/sleep-
|
|
5396
|
+
//#region ../types/dist/sleep-CDCONfkt.mjs
|
|
5388
5397
|
/**
|
|
5389
5398
|
* The audio chunk plane's byte format, and the ONE expansion from a coded
|
|
5390
5399
|
* window to float samples (D455).
|
|
@@ -7417,7 +7426,7 @@ var DeviceRole = /* @__PURE__ */ function(DeviceRole) {
|
|
|
7417
7426
|
return DeviceRole;
|
|
7418
7427
|
}({});
|
|
7419
7428
|
//#endregion
|
|
7420
|
-
//#region ../types/dist/composition-
|
|
7429
|
+
//#region ../types/dist/composition-43aqsZQj.mjs
|
|
7421
7430
|
/**
|
|
7422
7431
|
* The ffmpeg build every CamStack node downloads, per platform and arch.
|
|
7423
7432
|
*
|
|
@@ -7474,13 +7483,12 @@ portable("linux64"), portable("linuxarm64"), portable("macarm64"), portable("mac
|
|
|
7474
7483
|
* LA tabella "quale booleano di questo tipo di device conta come ALTO", e il
|
|
7475
7484
|
* valutatore puro del suo FRONTE.
|
|
7476
7485
|
*
|
|
7477
|
-
*
|
|
7478
|
-
*
|
|
7479
|
-
*
|
|
7480
|
-
*
|
|
7481
|
-
*
|
|
7482
|
-
* fa suonare il campanello ma non registra" — è esattamente D62.
|
|
7483
|
-
* SPOSTA qui e il doorbell la ri-esporta.
|
|
7486
|
+
* UNA tabella per tutti quelli che chiedono "questo device è ALTO?": il
|
|
7487
|
+
* trigger `RecordingTriggers.sensorDeviceIds` del recorder, i marker dei
|
|
7488
|
+
* sensori della post-analysis, il bridge camera-sensors dell'orchestratore e
|
|
7489
|
+
* le sorgenti di un campanello composto (`doorbellFeatureSources`, D671).
|
|
7490
|
+
* Una seconda copia divergerebbe alla prima cap aggiunta, e il sintomo — "il
|
|
7491
|
+
* sensore fa suonare il campanello ma non registra" — è esattamente D62.
|
|
7484
7492
|
*
|
|
7485
7493
|
* ⚠ NON è `DEVICE_STATE_READERS` (`catalogs/device-state-vocabulary.ts`), e le
|
|
7486
7494
|
* due non vanno unificate: quella risponde a "qual è la PAROLA di stato per una
|
|
@@ -7506,8 +7514,6 @@ var SOURCE_CAP_ACTIVE_FIELD = {
|
|
|
7506
7514
|
vibration: "detected",
|
|
7507
7515
|
tamper: "tampered"
|
|
7508
7516
|
};
|
|
7509
|
-
/** Cap names whose presence in a device's bindings qualify it as a source. */
|
|
7510
|
-
var SOURCE_CAPS = Object.keys(SOURCE_CAP_ACTIVE_FIELD);
|
|
7511
7517
|
/**
|
|
7512
7518
|
* Error types for the safe expression engine. Two distinct classes so callers
|
|
7513
7519
|
* can tell a compile-time (grammar) failure from a runtime (evaluation)
|
|
@@ -9237,7 +9243,7 @@ var RecordingTriggersSchema = object({
|
|
|
9237
9243
|
*
|
|
9238
9244
|
* Cosa vuol dire "alto" dipende dal TIPO di device e non è deciso qui:
|
|
9239
9245
|
* `SOURCE_CAP_ACTIVE_FIELD` (`catalogs/sensor-active-state.ts`) è LA tabella,
|
|
9240
|
-
* la stessa
|
|
9246
|
+
* la stessa del campanello composto (D671). Ed è il FRONTE, non
|
|
9241
9247
|
* il livello: un contatto trovato già aperto al riavvio del runner non fa
|
|
9242
9248
|
* registrare.
|
|
9243
9249
|
*/
|
|
@@ -14847,7 +14853,17 @@ var SiteZoneSchema = object({
|
|
|
14847
14853
|
var ProviderStatusSchema = object({
|
|
14848
14854
|
connected: boolean(),
|
|
14849
14855
|
deviceCount: number(),
|
|
14850
|
-
error: string().optional()
|
|
14856
|
+
error: string().optional(),
|
|
14857
|
+
/**
|
|
14858
|
+
* `'orchestrator'`: this provider writes NO camera's `motion` runtime slice
|
|
14859
|
+
* — the orchestrator is its only writer (D696). Set by `BaseDeviceProvider`,
|
|
14860
|
+
* which every provider bundles, so only a provider BUILT from D696-amended
|
|
14861
|
+
* code says it. ABSENT means an older provider that may still write it (raw
|
|
14862
|
+
* VMD writes, a boot seed), and the orchestrator's provider gate keeps that
|
|
14863
|
+
* provider's cameras unwritten (`camera-sensors/motion-slice-provider-gate.ts`)
|
|
14864
|
+
* — the provider twin of the runner's `getLocalLoad().motionSliceWriter`.
|
|
14865
|
+
*/
|
|
14866
|
+
motionSliceWriter: literal("orchestrator").optional()
|
|
14851
14867
|
});
|
|
14852
14868
|
object({
|
|
14853
14869
|
externalId: string(),
|
|
@@ -15726,109 +15742,10 @@ method(object({
|
|
|
15726
15742
|
deviceIds: array(number()).readonly(),
|
|
15727
15743
|
caps: array(string()).readonly().optional()
|
|
15728
15744
|
}), record(string(), record(string(), unknown().nullable())));
|
|
15729
|
-
|
|
15730
|
-
|
|
15731
|
-
* name (same pattern as `snapshot`):
|
|
15732
|
-
*
|
|
15733
|
-
* - **Native** providers: registered per-device by device-driver
|
|
15734
|
-
* addons via `ctx.registerNativeCap` — either on a
|
|
15735
|
-
* `DeviceType.Button` accessory with `role: DeviceRole.Doorbell`,
|
|
15736
|
-
* or directly on the camera (Reolink registers at camera level).
|
|
15737
|
-
* Emits an `onPressed` event every time the firmware pushes a
|
|
15738
|
-
* ring; status tracks the last press and a pressCount since start
|
|
15739
|
-
* (diagnostic).
|
|
15740
|
-
*
|
|
15741
|
-
* - **Wrapper** provider: the `virtual-doorbell` system builtin
|
|
15742
|
-
* (`@camstack/system/builtins/doorbell`). Turns ANY binary-ish
|
|
15743
|
-
* device (contact / switch / event-emitter …) into a doorbell for
|
|
15744
|
-
* a camera. `defaultActive: false` — the operator explicitly binds
|
|
15745
|
-
* it per camera in the device-bindings UI, then picks the source
|
|
15746
|
-
* device + trigger in the per-device settings.
|
|
15747
|
-
*
|
|
15748
|
-
* The DeviceEventPropagator re-emits `onPressed` on the camera parent
|
|
15749
|
-
* — subscribers listening at the camera level receive ring events
|
|
15750
|
-
* with `via[]` populated. No code on the parent needed.
|
|
15751
|
-
*/
|
|
15752
|
-
var DoorbellStatusSchema = object({
|
|
15753
|
-
/** Ms epoch of the last press. null = never observed since this provider started. */
|
|
15754
|
-
lastPressedAt: number().nullable(),
|
|
15755
|
-
/** Counter since provider start. Resets on reboot. Useful for metrics/debug. */
|
|
15756
|
-
pressCountSinceStart: number()
|
|
15757
|
-
});
|
|
15758
|
-
var DoorbellPressEventSchema = object({
|
|
15759
|
-
deviceId: number(),
|
|
15760
|
-
timestamp: number()
|
|
15761
|
-
});
|
|
15762
|
-
/**
|
|
15763
|
-
* The `doorbell-source` extension slot's editable value (D648): the camera's
|
|
15764
|
-
* virtual-doorbell trigger devices, exactly what the wrapper's
|
|
15765
|
-
* `doorbellSources` persists (as `{ deviceId }` records).
|
|
15766
|
-
*/
|
|
15767
|
-
var DoorbellSourceSlotValueSchema = object({ deviceIds: array(number().int().positive()) });
|
|
15768
|
-
/**
|
|
15769
|
-
* The `doorbell-source` slot — which devices ring this camera's VIRTUAL
|
|
15770
|
-
* doorbell, owned by the `virtual-doorbell` wrapper over its
|
|
15771
|
-
* `doorbellSources` store (D648). A source rings when one of its
|
|
15772
|
-
* `SOURCE_CAPS` rises. Whether the wrapper is bound to the camera at all is a
|
|
15773
|
-
* different authority (`deviceManager.setWrapperActive`, D62) — the slot
|
|
15774
|
-
* reports it, it never writes it.
|
|
15775
|
-
*/
|
|
15776
|
-
var DOORBELL_SOURCE_SLOT = {
|
|
15777
|
-
id: "doorbell-source",
|
|
15778
|
-
label: "Doorbell trigger devices",
|
|
15779
|
-
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.",
|
|
15780
|
-
shape: "association",
|
|
15781
|
-
hostDeviceTypes: [DeviceType.Camera],
|
|
15782
|
-
sourceCaps: SOURCE_CAPS,
|
|
15783
|
-
cardinality: "many",
|
|
15784
|
-
value: DoorbellSourceSlotValueSchema
|
|
15785
|
-
};
|
|
15786
|
-
var doorbellCapability = {
|
|
15787
|
-
name: "doorbell",
|
|
15788
|
-
scope: "device",
|
|
15789
|
-
deviceNative: true,
|
|
15790
|
-
mode: "singleton",
|
|
15791
|
-
kind: "wrapper",
|
|
15792
|
-
defaultActive: false,
|
|
15793
|
-
deviceTypes: [DeviceType.Button, DeviceType.Camera],
|
|
15794
|
-
extensionSlots: [DOORBELL_SOURCE_SLOT],
|
|
15795
|
-
methods: {},
|
|
15796
|
-
events: {
|
|
15797
|
-
/**
|
|
15798
|
-
* Fires once per physical press. Reolink delivers via Baichuan
|
|
15799
|
-
* push (`ReolinkSimpleEvent.type === 'doorbell'`). There is no
|
|
15800
|
-
* release/duration — it's a pulse.
|
|
15801
|
-
*/
|
|
15802
|
-
onPressed: {
|
|
15803
|
-
data: DoorbellPressEventSchema,
|
|
15804
|
-
fromState: {
|
|
15805
|
-
field: "lastPressedAt",
|
|
15806
|
-
on: "advance"
|
|
15807
|
-
}
|
|
15808
|
-
} },
|
|
15809
|
-
status: {
|
|
15810
|
-
schema: DoorbellStatusSchema,
|
|
15811
|
-
kind: "push"
|
|
15812
|
-
},
|
|
15813
|
-
/**
|
|
15814
|
-
* Runtime-state slice — last press timestamp + lifetime press count.
|
|
15815
|
-
* Mirrored by the kernel and readable via
|
|
15816
|
-
* `device.state.doorbell.value`. UIs can show "last ring 5m ago"
|
|
15817
|
-
* without subscribing.
|
|
15818
|
-
*/
|
|
15819
|
-
runtimeState: DoorbellStatusSchema,
|
|
15820
|
-
/**
|
|
15821
|
-
* 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.
|
|
15822
|
-
*
|
|
15823
|
-
* See `RuntimeStateDurability`. Enforced by
|
|
15824
|
-
* `scripts/check-runtime-state-durability.ts`.
|
|
15825
|
-
*/
|
|
15826
|
-
durability: "restored"
|
|
15827
|
-
};
|
|
15828
|
-
var ExtensionSlotIdSchema = _enum(["doorbell-source", "linked-devices"]);
|
|
15829
|
-
DeviceType.Camera, DeviceType.Camera;
|
|
15745
|
+
var ExtensionSlotIdSchema = _enum(["linked-devices"]);
|
|
15746
|
+
DeviceType.Camera;
|
|
15830
15747
|
/** A slot's editable value, discriminated on `slot`. */
|
|
15831
|
-
var ExtensionSlotValueSchema = discriminatedUnion("slot", [
|
|
15748
|
+
var ExtensionSlotValueSchema = discriminatedUnion("slot", [LinkedDevicesSlotValueSchema.extend({ slot: literal("linked-devices") })]);
|
|
15832
15749
|
DeviceType.Camera;
|
|
15833
15750
|
var ExtensionSourceAvailabilitySchema = discriminatedUnion("state", [object({ state: literal("ok") }), object({
|
|
15834
15751
|
state: literal("unreachable"),
|
|
@@ -15865,10 +15782,11 @@ var ExtensionSlotSourceSchema = object({
|
|
|
15865
15782
|
});
|
|
15866
15783
|
/**
|
|
15867
15784
|
* A DIFFERENT authority that gates whether the slot's sources do anything —
|
|
15868
|
-
* reported read-only, named, never written through this contract (D62).
|
|
15869
|
-
*
|
|
15870
|
-
*
|
|
15871
|
-
* `unknown` means the gate could
|
|
15785
|
+
* reported read-only, named, never written through this contract (D62) — e.g.
|
|
15786
|
+
* a slot whose sources act only while a wrapper is bound to the host, a
|
|
15787
|
+
* binding that belongs to `deviceManager.setWrapperActive`. No slot reports
|
|
15788
|
+
* one today (`linked-devices` answers `null`). `unknown` means the gate could
|
|
15789
|
+
* not be read; it is not "inactive" (D315).
|
|
15872
15790
|
*/
|
|
15873
15791
|
var ExtensionSlotActivationSchema = object({
|
|
15874
15792
|
state: _enum([
|
|
@@ -15884,7 +15802,7 @@ var ExtensionSlotRowSchema = discriminatedUnion("state", [object({
|
|
|
15884
15802
|
/** `<source>:<slotId>` — the routing key for `listCandidates` / `setSlot`. */
|
|
15885
15803
|
slotRef: string(),
|
|
15886
15804
|
slot: ExtensionSlotIdSchema,
|
|
15887
|
-
/** Names the store written (D62), e.g. `
|
|
15805
|
+
/** Names the store written (D62), e.g. `device-manager:_linkedDeviceIds`. */
|
|
15888
15806
|
authority: string(),
|
|
15889
15807
|
activation: ExtensionSlotActivationSchema.nullable(),
|
|
15890
15808
|
value: ExtensionSlotValueSchema,
|
|
@@ -20407,6 +20325,29 @@ object({
|
|
|
20407
20325
|
firstAt: number(),
|
|
20408
20326
|
lastAt: number()
|
|
20409
20327
|
});
|
|
20328
|
+
var NC_ALARM_TRIGGERED_MAX_SEC = 3600;
|
|
20329
|
+
var NcAlarmDelaySecSchema = number().int().min(0).max(600);
|
|
20330
|
+
var NcAlarmTriggeredSecSchema = number().int().min(0).max(NC_ALARM_TRIGGERED_MAX_SEC);
|
|
20331
|
+
var uniqueModes = (entries) => new Set(entries.map((e) => e.mode)).size === entries.length;
|
|
20332
|
+
var NcAlarmModeDelaysListSchema = array(object({
|
|
20333
|
+
mode: AlarmArmModeSchema,
|
|
20334
|
+
exitDelaySec: NcAlarmDelaySecSchema.optional(),
|
|
20335
|
+
entryDelaySec: NcAlarmDelaySecSchema.optional(),
|
|
20336
|
+
triggeredDurationSec: NcAlarmTriggeredSecSchema.optional()
|
|
20337
|
+
})).max(8).refine(uniqueModes, { message: "modeDelays: at most one entry per arm mode" });
|
|
20338
|
+
var NcAlarmModeDelaysEditListSchema = array(object({
|
|
20339
|
+
mode: AlarmArmModeSchema,
|
|
20340
|
+
exitDelaySec: NcAlarmDelaySecSchema.nullable().optional(),
|
|
20341
|
+
entryDelaySec: NcAlarmDelaySecSchema.nullable().optional(),
|
|
20342
|
+
triggeredDurationSec: NcAlarmTriggeredSecSchema.nullable().optional()
|
|
20343
|
+
})).max(8).refine(uniqueModes, { message: "modeDelays: at most one edit per arm mode" });
|
|
20344
|
+
/** One switch of the alarm table: put ONE device on, or take it off, ONE mode's list. */
|
|
20345
|
+
var NcAlarmNonBlockingEditSchema = object({
|
|
20346
|
+
mode: AlarmArmModeSchema,
|
|
20347
|
+
deviceId: number().int().positive(),
|
|
20348
|
+
/** true = on the list (may be open, does not block); false = off the list (blocks). */
|
|
20349
|
+
nonBlocking: boolean()
|
|
20350
|
+
});
|
|
20410
20351
|
/**
|
|
20411
20352
|
* The per-mode lists, at most one per mode. Two lists for one mode have no
|
|
20412
20353
|
* meaning a reader could agree on (union? the last one?), so they are refused
|
|
@@ -20415,10 +20356,10 @@ object({
|
|
|
20415
20356
|
var NcAlarmNonBlockingListSchema = array(object({
|
|
20416
20357
|
mode: AlarmArmModeSchema,
|
|
20417
20358
|
deviceIds: array(number().int()).max(200)
|
|
20418
|
-
})).max(8).refine(
|
|
20359
|
+
})).max(8).refine(uniqueModes, { message: "nonBlocking: at most one list per arm mode" });
|
|
20419
20360
|
/**
|
|
20420
|
-
* The three durations
|
|
20421
|
-
* transitions, and which openings each mode tolerates.
|
|
20361
|
+
* The three durations — global, and per mode since D697 — the panel's state
|
|
20362
|
+
* machine runs on, who hears about its transitions, and which openings each mode tolerates.
|
|
20422
20363
|
*
|
|
20423
20364
|
* They live on the NOTIFICATION-RULES cap, not on `alarm-panel`, on purpose:
|
|
20424
20365
|
* `alarm-panel` is `deviceNative` and its other provider mirrors somebody
|
|
@@ -20428,15 +20369,15 @@ var NcAlarmNonBlockingListSchema = array(object({
|
|
|
20428
20369
|
*/
|
|
20429
20370
|
var NcAlarmSettingsSchema = object({
|
|
20430
20371
|
/** Grace period between arming and the mode taking effect. 0 = immediate. */
|
|
20431
|
-
exitDelaySec:
|
|
20372
|
+
exitDelaySec: NcAlarmDelaySecSchema,
|
|
20432
20373
|
/** Grace period between a trigger and the alarm firing. 0 = immediate. */
|
|
20433
|
-
entryDelaySec:
|
|
20374
|
+
entryDelaySec: NcAlarmDelaySecSchema,
|
|
20434
20375
|
/**
|
|
20435
20376
|
* How long `triggered` lasts before the panel re-arms itself.
|
|
20436
20377
|
* **0 = until an operator disarms it** — the behaviour before this field
|
|
20437
20378
|
* existed, and therefore what an untouched install keeps doing.
|
|
20438
20379
|
*/
|
|
20439
|
-
triggeredDurationSec:
|
|
20380
|
+
triggeredDurationSec: NcAlarmTriggeredSecSchema,
|
|
20440
20381
|
/**
|
|
20441
20382
|
* Deprecated by D630 — every transition is announced. Parsed, never read;
|
|
20442
20383
|
* remove after one release. Optional so an older admin's patch (which still
|
|
@@ -20459,17 +20400,30 @@ var NcAlarmSettingsSchema = object({
|
|
|
20459
20400
|
* per mode is meaningful. Defaulted to `[]` so a blob stored before D630
|
|
20460
20401
|
* parses as "every opening blocks" — the behaviour it was written under.
|
|
20461
20402
|
*/
|
|
20462
|
-
nonBlocking: NcAlarmNonBlockingListSchema.default([])
|
|
20403
|
+
nonBlocking: NcAlarmNonBlockingListSchema.default([]),
|
|
20404
|
+
/**
|
|
20405
|
+
* Per arm mode, the delays that mode OVERRIDES (D697). A mode with no entry, or an entry
|
|
20406
|
+
* without a field, uses the global value above — so `[]`, the default, is exactly the
|
|
20407
|
+
* behaviour of a blob stored before this field existed. Read through `resolveModeDelays`.
|
|
20408
|
+
*/
|
|
20409
|
+
modeDelays: NcAlarmModeDelaysListSchema.default([])
|
|
20463
20410
|
});
|
|
20464
20411
|
/**
|
|
20465
20412
|
* Every field optional — a tab edits one control at a time.
|
|
20466
20413
|
*
|
|
20467
|
-
* `nonBlocking`
|
|
20468
|
-
* remove an inner `.default()`, so a patch that never
|
|
20469
|
-
* parse to `
|
|
20470
|
-
* (the same trap {@link NcRulePatchSchema} documents).
|
|
20414
|
+
* `nonBlocking` and `modeDelays` are re-declared WITHOUT their defaults:
|
|
20415
|
+
* `.partial()` does not remove an inner `.default()`, so a patch that never
|
|
20416
|
+
* named the field would parse to `[]` and wipe every mode's list on an
|
|
20417
|
+
* unrelated edit (the same trap {@link NcRulePatchSchema} documents). Here
|
|
20418
|
+
* `modeDelays` means EDITS, not the stored overrides.
|
|
20471
20419
|
*/
|
|
20472
|
-
var NcAlarmSettingsPatchSchema = NcAlarmSettingsSchema.extend({
|
|
20420
|
+
var NcAlarmSettingsPatchSchema = NcAlarmSettingsSchema.extend({
|
|
20421
|
+
nonBlocking: NcAlarmNonBlockingListSchema,
|
|
20422
|
+
/** Per-mode, per-field edits — NOT the whole list (see NcAlarmModeDelaysEditSchema). */
|
|
20423
|
+
modeDelays: NcAlarmModeDelaysEditListSchema,
|
|
20424
|
+
/** ONE membership change; the panel applies it to the list it holds at its turn. */
|
|
20425
|
+
nonBlockingEdit: NcAlarmNonBlockingEditSchema
|
|
20426
|
+
}).partial().refine((p) => p.nonBlocking === void 0 || p.nonBlockingEdit === void 0, { message: "nonBlocking and nonBlockingEdit are exclusive — send the whole lists or one edit" });
|
|
20473
20427
|
/**
|
|
20474
20428
|
* What one arm mode actually arms, DERIVED from the enabled rules gated on it.
|
|
20475
20429
|
* Never authored, never stored — see `alarm-mode-coverage.ts` for why a stored
|
|
@@ -20569,6 +20523,28 @@ var NcAlarmConfigSchema = object({
|
|
|
20569
20523
|
*/
|
|
20570
20524
|
live: NcAlarmLiveSchema.optional()
|
|
20571
20525
|
});
|
|
20526
|
+
var NcAlarmSensorReadingSchema = _enum([
|
|
20527
|
+
"open",
|
|
20528
|
+
"closed",
|
|
20529
|
+
"unknown",
|
|
20530
|
+
"not-opening"
|
|
20531
|
+
]);
|
|
20532
|
+
/** One device's state as the alarm reads it — the classifier the arm check uses. */
|
|
20533
|
+
var NcAlarmSensorStateSchema = object({
|
|
20534
|
+
deviceId: number().int().positive(),
|
|
20535
|
+
reading: NcAlarmSensorReadingSchema,
|
|
20536
|
+
/** The word as read (`open`, `closed`, `unlocked`, `stopped`, …); null when unknown / not an opening. */
|
|
20537
|
+
state: string().nullable(),
|
|
20538
|
+
/** Why `unknown`: the read failed, or the slice holds no word its cap defines (a placeholder included). */
|
|
20539
|
+
unknownWhy: _enum(["read-failed", "unknown-word"]).nullable(),
|
|
20540
|
+
/** The device manager's liveness as the centre mirrors it; null = not known. */
|
|
20541
|
+
online: boolean().nullable()
|
|
20542
|
+
});
|
|
20543
|
+
var NcAlarmSensorsInputSchema = object({ deviceIds: array(number().int().positive()).max(500) });
|
|
20544
|
+
var NcAlarmSensorsSchema = object({
|
|
20545
|
+
sensors: array(NcAlarmSensorStateSchema),
|
|
20546
|
+
readAt: number().int().nonnegative()
|
|
20547
|
+
});
|
|
20572
20548
|
/**
|
|
20573
20549
|
* ONE rule's demand on ONE camera's clip ring.
|
|
20574
20550
|
*
|
|
@@ -20659,7 +20635,7 @@ method(object({}), object({ rules: array(NcRuleSchema) }), { auth: "admin" }), m
|
|
|
20659
20635
|
}), method(object({}), NcAlarmConfigSchema, { auth: "admin" }), method(object({ patch: NcAlarmSettingsPatchSchema }), NcAlarmConfigSchema, {
|
|
20660
20636
|
kind: "mutation",
|
|
20661
20637
|
auth: "admin"
|
|
20662
|
-
});
|
|
20638
|
+
}), method(NcAlarmSensorsInputSchema, NcAlarmSensorsSchema, { auth: "admin" });
|
|
20663
20639
|
/**
|
|
20664
20640
|
* Zod schemas for persisted record types.
|
|
20665
20641
|
*
|
|
@@ -21006,12 +20982,26 @@ var CameraSensorRefSchema = object({
|
|
|
21006
20982
|
function cameraSensorRefKey(ref) {
|
|
21007
20983
|
return `${ref.addonId}/${ref.stableId}`;
|
|
21008
20984
|
}
|
|
21009
|
-
/**
|
|
21010
|
-
|
|
21011
|
-
|
|
21012
|
-
|
|
21013
|
-
|
|
21014
|
-
|
|
20985
|
+
/**
|
|
20986
|
+
* A sensor's mode (D673). `in-addition`: its motion is the camera's motion too.
|
|
20987
|
+
* `replace`: its motion is the camera's, and while one is READY the camera's
|
|
20988
|
+
* own motion no longer counts on its own. `confirm`: while one is READY the
|
|
20989
|
+
* camera's own motion counts only while a confirm sensor is active — with or
|
|
20990
|
+
* without a replace sensor (spec C2, the controller's ruling on Q1).
|
|
20991
|
+
*/
|
|
20992
|
+
var CameraSensorModeSchema = _enum([
|
|
20993
|
+
"in-addition",
|
|
20994
|
+
"confirm",
|
|
20995
|
+
"replace"
|
|
20996
|
+
]);
|
|
20997
|
+
/** A stored sensor with no mode — every CS-1 row — is in addition. */
|
|
20998
|
+
var DEFAULT_CAMERA_SENSOR_MODE = "in-addition";
|
|
20999
|
+
/**
|
|
21000
|
+
* A camera's attached sensors. A CS-1 row carried an envelope
|
|
21001
|
+
* `mode: 'in-addition'`: `z.object` drops it on parse, and the PARSED value is
|
|
21002
|
+
* what the write stores (D672 rule 3), so it is never written again.
|
|
21003
|
+
*/
|
|
21004
|
+
var CameraSensorsConfigSchema = object({ sensors: array(CameraSensorRefSchema.extend({ mode: CameraSensorModeSchema.default(DEFAULT_CAMERA_SENSOR_MODE) })).min(1).max(8) }).superRefine((value, ctx) => {
|
|
21015
21005
|
const seen = /* @__PURE__ */ new Set();
|
|
21016
21006
|
value.sensors.forEach((ref, index) => {
|
|
21017
21007
|
const key = cameraSensorRefKey(ref);
|
|
@@ -21023,39 +21013,41 @@ var CameraSensorsConfigSchema = object({
|
|
|
21023
21013
|
seen.add(key);
|
|
21024
21014
|
});
|
|
21025
21015
|
});
|
|
21026
|
-
|
|
21016
|
+
/** Every attachment names its mode; an older orchestrator that does not is in addition. */
|
|
21017
|
+
var AttachmentFields = {
|
|
21027
21018
|
addonId: string(),
|
|
21028
|
-
stableId: string()
|
|
21019
|
+
stableId: string(),
|
|
21020
|
+
mode: CameraSensorModeSchema.default(DEFAULT_CAMERA_SENSOR_MODE)
|
|
21029
21021
|
};
|
|
21030
21022
|
var CameraSensorAttachmentSchema = discriminatedUnion("state", [
|
|
21031
21023
|
object({
|
|
21032
|
-
...
|
|
21024
|
+
...AttachmentFields,
|
|
21033
21025
|
state: literal("ready"),
|
|
21034
21026
|
deviceId: number(),
|
|
21035
21027
|
name: string(),
|
|
21036
21028
|
caps: array(string()).readonly()
|
|
21037
21029
|
}),
|
|
21038
21030
|
object({
|
|
21039
|
-
...
|
|
21031
|
+
...AttachmentFields,
|
|
21040
21032
|
state: literal("offline"),
|
|
21041
21033
|
deviceId: number(),
|
|
21042
21034
|
name: string(),
|
|
21043
21035
|
reason: _enum(["offline", "disabled"])
|
|
21044
21036
|
}),
|
|
21045
21037
|
object({
|
|
21046
|
-
...
|
|
21038
|
+
...AttachmentFields,
|
|
21047
21039
|
state: literal("missing"),
|
|
21048
21040
|
reason: string()
|
|
21049
21041
|
}),
|
|
21050
21042
|
object({
|
|
21051
|
-
...
|
|
21043
|
+
...AttachmentFields,
|
|
21052
21044
|
state: literal("unsupported"),
|
|
21053
21045
|
deviceId: number(),
|
|
21054
21046
|
name: string(),
|
|
21055
21047
|
reason: string()
|
|
21056
21048
|
}),
|
|
21057
21049
|
object({
|
|
21058
|
-
...
|
|
21050
|
+
...AttachmentFields,
|
|
21059
21051
|
state: literal("unreadable"),
|
|
21060
21052
|
reason: string()
|
|
21061
21053
|
})
|
|
@@ -21068,17 +21060,49 @@ var CameraSensorAttachmentSchema = discriminatedUnion("state", [
|
|
|
21068
21060
|
* say.
|
|
21069
21061
|
*/
|
|
21070
21062
|
var CameraSensorsNotForwardedSchema = _enum(["onboard-not-listed", "no-active-detection"]);
|
|
21063
|
+
/** Whether the camera's OWN motion counts on its own (D673 C2), from its READY sensors (C3). */
|
|
21064
|
+
var CameraOwnMotionSchema = _enum([
|
|
21065
|
+
"always",
|
|
21066
|
+
"when-confirmed",
|
|
21067
|
+
"never"
|
|
21068
|
+
]);
|
|
21069
|
+
/** Confirm / Replace cannot suppress this camera's motion: its provider still emits it directly (D673 C13). */
|
|
21070
|
+
var CameraSensorsNotEnforcedSchema = _enum(["provider-not-split"]);
|
|
21071
|
+
/**
|
|
21072
|
+
* The camera's OWN onboard motion (D696): the raw input the bridge combines,
|
|
21073
|
+
* never the effective value. The camera's `motion` slice is the effective one,
|
|
21074
|
+
* so a view of the camera's own row reads this.
|
|
21075
|
+
* - `atMs`: when the bridge received the camera's last own edge.
|
|
21076
|
+
* - `detected`: evaluated WHEN THE STATUS IS ASKED — the last edge said `true`
|
|
21077
|
+
* AND it is younger than the window the bridge counts the camera's own motion
|
|
21078
|
+
* for (`motionCooldownMs`, floored at two sustain ticks). A camera that never
|
|
21079
|
+
* sends a clear (heartbeats only) therefore reads `false` once no push came
|
|
21080
|
+
* within the window; a status read is a snapshot, so a live view re-asks.
|
|
21081
|
+
* Absent = this orchestrator has not received the camera's own motion since it
|
|
21082
|
+
* started (unknown, never "quiet"), or an orchestrator older than D696.
|
|
21083
|
+
*/
|
|
21084
|
+
var CameraRawLevelSchema = object({
|
|
21085
|
+
detected: boolean(),
|
|
21086
|
+
atMs: number()
|
|
21087
|
+
});
|
|
21071
21088
|
var CameraSensorsStatusSchema = discriminatedUnion("state", [
|
|
21072
|
-
object({
|
|
21089
|
+
object({
|
|
21090
|
+
state: literal("not-configured"),
|
|
21091
|
+
cameraLevel: CameraRawLevelSchema.optional()
|
|
21092
|
+
}),
|
|
21073
21093
|
object({
|
|
21074
21094
|
state: literal("unreadable"),
|
|
21075
|
-
reason: string()
|
|
21095
|
+
reason: string(),
|
|
21096
|
+
cameraLevel: CameraRawLevelSchema.optional()
|
|
21076
21097
|
}),
|
|
21077
21098
|
object({
|
|
21078
21099
|
state: literal("configured"),
|
|
21079
|
-
|
|
21100
|
+
cameraLevel: CameraRawLevelSchema.optional(),
|
|
21080
21101
|
sensors: array(CameraSensorAttachmentSchema).readonly(),
|
|
21081
|
-
|
|
21102
|
+
/** Absent = an orchestrator older than CS-2: it does not say (D315). */
|
|
21103
|
+
cameraMotion: CameraOwnMotionSchema.optional(),
|
|
21104
|
+
notForwarded: CameraSensorsNotForwardedSchema.optional(),
|
|
21105
|
+
notEnforced: CameraSensorsNotEnforcedSchema.optional()
|
|
21082
21106
|
})
|
|
21083
21107
|
]);
|
|
21084
21108
|
Object.freeze({
|
|
@@ -24309,8 +24333,10 @@ var RunnerCameraConfigSchema = object({
|
|
|
24309
24333
|
* - `analyzer`: ML/frame-diff motion detection (motion-wasm or other
|
|
24310
24334
|
* analyzer providers) running on decoded frames in this runner.
|
|
24311
24335
|
* - `onboard`: camera-native motion pushes (Reolink Baichuan, ONVIF
|
|
24312
|
-
* analytics). The provider emits ProviderMotion
|
|
24313
|
-
*
|
|
24336
|
+
* analytics). The provider emits its raw push as `ProviderMotion`
|
|
24337
|
+
* (D673); the orchestrator's camera-sensors bridge re-emits the camera's
|
|
24338
|
+
* effective motion as `MotionOnMotionChanged`, which is what reaches
|
|
24339
|
+
* `reportMotion`.
|
|
24314
24340
|
*/
|
|
24315
24341
|
motionSources: MotionSourcesSchema.default(["analyzer"]),
|
|
24316
24342
|
/**
|
|
@@ -24487,7 +24513,15 @@ var RunnerLocalLoadSchema = object({
|
|
|
24487
24513
|
hasGpu: boolean(),
|
|
24488
24514
|
inferenceBackend: string().optional(),
|
|
24489
24515
|
cpuCores: number().optional()
|
|
24490
|
-
})
|
|
24516
|
+
}),
|
|
24517
|
+
/**
|
|
24518
|
+
* Who writes a camera's `motion` runtime slice (D696). `'orchestrator'`: this
|
|
24519
|
+
* runner never writes it. ABSENT = a runner older than D696, which still
|
|
24520
|
+
* writes the slice from its phase machine; the orchestrator does not write
|
|
24521
|
+
* while any runner node is absent-or-unanswered, so one slice never has two
|
|
24522
|
+
* writers.
|
|
24523
|
+
*/
|
|
24524
|
+
motionSliceWriter: literal("orchestrator").optional()
|
|
24491
24525
|
});
|
|
24492
24526
|
/**
|
|
24493
24527
|
* Aggregate runtime metrics for the runner's whole local pool. Mirrors the
|
|
@@ -30521,6 +30555,70 @@ onStatusChanged: { data: object({
|
|
|
30521
30555
|
* whether persisting is worth a SQLite commit. */
|
|
30522
30556
|
volatileStateFields: ["lastChangedAt"]
|
|
30523
30557
|
};
|
|
30558
|
+
var DoorbellStatusSchema = object({
|
|
30559
|
+
/** Ms epoch of the last press. null = never observed since this provider started. */
|
|
30560
|
+
lastPressedAt: number().nullable(),
|
|
30561
|
+
/** Counter since provider start. Resets on reboot. Useful for metrics/debug. */
|
|
30562
|
+
pressCountSinceStart: number()
|
|
30563
|
+
});
|
|
30564
|
+
var DoorbellPressEventSchema = object({
|
|
30565
|
+
deviceId: number(),
|
|
30566
|
+
timestamp: number()
|
|
30567
|
+
});
|
|
30568
|
+
/**
|
|
30569
|
+
* Doorbell button cap. Two kinds of provider serve it, and to every consumer
|
|
30570
|
+
* they are the same cap:
|
|
30571
|
+
*
|
|
30572
|
+
* - **Native** providers, registered per device by a driver addon through
|
|
30573
|
+
* `ctx.registerNativeCap` — a `DeviceType.Button` accessory with
|
|
30574
|
+
* `role: DeviceRole.Doorbell`, or the camera itself (Reolink registers at
|
|
30575
|
+
* camera level). They emit `onPressed` on every firmware ring.
|
|
30576
|
+
* - **A composed doorbell** (D670, D671): a camera's customization gives it
|
|
30577
|
+
* the cap, its `lastPressedAt` rising from the sources the operator
|
|
30578
|
+
* assigned in Customize → Doorbell, and the composer emits `onPressed`
|
|
30579
|
+
* from that field's advance (`fromState`).
|
|
30580
|
+
*
|
|
30581
|
+
* The DeviceEventPropagator re-emits `onPressed` on the camera parent.
|
|
30582
|
+
*/
|
|
30583
|
+
var doorbellCapability = {
|
|
30584
|
+
name: "doorbell",
|
|
30585
|
+
scope: "device",
|
|
30586
|
+
deviceNative: true,
|
|
30587
|
+
mode: "singleton",
|
|
30588
|
+
deviceTypes: [DeviceType.Button, DeviceType.Camera],
|
|
30589
|
+
methods: {},
|
|
30590
|
+
events: {
|
|
30591
|
+
/**
|
|
30592
|
+
* Fires once per physical press. Reolink delivers via Baichuan
|
|
30593
|
+
* push (`ReolinkSimpleEvent.type === 'doorbell'`). There is no
|
|
30594
|
+
* release/duration — it's a pulse.
|
|
30595
|
+
*/
|
|
30596
|
+
onPressed: {
|
|
30597
|
+
data: DoorbellPressEventSchema,
|
|
30598
|
+
fromState: {
|
|
30599
|
+
field: "lastPressedAt",
|
|
30600
|
+
on: "advance"
|
|
30601
|
+
}
|
|
30602
|
+
} },
|
|
30603
|
+
status: {
|
|
30604
|
+
schema: DoorbellStatusSchema,
|
|
30605
|
+
kind: "push"
|
|
30606
|
+
},
|
|
30607
|
+
/**
|
|
30608
|
+
* Runtime-state slice — last press timestamp + lifetime press count.
|
|
30609
|
+
* Mirrored by the kernel and readable via
|
|
30610
|
+
* `device.state.doorbell.value`. UIs can show "last ring 5m ago"
|
|
30611
|
+
* without subscribing.
|
|
30612
|
+
*/
|
|
30613
|
+
runtimeState: DoorbellStatusSchema,
|
|
30614
|
+
/**
|
|
30615
|
+
* 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.
|
|
30616
|
+
*
|
|
30617
|
+
* See `RuntimeStateDurability`. Enforced by
|
|
30618
|
+
* `scripts/check-runtime-state-durability.ts`.
|
|
30619
|
+
*/
|
|
30620
|
+
durability: "restored"
|
|
30621
|
+
};
|
|
30524
30622
|
/**
|
|
30525
30623
|
* Enum-state sensor — a string value picked from a finite option set.
|
|
30526
30624
|
* Drives HA `sensor` entries with `state_class: enum` (HVAC action
|
|
@@ -32660,39 +32758,14 @@ authKey: string().optional() }), object({
|
|
|
32660
32758
|
/** Human-readable error when `ok: false`. */
|
|
32661
32759
|
error: string().optional()
|
|
32662
32760
|
}), { kind: "mutation" });
|
|
32663
|
-
/**
|
|
32664
|
-
* Hardware / firmware motion sensor cap — binary detected state plus
|
|
32665
|
-
* a timestamp of the last observation. Distinct from
|
|
32666
|
-
* `motion-detection.cap.ts` which owns the LOCAL ML motion pipeline;
|
|
32667
|
-
* `motion` is the lightweight readout from on-camera motion (Reolink
|
|
32668
|
-
* `GetMdState`, Baichuan push `type: motion`, ONVIF analytics).
|
|
32669
|
-
*
|
|
32670
|
-
* Native-motion providers also fan out to `detection.camera-native`
|
|
32671
|
-
* with `source: 'onboard'` so cross-cutting system services
|
|
32672
|
-
* (alert-center, advanced-notifier) can subscribe once and receive
|
|
32673
|
-
* motion from every camera.
|
|
32674
|
-
*/
|
|
32675
|
-
/**
|
|
32676
|
-
* How long after a camera's last rising edge `pipeline-analytics` closes the
|
|
32677
|
-
* motion EPISODE row it kept open for it (D475) — "il delay della cap", in
|
|
32678
|
-
* the operator's words. One authority for the whole system: every provider
|
|
32679
|
-
* that seeds `autoClearAfterMs` on a `Camera` device writes this SAME
|
|
32680
|
-
* constant while `detected: true` (never its own number — Hikvision used to
|
|
32681
|
-
* seed its internal 3 s inactivity timer here, which is a different clock for
|
|
32682
|
-
* a different purpose), and `MotionEpisodeTracker` imports it directly rather
|
|
32683
|
-
* than reading the live cap value on the hot path. A `Sensor` device (HA
|
|
32684
|
-
* binary sensor, Homematic) gets a genuine push both ways and has no episode
|
|
32685
|
-
* to close on a timer, so it writes `null`, never this constant.
|
|
32686
|
-
*/
|
|
32687
|
-
var MOTION_CLOSE_AFTER_MS = 15e3;
|
|
32688
32761
|
var MotionStatusSchema = object({
|
|
32689
32762
|
detected: boolean(),
|
|
32690
32763
|
/** Ms epoch of the last detected-true observation. Null if never detected. */
|
|
32691
32764
|
lastDetectedAt: number().nullable(),
|
|
32692
32765
|
/**
|
|
32693
|
-
* `MOTION_CLOSE_AFTER_MS` while `detected: true` on a `Camera` device
|
|
32694
|
-
* `null` while false and on every `Sensor`
|
|
32695
|
-
* constant's doc for the one-authority rule.
|
|
32766
|
+
* `MOTION_CLOSE_AFTER_MS` while `detected: true` on a `Camera` device (the
|
|
32767
|
+
* orchestrator writes it, D696), `null` while false and on every `Sensor`
|
|
32768
|
+
* device (D475) — see that constant's doc for the one-authority rule.
|
|
32696
32769
|
*
|
|
32697
32770
|
* ## Reading this field still arms nothing
|
|
32698
32771
|
*
|
|
@@ -32710,34 +32783,31 @@ var MotionStatusSchema = object({
|
|
|
32710
32783
|
* `addon-export-alexa/src/motion-clear-hold.ts` and
|
|
32711
32784
|
* `addon-export-hap`'s `RESET_DEBOUNCE_MS` both do.
|
|
32712
32785
|
*
|
|
32713
|
-
* ## Who writes it
|
|
32714
|
-
*
|
|
32715
|
-
* - **Cameras** — the runner's phase machine, `active → watching` on
|
|
32716
|
-
* `cooldown_expired`, which then writes this slice with
|
|
32717
|
-
* `detected: false` (`handlePhaseChanged` in
|
|
32718
|
-
* `pipeline-runner/index.ts`). It produces the FALLING edge, which
|
|
32719
|
-
* matters most for the sources that only ever push a rising one:
|
|
32720
|
-
* Reolink emits `MotionOnMotionChanged { detected: true }` and never
|
|
32721
|
-
* a false.
|
|
32722
|
-
* - **Sensors** (Home Assistant binary sensors, Homematic) — the
|
|
32723
|
-
* provider pushes the false itself, from the upstream system's own
|
|
32724
|
-
* state change. No phase machine is involved.
|
|
32786
|
+
* ## Who writes it (D696)
|
|
32725
32787
|
*
|
|
32726
|
-
*
|
|
32788
|
+
* - **Cameras** — the orchestrator, and nothing else. It writes the camera's
|
|
32789
|
+
* EFFECTIVE motion (`addon-pipeline-orchestrator`
|
|
32790
|
+
* `camera-sensors/motion-slice.ts`), alongside the effective
|
|
32791
|
+
* `MotionOnMotionChanged` it emits or receives: the camera-sensors bridge
|
|
32792
|
+
* for a split camera, an unsplit emitter's direct event, the analyzer and
|
|
32793
|
+
* device activity, as the camera's `motionSources` counts them. A source
|
|
32794
|
+
* that never sends a clear falls one window after its last `true`.
|
|
32795
|
+
* `lastDetectedAt` moves only on an effective rise. No camera provider
|
|
32796
|
+
* writes this slice (not even a boot seed), and neither does the runner's
|
|
32797
|
+
* phase machine: its phase is "a session is attached" for every on-motion
|
|
32798
|
+
* session, not motion, and it now has its own `DetectionPhaseTransition`.
|
|
32799
|
+
* A provider's `isDetected` answers from the hub's slice.
|
|
32800
|
+
* - **Sensors** (Home Assistant binary sensors, Homematic, Matter, Alexa
|
|
32801
|
+
* imports) — the provider pushes both edges itself, from the upstream
|
|
32802
|
+
* system's own state change. That is the sensor's own fact.
|
|
32727
32803
|
*
|
|
32728
|
-
*
|
|
32729
|
-
*
|
|
32730
|
-
* `hikvision-camera.ts:3464` and `amcrest-camera.ts:445` both call
|
|
32731
|
-
* `setCapSlice(motionCapability, …)` on their own rising edge, and
|
|
32732
|
-
* Hikvision's comment says why: it read THIS docblock, agreed the
|
|
32733
|
-
* runner is canonical, and wrote anyway to avoid per-tick churn. So
|
|
32734
|
-
* two authorities can disagree about one slice, which this repo
|
|
32735
|
-
* forbids, and the doc said otherwise — which is worse than saying
|
|
32736
|
-
* nothing, because it reads as verification.
|
|
32804
|
+
* `scripts/check-onboard-motion-emitters.ts` refuses any other writer of a
|
|
32805
|
+
* `motion` slice; each sensor writer is a named exemption.
|
|
32737
32806
|
*
|
|
32738
|
-
* This
|
|
32739
|
-
*
|
|
32740
|
-
*
|
|
32807
|
+
* This replaces the D475 §4 rule ("the phase machine is the sole writer").
|
|
32808
|
+
* That rule was never true: Hikvision and Amcrest wrote the slice raw beside
|
|
32809
|
+
* it, every provider seeded it at boot, and every session left it
|
|
32810
|
+
* `detected: true` after its detach.
|
|
32741
32811
|
*/
|
|
32742
32812
|
autoClearAfterMs: number().nullable()
|
|
32743
32813
|
});
|
|
@@ -32809,11 +32879,12 @@ onMotionChanged: { data: MotionOnMotionChangedDataSchema } },
|
|
|
32809
32879
|
*/
|
|
32810
32880
|
runtimeState: MotionStatusSchema,
|
|
32811
32881
|
/**
|
|
32812
|
-
* Runtime-state durability: **session** —
|
|
32882
|
+
* Runtime-state durability: **session** — the writer of this slice
|
|
32813
32883
|
* writes only on an EDGE, so a restored `detected: true` would stay
|
|
32814
32884
|
* frozen until the next one instead of being corrected. The next edge
|
|
32815
|
-
* re-publishes the real state
|
|
32816
|
-
*
|
|
32885
|
+
* re-publishes the real state, and the orchestrator re-publishes every
|
|
32886
|
+
* camera when it starts writing (D696; on who writes it, see
|
|
32887
|
+
* `autoClearAfterMs` above).
|
|
32817
32888
|
*
|
|
32818
32889
|
* See `RuntimeStateDurability`. Enforced by
|
|
32819
32890
|
* `scripts/check-runtime-state-durability.ts`.
|
|
@@ -35354,8 +35425,7 @@ var ptzAutotrackCapability = {
|
|
|
35354
35425
|
deviceNative: true,
|
|
35355
35426
|
mode: "singleton",
|
|
35356
35427
|
/**
|
|
35357
|
-
* Two kinds of provider behind one name, the same shape `snapshot`
|
|
35358
|
-
* `doorbell` use:
|
|
35428
|
+
* Two kinds of provider behind one name, the same shape `snapshot` uses:
|
|
35359
35429
|
*
|
|
35360
35430
|
* - **Native**: the camera's own firmware autotrack, registered per device by
|
|
35361
35431
|
* the driver when the probe says it exists (Reolink: yes; Hikvision:
|
|
@@ -35363,7 +35433,7 @@ var ptzAutotrackCapability = {
|
|
|
35363
35433
|
* - **Wrapper**: the emulated implementation (`@camstack/system`'s `autotrack` builtin),
|
|
35364
35434
|
* which follows a subject with `ptz.move` pulses driven by the detection
|
|
35365
35435
|
* pipeline. `defaultActive: false` — the operator binds it per camera in
|
|
35366
|
-
* the bindings UI
|
|
35436
|
+
* the bindings UI.
|
|
35367
35437
|
*
|
|
35368
35438
|
* The binding chooses WHICH implementation answers; `setEnabled` stays the
|
|
35369
35439
|
* one switch for the function either way. A camera never has both driving its
|
|
@@ -40765,13 +40835,14 @@ var BaseDeviceProvider = class extends BaseAddon {
|
|
|
40765
40835
|
async getStatus() {
|
|
40766
40836
|
const all = await this.ctx.kernel.devices?.getAll() ?? [];
|
|
40767
40837
|
const summary = this.restoreFailureSummary();
|
|
40768
|
-
|
|
40838
|
+
const base = {
|
|
40769
40839
|
connected: true,
|
|
40770
|
-
deviceCount: all.length
|
|
40840
|
+
deviceCount: all.length,
|
|
40841
|
+
motionSliceWriter: "orchestrator"
|
|
40771
40842
|
};
|
|
40843
|
+
if (summary === null) return base;
|
|
40772
40844
|
return {
|
|
40773
|
-
|
|
40774
|
-
deviceCount: all.length,
|
|
40845
|
+
...base,
|
|
40775
40846
|
error: summary
|
|
40776
40847
|
};
|
|
40777
40848
|
}
|
|
@@ -46413,6 +46484,12 @@ Object.freeze({
|
|
|
46413
46484
|
addonId: null,
|
|
46414
46485
|
access: "view"
|
|
46415
46486
|
},
|
|
46487
|
+
"notificationRules.getAlarmSensors": {
|
|
46488
|
+
capName: "notification-rules",
|
|
46489
|
+
capScope: "system",
|
|
46490
|
+
addonId: null,
|
|
46491
|
+
access: "view"
|
|
46492
|
+
},
|
|
46416
46493
|
"notificationRules.getClipRetentionAsks": {
|
|
46417
46494
|
capName: "notification-rules",
|
|
46418
46495
|
capScope: "system",
|
|
@@ -50896,6 +50973,11 @@ Object.freeze({
|
|
|
50896
50973
|
form: "single",
|
|
50897
50974
|
optional: false
|
|
50898
50975
|
}],
|
|
50976
|
+
"notificationRules.getAlarmSensors": [{
|
|
50977
|
+
name: "deviceIds",
|
|
50978
|
+
form: "array",
|
|
50979
|
+
optional: false
|
|
50980
|
+
}],
|
|
50899
50981
|
"notificationRules.setDeviceMuted": [{
|
|
50900
50982
|
name: "deviceId",
|
|
50901
50983
|
form: "single",
|
|
@@ -52831,6 +52913,25 @@ TimelapseRuleInputSchema.extend({
|
|
|
52831
52913
|
createdAt: number(),
|
|
52832
52914
|
updatedAt: number()
|
|
52833
52915
|
});
|
|
52916
|
+
async function readHubMotionDetected(deviceState, deviceId) {
|
|
52917
|
+
let slice;
|
|
52918
|
+
try {
|
|
52919
|
+
slice = await deviceState.getCapSlice.query({
|
|
52920
|
+
deviceId,
|
|
52921
|
+
capName: "motion"
|
|
52922
|
+
});
|
|
52923
|
+
} catch (err) {
|
|
52924
|
+
const reason = err instanceof Error ? err.message : String(err);
|
|
52925
|
+
throw new Error(`motion state of device ${deviceId} is unreadable: ${reason}`, { cause: err });
|
|
52926
|
+
}
|
|
52927
|
+
const detected = slice?.["detected"];
|
|
52928
|
+
if (typeof detected !== "boolean") throw new Error(`motion state of device ${deviceId} is not published yet (unknown)`);
|
|
52929
|
+
return detected;
|
|
52930
|
+
}
|
|
52931
|
+
MotionOnMotionChangedDataSchema.omit({ triggeredBy: true });
|
|
52932
|
+
function providerMotionEvent(source, data) {
|
|
52933
|
+
return createEvent(EventCategory.ProviderMotion, source, data);
|
|
52934
|
+
}
|
|
52834
52935
|
object({
|
|
52835
52936
|
/**
|
|
52836
52937
|
* Fraction of the box's own size added on EACH side before cutting.
|
|
@@ -55200,17 +55301,14 @@ var AmcrestCamera = class AmcrestCamera extends BaseDevice {
|
|
|
55200
55301
|
}
|
|
55201
55302
|
/** Register the native `motion` cap. Its mere presence (a `kind:'native'`
|
|
55202
55303
|
* `motion` binding) is what the orchestrator reads to default a
|
|
55203
|
-
* freshly-adopted camera to `motionSources:['onboard']`.
|
|
55304
|
+
* freshly-adopted camera to `motionSources:['onboard']`. `isDetected`
|
|
55305
|
+
* answers from the HUB's slice; an unreadable hub, or a slice nobody has
|
|
55306
|
+
* published yet, fails the call — never `false` for unknown (D49, D393). */
|
|
55204
55307
|
registerMotionCap() {
|
|
55205
55308
|
this.ctx.registerNativeCap(motionCapability, { isDetected: async ({ deviceId }) => {
|
|
55206
55309
|
if (deviceId !== this.id) throw new Error(`AmcrestCamera: motion deviceId mismatch, expected ${this.id}, got ${deviceId}`);
|
|
55207
|
-
return this.
|
|
55310
|
+
return readHubMotionDetected(this.ctx.api.deviceState, this.id);
|
|
55208
55311
|
} });
|
|
55209
|
-
this.setCapSlice(motionCapability, {
|
|
55210
|
-
detected: false,
|
|
55211
|
-
lastDetectedAt: null,
|
|
55212
|
-
autoClearAfterMs: null
|
|
55213
|
-
});
|
|
55214
55312
|
this.ctx.logger.info("amcrest: motion cap registered", { tags: { deviceId: this.id } });
|
|
55215
55313
|
}
|
|
55216
55314
|
/** Reusable event-source identity for every emit on this device. */
|
|
@@ -55222,10 +55320,12 @@ var AmcrestCamera = class AmcrestCamera extends BaseDevice {
|
|
|
55222
55320
|
deviceId: this.id
|
|
55223
55321
|
};
|
|
55224
55322
|
}
|
|
55225
|
-
/** Emit
|
|
55226
|
-
*
|
|
55323
|
+
/** Emit the camera's RAW onboard motion transition (`ProviderMotion`,
|
|
55324
|
+
* D673). The orchestrator's camera-sensors bridge re-emits it as
|
|
55325
|
+
* `MotionOnMotionChanged`; the runner subscribes to that and drives its
|
|
55326
|
+
* phase machine via `reportMotion`. */
|
|
55227
55327
|
emitMotion(detected, timestamp) {
|
|
55228
|
-
this.ctx.eventBus.emit(
|
|
55328
|
+
this.ctx.eventBus.emit(providerMotionEvent(this.eventSource(), {
|
|
55229
55329
|
deviceId: this.id,
|
|
55230
55330
|
detected,
|
|
55231
55331
|
timestamp,
|
|
@@ -55320,14 +55420,7 @@ var AmcrestCamera = class AmcrestCamera extends BaseDevice {
|
|
|
55320
55420
|
const wasActive = this.motionActive;
|
|
55321
55421
|
this.motionActive = true;
|
|
55322
55422
|
this.emitMotion(true, now);
|
|
55323
|
-
if (!wasActive) {
|
|
55324
|
-
this.setCapSlice(motionCapability, {
|
|
55325
|
-
detected: true,
|
|
55326
|
-
lastDetectedAt: now,
|
|
55327
|
-
autoClearAfterMs: MOTION_CLOSE_AFTER_MS
|
|
55328
|
-
});
|
|
55329
|
-
this.ctx.logger.info("amcrest: onboard motion started", { tags: { deviceId: this.id } });
|
|
55330
|
-
}
|
|
55423
|
+
if (!wasActive) this.ctx.logger.info("amcrest: onboard motion started", { tags: { deviceId: this.id } });
|
|
55331
55424
|
if (this.motionKeepaliveTimer === null) this.motionKeepaliveTimer = setInterval(() => {
|
|
55332
55425
|
this.emitMotion(true, Date.now());
|
|
55333
55426
|
}, AmcrestCamera.MOTION_KEEPALIVE_MS);
|
|
@@ -55337,12 +55430,6 @@ var AmcrestCamera = class AmcrestCamera extends BaseDevice {
|
|
|
55337
55430
|
if (!this.motionActive) return;
|
|
55338
55431
|
this.motionActive = false;
|
|
55339
55432
|
this.emitMotion(false, now);
|
|
55340
|
-
const lastDetectedAt = this.getCapSlice(motionCapability)?.lastDetectedAt ?? now;
|
|
55341
|
-
this.setCapSlice(motionCapability, {
|
|
55342
|
-
detected: false,
|
|
55343
|
-
lastDetectedAt,
|
|
55344
|
-
autoClearAfterMs: null
|
|
55345
|
-
});
|
|
55346
55433
|
this.ctx.logger.info("amcrest: onboard motion stopped", { tags: { deviceId: this.id } });
|
|
55347
55434
|
}
|
|
55348
55435
|
clearMotionKeepalive() {
|