@camstack/addon-post-analysis 1.2.665 → 1.2.667
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/CHANGELOG.md +21 -0
- package/dist/{clip-model-registry-nn_STiGt.js → clip-model-registry-D7cqhFK3.js} +758 -486
- package/dist/{clip-model-registry-DZTzG2p4.mjs → clip-model-registry-DpzRS8q3.mjs} +758 -486
- package/dist/embedding-encoder/index.js +1 -1
- package/dist/embedding-encoder/index.mjs +1 -1
- package/dist/pipeline-analytics/_stub.js +2 -2
- package/dist/pipeline-analytics/{_virtual_mf-localSharedImportMap___mfe_internal__addon_pipeline_analytics_widgets-Hy74pMMo.mjs → _virtual_mf-localSharedImportMap___mfe_internal__addon_pipeline_analytics_widgets-DmTIEp0m.mjs} +2 -2
- package/dist/pipeline-analytics/_virtual_mf___mfe_internal__addon_pipeline_analytics_widgets__loadShare___mf_0_camstack_mf_1_types__loadShare__.js-DAAkZRnK.mjs +26 -0
- package/dist/pipeline-analytics/_virtual_mf___mfe_internal__addon_pipeline_analytics_widgets__loadShare___mf_0_camstack_mf_1_ui_mf_2_library__loadShare__.js-znrgUrN1.mjs +26 -0
- package/dist/pipeline-analytics/{hostInit-BAYLsfoZ.mjs → hostInit-ODebWXu_.mjs} +2 -2
- package/dist/pipeline-analytics/index.js +563 -122
- package/dist/pipeline-analytics/index.mjs +563 -122
- package/dist/pipeline-analytics/remoteEntry.js +1 -1
- package/package.json +2 -2
- package/dist/pipeline-analytics/_virtual_mf___mfe_internal__addon_pipeline_analytics_widgets__loadShare___mf_0_camstack_mf_1_types__loadShare__.js-BXTAZYaJ.mjs +0 -26
- package/dist/pipeline-analytics/_virtual_mf___mfe_internal__addon_pipeline_analytics_widgets__loadShare___mf_0_camstack_mf_1_ui_mf_2_library__loadShare__.js-Br6eWzp_.mjs +0 -26
|
@@ -2,7 +2,7 @@ Object.defineProperties(exports, {
|
|
|
2
2
|
__esModule: { value: true },
|
|
3
3
|
[Symbol.toStringTag]: { value: "Module" }
|
|
4
4
|
});
|
|
5
|
-
const require_clip_model_registry = require("../clip-model-registry-
|
|
5
|
+
const require_clip_model_registry = require("../clip-model-registry-D7cqhFK3.js");
|
|
6
6
|
let node_fs = require("node:fs");
|
|
7
7
|
node_fs = require_clip_model_registry.__toESM(node_fs, 1);
|
|
8
8
|
let node_path = require("node:path");
|
|
@@ -7354,6 +7354,18 @@ function sensorEdgeOf(kind, value) {
|
|
|
7354
7354
|
if (HALTED.has(opening.state)) return "neither";
|
|
7355
7355
|
if (OPENING.has(opening.state)) return "opening";
|
|
7356
7356
|
}
|
|
7357
|
+
/**
|
|
7358
|
+
* The WORD a sensor change is told with — the state the device moved to, read off
|
|
7359
|
+
* the same slice the edge is: `unlocked`, `open`, `closed`… for a lock, cover, contact
|
|
7360
|
+
* or switch, else `raised` / `cleared` for a boolean sensor (water, smoke…). Undefined
|
|
7361
|
+
* when the slice cannot say: a sentence never invents a state (D393).
|
|
7362
|
+
*/
|
|
7363
|
+
function sensorStateWordOf(kind, value, raised) {
|
|
7364
|
+
const opening = alarmOpeningFromSensorEvent(kind, value);
|
|
7365
|
+
if (opening.kind === "reading") return opening.state;
|
|
7366
|
+
if (raised === void 0) return void 0;
|
|
7367
|
+
return raised ? "raised" : "cleared";
|
|
7368
|
+
}
|
|
7357
7369
|
//#endregion
|
|
7358
7370
|
//#region src/notification-center/audio-rule-matcher.ts
|
|
7359
7371
|
/**
|
|
@@ -7861,6 +7873,7 @@ function subjectFromSensorEvent(ev, markerTrackId, linked) {
|
|
|
7861
7873
|
const eventType = readSensorEventType(ev.value);
|
|
7862
7874
|
const sensorEdge = sensorEdgeOf(ev.kind, ev.value);
|
|
7863
7875
|
const sensorRaised = require_clip_model_registry.sensorRaisedOf(ev.kind, ev.value);
|
|
7876
|
+
const sensorState = sensorStateWordOf(ev.kind, ev.value, sensorRaised);
|
|
7864
7877
|
return {
|
|
7865
7878
|
kind: "device-event",
|
|
7866
7879
|
recordId: String(ev.id),
|
|
@@ -7875,6 +7888,7 @@ function subjectFromSensorEvent(ev, markerTrackId, linked) {
|
|
|
7875
7888
|
...eventType !== void 0 ? { eventType } : {},
|
|
7876
7889
|
...sensorEdge !== void 0 ? { sensorEdge } : {},
|
|
7877
7890
|
...sensorRaised !== void 0 ? { sensorRaised } : {},
|
|
7891
|
+
...sensorState !== void 0 ? { sensorState } : {},
|
|
7878
7892
|
...linked !== void 0 ? {
|
|
7879
7893
|
linkedSensorId: ev.sourceDeviceId,
|
|
7880
7894
|
linkedCameraIds: [...linked.linkedCameraIds],
|
|
@@ -9213,6 +9227,17 @@ var en_default = {
|
|
|
9213
9227
|
"alarm.combined.title": "Alarm: {{deviceName}}",
|
|
9214
9228
|
"alarm.intrusion.title": "Intrusion — {{locations}}",
|
|
9215
9229
|
"alarm.intrusion.body": "Intrusion: {{locations}}",
|
|
9230
|
+
"alarm.intrusion.body.withChanges": "Intrusion: {{locations}} — {{changes}}",
|
|
9231
|
+
"alarm.change.lock.unlocked": "{{device}} unlocked",
|
|
9232
|
+
"alarm.change.lock.unlocking": "{{device}} unlocking",
|
|
9233
|
+
"alarm.change.lock.jammed": "{{device}} jammed",
|
|
9234
|
+
"alarm.change.lock.door-open": "{{device}} open",
|
|
9235
|
+
"alarm.change.open": "{{device}} open",
|
|
9236
|
+
"alarm.change.opening": "{{device}} opening",
|
|
9237
|
+
"alarm.change.closed": "{{device}} closed",
|
|
9238
|
+
"alarm.change.on": "{{device}} on",
|
|
9239
|
+
"alarm.change.raised": "{{device}} triggered",
|
|
9240
|
+
"alarm.change.cleared": "{{device}} cleared",
|
|
9216
9241
|
"system.alarm-arm-refused.title.home": "Arming home refused",
|
|
9217
9242
|
"system.alarm-arm-refused.title.away": "Arming away refused",
|
|
9218
9243
|
"system.alarm-arm-refused.title.night": "Arming night refused",
|
|
@@ -9630,6 +9655,17 @@ var it_default = {
|
|
|
9630
9655
|
"alarm.combined.title": "Allarme: {{deviceName}}",
|
|
9631
9656
|
"alarm.intrusion.title": "Intrusione — {{locations}}",
|
|
9632
9657
|
"alarm.intrusion.body": "Intrusione: {{locations}}",
|
|
9658
|
+
"alarm.intrusion.body.withChanges": "Intrusione: {{locations}} — {{changes}}",
|
|
9659
|
+
"alarm.change.lock.unlocked": "{{device}} aperta",
|
|
9660
|
+
"alarm.change.lock.unlocking": "{{device}} in apertura",
|
|
9661
|
+
"alarm.change.lock.jammed": "{{device}} bloccata",
|
|
9662
|
+
"alarm.change.lock.door-open": "{{device}} aperta",
|
|
9663
|
+
"alarm.change.open": "{{device}} aperto",
|
|
9664
|
+
"alarm.change.opening": "{{device}} in apertura",
|
|
9665
|
+
"alarm.change.closed": "{{device}} chiuso",
|
|
9666
|
+
"alarm.change.on": "{{device}} acceso",
|
|
9667
|
+
"alarm.change.raised": "{{device}} attivato",
|
|
9668
|
+
"alarm.change.cleared": "{{device}} ripristinato",
|
|
9633
9669
|
"system.alarm-arm-refused.title.home": "Inserimento Casa rifiutato",
|
|
9634
9670
|
"system.alarm-arm-refused.title.away": "Inserimento Fuori rifiutato",
|
|
9635
9671
|
"system.alarm-arm-refused.title.night": "Inserimento Notte rifiutato",
|
|
@@ -59718,6 +59754,140 @@ async function promoteWithStill(input) {
|
|
|
59718
59754
|
});
|
|
59719
59755
|
}
|
|
59720
59756
|
//#endregion
|
|
59757
|
+
//#region src/pipeline-analytics/pipeline/key-frame-persist.ts
|
|
59758
|
+
/**
|
|
59759
|
+
* A box is written only when it can be USED — a degenerate rectangle, or one
|
|
59760
|
+
* measured on a degenerate frame, says nothing and would read back as a box at
|
|
59761
|
+
* the origin. Absent is the honest answer; the reader then pairs a snapshot,
|
|
59762
|
+
* exactly as it did before D510.
|
|
59763
|
+
*
|
|
59764
|
+
* Its result is spread into BOTH writes: the small companion is derived from
|
|
59765
|
+
* the very bytes of the native one, so it is the same scene at the same
|
|
59766
|
+
* instant and the box maps into it by ratio like any other scale of the frame.
|
|
59767
|
+
* A box on one row and not the other would make the summary's answer depend on
|
|
59768
|
+
* which row a reader happened to pick.
|
|
59769
|
+
*/
|
|
59770
|
+
function isStatableSubject(subject) {
|
|
59771
|
+
if (subject === void 0) return false;
|
|
59772
|
+
return subject.w > 0 && subject.h > 0 && subject.frameWidth > 0 && subject.frameHeight > 0;
|
|
59773
|
+
}
|
|
59774
|
+
/**
|
|
59775
|
+
* Read, derive and store one track's key frame + its web companion.
|
|
59776
|
+
*
|
|
59777
|
+
* PARCEL-OR-NOTHING: no parcel writes nothing and the caller's bounded retry
|
|
59778
|
+
* parks a later frame. The `keyFrame` write lands FIRST and its media id (D482)
|
|
59779
|
+
* is what the outcome carries — a `keyFrameSmall` failure afterwards must not
|
|
59780
|
+
* re-arm a retry that would re-shoot a key frame the rebuild can already read.
|
|
59781
|
+
*/
|
|
59782
|
+
async function persistTrackKeyFrame(deps, input) {
|
|
59783
|
+
const { deviceId, trackId, retry } = input;
|
|
59784
|
+
const drop = {
|
|
59785
|
+
tags: { deviceId },
|
|
59786
|
+
meta: {
|
|
59787
|
+
trackId,
|
|
59788
|
+
retry
|
|
59789
|
+
}
|
|
59790
|
+
};
|
|
59791
|
+
try {
|
|
59792
|
+
const parcel = await deps.fetchParkedKeyFrame(deviceId, trackId);
|
|
59793
|
+
if (parcel === null || parcel.jpeg.byteLength === 0) {
|
|
59794
|
+
deps.logger.error("key-frame parcel missing — nothing to write, retry parks a later frame", { ...drop });
|
|
59795
|
+
return { kind: "parcel-missing" };
|
|
59796
|
+
}
|
|
59797
|
+
const subjectBox = isStatableSubject(parcel.subject) ? { subjectBox: parcel.subject } : {};
|
|
59798
|
+
const keyFrameMediaId = await deps.media.putReplacing({
|
|
59799
|
+
deviceId,
|
|
59800
|
+
ownerKind: "track",
|
|
59801
|
+
ownerId: trackId,
|
|
59802
|
+
kind: "keyFrame",
|
|
59803
|
+
timestamp: parcel.timestamp,
|
|
59804
|
+
data: parcel.jpeg,
|
|
59805
|
+
...subjectBox
|
|
59806
|
+
});
|
|
59807
|
+
try {
|
|
59808
|
+
const keyFrameSmall = await deps.deriveSmall(parcel.jpeg);
|
|
59809
|
+
await deps.media.putReplacing({
|
|
59810
|
+
deviceId,
|
|
59811
|
+
ownerKind: "track",
|
|
59812
|
+
ownerId: trackId,
|
|
59813
|
+
kind: "keyFrameSmall",
|
|
59814
|
+
timestamp: parcel.timestamp,
|
|
59815
|
+
data: keyFrameSmall,
|
|
59816
|
+
...subjectBox
|
|
59817
|
+
});
|
|
59818
|
+
} catch (err) {
|
|
59819
|
+
deps.logger.warn("keyFrameSmall write failed — the native keyFrame is stored", {
|
|
59820
|
+
...drop,
|
|
59821
|
+
meta: {
|
|
59822
|
+
...drop.meta,
|
|
59823
|
+
nodeId: parcel.nodeId,
|
|
59824
|
+
error: require_clip_model_registry.errMsg(err)
|
|
59825
|
+
}
|
|
59826
|
+
});
|
|
59827
|
+
}
|
|
59828
|
+
deps.logger.debug("key frame written from the parked parcel", {
|
|
59829
|
+
...drop,
|
|
59830
|
+
meta: {
|
|
59831
|
+
...drop.meta,
|
|
59832
|
+
nodeId: parcel.nodeId,
|
|
59833
|
+
width: parcel.width,
|
|
59834
|
+
height: parcel.height,
|
|
59835
|
+
bytes: parcel.jpeg.byteLength,
|
|
59836
|
+
parkedTimestamp: parcel.timestamp
|
|
59837
|
+
}
|
|
59838
|
+
});
|
|
59839
|
+
return {
|
|
59840
|
+
kind: "landed",
|
|
59841
|
+
keyFrameMediaId
|
|
59842
|
+
};
|
|
59843
|
+
} catch (err) {
|
|
59844
|
+
const error = require_clip_model_registry.errMsg(err);
|
|
59845
|
+
deps.logger.warn("key-frame write failed", {
|
|
59846
|
+
...drop,
|
|
59847
|
+
meta: {
|
|
59848
|
+
...drop.meta,
|
|
59849
|
+
error
|
|
59850
|
+
}
|
|
59851
|
+
});
|
|
59852
|
+
return {
|
|
59853
|
+
kind: "error",
|
|
59854
|
+
error
|
|
59855
|
+
};
|
|
59856
|
+
}
|
|
59857
|
+
}
|
|
59858
|
+
/**
|
|
59859
|
+
* Write one parked frame under an OBJECT event (`object:<id>`), once.
|
|
59860
|
+
*
|
|
59861
|
+
* A package delivery has no track row owning a key frame, and the notification
|
|
59862
|
+
* ladder reads only `track:` and `object:` owners — so without this the
|
|
59863
|
+
* delivery ships text-only. `null` = no parcel to write (said at the caller).
|
|
59864
|
+
* `putReplacing` makes a repeat the same row, never a second one.
|
|
59865
|
+
*/
|
|
59866
|
+
async function persistObjectFrame(deps, input) {
|
|
59867
|
+
const { deviceId, trackId, objectId, timestamp } = input;
|
|
59868
|
+
const parcel = await deps.fetchParkedKeyFrame(deviceId, trackId);
|
|
59869
|
+
if (parcel === null || parcel.jpeg.byteLength === 0) {
|
|
59870
|
+
deps.logger.warn("object frame parcel missing — nothing to write", {
|
|
59871
|
+
tags: { deviceId },
|
|
59872
|
+
meta: {
|
|
59873
|
+
trackId,
|
|
59874
|
+
objectId
|
|
59875
|
+
}
|
|
59876
|
+
});
|
|
59877
|
+
return null;
|
|
59878
|
+
}
|
|
59879
|
+
const subjectBox = isStatableSubject(parcel.subject) ? { subjectBox: parcel.subject } : {};
|
|
59880
|
+
return deps.media.putReplacing({
|
|
59881
|
+
deviceId,
|
|
59882
|
+
ownerKind: "object",
|
|
59883
|
+
ownerId: objectId,
|
|
59884
|
+
kind: "keyFrame",
|
|
59885
|
+
timestamp,
|
|
59886
|
+
data: parcel.jpeg,
|
|
59887
|
+
...subjectBox
|
|
59888
|
+
});
|
|
59889
|
+
}
|
|
59890
|
+
//#endregion
|
|
59721
59891
|
//#region src/pipeline-analytics/pipeline/person-over-vehicle.ts
|
|
59722
59892
|
/**
|
|
59723
59893
|
* How much of the PERSON box must sit inside a vehicle box to be counted.
|
|
@@ -71125,6 +71295,12 @@ Object.freeze({
|
|
|
71125
71295
|
"idleStopMinutes": 30
|
|
71126
71296
|
} } }
|
|
71127
71297
|
},
|
|
71298
|
+
"media-player": {
|
|
71299
|
+
"channelDown": { "steps": 1 },
|
|
71300
|
+
"channelUp": { "steps": 1 },
|
|
71301
|
+
"volumeDown": { "steps": 1 },
|
|
71302
|
+
"volumeUp": { "steps": 1 }
|
|
71303
|
+
},
|
|
71128
71304
|
"mqtt-broker": { "startEmbeddedBroker": {
|
|
71129
71305
|
"port": 1883,
|
|
71130
71306
|
"allowAnonymous": false
|
|
@@ -71491,8 +71667,8 @@ Object.freeze({
|
|
|
71491
71667
|
* ## Caps with no boolean level pass through untouched
|
|
71492
71668
|
*
|
|
71493
71669
|
* `EVENT_KIND_BY_CAP` covers sixteen caps; `SOURCE_CAP_ACTIVE_FIELD` covers the
|
|
71494
|
-
* ten that have a boolean whose edge means something. `button`, `presence
|
|
71495
|
-
*
|
|
71670
|
+
* ten that have a boolean whose edge means something. `button`, `presence`
|
|
71671
|
+
* and `event-emitter` are NOT levels (`enum-sensor` is a stampless word level, see `UNSTAMPED_WORD_FIELD`) — their ARRIVAL is the
|
|
71496
71672
|
* event, and three presses in a row are three markers. Gating them would
|
|
71497
71673
|
* re-ship the failure of 2026-08-08, when camera 101's press produced a row, a
|
|
71498
71674
|
* ring, a matched rule and no marker. (`doorbell` is not a level either, but
|
|
@@ -71618,6 +71794,24 @@ var WORD_LEVEL_FIELD = {
|
|
|
71618
71794
|
* running for an hour at boot did not just start).
|
|
71619
71795
|
*/
|
|
71620
71796
|
var BOOLEAN_LEVEL_FIELD = { "running-state": "running" };
|
|
71797
|
+
/**
|
|
71798
|
+
* The device-history series key of a level cap's boolean — `cap:<cap>.<field>`,
|
|
71799
|
+
* the shape of `capSeriesKey` (system builtin, not importable from an addon;
|
|
71800
|
+
* the wiring spec pins the literal). `null` for a cap with no boolean level.
|
|
71801
|
+
*/
|
|
71802
|
+
function levelSeriesKey(capName) {
|
|
71803
|
+
const field = BOOLEAN_LEVEL_FIELD[capName];
|
|
71804
|
+
return field === void 0 ? null : `cap:${capName}.${field}`;
|
|
71805
|
+
}
|
|
71806
|
+
/**
|
|
71807
|
+
* Caps whose level is a WORD with NO change stamp: cap → field. `enum-sensor`
|
|
71808
|
+
* carries `{ value, lastFetchedAt }`, and `lastFetchedAt` moves on every
|
|
71809
|
+
* provider refresh, so it says nothing about when the word changed. Gated by
|
|
71810
|
+
* the word alone: the first sighting seeds, only a different word passes, and
|
|
71811
|
+
* a null / missing value is dropped and never remembered (planning#396: such
|
|
71812
|
+
* slices passed as `changed` many times a minute and could match an NC rule).
|
|
71813
|
+
*/
|
|
71814
|
+
var UNSTAMPED_WORD_FIELD = { "enum-sensor": "value" };
|
|
71621
71815
|
/** The word a word-level cap rests in — its `false`, for the first-sighting rule. */
|
|
71622
71816
|
var WORD_LEVEL_INACTIVE = {
|
|
71623
71817
|
cover: "closed",
|
|
@@ -71636,6 +71830,7 @@ var WORD_LEVEL_INACTIVE = {
|
|
|
71636
71830
|
* post-analysis runner, D28).
|
|
71637
71831
|
*/
|
|
71638
71832
|
var ADVANCE_FIELD = require_clip_model_registry.CAP_ADVANCE_FIELDS;
|
|
71833
|
+
var DEFAULT_SEED_WAIT_MS = 2e3;
|
|
71639
71834
|
/** Default drop-summary window: one line per device per ten minutes, at most. */
|
|
71640
71835
|
var DEFAULT_DROP_SUMMARY_WINDOW_MS = 6e5;
|
|
71641
71836
|
var SensorTransitionGate = class {
|
|
@@ -71647,6 +71842,14 @@ var SensorTransitionGate = class {
|
|
|
71647
71842
|
lastStamps = /* @__PURE__ */ new Map();
|
|
71648
71843
|
/** Drops since the window opened, keyed by `${deviceId}:${capName}:${reason}`. */
|
|
71649
71844
|
drops = /* @__PURE__ */ new Map();
|
|
71845
|
+
/** Seed reads started, by key; a settled one stays so a seed is read once. */
|
|
71846
|
+
seeds = /* @__PURE__ */ new Map();
|
|
71847
|
+
seedsSettled = /* @__PURE__ */ new Set();
|
|
71848
|
+
/** Seeds whose wait ran out: decided as unseeded from then on. */
|
|
71849
|
+
seedsAbandoned = /* @__PURE__ */ new Set();
|
|
71850
|
+
readSeed;
|
|
71851
|
+
onSeedFailed;
|
|
71852
|
+
seedWaitMs;
|
|
71650
71853
|
windowStartedAt;
|
|
71651
71854
|
startedAtMs;
|
|
71652
71855
|
now;
|
|
@@ -71658,6 +71861,55 @@ var SensorTransitionGate = class {
|
|
|
71658
71861
|
this.freshnessMs = options.firstSightingFreshnessMs ?? require_clip_model_registry.DEFAULT_FIRST_SIGHTING_FRESHNESS_MS;
|
|
71659
71862
|
this.summaryWindowMs = options.summaryWindowMs ?? DEFAULT_DROP_SUMMARY_WINDOW_MS;
|
|
71660
71863
|
this.windowStartedAt = options.now();
|
|
71864
|
+
this.readSeed = options.readSeed;
|
|
71865
|
+
this.onSeedFailed = options.onSeedFailed;
|
|
71866
|
+
this.seedWaitMs = options.seedWaitMs ?? DEFAULT_SEED_WAIT_MS;
|
|
71867
|
+
}
|
|
71868
|
+
/**
|
|
71869
|
+
* {@link observe}, plus: a level cap's first boolean `false` whose persisted
|
|
71870
|
+
* prior is still being read waits for that read, bounded by `seedWaitMs`, and
|
|
71871
|
+
* is decided again. This is the only wait on the path, and it is on a read
|
|
71872
|
+
* that was started at the FIRST sighting of the device (a null at boot, long
|
|
71873
|
+
* before the boolean) — never one begun by the event itself unless the
|
|
71874
|
+
* boolean really is the first thing seen. A read that fails or times out
|
|
71875
|
+
* decides as unseeded, i.e. exactly as before the seed existed (D49).
|
|
71876
|
+
*/
|
|
71877
|
+
async observeSettled(deviceId, capName, slice) {
|
|
71878
|
+
const first = this.observe(deviceId, capName, slice);
|
|
71879
|
+
if (first.changed || first.reason !== "seed-pending") return first;
|
|
71880
|
+
const key = `${deviceId}:${capName}`;
|
|
71881
|
+
const pending = this.seeds.get(key);
|
|
71882
|
+
if (pending !== void 0) {
|
|
71883
|
+
let timer;
|
|
71884
|
+
const timeout = new Promise((resolve) => {
|
|
71885
|
+
timer = setTimeout(resolve, this.seedWaitMs);
|
|
71886
|
+
});
|
|
71887
|
+
await Promise.race([pending, timeout]);
|
|
71888
|
+
if (timer !== void 0) clearTimeout(timer);
|
|
71889
|
+
if (!this.seedsSettled.has(key)) this.seedsAbandoned.add(key);
|
|
71890
|
+
}
|
|
71891
|
+
return this.observe(deviceId, capName, slice);
|
|
71892
|
+
}
|
|
71893
|
+
/** Start the one seed read of a level cap's key; never awaited here. */
|
|
71894
|
+
ensureSeed(deviceId, capName) {
|
|
71895
|
+
const read = this.readSeed;
|
|
71896
|
+
if (read === void 0) return;
|
|
71897
|
+
const key = `${deviceId}:${capName}`;
|
|
71898
|
+
if (this.seeds.has(key)) return;
|
|
71899
|
+
const settle = () => {
|
|
71900
|
+
this.seedsSettled.add(key);
|
|
71901
|
+
};
|
|
71902
|
+
const started = read(deviceId, capName).then((seed) => {
|
|
71903
|
+
if (seed !== null && !this.lastValues.has(key)) this.lastValues.set(key, seed.value);
|
|
71904
|
+
settle();
|
|
71905
|
+
}, (error) => {
|
|
71906
|
+
this.onSeedFailed?.(deviceId, capName, error);
|
|
71907
|
+
settle();
|
|
71908
|
+
});
|
|
71909
|
+
this.seeds.set(key, started);
|
|
71910
|
+
}
|
|
71911
|
+
seedPending(key) {
|
|
71912
|
+
return this.seeds.has(key) && !this.seedsSettled.has(key) && !this.seedsAbandoned.has(key);
|
|
71661
71913
|
}
|
|
71662
71914
|
/**
|
|
71663
71915
|
* Feed one `DeviceStateChanged` slice for a cap `EVENT_KIND_BY_CAP` maps.
|
|
@@ -71669,7 +71921,10 @@ var SensorTransitionGate = class {
|
|
|
71669
71921
|
* a boolean in either direction, or a word-level cap's word changing.
|
|
71670
71922
|
*/
|
|
71671
71923
|
observe(deviceId, capName, slice) {
|
|
71924
|
+
if (BOOLEAN_LEVEL_FIELD[capName] !== void 0) this.ensureSeed(deviceId, capName);
|
|
71672
71925
|
if (slice["lastChangedAt"] === 0) return this.drop(deviceId, capName, "cold-slice");
|
|
71926
|
+
const unstampedField = UNSTAMPED_WORD_FIELD[capName];
|
|
71927
|
+
if (unstampedField !== void 0) return this.observeUnstampedWord(deviceId, capName, slice, unstampedField);
|
|
71673
71928
|
const wordField = WORD_LEVEL_FIELD[capName];
|
|
71674
71929
|
if (wordField !== void 0) return this.observeWord(deviceId, capName, slice, wordField);
|
|
71675
71930
|
const levelField = BOOLEAN_LEVEL_FIELD[capName];
|
|
@@ -71722,6 +71977,10 @@ var SensorTransitionGate = class {
|
|
|
71722
71977
|
if (typeof raw !== "boolean") return this.drop(deviceId, capName, "non-boolean-value");
|
|
71723
71978
|
const key = `${deviceId}:${capName}`;
|
|
71724
71979
|
const prior = this.lastValues.get(key);
|
|
71980
|
+
if (prior === void 0 && !raw && this.seedPending(key)) return {
|
|
71981
|
+
changed: false,
|
|
71982
|
+
reason: "seed-pending"
|
|
71983
|
+
};
|
|
71725
71984
|
this.lastValues.set(key, raw);
|
|
71726
71985
|
if (prior === void 0) {
|
|
71727
71986
|
if (!raw) return this.drop(deviceId, capName, "baseline-seeded-inactive");
|
|
@@ -71740,11 +71999,25 @@ var SensorTransitionGate = class {
|
|
|
71740
71999
|
observeWord(deviceId, capName, slice, wordField) {
|
|
71741
72000
|
const raw = slice[wordField];
|
|
71742
72001
|
if (typeof raw !== "string" || raw.length === 0) return this.drop(deviceId, capName, "non-word-value");
|
|
72002
|
+
const stamp = slice["lastChangedAt"];
|
|
72003
|
+
if (typeof stamp !== "number" || !Number.isFinite(stamp) || stamp <= 0) return this.drop(deviceId, capName, "cold-slice");
|
|
71743
72004
|
const key = `${deviceId}:${capName}`;
|
|
71744
72005
|
const prior = this.lastWords.get(key);
|
|
71745
72006
|
this.lastWords.set(key, raw);
|
|
71746
72007
|
if (prior === void 0) return this.firstWordSighting(deviceId, capName, slice, raw);
|
|
71747
72008
|
if (prior === raw) return this.drop(deviceId, capName, "no-change");
|
|
72009
|
+
if (stamp < this.startedAtMs) return this.drop(deviceId, capName, "baseline-seeded-stale-active");
|
|
72010
|
+
return { changed: true };
|
|
72011
|
+
}
|
|
72012
|
+
/** A word-level cap with no change stamp: only a DIFFERENT word is a transition. */
|
|
72013
|
+
observeUnstampedWord(deviceId, capName, slice, field) {
|
|
72014
|
+
const raw = slice[field];
|
|
72015
|
+
if (typeof raw !== "string" || raw.length === 0) return this.drop(deviceId, capName, "non-word-value");
|
|
72016
|
+
const key = `${deviceId}:${capName}`;
|
|
72017
|
+
const prior = this.lastWords.get(key);
|
|
72018
|
+
this.lastWords.set(key, raw);
|
|
72019
|
+
if (prior === void 0) return this.drop(deviceId, capName, "baseline-seeded-inactive");
|
|
72020
|
+
if (prior === raw) return this.drop(deviceId, capName, "no-change");
|
|
71748
72021
|
return { changed: true };
|
|
71749
72022
|
}
|
|
71750
72023
|
/**
|
|
@@ -81820,11 +82093,11 @@ function unique(values) {
|
|
|
81820
82093
|
* therefore keeps the alarm's own sentence, which is also what every row
|
|
81821
82094
|
* enqueued before this deploy carries.
|
|
81822
82095
|
*/
|
|
81823
|
-
function combinedAlarmText(kind, source) {
|
|
82096
|
+
function combinedAlarmText(kind, source, followers = []) {
|
|
81824
82097
|
if (kind !== "alarm-triggered" || source === void 0) return null;
|
|
81825
82098
|
return {
|
|
81826
82099
|
titleKey: source.className !== void 0 ? "alarm.combined.title" : "system.alarm-triggered.title",
|
|
81827
|
-
bodyKey: "alarm.intrusion.body"
|
|
82100
|
+
bodyKey: [source, ...followers].some((ref) => ref.sensorChange !== void 0) ? "alarm.intrusion.body.withChanges" : "alarm.intrusion.body"
|
|
81828
82101
|
};
|
|
81829
82102
|
}
|
|
81830
82103
|
/**
|
|
@@ -81839,7 +82112,7 @@ function combinedAlarmText(kind, source) {
|
|
|
81839
82112
|
*/
|
|
81840
82113
|
function alarmTextKeys(transition, body) {
|
|
81841
82114
|
const { kind, source } = transition;
|
|
81842
|
-
const combined = combinedAlarmText(kind, source);
|
|
82115
|
+
const combined = combinedAlarmText(kind, source, transition.followers);
|
|
81843
82116
|
if (combined !== null) return combined;
|
|
81844
82117
|
const baseTitle = transition.mode !== void 0 ? `system.${kind}.title.${transition.mode}` : `system.${kind}.title`;
|
|
81845
82118
|
const titleKey = body?.titleKeySuffix !== void 0 ? `${baseTitle}.${body.titleKeySuffix}` : baseTitle;
|
|
@@ -85546,6 +85819,15 @@ function isLinkedStillId(id) {
|
|
|
85546
85819
|
var NC_LINKED_WAKE_CAPTURE_TIMEOUT_MS = 15e3;
|
|
85547
85820
|
var NC_LINKED_WAKE_POLL_MS = 1e3;
|
|
85548
85821
|
var NC_LINKED_WAKE_MIN_GAP_MS = 6e4;
|
|
85822
|
+
/**
|
|
85823
|
+
* D60 — a tile the freshness read calls stale is RE-POLLED, unforced, until the
|
|
85824
|
+
* wrapper hands back a frame from the event or this bound is spent. The wrapper
|
|
85825
|
+
* settles a soft miss with its old cached hit; one more ask, a moment later,
|
|
85826
|
+
* usually finds the refreshed one. The bound sits inside the dispatcher's still
|
|
85827
|
+
* deadline (`NC_STILL_WAIT_MS`, 3 s): the row waits for it through `pendingUntil`.
|
|
85828
|
+
*/
|
|
85829
|
+
var NC_LINKED_STALE_POLL_MS = 500;
|
|
85830
|
+
var NC_LINKED_STALE_MAX_POLLS = 5;
|
|
85549
85831
|
/** Per camera — the marker's own bound (`SENSOR_MARKER_SNAPSHOT_TIMEOUT_MS`). */
|
|
85550
85832
|
var SENSOR_LINKED_CAPTURE_TIMEOUT_MS = 2500;
|
|
85551
85833
|
/** The capture-free wrapper-state read, per camera. It runs IN PARALLEL with
|
|
@@ -85555,6 +85837,8 @@ var SENSOR_LINKED_CAPTURE_TIMEOUT_MS = 2500;
|
|
|
85555
85837
|
* photograph). A read that misses the bound or fails is UNKNOWN and keeps the
|
|
85556
85838
|
* tile (D49). */
|
|
85557
85839
|
var SENSOR_LINKED_STATE_READ_TIMEOUT_MS = SENSOR_LINKED_CAPTURE_TIMEOUT_MS;
|
|
85840
|
+
/** The battery-slice read behind the unknown-state wake: cheap, and bounded. */
|
|
85841
|
+
var SENSOR_LINKED_BATTERY_READ_TIMEOUT_MS = 1e3;
|
|
85558
85842
|
/** The one cache-only freshness read for the set — one hop, after the
|
|
85559
85843
|
* captures. A read that misses it keeps every tile and says `freshness
|
|
85560
85844
|
* unverified` (D49). */
|
|
@@ -85660,13 +85944,13 @@ var NcLinkedStillShelf = class {
|
|
|
85660
85944
|
startedAt,
|
|
85661
85945
|
options
|
|
85662
85946
|
};
|
|
85663
|
-
let
|
|
85947
|
+
let judged;
|
|
85664
85948
|
try {
|
|
85665
|
-
results = await Promise.all(stills.cameraIds.map((id) => this.captureOne(id, ctx)));
|
|
85949
|
+
const results = await Promise.all(stills.cameraIds.map((id) => this.captureOne(id, ctx)));
|
|
85950
|
+
judged = await this.repollStale(ctx, atMs, await this.judgeFreshness(stills.sensorId, results, atMs));
|
|
85666
85951
|
} finally {
|
|
85667
85952
|
this.pendingWakes.delete(setId);
|
|
85668
85953
|
}
|
|
85669
|
-
const judged = await this.judgeFreshness(stills.sensorId, results, atMs);
|
|
85670
85954
|
for (const r of judged) this.logTile(stills.sensorId, r);
|
|
85671
85955
|
const landed = judged.filter(isLandedTile);
|
|
85672
85956
|
const composed = await this.compose(stills.sensorId, landed, atMs);
|
|
@@ -85713,6 +85997,16 @@ var NcLinkedStillShelf = class {
|
|
|
85713
85997
|
if (verdict.reason === "waking" && ctx.options.wakeSleeping) return this.awaitWaking(cameraId, ctx);
|
|
85714
85998
|
if (verdict.reason === "unreachable" && ctx.options.wakeSleeping) return excluded("wake-skipped:unreachable");
|
|
85715
85999
|
if (verdict.reason !== null) return excluded(verdict.reason);
|
|
86000
|
+
if (verdict.unknown !== null && capture.outcome === "empty" && ctx.options.wakeSleeping && await this.isBatteryCamera(cameraId)) {
|
|
86001
|
+
this.deps.logger.info("linked camera state unknown and capture empty — waking as a battery camera", {
|
|
86002
|
+
tags: { deviceId: cameraId },
|
|
86003
|
+
meta: {
|
|
86004
|
+
sensorId: ctx.sensorId,
|
|
86005
|
+
stateUnknown: verdict.unknown
|
|
86006
|
+
}
|
|
86007
|
+
});
|
|
86008
|
+
return this.wakeOne(cameraId, ctx);
|
|
86009
|
+
}
|
|
85716
86010
|
return {
|
|
85717
86011
|
cameraId,
|
|
85718
86012
|
...capture,
|
|
@@ -85720,6 +86014,20 @@ var NcLinkedStillShelf = class {
|
|
|
85720
86014
|
tookMs: took()
|
|
85721
86015
|
};
|
|
85722
86016
|
}
|
|
86017
|
+
/** Only a camera KNOWN to be a battery camera is woken on an unknown state. */
|
|
86018
|
+
async isBatteryCamera(cameraId) {
|
|
86019
|
+
const read = this.deps.isBatteryCamera;
|
|
86020
|
+
if (read === void 0) return false;
|
|
86021
|
+
try {
|
|
86022
|
+
return await withTimeout$2(read(cameraId), SENSOR_LINKED_BATTERY_READ_TIMEOUT_MS, "battery read") === true;
|
|
86023
|
+
} catch (err) {
|
|
86024
|
+
this.deps.logger.warn("linked camera battery read failed — not woken on an unknown state", {
|
|
86025
|
+
tags: { deviceId: cameraId },
|
|
86026
|
+
meta: { error: String(err) }
|
|
86027
|
+
});
|
|
86028
|
+
return false;
|
|
86029
|
+
}
|
|
86030
|
+
}
|
|
85723
86031
|
/** D998: one forced capture, by the rule's leave, at most once per gap per camera. */
|
|
85724
86032
|
async wakeOne(cameraId, ctx) {
|
|
85725
86033
|
const took = () => this.deps.now() - ctx.startedAt;
|
|
@@ -85781,8 +86089,46 @@ var NcLinkedStillShelf = class {
|
|
|
85781
86089
|
};
|
|
85782
86090
|
}
|
|
85783
86091
|
markPending(ctx) {
|
|
85784
|
-
|
|
85785
|
-
|
|
86092
|
+
this.markPendingUntil(ctx.setId, ctx.startedAt + NC_LINKED_WAKE_CAPTURE_TIMEOUT_MS + NC_LINKED_WAKE_POLL_MS);
|
|
86093
|
+
}
|
|
86094
|
+
markPendingUntil(setId, until) {
|
|
86095
|
+
this.pendingWakes.set(setId, Math.max(this.pendingWakes.get(setId) ?? 0, until));
|
|
86096
|
+
}
|
|
86097
|
+
/**
|
|
86098
|
+
* D60: a `stale` tile asks again (unforced — a poll never wakes) until its
|
|
86099
|
+
* frame is from the event or the bound is spent; the row's still-wait is held
|
|
86100
|
+
* open meanwhile. A camera that never freshens stays `stale`, said on its line.
|
|
86101
|
+
*/
|
|
86102
|
+
async repollStale(ctx, atMs, judged) {
|
|
86103
|
+
let current = judged;
|
|
86104
|
+
if (!current.some((r) => r.outcome === "stale")) return current;
|
|
86105
|
+
this.markPendingUntil(ctx.setId, this.deps.now() + NC_LINKED_STALE_POLL_MS * 6);
|
|
86106
|
+
for (let i = 0; i < NC_LINKED_STALE_MAX_POLLS; i++) {
|
|
86107
|
+
const waiting = current.filter((r) => r.outcome === "stale").map((r) => r.cameraId);
|
|
86108
|
+
if (waiting.length === 0) break;
|
|
86109
|
+
await new Promise((resolve) => setTimeout(resolve, NC_LINKED_STALE_POLL_MS));
|
|
86110
|
+
const landedAgain = (await Promise.all(waiting.map(async (cameraId) => {
|
|
86111
|
+
const capture = await this.captureStill(cameraId, false, SENSOR_LINKED_CAPTURE_TIMEOUT_MS);
|
|
86112
|
+
return capture.outcome === "landed" && capture.jpeg !== void 0 ? {
|
|
86113
|
+
cameraId,
|
|
86114
|
+
outcome: "landed",
|
|
86115
|
+
jpeg: capture.jpeg,
|
|
86116
|
+
tookMs: this.deps.now() - ctx.startedAt
|
|
86117
|
+
} : null;
|
|
86118
|
+
}))).filter((r) => r !== null);
|
|
86119
|
+
if (landedAgain.length === 0) continue;
|
|
86120
|
+
const rejudged = await this.judgeFreshness(ctx.sensorId, landedAgain, atMs);
|
|
86121
|
+
const fresh = new Map(rejudged.filter((r) => r.outcome === "landed").map((r) => [r.cameraId, r]));
|
|
86122
|
+
current = current.map((r) => fresh.get(r.cameraId) ?? r);
|
|
86123
|
+
for (const cameraId of fresh.keys()) this.deps.logger.info("linked camera frame was stale — a re-poll brought one from the event", {
|
|
86124
|
+
tags: { deviceId: cameraId },
|
|
86125
|
+
meta: {
|
|
86126
|
+
sensorId: ctx.sensorId,
|
|
86127
|
+
polls: i + 1
|
|
86128
|
+
}
|
|
86129
|
+
});
|
|
86130
|
+
}
|
|
86131
|
+
return current;
|
|
85786
86132
|
}
|
|
85787
86133
|
/** A wake that brought nothing back names itself, not the generic `empty`. */
|
|
85788
86134
|
asWakeResult(capture) {
|
|
@@ -85851,9 +86197,17 @@ var NcLinkedStillShelf = class {
|
|
|
85851
86197
|
unknown: null
|
|
85852
86198
|
};
|
|
85853
86199
|
} catch (err) {
|
|
86200
|
+
const timedOut = err instanceof LinkedReadTimeout;
|
|
86201
|
+
this.deps.logger.warn("linked camera snapshot state read failed — state unknown", {
|
|
86202
|
+
tags: { deviceId: cameraId },
|
|
86203
|
+
meta: {
|
|
86204
|
+
error: String(err),
|
|
86205
|
+
timedOut
|
|
86206
|
+
}
|
|
86207
|
+
});
|
|
85854
86208
|
return {
|
|
85855
86209
|
reason: null,
|
|
85856
|
-
unknown:
|
|
86210
|
+
unknown: timedOut ? "timeout" : "error"
|
|
85857
86211
|
};
|
|
85858
86212
|
}
|
|
85859
86213
|
}
|
|
@@ -89663,10 +90017,31 @@ function buildTemplateVars(texts, entry, deviceName, zoneLabels, timeZone, cycle
|
|
|
89663
90017
|
latestVersion: typeof systemEvent?.data["latestVersion"] === "string" ? systemEvent.data["latestVersion"] : "",
|
|
89664
90018
|
...systemEvent?.deviceId !== void 0 ? { deviceName: String(systemEvent.deviceId) } : {},
|
|
89665
90019
|
...systemEvent?.nodeId !== void 0 && (systemEvent.kind === "node-online" || systemEvent.kind === "node-offline") ? { nodeName: systemEvent.nodeId } : {},
|
|
90020
|
+
...systemEvent?.alarmSource !== void 0 ? { changes: alarmChangesText(texts, systemEvent.alarmSource, systemEvent.alarmFollowers) } : {},
|
|
89666
90021
|
...systemEventParamsForTemplate(systemEvent?.textParams)
|
|
89667
90022
|
};
|
|
89668
90023
|
}
|
|
89669
90024
|
/**
|
|
90025
|
+
* `{{changes}}` of the combined alarm body: what each sensor of the incident did, in
|
|
90026
|
+
* arrival order and one phrase per device — "Laundry lock unlocked". The phrase is a
|
|
90027
|
+
* catalog key per kind and state (`alarm.change.<kind>.<state>`), then per state
|
|
90028
|
+
* (`alarm.change.<state>`), then the bare name: a state nobody worded never drops the device.
|
|
90029
|
+
*/
|
|
90030
|
+
function alarmChangesText(texts, source, followers) {
|
|
90031
|
+
const phrases = [];
|
|
90032
|
+
for (const ref of [source, ...followers ?? []]) {
|
|
90033
|
+
const change = ref.sensorChange;
|
|
90034
|
+
if (change === void 0) continue;
|
|
90035
|
+
const key = [`alarm.change.${change.kind}.${change.state}`, `alarm.change.${change.state}`].find((candidate) => texts.has(candidate));
|
|
90036
|
+
const phrase = key === void 0 ? change.name : texts.text({
|
|
90037
|
+
key,
|
|
90038
|
+
vars: { device: change.name }
|
|
90039
|
+
});
|
|
90040
|
+
if (!phrases.includes(phrase)) phrases.push(phrase);
|
|
90041
|
+
}
|
|
90042
|
+
return phrases.join(", ");
|
|
90043
|
+
}
|
|
90044
|
+
/**
|
|
89670
90045
|
* Which catalog key supplies the DEFAULT body for a row.
|
|
89671
90046
|
*
|
|
89672
90047
|
* A system event names its own at intake (the variant selection — profiled or
|
|
@@ -105952,7 +106327,7 @@ function groupedAlarmPhaseKey(ruleId, trackId) {
|
|
|
105952
106327
|
* combined alarm notification would then arrive with no picture, which is the
|
|
105953
106328
|
* one thing it exists to fix.
|
|
105954
106329
|
*/
|
|
105955
|
-
function fireRefFor(rule, subject, kind) {
|
|
106330
|
+
function fireRefFor(rule, subject, kind, sensorName) {
|
|
105956
106331
|
const hasEventMedia = kind === "object-event" || kind === "package-event";
|
|
105957
106332
|
const className = subject.classNames[0];
|
|
105958
106333
|
const linkedOwners = subject.trackId === void 0 && subject.linkedStills !== void 0 ? linkedMediaOwners(subject.linkedStills, linkedCameraMode(rule.media)) : void 0;
|
|
@@ -105962,6 +106337,11 @@ function fireRefFor(rule, subject, kind) {
|
|
|
105962
106337
|
ruleName: rule.name,
|
|
105963
106338
|
deviceId: subject.deviceId,
|
|
105964
106339
|
...subject.sourceDeviceId !== void 0 ? { sourceDeviceId: subject.sourceDeviceId } : {},
|
|
106340
|
+
...subject.sensorKind !== void 0 && subject.sensorState !== void 0 && sensorName !== void 0 ? { sensorChange: {
|
|
106341
|
+
name: sensorName,
|
|
106342
|
+
kind: subject.sensorKind,
|
|
106343
|
+
state: subject.sensorState
|
|
106344
|
+
} } : {},
|
|
105965
106345
|
recordId: subject.recordId,
|
|
105966
106346
|
...trackId !== void 0 ? { trackId } : {},
|
|
105967
106347
|
...linkedOwners !== void 0 && linkedOwners.extraStills.length > 0 ? { extraStills: linkedOwners.extraStills.map((x) => ({ ...x })) } : {},
|
|
@@ -105973,6 +106353,12 @@ function fireRefFor(rule, subject, kind) {
|
|
|
105973
106353
|
...subject.confidence !== void 0 ? { confidence: subject.confidence } : {}
|
|
105974
106354
|
};
|
|
105975
106355
|
}
|
|
106356
|
+
/** The directory name of the sensor a match was about; absent for a camera subject. */
|
|
106357
|
+
function sensorNameOf(host, subject) {
|
|
106358
|
+
if (subject.source !== "sensor") return void 0;
|
|
106359
|
+
const name = host.deviceDirectory.get(subject.sourceDeviceId ?? subject.deviceId)?.name.trim();
|
|
106360
|
+
return name !== void 0 && name.length > 0 ? name : void 0;
|
|
106361
|
+
}
|
|
105976
106362
|
/**
|
|
105977
106363
|
* Run this rule's onTrigger / onTriggerOff bindings for this match, and —
|
|
105978
106364
|
* when the rule is gated on `armed_*` — auto-trigger the CamStack
|
|
@@ -105994,7 +106380,7 @@ function runRuleActions(host, rule, subject, kind, controls) {
|
|
|
105994
106380
|
deviceId: subject.deviceId,
|
|
105995
106381
|
bindings,
|
|
105996
106382
|
lookup: (id) => host.sequences.get(id),
|
|
105997
|
-
origin: fireRefFor(rule, subject, kind)
|
|
106383
|
+
origin: fireRefFor(rule, subject, kind, sensorNameOf(host, subject))
|
|
105998
106384
|
});
|
|
105999
106385
|
return phase;
|
|
106000
106386
|
}
|
|
@@ -106018,7 +106404,7 @@ function autoTriggerAlarmPanel(host, rule, subject, kind) {
|
|
|
106018
106404
|
});
|
|
106019
106405
|
return NO_ALARM_PHASE;
|
|
106020
106406
|
}
|
|
106021
|
-
const origin = fireRefFor(rule, subject, kind);
|
|
106407
|
+
const origin = fireRefFor(rule, subject, kind, sensorNameOf(host, subject));
|
|
106022
106408
|
const phase = panel.classifyRuleMatch(origin);
|
|
106023
106409
|
try {
|
|
106024
106410
|
panel.triggerFromRule(origin);
|
|
@@ -107463,6 +107849,7 @@ var NotificationCenter = class {
|
|
|
107463
107849
|
this.linkedStills = getSnapshot !== void 0 ? new NcLinkedStillShelf({
|
|
107464
107850
|
getSnapshot,
|
|
107465
107851
|
...deps.getSnapshotState !== void 0 ? { getSnapshotState: deps.getSnapshotState } : {},
|
|
107852
|
+
...deps.isBatteryCamera !== void 0 ? { isBatteryCamera: deps.isBatteryCamera } : {},
|
|
107466
107853
|
...deps.getFrameCapturedAt !== void 0 ? { getFrameCapturedAt: deps.getFrameCapturedAt } : {},
|
|
107467
107854
|
isOnline: (id) => this.deviceDirectory.get(id)?.online,
|
|
107468
107855
|
logger: this.logger.child("linked-stills"),
|
|
@@ -110234,6 +110621,7 @@ var BuildNotificationCenter = class extends BuildNotificationCenterBase {
|
|
|
110234
110621
|
now: () => Date.now()
|
|
110235
110622
|
}),
|
|
110236
110623
|
...buildLinkedSnapshotReads(api.snapshot),
|
|
110624
|
+
isBatteryCamera: async (deviceId) => await api.battery.getStatus.query({ deviceId }) !== null,
|
|
110237
110625
|
identityIdsByName: async () => {
|
|
110238
110626
|
const byName = /* @__PURE__ */ new Map();
|
|
110239
110627
|
const identities = await stores.identityStore.listIdentities();
|
|
@@ -114431,6 +114819,12 @@ function deliveredIdempotencyKey(entryId) {
|
|
|
114431
114819
|
function pickedUpIdempotencyKey(entryId) {
|
|
114432
114820
|
return `pa-pkg-${entryId}-pickedup`;
|
|
114433
114821
|
}
|
|
114822
|
+
/**
|
|
114823
|
+
* How long a delivery waits for its still to be written before it ships
|
|
114824
|
+
* without one. Under the notification still poll (`NC_STILL_WAIT_MS`, 3 s) so
|
|
114825
|
+
* a slow store never holds the alert longer than the dispatcher would.
|
|
114826
|
+
*/
|
|
114827
|
+
var PACKAGE_FRAME_PERSIST_BOUND_MS = 2e3;
|
|
114434
114828
|
var PackageDropDetector = class {
|
|
114435
114829
|
deps;
|
|
114436
114830
|
/** Per-device mirror of the last LOGGED value of the `packageDropEnabled`
|
|
@@ -114444,6 +114838,62 @@ var PackageDropDetector = class {
|
|
|
114444
114838
|
* LLM gate (D931). `null` means the delivery is dropped (no, or no yes).
|
|
114445
114839
|
* An empty object means the rule did not ask. A verdict is spread onto the row.
|
|
114446
114840
|
*/
|
|
114841
|
+
/**
|
|
114842
|
+
* The delivery's still, written once under `object:<rowId>` when the entry
|
|
114843
|
+
* had no key frame. Never throws and never waits past
|
|
114844
|
+
* {@link PACKAGE_FRAME_PERSIST_BOUND_MS}: a delivery without a picture still
|
|
114845
|
+
* ships, and says why (D391).
|
|
114846
|
+
*/
|
|
114847
|
+
async persistDeliveryStill(entry, rowId, timestamp) {
|
|
114848
|
+
const persist = this.deps.persistDeliveryFrame;
|
|
114849
|
+
if (persist === void 0) return void 0;
|
|
114850
|
+
const tags = { deviceId: entry.deviceId };
|
|
114851
|
+
const meta = {
|
|
114852
|
+
entryId: entry.id,
|
|
114853
|
+
rowId
|
|
114854
|
+
};
|
|
114855
|
+
let timer;
|
|
114856
|
+
try {
|
|
114857
|
+
const bound = new Promise((resolve) => {
|
|
114858
|
+
timer = setTimeout(() => resolve("timeout"), PACKAGE_FRAME_PERSIST_BOUND_MS);
|
|
114859
|
+
});
|
|
114860
|
+
const written = await Promise.race([persist({
|
|
114861
|
+
deviceId: entry.deviceId,
|
|
114862
|
+
trackId: entry.sourceTrackId ?? entry.id,
|
|
114863
|
+
rowId,
|
|
114864
|
+
timestamp
|
|
114865
|
+
}), bound]);
|
|
114866
|
+
if (written === "timeout") {
|
|
114867
|
+
this.deps.logger.warn("package delivery still not written in time — shipping without it", {
|
|
114868
|
+
tags,
|
|
114869
|
+
meta: {
|
|
114870
|
+
...meta,
|
|
114871
|
+
boundMs: PACKAGE_FRAME_PERSIST_BOUND_MS
|
|
114872
|
+
}
|
|
114873
|
+
});
|
|
114874
|
+
return;
|
|
114875
|
+
}
|
|
114876
|
+
if (written === null) {
|
|
114877
|
+
this.deps.logger.warn("package delivery has no frame to write — shipping without a still", {
|
|
114878
|
+
tags,
|
|
114879
|
+
meta
|
|
114880
|
+
});
|
|
114881
|
+
return;
|
|
114882
|
+
}
|
|
114883
|
+
return written;
|
|
114884
|
+
} catch (err) {
|
|
114885
|
+
this.deps.logger.warn("package delivery still write failed — shipping without it", {
|
|
114886
|
+
tags,
|
|
114887
|
+
meta: {
|
|
114888
|
+
...meta,
|
|
114889
|
+
error: require_clip_model_registry.errMsg(err)
|
|
114890
|
+
}
|
|
114891
|
+
});
|
|
114892
|
+
return;
|
|
114893
|
+
} finally {
|
|
114894
|
+
if (timer !== void 0) clearTimeout(timer);
|
|
114895
|
+
}
|
|
114896
|
+
}
|
|
114447
114897
|
async llmFields(entry, rules, hitZones, tags) {
|
|
114448
114898
|
if (!rules.some((rule) => rule.confirmWithLlm === true && rule.zoneIds.some((id) => hitZones.includes(id)))) return {};
|
|
114449
114899
|
const confirm = this.deps.confirmPackage;
|
|
@@ -114705,6 +115155,7 @@ var PackageDropDetector = class {
|
|
|
114705
115155
|
});
|
|
114706
115156
|
return;
|
|
114707
115157
|
}
|
|
115158
|
+
const keyFrameMediaId = entry.keyFrameMediaId ?? await this.persistDeliveryStill(entry, rowId, rowTimestamp);
|
|
114708
115159
|
const ev = {
|
|
114709
115160
|
...draft,
|
|
114710
115161
|
id: rowId
|
|
@@ -114716,7 +115167,7 @@ var PackageDropDetector = class {
|
|
|
114716
115167
|
eventId: String(rowId),
|
|
114717
115168
|
className: entry.className,
|
|
114718
115169
|
zoneIds: hitZones,
|
|
114719
|
-
...
|
|
115170
|
+
...keyFrameMediaId !== void 0 ? { keyFrameMediaId } : {},
|
|
114720
115171
|
bbox: {
|
|
114721
115172
|
x: entry.bbox.x,
|
|
114722
115173
|
y: entry.bbox.y,
|
|
@@ -118279,6 +118730,7 @@ var BuildStores = class extends BuildStoresBase {
|
|
|
118279
118730
|
resolveSettings: (deviceId) => this.resolveDevicePackageDropSettings(deviceId),
|
|
118280
118731
|
resolveZoneMembershipMinOverlap: async (deviceId) => (await this.resolveDeviceTrackingSettings(deviceId)).zoneMembershipMinOverlap,
|
|
118281
118732
|
confirmPackage: (input) => this.deps.confirmPackage(input),
|
|
118733
|
+
persistDeliveryFrame: (request) => this.deps.persistPackageDeliveryFrame(request),
|
|
118282
118734
|
onLlmSilence: (edge) => {
|
|
118283
118735
|
const deviceId = edge.phase === "silent" ? edge.note.deviceId : edge.deviceId;
|
|
118284
118736
|
this.ctx.eventBus.emit({
|
|
@@ -120400,108 +120852,6 @@ async function ensureTrackHierarchyMedia(deps, input) {
|
|
|
120400
120852
|
await Promise.all(tasks);
|
|
120401
120853
|
}
|
|
120402
120854
|
//#endregion
|
|
120403
|
-
//#region src/pipeline-analytics/pipeline/key-frame-persist.ts
|
|
120404
|
-
/**
|
|
120405
|
-
* A box is written only when it can be USED — a degenerate rectangle, or one
|
|
120406
|
-
* measured on a degenerate frame, says nothing and would read back as a box at
|
|
120407
|
-
* the origin. Absent is the honest answer; the reader then pairs a snapshot,
|
|
120408
|
-
* exactly as it did before D510.
|
|
120409
|
-
*
|
|
120410
|
-
* Its result is spread into BOTH writes: the small companion is derived from
|
|
120411
|
-
* the very bytes of the native one, so it is the same scene at the same
|
|
120412
|
-
* instant and the box maps into it by ratio like any other scale of the frame.
|
|
120413
|
-
* A box on one row and not the other would make the summary's answer depend on
|
|
120414
|
-
* which row a reader happened to pick.
|
|
120415
|
-
*/
|
|
120416
|
-
function isStatableSubject(subject) {
|
|
120417
|
-
if (subject === void 0) return false;
|
|
120418
|
-
return subject.w > 0 && subject.h > 0 && subject.frameWidth > 0 && subject.frameHeight > 0;
|
|
120419
|
-
}
|
|
120420
|
-
/**
|
|
120421
|
-
* Read, derive and store one track's key frame + its web companion.
|
|
120422
|
-
*
|
|
120423
|
-
* PARCEL-OR-NOTHING: no parcel writes nothing and the caller's bounded retry
|
|
120424
|
-
* parks a later frame. The `keyFrame` write lands FIRST and its media id (D482)
|
|
120425
|
-
* is what the outcome carries — a `keyFrameSmall` failure afterwards must not
|
|
120426
|
-
* re-arm a retry that would re-shoot a key frame the rebuild can already read.
|
|
120427
|
-
*/
|
|
120428
|
-
async function persistTrackKeyFrame(deps, input) {
|
|
120429
|
-
const { deviceId, trackId, retry } = input;
|
|
120430
|
-
const drop = {
|
|
120431
|
-
tags: { deviceId },
|
|
120432
|
-
meta: {
|
|
120433
|
-
trackId,
|
|
120434
|
-
retry
|
|
120435
|
-
}
|
|
120436
|
-
};
|
|
120437
|
-
try {
|
|
120438
|
-
const parcel = await deps.fetchParkedKeyFrame(deviceId, trackId);
|
|
120439
|
-
if (parcel === null || parcel.jpeg.byteLength === 0) {
|
|
120440
|
-
deps.logger.error("key-frame parcel missing — nothing to write, retry parks a later frame", { ...drop });
|
|
120441
|
-
return { kind: "parcel-missing" };
|
|
120442
|
-
}
|
|
120443
|
-
const subjectBox = isStatableSubject(parcel.subject) ? { subjectBox: parcel.subject } : {};
|
|
120444
|
-
const keyFrameMediaId = await deps.media.putReplacing({
|
|
120445
|
-
deviceId,
|
|
120446
|
-
ownerKind: "track",
|
|
120447
|
-
ownerId: trackId,
|
|
120448
|
-
kind: "keyFrame",
|
|
120449
|
-
timestamp: parcel.timestamp,
|
|
120450
|
-
data: parcel.jpeg,
|
|
120451
|
-
...subjectBox
|
|
120452
|
-
});
|
|
120453
|
-
try {
|
|
120454
|
-
const keyFrameSmall = await deps.deriveSmall(parcel.jpeg);
|
|
120455
|
-
await deps.media.putReplacing({
|
|
120456
|
-
deviceId,
|
|
120457
|
-
ownerKind: "track",
|
|
120458
|
-
ownerId: trackId,
|
|
120459
|
-
kind: "keyFrameSmall",
|
|
120460
|
-
timestamp: parcel.timestamp,
|
|
120461
|
-
data: keyFrameSmall,
|
|
120462
|
-
...subjectBox
|
|
120463
|
-
});
|
|
120464
|
-
} catch (err) {
|
|
120465
|
-
deps.logger.warn("keyFrameSmall write failed — the native keyFrame is stored", {
|
|
120466
|
-
...drop,
|
|
120467
|
-
meta: {
|
|
120468
|
-
...drop.meta,
|
|
120469
|
-
nodeId: parcel.nodeId,
|
|
120470
|
-
error: require_clip_model_registry.errMsg(err)
|
|
120471
|
-
}
|
|
120472
|
-
});
|
|
120473
|
-
}
|
|
120474
|
-
deps.logger.debug("key frame written from the parked parcel", {
|
|
120475
|
-
...drop,
|
|
120476
|
-
meta: {
|
|
120477
|
-
...drop.meta,
|
|
120478
|
-
nodeId: parcel.nodeId,
|
|
120479
|
-
width: parcel.width,
|
|
120480
|
-
height: parcel.height,
|
|
120481
|
-
bytes: parcel.jpeg.byteLength,
|
|
120482
|
-
parkedTimestamp: parcel.timestamp
|
|
120483
|
-
}
|
|
120484
|
-
});
|
|
120485
|
-
return {
|
|
120486
|
-
kind: "landed",
|
|
120487
|
-
keyFrameMediaId
|
|
120488
|
-
};
|
|
120489
|
-
} catch (err) {
|
|
120490
|
-
const error = require_clip_model_registry.errMsg(err);
|
|
120491
|
-
deps.logger.warn("key-frame write failed", {
|
|
120492
|
-
...drop,
|
|
120493
|
-
meta: {
|
|
120494
|
-
...drop.meta,
|
|
120495
|
-
error
|
|
120496
|
-
}
|
|
120497
|
-
});
|
|
120498
|
-
return {
|
|
120499
|
-
kind: "error",
|
|
120500
|
-
error
|
|
120501
|
-
};
|
|
120502
|
-
}
|
|
120503
|
-
}
|
|
120504
|
-
//#endregion
|
|
120505
120855
|
//#region src/pipeline-analytics/pipeline/key-frame-retry.ts
|
|
120506
120856
|
/** Retry-path attempts allowed per track. The operator's number. */
|
|
120507
120857
|
var KEY_FRAME_MAX_RETRIES = 3;
|
|
@@ -122085,6 +122435,8 @@ var DEFAULT_ASSIST_MIN_SCORE = .3;
|
|
|
122085
122435
|
* cannot page the whole table into memory to build a manifest.
|
|
122086
122436
|
*/
|
|
122087
122437
|
var RETRAIN_EXPORT_MAX = 2e4;
|
|
122438
|
+
/** How far back a restart looks for a level cap's last persisted boolean. */
|
|
122439
|
+
var LEVEL_SEED_WINDOW_MS = 48 * 36e5;
|
|
122088
122440
|
/**
|
|
122089
122441
|
* Pipeline Analytics addon — subscribes to `PipelineInferenceResult` +
|
|
122090
122442
|
* `PipelineAudioInferenceResult` on the bus, refines raw detections
|
|
@@ -122360,7 +122712,15 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends require_clip_m
|
|
|
122360
122712
|
*/
|
|
122361
122713
|
sensorTransitions = new SensorTransitionGate({
|
|
122362
122714
|
startedAtMs: Date.now(),
|
|
122363
|
-
now: () => Date.now()
|
|
122715
|
+
now: () => Date.now(),
|
|
122716
|
+
readSeed: (deviceId, capName) => this.readPersistedLevel(deviceId, capName),
|
|
122717
|
+
onSeedFailed: (deviceId, capName, error) => this.ctx.logger.warn("level seed read failed — gate starts unseeded", {
|
|
122718
|
+
tags: { deviceId },
|
|
122719
|
+
meta: {
|
|
122720
|
+
capName,
|
|
122721
|
+
error: require_clip_model_registry.errMsg(error)
|
|
122722
|
+
}
|
|
122723
|
+
})
|
|
122364
122724
|
});
|
|
122365
122725
|
/** `camera-index-unknown` drops, summarised per window — see the file. */
|
|
122366
122726
|
cameraIndexUnknownDrops = new CameraIndexUnknownDrops({ nowMs: Date.now() });
|
|
@@ -124172,6 +124532,7 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends require_clip_m
|
|
|
124172
124532
|
this.packageDropDetector = value;
|
|
124173
124533
|
},
|
|
124174
124534
|
confirmPackage: (input) => this.confirmPackageDelivery(input),
|
|
124535
|
+
persistPackageDeliveryFrame: (request) => this.persistPackageDeliveryFrame(request),
|
|
124175
124536
|
stationaryEvidence: () => this.stationaryEvidence,
|
|
124176
124537
|
setStationaryEvidence: (value) => {
|
|
124177
124538
|
this.stationaryEvidence = value;
|
|
@@ -128327,6 +128688,44 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends require_clip_m
|
|
|
128327
128688
|
via: "frame"
|
|
128328
128689
|
});
|
|
128329
128690
|
}
|
|
128691
|
+
/**
|
|
128692
|
+
* #383: a delivery whose entry had no key frame writes the source track's
|
|
128693
|
+
* parked frame under the object row it just created — the one owner the
|
|
128694
|
+
* notification ladder reads for a package event.
|
|
128695
|
+
*/
|
|
128696
|
+
async persistPackageDeliveryFrame(request) {
|
|
128697
|
+
const mediaStore = this.mediaStore;
|
|
128698
|
+
if (mediaStore === null) return null;
|
|
128699
|
+
return persistObjectFrame({
|
|
128700
|
+
fetchParkedKeyFrame: async (deviceId, trackId) => {
|
|
128701
|
+
const parcel = await this.mediaCapture.fetchParkedParcel(trackId, "keyFrame");
|
|
128702
|
+
if (parcel === null || parcel.info.deviceId !== deviceId) return null;
|
|
128703
|
+
const subject = parcel.info.subject;
|
|
128704
|
+
return {
|
|
128705
|
+
jpeg: parcel.jpeg,
|
|
128706
|
+
width: parcel.width,
|
|
128707
|
+
height: parcel.height,
|
|
128708
|
+
timestamp: parcel.timestamp,
|
|
128709
|
+
nodeId: parcel.info.nodeId,
|
|
128710
|
+
subject: {
|
|
128711
|
+
x: subject.bbox.x,
|
|
128712
|
+
y: subject.bbox.y,
|
|
128713
|
+
w: subject.bbox.w,
|
|
128714
|
+
h: subject.bbox.h,
|
|
128715
|
+
frameWidth: subject.frameWidth,
|
|
128716
|
+
frameHeight: subject.frameHeight
|
|
128717
|
+
}
|
|
128718
|
+
};
|
|
128719
|
+
},
|
|
128720
|
+
media: mediaStore,
|
|
128721
|
+
logger: this.ctx.logger
|
|
128722
|
+
}, {
|
|
128723
|
+
deviceId: request.deviceId,
|
|
128724
|
+
trackId: request.trackId,
|
|
128725
|
+
objectId: String(request.rowId),
|
|
128726
|
+
timestamp: request.timestamp
|
|
128727
|
+
});
|
|
128728
|
+
}
|
|
128330
128729
|
async confirmPackageDelivery(input) {
|
|
128331
128730
|
const crops = this.packageScene?.cropsFor(input.deviceId);
|
|
128332
128731
|
return confirmPackageWithLlm({
|
|
@@ -129328,7 +129727,7 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends require_clip_m
|
|
|
129328
129727
|
const store = this.sensorEventStore;
|
|
129329
129728
|
const cache = this.linkedCamerasCache;
|
|
129330
129729
|
if (store === null || cache === null) return;
|
|
129331
|
-
if (!this.sensorSliceCarriedATransition(data)) return;
|
|
129730
|
+
if (!await this.sensorSliceCarriedATransition(data)) return;
|
|
129332
129731
|
const cameras = await resolveSensorNotificationCameras(cache, data).catch((err) => {
|
|
129333
129732
|
this.ctx.logger.debug("linked-camera resolution failed — evaluated per row", {
|
|
129334
129733
|
tags: { deviceId: data.deviceId },
|
|
@@ -129496,7 +129895,7 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends require_clip_m
|
|
|
129496
129895
|
* The drops are SUMMARISED, never logged one by one: this path carries every
|
|
129497
129896
|
* camera's native `motion` slice at motion rate.
|
|
129498
129897
|
*/
|
|
129499
|
-
sensorSliceCarriedATransition(data) {
|
|
129898
|
+
async sensorSliceCarriedATransition(data) {
|
|
129500
129899
|
const slice = data.slice;
|
|
129501
129900
|
if (typeof slice !== "object" || slice === null || Array.isArray(slice)) {
|
|
129502
129901
|
this.ctx.logger.debug("sensor state change carried no slice — dropped", {
|
|
@@ -129505,11 +129904,53 @@ var PipelineAnalyticsAddon = class PipelineAnalyticsAddon extends require_clip_m
|
|
|
129505
129904
|
});
|
|
129506
129905
|
return false;
|
|
129507
129906
|
}
|
|
129508
|
-
const verdict = this.sensorTransitions.
|
|
129907
|
+
const verdict = await this.sensorTransitions.observeSettled(data.deviceId, data.capName, { ...slice });
|
|
129509
129908
|
this.flushSensorTransitionDrops();
|
|
129909
|
+
if (verdict.changed) {
|
|
129910
|
+
const entries = Object.entries(slice);
|
|
129911
|
+
const field = (key) => entries.find(([k]) => k === key)?.[1];
|
|
129912
|
+
this.ctx.logger.info("sensor slice passed the transition gate", {
|
|
129913
|
+
tags: { deviceId: data.deviceId },
|
|
129914
|
+
meta: {
|
|
129915
|
+
capName: data.capName,
|
|
129916
|
+
state: field("state") ?? field("value") ?? field("entryOpen") ?? field("active") ?? null,
|
|
129917
|
+
lastChangedAt: field("lastChangedAt") ?? null,
|
|
129918
|
+
verdict: "changed"
|
|
129919
|
+
}
|
|
129920
|
+
});
|
|
129921
|
+
}
|
|
129510
129922
|
return verdict.changed;
|
|
129511
129923
|
}
|
|
129512
129924
|
/**
|
|
129925
|
+
* The last persisted boolean of a level cap, from device-history — the
|
|
129926
|
+
* gate's seed after a restart. Off the event path (the gate starts it at the
|
|
129927
|
+
* first sighting). A cap that declares no history field, or a series with no
|
|
129928
|
+
* point, answers `null`.
|
|
129929
|
+
*/
|
|
129930
|
+
async readPersistedLevel(deviceId, capName) {
|
|
129931
|
+
const key = levelSeriesKey(capName);
|
|
129932
|
+
if (key === null) return null;
|
|
129933
|
+
const to = Date.now();
|
|
129934
|
+
const result = await this.ctx.api.deviceHistory.query.query({
|
|
129935
|
+
deviceId,
|
|
129936
|
+
key,
|
|
129937
|
+
from: to - LEVEL_SEED_WINDOW_MS,
|
|
129938
|
+
to,
|
|
129939
|
+
resolution: "raw",
|
|
129940
|
+
maxPoints: 200
|
|
129941
|
+
});
|
|
129942
|
+
const latest = result.series.flatMap((s) => s.points).reduce((best, p) => best === null || p.t > best.t ? p : best, null);
|
|
129943
|
+
if (latest !== null && latest.last !== null) return {
|
|
129944
|
+
value: latest.last >= .5,
|
|
129945
|
+
since: latest.t
|
|
129946
|
+
};
|
|
129947
|
+
const held = result.series.flatMap((s) => s.before === null ? [] : [s.before]).reduce((best, b) => best === null || b.t > best.t ? b : best, null);
|
|
129948
|
+
return held !== null && held.value !== null ? {
|
|
129949
|
+
value: held.value >= .5,
|
|
129950
|
+
since: held.t
|
|
129951
|
+
} : null;
|
|
129952
|
+
}
|
|
129953
|
+
/**
|
|
129513
129954
|
* One `info` line per (device, cap, reason) per window, or none.
|
|
129514
129955
|
*
|
|
129515
129956
|
* A branch that drops work says so — but this one drops ~0.6 events/s
|