@camstack/addon-provider-reolink 1.2.45 → 1.2.47
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 +346 -74
- package/dist/addon.mjs +346 -74
- package/package.json +1 -1
package/dist/addon.js
CHANGED
|
@@ -7605,7 +7605,7 @@ var RecordingBandSchema = object({
|
|
|
7605
7605
|
postBufferSec: number().min(0).optional()
|
|
7606
7606
|
});
|
|
7607
7607
|
({
|
|
7608
|
-
preBufferSec:
|
|
7608
|
+
preBufferSec: 15,
|
|
7609
7609
|
postBufferSec: 30
|
|
7610
7610
|
}).postBufferSec * 1e3;
|
|
7611
7611
|
/**
|
|
@@ -13995,128 +13995,103 @@ var COCO_TO_MACRO = {
|
|
|
13995
13995
|
var AUDIO_MACRO_LABELS = [
|
|
13996
13996
|
{
|
|
13997
13997
|
id: "speech",
|
|
13998
|
-
name: "Speech"
|
|
13999
|
-
icon: "🗣️"
|
|
13998
|
+
name: "Speech"
|
|
14000
13999
|
},
|
|
14001
14000
|
{
|
|
14002
14001
|
id: "scream",
|
|
14003
|
-
name: "Scream / Shout"
|
|
14004
|
-
icon: "😱"
|
|
14002
|
+
name: "Scream / Shout"
|
|
14005
14003
|
},
|
|
14006
14004
|
{
|
|
14007
14005
|
id: "crying",
|
|
14008
|
-
name: "Crying / Baby"
|
|
14009
|
-
icon: "😢"
|
|
14006
|
+
name: "Crying / Baby"
|
|
14010
14007
|
},
|
|
14011
14008
|
{
|
|
14012
14009
|
id: "laughter",
|
|
14013
|
-
name: "Laughter"
|
|
14014
|
-
icon: "😂"
|
|
14010
|
+
name: "Laughter"
|
|
14015
14011
|
},
|
|
14016
14012
|
{
|
|
14017
14013
|
id: "music",
|
|
14018
|
-
name: "Music"
|
|
14019
|
-
icon: "🎵"
|
|
14014
|
+
name: "Music"
|
|
14020
14015
|
},
|
|
14021
14016
|
{
|
|
14022
14017
|
id: "dog",
|
|
14023
|
-
name: "Dog"
|
|
14024
|
-
icon: "🐕"
|
|
14018
|
+
name: "Dog"
|
|
14025
14019
|
},
|
|
14026
14020
|
{
|
|
14027
14021
|
id: "cat",
|
|
14028
|
-
name: "Cat"
|
|
14029
|
-
icon: "🐈"
|
|
14022
|
+
name: "Cat"
|
|
14030
14023
|
},
|
|
14031
14024
|
{
|
|
14032
14025
|
id: "bird",
|
|
14033
|
-
name: "Bird"
|
|
14034
|
-
icon: "🐦"
|
|
14026
|
+
name: "Bird"
|
|
14035
14027
|
},
|
|
14036
14028
|
{
|
|
14037
14029
|
id: "animal",
|
|
14038
|
-
name: "Animal (other)"
|
|
14039
|
-
icon: "🐾"
|
|
14030
|
+
name: "Animal (other)"
|
|
14040
14031
|
},
|
|
14041
14032
|
{
|
|
14042
14033
|
id: "alarm",
|
|
14043
|
-
name: "Alarm / Siren"
|
|
14044
|
-
icon: "🚨"
|
|
14034
|
+
name: "Alarm / Siren"
|
|
14045
14035
|
},
|
|
14046
14036
|
{
|
|
14047
14037
|
id: "doorbell",
|
|
14048
|
-
name: "Doorbell / Knock"
|
|
14049
|
-
icon: "🔔"
|
|
14038
|
+
name: "Doorbell / Knock"
|
|
14050
14039
|
},
|
|
14051
14040
|
{
|
|
14052
14041
|
id: "glass_breaking",
|
|
14053
|
-
name: "Glass Breaking"
|
|
14054
|
-
icon: "💥"
|
|
14042
|
+
name: "Glass Breaking"
|
|
14055
14043
|
},
|
|
14056
14044
|
{
|
|
14057
14045
|
id: "gunshot",
|
|
14058
|
-
name: "Gunshot / Explosion"
|
|
14059
|
-
icon: "💣"
|
|
14046
|
+
name: "Gunshot / Explosion"
|
|
14060
14047
|
},
|
|
14061
14048
|
{
|
|
14062
14049
|
id: "vehicle",
|
|
14063
|
-
name: "Vehicle"
|
|
14064
|
-
icon: "🚗"
|
|
14050
|
+
name: "Vehicle"
|
|
14065
14051
|
},
|
|
14066
14052
|
{
|
|
14067
14053
|
id: "siren",
|
|
14068
|
-
name: "Emergency Siren"
|
|
14069
|
-
icon: "🚑"
|
|
14054
|
+
name: "Emergency Siren"
|
|
14070
14055
|
},
|
|
14071
14056
|
{
|
|
14072
14057
|
id: "fire",
|
|
14073
|
-
name: "Fire / Smoke"
|
|
14074
|
-
icon: "🔥"
|
|
14058
|
+
name: "Fire / Smoke"
|
|
14075
14059
|
},
|
|
14076
14060
|
{
|
|
14077
14061
|
id: "water",
|
|
14078
|
-
name: "Water"
|
|
14079
|
-
icon: "💧"
|
|
14062
|
+
name: "Water"
|
|
14080
14063
|
},
|
|
14081
14064
|
{
|
|
14082
14065
|
id: "wind",
|
|
14083
|
-
name: "Wind / Weather"
|
|
14084
|
-
icon: "🌬️"
|
|
14066
|
+
name: "Wind / Weather"
|
|
14085
14067
|
},
|
|
14086
14068
|
{
|
|
14087
14069
|
id: "door",
|
|
14088
|
-
name: "Door"
|
|
14089
|
-
icon: "🚪"
|
|
14070
|
+
name: "Door"
|
|
14090
14071
|
},
|
|
14091
14072
|
{
|
|
14092
14073
|
id: "footsteps",
|
|
14093
|
-
name: "Footsteps"
|
|
14094
|
-
icon: "👣"
|
|
14074
|
+
name: "Footsteps"
|
|
14095
14075
|
},
|
|
14096
14076
|
{
|
|
14097
14077
|
id: "crowd",
|
|
14098
|
-
name: "Crowd / Chatter"
|
|
14099
|
-
icon: "👥"
|
|
14078
|
+
name: "Crowd / Chatter"
|
|
14100
14079
|
},
|
|
14101
14080
|
{
|
|
14102
14081
|
id: "telephone",
|
|
14103
|
-
name: "Telephone"
|
|
14104
|
-
icon: "📞"
|
|
14082
|
+
name: "Telephone"
|
|
14105
14083
|
},
|
|
14106
14084
|
{
|
|
14107
14085
|
id: "engine",
|
|
14108
|
-
name: "Engine / Motor"
|
|
14109
|
-
icon: "⚙️"
|
|
14086
|
+
name: "Engine / Motor"
|
|
14110
14087
|
},
|
|
14111
14088
|
{
|
|
14112
14089
|
id: "tools",
|
|
14113
|
-
name: "Tools / Construction"
|
|
14114
|
-
icon: "🔨"
|
|
14090
|
+
name: "Tools / Construction"
|
|
14115
14091
|
},
|
|
14116
14092
|
{
|
|
14117
14093
|
id: "silence",
|
|
14118
|
-
name: "Silence"
|
|
14119
|
-
icon: "🤫"
|
|
14094
|
+
name: "Silence"
|
|
14120
14095
|
}
|
|
14121
14096
|
];
|
|
14122
14097
|
var YAMNET_TO_MACRO = {
|
|
@@ -16618,7 +16593,7 @@ var TrackEnvelopeSchema = object({
|
|
|
16618
16593
|
* `snapshots[]` references — megabytes across a page of tracks. `slim`
|
|
16619
16594
|
* keeps every scalar the list surfaces actually render (ids, class(es),
|
|
16620
16595
|
* label / audioLabels / importance enrichment, firstSeen/lastSeen, state,
|
|
16621
|
-
* zonesVisited, bestEventId, envelope, hasFace, hasRider) and returns `positions` /
|
|
16596
|
+
* zonesVisited, bestEventId, envelope, hasFace, hasEmbeddedFace, hasRider) and returns `positions` /
|
|
16622
16597
|
* `snapshots` as EMPTY arrays — detail views re-fetch the full row via
|
|
16623
16598
|
* `getTrack`. Mirrors the event-store `projection` convention
|
|
16624
16599
|
* (`getObjectEvents` et al.).
|
|
@@ -16913,6 +16888,35 @@ var TrackSchema = object({
|
|
|
16913
16888
|
*/
|
|
16914
16889
|
hasFace: boolean().optional(),
|
|
16915
16890
|
/**
|
|
16891
|
+
* This track has a face row IN THE GALLERY: a crop **and** an embedding — a
|
|
16892
|
+
* face an operator could ASSIGN to an identity.
|
|
16893
|
+
*
|
|
16894
|
+
* The STRICT twin of {@link hasFace}, and the pair only earns its keep
|
|
16895
|
+
* because the two disagree. `hasFace` is stamped at the TOP of the face
|
|
16896
|
+
* branch, before every gate, and means no more than "a face detector produced
|
|
16897
|
+
* a face detail". This one is stamped at the single moment the gallery row
|
|
16898
|
+
* LANDS — after `FaceRecognizer.onTrackEnd` successfully persists it, i.e.
|
|
16899
|
+
* past the embedding-magnitude verdict, the `minFacePx` detection gate, the
|
|
16900
|
+
* candidate gate, the imageless-track drop (no crop was ever captured) and
|
|
16901
|
+
* the crop-store drop. Everything between the detector and that insert can
|
|
16902
|
+
* legitimately refuse the face, so a flag written any earlier promises the
|
|
16903
|
+
* operator something to assign and delivers nothing.
|
|
16904
|
+
*
|
|
16905
|
+
* **Independent of recognition.** A face collected but never auto-matched is
|
|
16906
|
+
* still assignable — it is in fact the face an operator most wants to reach —
|
|
16907
|
+
* so this is NOT gated on `recognizedIdentityId`. Recognition lands in
|
|
16908
|
+
* `subLabel`; this says only that the raw material exists.
|
|
16909
|
+
*
|
|
16910
|
+
* **Set once, never cleared.** A track that produced a gallery row produced
|
|
16911
|
+
* one; deleting the row later is the gallery's business, not this flag's.
|
|
16912
|
+
*
|
|
16913
|
+
* **Absent ≠ false**, the same rule as {@link hasFace}: every row written
|
|
16914
|
+
* before the column omits it, and so does every server that predates the
|
|
16915
|
+
* field. A consumer must test `=== true` and render nothing otherwise —
|
|
16916
|
+
* never infer "no assignable face".
|
|
16917
|
+
*/
|
|
16918
|
+
hasEmbeddedFace: boolean().optional(),
|
|
16919
|
+
/**
|
|
16916
16920
|
* This subject CONTAINS a folded rider — a person the rider-pairing step
|
|
16917
16921
|
* ([D34](../decisions/adr-0034.md)) removed from the frame BEFORE the tracker,
|
|
16918
16922
|
* so the passage is tracked once and as a VEHICLE.
|
|
@@ -18302,6 +18306,32 @@ var DetailResultSchema = object({
|
|
|
18302
18306
|
/** FRAME-space bbox (already mapped back from crop space). */
|
|
18303
18307
|
bbox: NativeCropBboxSchema.optional(),
|
|
18304
18308
|
embedding: string().optional(),
|
|
18309
|
+
/**
|
|
18310
|
+
* L2 magnitude of the RAW pre-normalization embedding. Additive and optional
|
|
18311
|
+
* so an older runner that does not send it degrades to "unmeasurable", which
|
|
18312
|
+
* the consuming gate treats as accept — never as reject.
|
|
18313
|
+
*/
|
|
18314
|
+
embeddingMagnitude: number().optional(),
|
|
18315
|
+
/**
|
|
18316
|
+
* Model that produced `embedding` — the SAME contract `labelModelId` has for
|
|
18317
|
+
* `label`, and for the same reason: the producing node is the only side that
|
|
18318
|
+
* knows which model actually ran, and re-deriving it downstream is exactly
|
|
18319
|
+
* the inference this model exists to forbid.
|
|
18320
|
+
*
|
|
18321
|
+
* Additive and optional, so a runner that predates it degrades to
|
|
18322
|
+
* "unmeasurable" rather than to a wrong answer — the consumer falls back to
|
|
18323
|
+
* its previous assumption instead of refusing.
|
|
18324
|
+
*
|
|
18325
|
+
* Its absence was not cosmetic. `FaceRecognizer` needs the probe's model id
|
|
18326
|
+
* to use `face-matcher`'s same-model gate (`sample.modelId !== probe.modelId`);
|
|
18327
|
+
* with nothing on the wire it took the id from the GALLERY, so the gate
|
|
18328
|
+
* compared the gallery against itself and could never fire. That turned the
|
|
18329
|
+
* one guard protecting the face feature space into a tautology, and it is
|
|
18330
|
+
* why swapping `face-embedding`'s model would NOT have stranded the enrolled
|
|
18331
|
+
* gallery as the catalog comment predicts — it would have scored the new
|
|
18332
|
+
* model's vectors against the old model's samples in one cosine space.
|
|
18333
|
+
*/
|
|
18334
|
+
embeddingModelId: string().optional(),
|
|
18305
18335
|
label: string().optional(),
|
|
18306
18336
|
/**
|
|
18307
18337
|
* The tier `label` occupies, copied VERBATIM from the producing step's
|
|
@@ -19177,6 +19207,12 @@ var CameraStatusSchema = object({
|
|
|
19177
19207
|
/** Unix timestamp (ms) when this snapshot was composed server-side. */
|
|
19178
19208
|
fetchedAt: number()
|
|
19179
19209
|
});
|
|
19210
|
+
var InferenceDeviceExclusionReasonSchema = _enum([
|
|
19211
|
+
"disabled",
|
|
19212
|
+
"unavailable",
|
|
19213
|
+
"cannot-host-camera-root",
|
|
19214
|
+
"accelerator-preferred"
|
|
19215
|
+
]);
|
|
19180
19216
|
var NodeInferenceDeviceSchema = object({
|
|
19181
19217
|
/** Stable per-node device key, e.g. `openvino:npu`, `edgetpu:usb`, `cpu`. */
|
|
19182
19218
|
key: string(),
|
|
@@ -19207,7 +19243,17 @@ var NodeInferenceDeviceSchema = object({
|
|
|
19207
19243
|
* available per format; this is the stored selection that becomes the
|
|
19208
19244
|
* default for EVERY camera landing on this accelerator.
|
|
19209
19245
|
*/
|
|
19210
|
-
steps: record(string(), DeviceStepConfigSchema).optional()
|
|
19246
|
+
steps: record(string(), DeviceStepConfigSchema).optional(),
|
|
19247
|
+
/**
|
|
19248
|
+
* `null` when the device IS a camera-root candidate on this node; otherwise
|
|
19249
|
+
* the reason the dispatcher drops it. Computed by the SAME
|
|
19250
|
+
* `resolveInferenceDeviceEligibility` the dispatcher runs, so this view can
|
|
19251
|
+
* never disagree with the election — deriving it in the UI from
|
|
19252
|
+
* `enabled`/`available` would silently miss `cannot-host-camera-root` (needs
|
|
19253
|
+
* the node's model catalog) and `accelerator-preferred` (needs the node-wide
|
|
19254
|
+
* "an accelerator is serving" predicate).
|
|
19255
|
+
*/
|
|
19256
|
+
exclusion: InferenceDeviceExclusionReasonSchema.nullable()
|
|
19211
19257
|
});
|
|
19212
19258
|
var NodeInferenceDevicesSchema = object({
|
|
19213
19259
|
nodeId: string(),
|
|
@@ -19766,6 +19812,7 @@ var snapshotCapability = {
|
|
|
19766
19812
|
isBattery: boolean(),
|
|
19767
19813
|
reason: _enum([
|
|
19768
19814
|
"disabled",
|
|
19815
|
+
"offline",
|
|
19769
19816
|
"sleeping",
|
|
19770
19817
|
"unreachable",
|
|
19771
19818
|
"waking"
|
|
@@ -19855,9 +19902,17 @@ targets: array(object({
|
|
|
19855
19902
|
sleeping: boolean(),
|
|
19856
19903
|
/** Current device state rendered over the cached frame. State images
|
|
19857
19904
|
* remain authoritative even when their photographic background is
|
|
19858
|
-
* old; null means the link must carry a current camera frame.
|
|
19905
|
+
* old; null means the link must carry a current camera frame.
|
|
19906
|
+
*
|
|
19907
|
+
* **This is the ONLY source a surface may derive an inactive tile
|
|
19908
|
+
* from.** A client that reads a device's `online`/`disabled` flags and
|
|
19909
|
+
* draws its own verdict will disagree with the pixels it is painting
|
|
19910
|
+
* — the frame comes from here, and so must the sentence over it
|
|
19911
|
+
* (D190). Pair with `ageMs` for the age of the photograph behind the
|
|
19912
|
+
* state. */
|
|
19859
19913
|
stateReason: _enum([
|
|
19860
19914
|
"disabled",
|
|
19915
|
+
"offline",
|
|
19861
19916
|
"sleeping",
|
|
19862
19917
|
"unreachable",
|
|
19863
19918
|
"waking"
|
|
@@ -24153,7 +24208,12 @@ var ListResultSchema = object({
|
|
|
24153
24208
|
probedAt: number()
|
|
24154
24209
|
});
|
|
24155
24210
|
var PreferredSchema = LocalInterfaceSchema.nullable();
|
|
24156
|
-
|
|
24211
|
+
/**
|
|
24212
|
+
* Candidate base URL for the SDK to race on connect. Order matters —
|
|
24213
|
+
* the SDK should attempt these top-to-bottom with a short per-candidate
|
|
24214
|
+
* timeout (e.g. 1500ms) and cache the winner for the session.
|
|
24215
|
+
*/
|
|
24216
|
+
var ConnectionEndpointSchema = object({
|
|
24157
24217
|
/** Operator-facing label (e.g. "LAN — en0", "Public tunnel"). */
|
|
24158
24218
|
label: string(),
|
|
24159
24219
|
/** Fully-formed base URL with scheme + host + port. */
|
|
@@ -24196,7 +24256,42 @@ var GetConnectionEndpointsResultSchema = object({ endpoints: array(object({
|
|
|
24196
24256
|
* ordering between polls.
|
|
24197
24257
|
*/
|
|
24198
24258
|
priority: number()
|
|
24199
|
-
})
|
|
24259
|
+
});
|
|
24260
|
+
/**
|
|
24261
|
+
* Where the advertised local port came from. Ordered most → least
|
|
24262
|
+
* authoritative, and the whole point of returning it: a client must be able to
|
|
24263
|
+
* tell a FACT about the hub's socket from an echo of its own guess.
|
|
24264
|
+
*/
|
|
24265
|
+
var LocalPortSourceEnum = _enum([
|
|
24266
|
+
"server-config",
|
|
24267
|
+
"server-env",
|
|
24268
|
+
"caller-hint",
|
|
24269
|
+
"default"
|
|
24270
|
+
]);
|
|
24271
|
+
/** The port every LAN/loopback `baseUrl` in the same result was built with. */
|
|
24272
|
+
var AdvertisedLocalPortSchema = object({
|
|
24273
|
+
port: number().int().min(1).max(65535),
|
|
24274
|
+
source: LocalPortSourceEnum
|
|
24275
|
+
});
|
|
24276
|
+
var GetConnectionEndpointsResultSchema = object({
|
|
24277
|
+
endpoints: array(ConnectionEndpointSchema).readonly(),
|
|
24278
|
+
/**
|
|
24279
|
+
* The port the hub built the LAN/loopback URLs with, and where that number
|
|
24280
|
+
* came from.
|
|
24281
|
+
*
|
|
24282
|
+
* Returned rather than merely applied, because "the URL is right" and "the
|
|
24283
|
+
* client can KNOW the URL is right" are different properties. A client that
|
|
24284
|
+
* only sees a corrected URL cannot distinguish a hub that fixed the port from
|
|
24285
|
+
* a hub that echoed the port the client sent, so it cannot decide whether to
|
|
24286
|
+
* race the candidate or discard it. With `source` it can: anything but
|
|
24287
|
+
* `caller-hint` is the hub's own socket.
|
|
24288
|
+
*
|
|
24289
|
+
* Absent on hubs predating this field — a client that finds it missing is
|
|
24290
|
+
* talking to an echoing hub and must degrade exactly as it does for
|
|
24291
|
+
* `caller-hint`.
|
|
24292
|
+
*/
|
|
24293
|
+
localPort: AdvertisedLocalPortSchema
|
|
24294
|
+
});
|
|
24200
24295
|
/**
|
|
24201
24296
|
* The chosen outbound endpoint for notification artifacts. `baseUrl: null` =
|
|
24202
24297
|
* AUTO (resolved from the candidate ranking at send time); `resolved` reports
|
|
@@ -24217,8 +24312,13 @@ var AllowedAddressesSchema = object({
|
|
|
24217
24312
|
*/
|
|
24218
24313
|
addresses: array(string()).readonly() });
|
|
24219
24314
|
method(_void(), ListResultSchema), method(_void(), PreferredSchema), method(object({
|
|
24220
|
-
/**
|
|
24221
|
-
|
|
24315
|
+
/**
|
|
24316
|
+
* LEGACY HINT — do not send from new code. Kept optional so clients
|
|
24317
|
+
* written against the echoing contract keep working; the hub uses it
|
|
24318
|
+
* only when it cannot read its own port, and says so via
|
|
24319
|
+
* `localPort.source === 'caller-hint'`.
|
|
24320
|
+
*/
|
|
24321
|
+
port: number().int().min(1).max(65535).optional(),
|
|
24222
24322
|
/** Include `http(s)://127.0.0.1:<port>` as the lowest-priority
|
|
24223
24323
|
* candidate. Default `true`. */
|
|
24224
24324
|
includeLoopback: boolean().optional(),
|
|
@@ -30325,11 +30425,63 @@ DeviceType.Cover, DeviceType.Valve, DeviceType.Humidifier, DeviceType.WaterHeate
|
|
|
30325
30425
|
new Set(Object.values(DeviceType));
|
|
30326
30426
|
DeviceFeature.BatteryOperated;
|
|
30327
30427
|
var LAST_FETCHED_FIELD = "lastFetchedAt";
|
|
30428
|
+
/**
|
|
30429
|
+
* How long a bridge stops re-attempting a refresh that did not land.
|
|
30430
|
+
*
|
|
30431
|
+
* Sized against the failure it exists for: a camera whose control plane
|
|
30432
|
+
* is unreachable costs the FULL connect give-up (~3s on Linux when the
|
|
30433
|
+
* neighbour never answers, up to the client's own timeout otherwise) on
|
|
30434
|
+
* every attempt, and a failed refresh never advances `lastFetchedAt`, so
|
|
30435
|
+
* without a cooldown the slice is permanently stale and EVERY read pays
|
|
30436
|
+
* that price. One attempt per minute is enough to notice the camera
|
|
30437
|
+
* coming back; per-read is enough to stall the viewer's first paint.
|
|
30438
|
+
*/
|
|
30439
|
+
var RUNTIME_STATE_REFRESH_MISS_COOLDOWN_MS = 6e4;
|
|
30440
|
+
/**
|
|
30441
|
+
* The freshness window for a cap whose value changes ONLY when WE write it.
|
|
30442
|
+
*
|
|
30443
|
+
* The usual `staleMs: 10_000` is tuned for a reading that moves on its own — a
|
|
30444
|
+
* battery level, a day/night state that flips at dusk. It is the wrong number
|
|
30445
|
+
* for a cap like `privacy-mask`, whose two facts (is the video mask on, is the
|
|
30446
|
+
* microphone on) change when an operator changes them, through a `setMask` /
|
|
30447
|
+
* `setAudioEnabled` that **re-reads the camera and rewrites this very slice**.
|
|
30448
|
+
* A write is therefore its own invalidation: the window never delays an
|
|
30449
|
+
* operator's own change, however long it is.
|
|
30450
|
+
*
|
|
30451
|
+
* What the window really bounds is the ONE case we do not write: somebody
|
|
30452
|
+
* changing the mask in the vendor app or the camera's web UI. Ten minutes is
|
|
30453
|
+
* the trade — that change is visible within ten minutes, and a fleet of 29
|
|
30454
|
+
* cameras behind a page that polls every 5s costs about 3 camera round trips a
|
|
30455
|
+
* minute instead of 696
|
|
30456
|
+
* ([D221](../../../../docs/decisions/adr-0221-a-polled-list-never-dials-a-camera.md)).
|
|
30457
|
+
*
|
|
30458
|
+
* Use it ONLY with {@link RuntimeStateStaleReadPolicy} `'serve-and-revalidate'`.
|
|
30459
|
+
* On the awaiting default a window this long does not remove the stall, it
|
|
30460
|
+
* merely makes it rarer and just as long.
|
|
30461
|
+
*/
|
|
30462
|
+
var OPERATOR_WRITTEN_STALE_MS = 10 * 6e4;
|
|
30328
30463
|
function createRuntimeStateBridge(params) {
|
|
30329
30464
|
const { runtimeState, cap, ownDeviceId, refresh, staleMs, empty, logger } = params;
|
|
30330
30465
|
const missCooldownMs = params.refreshMissCooldownMs ?? 6e4;
|
|
30466
|
+
const staleRead = params.staleRead ?? "await-refresh";
|
|
30331
30467
|
/** Epoch ms until which a refresh is not re-attempted. 0 = no cooldown. */
|
|
30332
30468
|
let missCooldownUntil = 0;
|
|
30469
|
+
/**
|
|
30470
|
+
* The refresh this bridge currently has in the air, if any.
|
|
30471
|
+
*
|
|
30472
|
+
* Providers single-flight their own camera client, so this is not what stops
|
|
30473
|
+
* a second round trip. What it stops is a POLLED reader JOINING one: under
|
|
30474
|
+
* `'serve-and-revalidate'` a read that finds a refresh already outstanding is
|
|
30475
|
+
* answered from the slice at once — even a cold, empty slice, which reports
|
|
30476
|
+
* UNKNOWN, which is the truth about a camera nobody has reached.
|
|
30477
|
+
*
|
|
30478
|
+
* That distinction is the whole cost of an unreachable camera. Measured on
|
|
30479
|
+
* the live hub (device 3629, an offline battery Reolink): its refresh takes
|
|
30480
|
+
* 23.1s to give up, so without this every 5s poll landing inside those 23s
|
|
30481
|
+
* joined the wait and spent the caller's full 1.2s source budget — five
|
|
30482
|
+
* stalled polls per cooldown cycle, for one camera, forever.
|
|
30483
|
+
*/
|
|
30484
|
+
let refreshInFlight = null;
|
|
30333
30485
|
const readFetchedAt = () => {
|
|
30334
30486
|
const value = runtimeState.getCapState(cap.name)?.[LAST_FETCHED_FIELD];
|
|
30335
30487
|
return typeof value === "number" ? value : 0;
|
|
@@ -30350,14 +30502,14 @@ function createRuntimeStateBridge(params) {
|
|
|
30350
30502
|
}
|
|
30351
30503
|
});
|
|
30352
30504
|
};
|
|
30353
|
-
|
|
30354
|
-
|
|
30355
|
-
|
|
30356
|
-
|
|
30357
|
-
|
|
30358
|
-
|
|
30359
|
-
|
|
30360
|
-
|
|
30505
|
+
/**
|
|
30506
|
+
* One refresh attempt, plus the LANDED check that decides the cooldown.
|
|
30507
|
+
*
|
|
30508
|
+
* @param fetchedAt What `lastFetchedAt` was before the attempt — the only
|
|
30509
|
+
* evidence the bridge has that the refresh persisted
|
|
30510
|
+
* anything, since providers swallow their own camera errors.
|
|
30511
|
+
*/
|
|
30512
|
+
const runRefresh = async (fetchedAt) => {
|
|
30361
30513
|
try {
|
|
30362
30514
|
await refresh();
|
|
30363
30515
|
} catch (err) {
|
|
@@ -30373,6 +30525,41 @@ function createRuntimeStateBridge(params) {
|
|
|
30373
30525
|
}
|
|
30374
30526
|
openMissCooldown(void 0);
|
|
30375
30527
|
};
|
|
30528
|
+
/**
|
|
30529
|
+
* Start a refresh and remember it, at most one at a time.
|
|
30530
|
+
*
|
|
30531
|
+
* It never rejects: under `'serve-and-revalidate'` the caller is answered
|
|
30532
|
+
* from the slice either way, so a floating rejection would take the process
|
|
30533
|
+
* down for a fault the miss cooldown has already recorded and logged.
|
|
30534
|
+
*/
|
|
30535
|
+
const startRefresh = (fetchedAt) => {
|
|
30536
|
+
const existing = refreshInFlight;
|
|
30537
|
+
if (existing !== null) return existing;
|
|
30538
|
+
const started = runRefresh(fetchedAt).catch(() => void 0).finally(() => {
|
|
30539
|
+
refreshInFlight = null;
|
|
30540
|
+
});
|
|
30541
|
+
refreshInFlight = started;
|
|
30542
|
+
return started;
|
|
30543
|
+
};
|
|
30544
|
+
const ensureFresh = async () => {
|
|
30545
|
+
const slice = runtimeState.getCapState(cap.name);
|
|
30546
|
+
const fetchedAt = readFetchedAt();
|
|
30547
|
+
if (slice && Date.now() - fetchedAt <= staleMs) {
|
|
30548
|
+
missCooldownUntil = 0;
|
|
30549
|
+
return;
|
|
30550
|
+
}
|
|
30551
|
+
if (Date.now() < missCooldownUntil) return;
|
|
30552
|
+
if (staleRead === "serve-and-revalidate") {
|
|
30553
|
+
if (refreshInFlight !== null) return;
|
|
30554
|
+
if (slice && fetchedAt > 0) {
|
|
30555
|
+
startRefresh(fetchedAt);
|
|
30556
|
+
return;
|
|
30557
|
+
}
|
|
30558
|
+
await startRefresh(fetchedAt);
|
|
30559
|
+
return;
|
|
30560
|
+
}
|
|
30561
|
+
await runRefresh(fetchedAt);
|
|
30562
|
+
};
|
|
30376
30563
|
const projectStatus = () => {
|
|
30377
30564
|
const slice = runtimeState.getCapState(cap.name);
|
|
30378
30565
|
if (!slice) return empty();
|
|
@@ -37771,7 +37958,6 @@ var NC_AUDIO_DEFAULTS = {
|
|
|
37771
37958
|
hitPercent: 60,
|
|
37772
37959
|
samplingSeconds: 10
|
|
37773
37960
|
};
|
|
37774
|
-
new Map(AUDIO_MACRO_LABELS.flatMap((macro) => macro.icon === void 0 ? [] : [[macro.id, macro.icon]]));
|
|
37775
37961
|
new Set(["devices", "classes"]);
|
|
37776
37962
|
NC_AUDIO_DEFAULTS.hitPercent, NC_AUDIO_DEFAULTS.samplingSeconds;
|
|
37777
37963
|
/**
|
|
@@ -38005,6 +38191,39 @@ TimelapseRuleInputSchema.extend({
|
|
|
38005
38191
|
createdAt: number(),
|
|
38006
38192
|
updatedAt: number()
|
|
38007
38193
|
});
|
|
38194
|
+
Object.freeze(Object.fromEntries([{
|
|
38195
|
+
stepId: "face-embedding",
|
|
38196
|
+
label: "Face recognition model",
|
|
38197
|
+
defaultModelId: "arcface-r100",
|
|
38198
|
+
indexName: "the enrolled face gallery",
|
|
38199
|
+
options: [
|
|
38200
|
+
{
|
|
38201
|
+
id: "arcface-r100",
|
|
38202
|
+
label: "ArcFace (ResNet34)"
|
|
38203
|
+
},
|
|
38204
|
+
{
|
|
38205
|
+
id: "auraface-r100",
|
|
38206
|
+
label: "AuraFace R100"
|
|
38207
|
+
},
|
|
38208
|
+
{
|
|
38209
|
+
id: "inception-resnet-v1",
|
|
38210
|
+
label: "Inception ResNet V1"
|
|
38211
|
+
}
|
|
38212
|
+
]
|
|
38213
|
+
}, {
|
|
38214
|
+
stepId: "clip-embedding",
|
|
38215
|
+
label: "Semantic search model",
|
|
38216
|
+
defaultModelId: "mobileclip-s1",
|
|
38217
|
+
indexName: "the object semantic-search index",
|
|
38218
|
+
options: [{
|
|
38219
|
+
id: "mobileclip-s1",
|
|
38220
|
+
label: "MobileCLIP S1"
|
|
38221
|
+
}, {
|
|
38222
|
+
id: "mobileclip-s2",
|
|
38223
|
+
label: "MobileCLIP S2"
|
|
38224
|
+
}]
|
|
38225
|
+
}].map((s) => [s.stepId, s.defaultModelId])));
|
|
38226
|
+
string().min(1);
|
|
38008
38227
|
object({
|
|
38009
38228
|
/**
|
|
38010
38229
|
* Fraction of the box's own size added on EACH side before cutting.
|
|
@@ -38025,10 +38244,21 @@ object({
|
|
|
38025
38244
|
*/
|
|
38026
38245
|
square: boolean()
|
|
38027
38246
|
});
|
|
38028
|
-
|
|
38247
|
+
/**
|
|
38248
|
+
* The convention in force when nobody has configured one — byte-for-byte the
|
|
38249
|
+
* behaviour of the pre-unification LIVE path (`DETAIL_CROP_PADDING_RATIO`).
|
|
38250
|
+
*/
|
|
38251
|
+
var DEFAULT_DETAIL_CROP_CONVENTION = {
|
|
38029
38252
|
paddingRatio: .15,
|
|
38030
38253
|
square: false
|
|
38031
|
-
}
|
|
38254
|
+
};
|
|
38255
|
+
Object.freeze({
|
|
38256
|
+
x: 0,
|
|
38257
|
+
y: 0,
|
|
38258
|
+
w: 1,
|
|
38259
|
+
h: 1
|
|
38260
|
+
});
|
|
38261
|
+
DEFAULT_DETAIL_CROP_CONVENTION.paddingRatio;
|
|
38032
38262
|
/**
|
|
38033
38263
|
* WHICH delivered frames the decode worker retains a native copy of.
|
|
38034
38264
|
*
|
|
@@ -232313,6 +232543,34 @@ var ReolinkCamera = class ReolinkCamera extends BaseDevice {
|
|
|
232313
232543
|
* within a day while removing the per-poll round-trip entirely. */
|
|
232314
232544
|
static CAP_OPTIONS_TTL_MS = 360 * 6e4;
|
|
232315
232545
|
/**
|
|
232546
|
+
* Per-cap epoch-ms until which a FAILED options probe is not re-attempted.
|
|
232547
|
+
*
|
|
232548
|
+
* The same discipline `createRuntimeStateBridge` applies to a refresh that
|
|
232549
|
+
* did not land, for the same reason and against the same measurement. A probe
|
|
232550
|
+
* that fails persists nothing, so without this the next caller probes again —
|
|
232551
|
+
* and on a camera that is merely unreachable each attempt burns the full
|
|
232552
|
+
* connect give-up. Measured on the live hub (2026-08-21, device 3629, a
|
|
232553
|
+
* battery Reolink the registry already calls offline): `getOptions` took
|
|
232554
|
+
* **23.1s on every single call**, while its `getStatus` sibling answered in
|
|
232555
|
+
* 8ms because the bridge had already opened its cooldown. The status side was
|
|
232556
|
+
* bounded and the options side was not, and only one of them was visible.
|
|
232557
|
+
*/
|
|
232558
|
+
capOptionsMissCooldownUntil = /* @__PURE__ */ new Map();
|
|
232559
|
+
/**
|
|
232560
|
+
* Per-cap in-flight options probe, so concurrent callers share ONE.
|
|
232561
|
+
*
|
|
232562
|
+
* The cooldown above only opens once a probe has FAILED — and on an
|
|
232563
|
+
* unreachable camera that takes the full connect give-up (23.1s measured on
|
|
232564
|
+
* device 3629). Every poll arriving inside that window used to start its own
|
|
232565
|
+
* probe, so the cooldown never got a chance to bound anything: the fix for
|
|
232566
|
+
* "one dial per poll" has to cover the dial that is still in the air.
|
|
232567
|
+
*
|
|
232568
|
+
* A caller that finds one here is SERVED, not queued behind it — this method
|
|
232569
|
+
* sits on a polled path, and waiting for a probe that is not coming back is
|
|
232570
|
+
* how one unreachable camera stalls every poll it overlaps.
|
|
232571
|
+
*/
|
|
232572
|
+
capOptionsInFlight = /* @__PURE__ */ new Map();
|
|
232573
|
+
/**
|
|
232316
232574
|
* Read a persisted `getOptions` descriptor, validated with the cap's
|
|
232317
232575
|
* OWN Zod schema. Validation (not a cast) is what makes the
|
|
232318
232576
|
* `z.unknown()` blob type-safe: an entry written by an older addon
|
|
@@ -232370,7 +232628,11 @@ var ReolinkCamera = class ReolinkCamera extends BaseDevice {
|
|
|
232370
232628
|
async resolveCapOptions(params) {
|
|
232371
232629
|
const { capName, schema, probe, fallback } = params;
|
|
232372
232630
|
const cached = this.readCapOptionsCache(capName, schema);
|
|
232373
|
-
if (
|
|
232631
|
+
if (this.capOptionsInFlight.has(capName)) return cached?.value ?? fallback();
|
|
232632
|
+
if (cached && Date.now() - cached.fetchedAt < ReolinkCamera.CAP_OPTIONS_TTL_MS) {
|
|
232633
|
+
this.capOptionsMissCooldownUntil.delete(capName);
|
|
232634
|
+
return cached.value;
|
|
232635
|
+
}
|
|
232374
232636
|
if (this.isBattery && this.sleeping) {
|
|
232375
232637
|
this.ctx.logger.debug("cap options: battery cam sleeping — serving cache, not probing", {
|
|
232376
232638
|
tags: { deviceId: this.id },
|
|
@@ -232381,19 +232643,29 @@ var ReolinkCamera = class ReolinkCamera extends BaseDevice {
|
|
|
232381
232643
|
});
|
|
232382
232644
|
return cached?.value ?? fallback();
|
|
232383
232645
|
}
|
|
232646
|
+
const cooldownUntil = this.capOptionsMissCooldownUntil.get(capName) ?? 0;
|
|
232647
|
+
if (Date.now() < cooldownUntil) return cached?.value ?? fallback();
|
|
232648
|
+
const started = probe();
|
|
232649
|
+
this.capOptionsInFlight.set(capName, started);
|
|
232384
232650
|
try {
|
|
232385
|
-
const value = await
|
|
232651
|
+
const value = await started;
|
|
232386
232652
|
await this.persistCapOptions(capName, value);
|
|
232653
|
+
this.capOptionsMissCooldownUntil.delete(capName);
|
|
232387
232654
|
return value;
|
|
232388
232655
|
} catch (err) {
|
|
232389
|
-
this.
|
|
232656
|
+
this.capOptionsMissCooldownUntil.set(capName, Date.now() + RUNTIME_STATE_REFRESH_MISS_COOLDOWN_MS);
|
|
232657
|
+
this.ctx.logger.warn("cap options probe failed — serving cache/fallback and not re-probing for a minute", {
|
|
232390
232658
|
tags: { deviceId: this.id },
|
|
232391
232659
|
meta: {
|
|
232392
232660
|
capName,
|
|
232661
|
+
cooldownMs: RUNTIME_STATE_REFRESH_MISS_COOLDOWN_MS,
|
|
232662
|
+
hasCache: cached !== null,
|
|
232393
232663
|
error: err instanceof Error ? err.message : String(err)
|
|
232394
232664
|
}
|
|
232395
232665
|
});
|
|
232396
232666
|
return cached?.value ?? fallback();
|
|
232667
|
+
} finally {
|
|
232668
|
+
if (this.capOptionsInFlight.get(capName) === started) this.capOptionsInFlight.delete(capName);
|
|
232397
232669
|
}
|
|
232398
232670
|
}
|
|
232399
232671
|
/**
|
|
@@ -232834,7 +233106,6 @@ var ReolinkCamera = class ReolinkCamera extends BaseDevice {
|
|
|
232834
233106
|
registerPrivacyMaskCap() {
|
|
232835
233107
|
const channel = this.getChannel();
|
|
232836
233108
|
const CAP_NAME = "privacy-mask";
|
|
232837
|
-
const STALE_MS = 1e4;
|
|
232838
233109
|
const refreshFromCamera = async () => {
|
|
232839
233110
|
if (this.privacyMaskRefreshInFlight) return this.privacyMaskRefreshInFlight;
|
|
232840
233111
|
const promise = (async () => {
|
|
@@ -232887,7 +233158,8 @@ var ReolinkCamera = class ReolinkCamera extends BaseDevice {
|
|
|
232887
233158
|
cap: privacyMaskCapability,
|
|
232888
233159
|
ownDeviceId: this.id,
|
|
232889
233160
|
refresh: this.sleepGatedRefresh(CAP_NAME, refreshFromCamera),
|
|
232890
|
-
staleMs:
|
|
233161
|
+
staleMs: OPERATOR_WRITTEN_STALE_MS,
|
|
233162
|
+
staleRead: "serve-and-revalidate",
|
|
232891
233163
|
empty: () => ({
|
|
232892
233164
|
enabled: false,
|
|
232893
233165
|
regions: [],
|
package/dist/addon.mjs
CHANGED
|
@@ -7600,7 +7600,7 @@ var RecordingBandSchema = object({
|
|
|
7600
7600
|
postBufferSec: number().min(0).optional()
|
|
7601
7601
|
});
|
|
7602
7602
|
({
|
|
7603
|
-
preBufferSec:
|
|
7603
|
+
preBufferSec: 15,
|
|
7604
7604
|
postBufferSec: 30
|
|
7605
7605
|
}).postBufferSec * 1e3;
|
|
7606
7606
|
/**
|
|
@@ -13990,128 +13990,103 @@ var COCO_TO_MACRO = {
|
|
|
13990
13990
|
var AUDIO_MACRO_LABELS = [
|
|
13991
13991
|
{
|
|
13992
13992
|
id: "speech",
|
|
13993
|
-
name: "Speech"
|
|
13994
|
-
icon: "🗣️"
|
|
13993
|
+
name: "Speech"
|
|
13995
13994
|
},
|
|
13996
13995
|
{
|
|
13997
13996
|
id: "scream",
|
|
13998
|
-
name: "Scream / Shout"
|
|
13999
|
-
icon: "😱"
|
|
13997
|
+
name: "Scream / Shout"
|
|
14000
13998
|
},
|
|
14001
13999
|
{
|
|
14002
14000
|
id: "crying",
|
|
14003
|
-
name: "Crying / Baby"
|
|
14004
|
-
icon: "😢"
|
|
14001
|
+
name: "Crying / Baby"
|
|
14005
14002
|
},
|
|
14006
14003
|
{
|
|
14007
14004
|
id: "laughter",
|
|
14008
|
-
name: "Laughter"
|
|
14009
|
-
icon: "😂"
|
|
14005
|
+
name: "Laughter"
|
|
14010
14006
|
},
|
|
14011
14007
|
{
|
|
14012
14008
|
id: "music",
|
|
14013
|
-
name: "Music"
|
|
14014
|
-
icon: "🎵"
|
|
14009
|
+
name: "Music"
|
|
14015
14010
|
},
|
|
14016
14011
|
{
|
|
14017
14012
|
id: "dog",
|
|
14018
|
-
name: "Dog"
|
|
14019
|
-
icon: "🐕"
|
|
14013
|
+
name: "Dog"
|
|
14020
14014
|
},
|
|
14021
14015
|
{
|
|
14022
14016
|
id: "cat",
|
|
14023
|
-
name: "Cat"
|
|
14024
|
-
icon: "🐈"
|
|
14017
|
+
name: "Cat"
|
|
14025
14018
|
},
|
|
14026
14019
|
{
|
|
14027
14020
|
id: "bird",
|
|
14028
|
-
name: "Bird"
|
|
14029
|
-
icon: "🐦"
|
|
14021
|
+
name: "Bird"
|
|
14030
14022
|
},
|
|
14031
14023
|
{
|
|
14032
14024
|
id: "animal",
|
|
14033
|
-
name: "Animal (other)"
|
|
14034
|
-
icon: "🐾"
|
|
14025
|
+
name: "Animal (other)"
|
|
14035
14026
|
},
|
|
14036
14027
|
{
|
|
14037
14028
|
id: "alarm",
|
|
14038
|
-
name: "Alarm / Siren"
|
|
14039
|
-
icon: "🚨"
|
|
14029
|
+
name: "Alarm / Siren"
|
|
14040
14030
|
},
|
|
14041
14031
|
{
|
|
14042
14032
|
id: "doorbell",
|
|
14043
|
-
name: "Doorbell / Knock"
|
|
14044
|
-
icon: "🔔"
|
|
14033
|
+
name: "Doorbell / Knock"
|
|
14045
14034
|
},
|
|
14046
14035
|
{
|
|
14047
14036
|
id: "glass_breaking",
|
|
14048
|
-
name: "Glass Breaking"
|
|
14049
|
-
icon: "💥"
|
|
14037
|
+
name: "Glass Breaking"
|
|
14050
14038
|
},
|
|
14051
14039
|
{
|
|
14052
14040
|
id: "gunshot",
|
|
14053
|
-
name: "Gunshot / Explosion"
|
|
14054
|
-
icon: "💣"
|
|
14041
|
+
name: "Gunshot / Explosion"
|
|
14055
14042
|
},
|
|
14056
14043
|
{
|
|
14057
14044
|
id: "vehicle",
|
|
14058
|
-
name: "Vehicle"
|
|
14059
|
-
icon: "🚗"
|
|
14045
|
+
name: "Vehicle"
|
|
14060
14046
|
},
|
|
14061
14047
|
{
|
|
14062
14048
|
id: "siren",
|
|
14063
|
-
name: "Emergency Siren"
|
|
14064
|
-
icon: "🚑"
|
|
14049
|
+
name: "Emergency Siren"
|
|
14065
14050
|
},
|
|
14066
14051
|
{
|
|
14067
14052
|
id: "fire",
|
|
14068
|
-
name: "Fire / Smoke"
|
|
14069
|
-
icon: "🔥"
|
|
14053
|
+
name: "Fire / Smoke"
|
|
14070
14054
|
},
|
|
14071
14055
|
{
|
|
14072
14056
|
id: "water",
|
|
14073
|
-
name: "Water"
|
|
14074
|
-
icon: "💧"
|
|
14057
|
+
name: "Water"
|
|
14075
14058
|
},
|
|
14076
14059
|
{
|
|
14077
14060
|
id: "wind",
|
|
14078
|
-
name: "Wind / Weather"
|
|
14079
|
-
icon: "🌬️"
|
|
14061
|
+
name: "Wind / Weather"
|
|
14080
14062
|
},
|
|
14081
14063
|
{
|
|
14082
14064
|
id: "door",
|
|
14083
|
-
name: "Door"
|
|
14084
|
-
icon: "🚪"
|
|
14065
|
+
name: "Door"
|
|
14085
14066
|
},
|
|
14086
14067
|
{
|
|
14087
14068
|
id: "footsteps",
|
|
14088
|
-
name: "Footsteps"
|
|
14089
|
-
icon: "👣"
|
|
14069
|
+
name: "Footsteps"
|
|
14090
14070
|
},
|
|
14091
14071
|
{
|
|
14092
14072
|
id: "crowd",
|
|
14093
|
-
name: "Crowd / Chatter"
|
|
14094
|
-
icon: "👥"
|
|
14073
|
+
name: "Crowd / Chatter"
|
|
14095
14074
|
},
|
|
14096
14075
|
{
|
|
14097
14076
|
id: "telephone",
|
|
14098
|
-
name: "Telephone"
|
|
14099
|
-
icon: "📞"
|
|
14077
|
+
name: "Telephone"
|
|
14100
14078
|
},
|
|
14101
14079
|
{
|
|
14102
14080
|
id: "engine",
|
|
14103
|
-
name: "Engine / Motor"
|
|
14104
|
-
icon: "⚙️"
|
|
14081
|
+
name: "Engine / Motor"
|
|
14105
14082
|
},
|
|
14106
14083
|
{
|
|
14107
14084
|
id: "tools",
|
|
14108
|
-
name: "Tools / Construction"
|
|
14109
|
-
icon: "🔨"
|
|
14085
|
+
name: "Tools / Construction"
|
|
14110
14086
|
},
|
|
14111
14087
|
{
|
|
14112
14088
|
id: "silence",
|
|
14113
|
-
name: "Silence"
|
|
14114
|
-
icon: "🤫"
|
|
14089
|
+
name: "Silence"
|
|
14115
14090
|
}
|
|
14116
14091
|
];
|
|
14117
14092
|
var YAMNET_TO_MACRO = {
|
|
@@ -16613,7 +16588,7 @@ var TrackEnvelopeSchema = object({
|
|
|
16613
16588
|
* `snapshots[]` references — megabytes across a page of tracks. `slim`
|
|
16614
16589
|
* keeps every scalar the list surfaces actually render (ids, class(es),
|
|
16615
16590
|
* label / audioLabels / importance enrichment, firstSeen/lastSeen, state,
|
|
16616
|
-
* zonesVisited, bestEventId, envelope, hasFace, hasRider) and returns `positions` /
|
|
16591
|
+
* zonesVisited, bestEventId, envelope, hasFace, hasEmbeddedFace, hasRider) and returns `positions` /
|
|
16617
16592
|
* `snapshots` as EMPTY arrays — detail views re-fetch the full row via
|
|
16618
16593
|
* `getTrack`. Mirrors the event-store `projection` convention
|
|
16619
16594
|
* (`getObjectEvents` et al.).
|
|
@@ -16908,6 +16883,35 @@ var TrackSchema = object({
|
|
|
16908
16883
|
*/
|
|
16909
16884
|
hasFace: boolean().optional(),
|
|
16910
16885
|
/**
|
|
16886
|
+
* This track has a face row IN THE GALLERY: a crop **and** an embedding — a
|
|
16887
|
+
* face an operator could ASSIGN to an identity.
|
|
16888
|
+
*
|
|
16889
|
+
* The STRICT twin of {@link hasFace}, and the pair only earns its keep
|
|
16890
|
+
* because the two disagree. `hasFace` is stamped at the TOP of the face
|
|
16891
|
+
* branch, before every gate, and means no more than "a face detector produced
|
|
16892
|
+
* a face detail". This one is stamped at the single moment the gallery row
|
|
16893
|
+
* LANDS — after `FaceRecognizer.onTrackEnd` successfully persists it, i.e.
|
|
16894
|
+
* past the embedding-magnitude verdict, the `minFacePx` detection gate, the
|
|
16895
|
+
* candidate gate, the imageless-track drop (no crop was ever captured) and
|
|
16896
|
+
* the crop-store drop. Everything between the detector and that insert can
|
|
16897
|
+
* legitimately refuse the face, so a flag written any earlier promises the
|
|
16898
|
+
* operator something to assign and delivers nothing.
|
|
16899
|
+
*
|
|
16900
|
+
* **Independent of recognition.** A face collected but never auto-matched is
|
|
16901
|
+
* still assignable — it is in fact the face an operator most wants to reach —
|
|
16902
|
+
* so this is NOT gated on `recognizedIdentityId`. Recognition lands in
|
|
16903
|
+
* `subLabel`; this says only that the raw material exists.
|
|
16904
|
+
*
|
|
16905
|
+
* **Set once, never cleared.** A track that produced a gallery row produced
|
|
16906
|
+
* one; deleting the row later is the gallery's business, not this flag's.
|
|
16907
|
+
*
|
|
16908
|
+
* **Absent ≠ false**, the same rule as {@link hasFace}: every row written
|
|
16909
|
+
* before the column omits it, and so does every server that predates the
|
|
16910
|
+
* field. A consumer must test `=== true` and render nothing otherwise —
|
|
16911
|
+
* never infer "no assignable face".
|
|
16912
|
+
*/
|
|
16913
|
+
hasEmbeddedFace: boolean().optional(),
|
|
16914
|
+
/**
|
|
16911
16915
|
* This subject CONTAINS a folded rider — a person the rider-pairing step
|
|
16912
16916
|
* ([D34](../decisions/adr-0034.md)) removed from the frame BEFORE the tracker,
|
|
16913
16917
|
* so the passage is tracked once and as a VEHICLE.
|
|
@@ -18297,6 +18301,32 @@ var DetailResultSchema = object({
|
|
|
18297
18301
|
/** FRAME-space bbox (already mapped back from crop space). */
|
|
18298
18302
|
bbox: NativeCropBboxSchema.optional(),
|
|
18299
18303
|
embedding: string().optional(),
|
|
18304
|
+
/**
|
|
18305
|
+
* L2 magnitude of the RAW pre-normalization embedding. Additive and optional
|
|
18306
|
+
* so an older runner that does not send it degrades to "unmeasurable", which
|
|
18307
|
+
* the consuming gate treats as accept — never as reject.
|
|
18308
|
+
*/
|
|
18309
|
+
embeddingMagnitude: number().optional(),
|
|
18310
|
+
/**
|
|
18311
|
+
* Model that produced `embedding` — the SAME contract `labelModelId` has for
|
|
18312
|
+
* `label`, and for the same reason: the producing node is the only side that
|
|
18313
|
+
* knows which model actually ran, and re-deriving it downstream is exactly
|
|
18314
|
+
* the inference this model exists to forbid.
|
|
18315
|
+
*
|
|
18316
|
+
* Additive and optional, so a runner that predates it degrades to
|
|
18317
|
+
* "unmeasurable" rather than to a wrong answer — the consumer falls back to
|
|
18318
|
+
* its previous assumption instead of refusing.
|
|
18319
|
+
*
|
|
18320
|
+
* Its absence was not cosmetic. `FaceRecognizer` needs the probe's model id
|
|
18321
|
+
* to use `face-matcher`'s same-model gate (`sample.modelId !== probe.modelId`);
|
|
18322
|
+
* with nothing on the wire it took the id from the GALLERY, so the gate
|
|
18323
|
+
* compared the gallery against itself and could never fire. That turned the
|
|
18324
|
+
* one guard protecting the face feature space into a tautology, and it is
|
|
18325
|
+
* why swapping `face-embedding`'s model would NOT have stranded the enrolled
|
|
18326
|
+
* gallery as the catalog comment predicts — it would have scored the new
|
|
18327
|
+
* model's vectors against the old model's samples in one cosine space.
|
|
18328
|
+
*/
|
|
18329
|
+
embeddingModelId: string().optional(),
|
|
18300
18330
|
label: string().optional(),
|
|
18301
18331
|
/**
|
|
18302
18332
|
* The tier `label` occupies, copied VERBATIM from the producing step's
|
|
@@ -19172,6 +19202,12 @@ var CameraStatusSchema = object({
|
|
|
19172
19202
|
/** Unix timestamp (ms) when this snapshot was composed server-side. */
|
|
19173
19203
|
fetchedAt: number()
|
|
19174
19204
|
});
|
|
19205
|
+
var InferenceDeviceExclusionReasonSchema = _enum([
|
|
19206
|
+
"disabled",
|
|
19207
|
+
"unavailable",
|
|
19208
|
+
"cannot-host-camera-root",
|
|
19209
|
+
"accelerator-preferred"
|
|
19210
|
+
]);
|
|
19175
19211
|
var NodeInferenceDeviceSchema = object({
|
|
19176
19212
|
/** Stable per-node device key, e.g. `openvino:npu`, `edgetpu:usb`, `cpu`. */
|
|
19177
19213
|
key: string(),
|
|
@@ -19202,7 +19238,17 @@ var NodeInferenceDeviceSchema = object({
|
|
|
19202
19238
|
* available per format; this is the stored selection that becomes the
|
|
19203
19239
|
* default for EVERY camera landing on this accelerator.
|
|
19204
19240
|
*/
|
|
19205
|
-
steps: record(string(), DeviceStepConfigSchema).optional()
|
|
19241
|
+
steps: record(string(), DeviceStepConfigSchema).optional(),
|
|
19242
|
+
/**
|
|
19243
|
+
* `null` when the device IS a camera-root candidate on this node; otherwise
|
|
19244
|
+
* the reason the dispatcher drops it. Computed by the SAME
|
|
19245
|
+
* `resolveInferenceDeviceEligibility` the dispatcher runs, so this view can
|
|
19246
|
+
* never disagree with the election — deriving it in the UI from
|
|
19247
|
+
* `enabled`/`available` would silently miss `cannot-host-camera-root` (needs
|
|
19248
|
+
* the node's model catalog) and `accelerator-preferred` (needs the node-wide
|
|
19249
|
+
* "an accelerator is serving" predicate).
|
|
19250
|
+
*/
|
|
19251
|
+
exclusion: InferenceDeviceExclusionReasonSchema.nullable()
|
|
19206
19252
|
});
|
|
19207
19253
|
var NodeInferenceDevicesSchema = object({
|
|
19208
19254
|
nodeId: string(),
|
|
@@ -19761,6 +19807,7 @@ var snapshotCapability = {
|
|
|
19761
19807
|
isBattery: boolean(),
|
|
19762
19808
|
reason: _enum([
|
|
19763
19809
|
"disabled",
|
|
19810
|
+
"offline",
|
|
19764
19811
|
"sleeping",
|
|
19765
19812
|
"unreachable",
|
|
19766
19813
|
"waking"
|
|
@@ -19850,9 +19897,17 @@ targets: array(object({
|
|
|
19850
19897
|
sleeping: boolean(),
|
|
19851
19898
|
/** Current device state rendered over the cached frame. State images
|
|
19852
19899
|
* remain authoritative even when their photographic background is
|
|
19853
|
-
* old; null means the link must carry a current camera frame.
|
|
19900
|
+
* old; null means the link must carry a current camera frame.
|
|
19901
|
+
*
|
|
19902
|
+
* **This is the ONLY source a surface may derive an inactive tile
|
|
19903
|
+
* from.** A client that reads a device's `online`/`disabled` flags and
|
|
19904
|
+
* draws its own verdict will disagree with the pixels it is painting
|
|
19905
|
+
* — the frame comes from here, and so must the sentence over it
|
|
19906
|
+
* (D190). Pair with `ageMs` for the age of the photograph behind the
|
|
19907
|
+
* state. */
|
|
19854
19908
|
stateReason: _enum([
|
|
19855
19909
|
"disabled",
|
|
19910
|
+
"offline",
|
|
19856
19911
|
"sleeping",
|
|
19857
19912
|
"unreachable",
|
|
19858
19913
|
"waking"
|
|
@@ -24148,7 +24203,12 @@ var ListResultSchema = object({
|
|
|
24148
24203
|
probedAt: number()
|
|
24149
24204
|
});
|
|
24150
24205
|
var PreferredSchema = LocalInterfaceSchema.nullable();
|
|
24151
|
-
|
|
24206
|
+
/**
|
|
24207
|
+
* Candidate base URL for the SDK to race on connect. Order matters —
|
|
24208
|
+
* the SDK should attempt these top-to-bottom with a short per-candidate
|
|
24209
|
+
* timeout (e.g. 1500ms) and cache the winner for the session.
|
|
24210
|
+
*/
|
|
24211
|
+
var ConnectionEndpointSchema = object({
|
|
24152
24212
|
/** Operator-facing label (e.g. "LAN — en0", "Public tunnel"). */
|
|
24153
24213
|
label: string(),
|
|
24154
24214
|
/** Fully-formed base URL with scheme + host + port. */
|
|
@@ -24191,7 +24251,42 @@ var GetConnectionEndpointsResultSchema = object({ endpoints: array(object({
|
|
|
24191
24251
|
* ordering between polls.
|
|
24192
24252
|
*/
|
|
24193
24253
|
priority: number()
|
|
24194
|
-
})
|
|
24254
|
+
});
|
|
24255
|
+
/**
|
|
24256
|
+
* Where the advertised local port came from. Ordered most → least
|
|
24257
|
+
* authoritative, and the whole point of returning it: a client must be able to
|
|
24258
|
+
* tell a FACT about the hub's socket from an echo of its own guess.
|
|
24259
|
+
*/
|
|
24260
|
+
var LocalPortSourceEnum = _enum([
|
|
24261
|
+
"server-config",
|
|
24262
|
+
"server-env",
|
|
24263
|
+
"caller-hint",
|
|
24264
|
+
"default"
|
|
24265
|
+
]);
|
|
24266
|
+
/** The port every LAN/loopback `baseUrl` in the same result was built with. */
|
|
24267
|
+
var AdvertisedLocalPortSchema = object({
|
|
24268
|
+
port: number().int().min(1).max(65535),
|
|
24269
|
+
source: LocalPortSourceEnum
|
|
24270
|
+
});
|
|
24271
|
+
var GetConnectionEndpointsResultSchema = object({
|
|
24272
|
+
endpoints: array(ConnectionEndpointSchema).readonly(),
|
|
24273
|
+
/**
|
|
24274
|
+
* The port the hub built the LAN/loopback URLs with, and where that number
|
|
24275
|
+
* came from.
|
|
24276
|
+
*
|
|
24277
|
+
* Returned rather than merely applied, because "the URL is right" and "the
|
|
24278
|
+
* client can KNOW the URL is right" are different properties. A client that
|
|
24279
|
+
* only sees a corrected URL cannot distinguish a hub that fixed the port from
|
|
24280
|
+
* a hub that echoed the port the client sent, so it cannot decide whether to
|
|
24281
|
+
* race the candidate or discard it. With `source` it can: anything but
|
|
24282
|
+
* `caller-hint` is the hub's own socket.
|
|
24283
|
+
*
|
|
24284
|
+
* Absent on hubs predating this field — a client that finds it missing is
|
|
24285
|
+
* talking to an echoing hub and must degrade exactly as it does for
|
|
24286
|
+
* `caller-hint`.
|
|
24287
|
+
*/
|
|
24288
|
+
localPort: AdvertisedLocalPortSchema
|
|
24289
|
+
});
|
|
24195
24290
|
/**
|
|
24196
24291
|
* The chosen outbound endpoint for notification artifacts. `baseUrl: null` =
|
|
24197
24292
|
* AUTO (resolved from the candidate ranking at send time); `resolved` reports
|
|
@@ -24212,8 +24307,13 @@ var AllowedAddressesSchema = object({
|
|
|
24212
24307
|
*/
|
|
24213
24308
|
addresses: array(string()).readonly() });
|
|
24214
24309
|
method(_void(), ListResultSchema), method(_void(), PreferredSchema), method(object({
|
|
24215
|
-
/**
|
|
24216
|
-
|
|
24310
|
+
/**
|
|
24311
|
+
* LEGACY HINT — do not send from new code. Kept optional so clients
|
|
24312
|
+
* written against the echoing contract keep working; the hub uses it
|
|
24313
|
+
* only when it cannot read its own port, and says so via
|
|
24314
|
+
* `localPort.source === 'caller-hint'`.
|
|
24315
|
+
*/
|
|
24316
|
+
port: number().int().min(1).max(65535).optional(),
|
|
24217
24317
|
/** Include `http(s)://127.0.0.1:<port>` as the lowest-priority
|
|
24218
24318
|
* candidate. Default `true`. */
|
|
24219
24319
|
includeLoopback: boolean().optional(),
|
|
@@ -30320,11 +30420,63 @@ DeviceType.Cover, DeviceType.Valve, DeviceType.Humidifier, DeviceType.WaterHeate
|
|
|
30320
30420
|
new Set(Object.values(DeviceType));
|
|
30321
30421
|
DeviceFeature.BatteryOperated;
|
|
30322
30422
|
var LAST_FETCHED_FIELD = "lastFetchedAt";
|
|
30423
|
+
/**
|
|
30424
|
+
* How long a bridge stops re-attempting a refresh that did not land.
|
|
30425
|
+
*
|
|
30426
|
+
* Sized against the failure it exists for: a camera whose control plane
|
|
30427
|
+
* is unreachable costs the FULL connect give-up (~3s on Linux when the
|
|
30428
|
+
* neighbour never answers, up to the client's own timeout otherwise) on
|
|
30429
|
+
* every attempt, and a failed refresh never advances `lastFetchedAt`, so
|
|
30430
|
+
* without a cooldown the slice is permanently stale and EVERY read pays
|
|
30431
|
+
* that price. One attempt per minute is enough to notice the camera
|
|
30432
|
+
* coming back; per-read is enough to stall the viewer's first paint.
|
|
30433
|
+
*/
|
|
30434
|
+
var RUNTIME_STATE_REFRESH_MISS_COOLDOWN_MS = 6e4;
|
|
30435
|
+
/**
|
|
30436
|
+
* The freshness window for a cap whose value changes ONLY when WE write it.
|
|
30437
|
+
*
|
|
30438
|
+
* The usual `staleMs: 10_000` is tuned for a reading that moves on its own — a
|
|
30439
|
+
* battery level, a day/night state that flips at dusk. It is the wrong number
|
|
30440
|
+
* for a cap like `privacy-mask`, whose two facts (is the video mask on, is the
|
|
30441
|
+
* microphone on) change when an operator changes them, through a `setMask` /
|
|
30442
|
+
* `setAudioEnabled` that **re-reads the camera and rewrites this very slice**.
|
|
30443
|
+
* A write is therefore its own invalidation: the window never delays an
|
|
30444
|
+
* operator's own change, however long it is.
|
|
30445
|
+
*
|
|
30446
|
+
* What the window really bounds is the ONE case we do not write: somebody
|
|
30447
|
+
* changing the mask in the vendor app or the camera's web UI. Ten minutes is
|
|
30448
|
+
* the trade — that change is visible within ten minutes, and a fleet of 29
|
|
30449
|
+
* cameras behind a page that polls every 5s costs about 3 camera round trips a
|
|
30450
|
+
* minute instead of 696
|
|
30451
|
+
* ([D221](../../../../docs/decisions/adr-0221-a-polled-list-never-dials-a-camera.md)).
|
|
30452
|
+
*
|
|
30453
|
+
* Use it ONLY with {@link RuntimeStateStaleReadPolicy} `'serve-and-revalidate'`.
|
|
30454
|
+
* On the awaiting default a window this long does not remove the stall, it
|
|
30455
|
+
* merely makes it rarer and just as long.
|
|
30456
|
+
*/
|
|
30457
|
+
var OPERATOR_WRITTEN_STALE_MS = 10 * 6e4;
|
|
30323
30458
|
function createRuntimeStateBridge(params) {
|
|
30324
30459
|
const { runtimeState, cap, ownDeviceId, refresh, staleMs, empty, logger } = params;
|
|
30325
30460
|
const missCooldownMs = params.refreshMissCooldownMs ?? 6e4;
|
|
30461
|
+
const staleRead = params.staleRead ?? "await-refresh";
|
|
30326
30462
|
/** Epoch ms until which a refresh is not re-attempted. 0 = no cooldown. */
|
|
30327
30463
|
let missCooldownUntil = 0;
|
|
30464
|
+
/**
|
|
30465
|
+
* The refresh this bridge currently has in the air, if any.
|
|
30466
|
+
*
|
|
30467
|
+
* Providers single-flight their own camera client, so this is not what stops
|
|
30468
|
+
* a second round trip. What it stops is a POLLED reader JOINING one: under
|
|
30469
|
+
* `'serve-and-revalidate'` a read that finds a refresh already outstanding is
|
|
30470
|
+
* answered from the slice at once — even a cold, empty slice, which reports
|
|
30471
|
+
* UNKNOWN, which is the truth about a camera nobody has reached.
|
|
30472
|
+
*
|
|
30473
|
+
* That distinction is the whole cost of an unreachable camera. Measured on
|
|
30474
|
+
* the live hub (device 3629, an offline battery Reolink): its refresh takes
|
|
30475
|
+
* 23.1s to give up, so without this every 5s poll landing inside those 23s
|
|
30476
|
+
* joined the wait and spent the caller's full 1.2s source budget — five
|
|
30477
|
+
* stalled polls per cooldown cycle, for one camera, forever.
|
|
30478
|
+
*/
|
|
30479
|
+
let refreshInFlight = null;
|
|
30328
30480
|
const readFetchedAt = () => {
|
|
30329
30481
|
const value = runtimeState.getCapState(cap.name)?.[LAST_FETCHED_FIELD];
|
|
30330
30482
|
return typeof value === "number" ? value : 0;
|
|
@@ -30345,14 +30497,14 @@ function createRuntimeStateBridge(params) {
|
|
|
30345
30497
|
}
|
|
30346
30498
|
});
|
|
30347
30499
|
};
|
|
30348
|
-
|
|
30349
|
-
|
|
30350
|
-
|
|
30351
|
-
|
|
30352
|
-
|
|
30353
|
-
|
|
30354
|
-
|
|
30355
|
-
|
|
30500
|
+
/**
|
|
30501
|
+
* One refresh attempt, plus the LANDED check that decides the cooldown.
|
|
30502
|
+
*
|
|
30503
|
+
* @param fetchedAt What `lastFetchedAt` was before the attempt — the only
|
|
30504
|
+
* evidence the bridge has that the refresh persisted
|
|
30505
|
+
* anything, since providers swallow their own camera errors.
|
|
30506
|
+
*/
|
|
30507
|
+
const runRefresh = async (fetchedAt) => {
|
|
30356
30508
|
try {
|
|
30357
30509
|
await refresh();
|
|
30358
30510
|
} catch (err) {
|
|
@@ -30368,6 +30520,41 @@ function createRuntimeStateBridge(params) {
|
|
|
30368
30520
|
}
|
|
30369
30521
|
openMissCooldown(void 0);
|
|
30370
30522
|
};
|
|
30523
|
+
/**
|
|
30524
|
+
* Start a refresh and remember it, at most one at a time.
|
|
30525
|
+
*
|
|
30526
|
+
* It never rejects: under `'serve-and-revalidate'` the caller is answered
|
|
30527
|
+
* from the slice either way, so a floating rejection would take the process
|
|
30528
|
+
* down for a fault the miss cooldown has already recorded and logged.
|
|
30529
|
+
*/
|
|
30530
|
+
const startRefresh = (fetchedAt) => {
|
|
30531
|
+
const existing = refreshInFlight;
|
|
30532
|
+
if (existing !== null) return existing;
|
|
30533
|
+
const started = runRefresh(fetchedAt).catch(() => void 0).finally(() => {
|
|
30534
|
+
refreshInFlight = null;
|
|
30535
|
+
});
|
|
30536
|
+
refreshInFlight = started;
|
|
30537
|
+
return started;
|
|
30538
|
+
};
|
|
30539
|
+
const ensureFresh = async () => {
|
|
30540
|
+
const slice = runtimeState.getCapState(cap.name);
|
|
30541
|
+
const fetchedAt = readFetchedAt();
|
|
30542
|
+
if (slice && Date.now() - fetchedAt <= staleMs) {
|
|
30543
|
+
missCooldownUntil = 0;
|
|
30544
|
+
return;
|
|
30545
|
+
}
|
|
30546
|
+
if (Date.now() < missCooldownUntil) return;
|
|
30547
|
+
if (staleRead === "serve-and-revalidate") {
|
|
30548
|
+
if (refreshInFlight !== null) return;
|
|
30549
|
+
if (slice && fetchedAt > 0) {
|
|
30550
|
+
startRefresh(fetchedAt);
|
|
30551
|
+
return;
|
|
30552
|
+
}
|
|
30553
|
+
await startRefresh(fetchedAt);
|
|
30554
|
+
return;
|
|
30555
|
+
}
|
|
30556
|
+
await runRefresh(fetchedAt);
|
|
30557
|
+
};
|
|
30371
30558
|
const projectStatus = () => {
|
|
30372
30559
|
const slice = runtimeState.getCapState(cap.name);
|
|
30373
30560
|
if (!slice) return empty();
|
|
@@ -37766,7 +37953,6 @@ var NC_AUDIO_DEFAULTS = {
|
|
|
37766
37953
|
hitPercent: 60,
|
|
37767
37954
|
samplingSeconds: 10
|
|
37768
37955
|
};
|
|
37769
|
-
new Map(AUDIO_MACRO_LABELS.flatMap((macro) => macro.icon === void 0 ? [] : [[macro.id, macro.icon]]));
|
|
37770
37956
|
new Set(["devices", "classes"]);
|
|
37771
37957
|
NC_AUDIO_DEFAULTS.hitPercent, NC_AUDIO_DEFAULTS.samplingSeconds;
|
|
37772
37958
|
/**
|
|
@@ -38000,6 +38186,39 @@ TimelapseRuleInputSchema.extend({
|
|
|
38000
38186
|
createdAt: number(),
|
|
38001
38187
|
updatedAt: number()
|
|
38002
38188
|
});
|
|
38189
|
+
Object.freeze(Object.fromEntries([{
|
|
38190
|
+
stepId: "face-embedding",
|
|
38191
|
+
label: "Face recognition model",
|
|
38192
|
+
defaultModelId: "arcface-r100",
|
|
38193
|
+
indexName: "the enrolled face gallery",
|
|
38194
|
+
options: [
|
|
38195
|
+
{
|
|
38196
|
+
id: "arcface-r100",
|
|
38197
|
+
label: "ArcFace (ResNet34)"
|
|
38198
|
+
},
|
|
38199
|
+
{
|
|
38200
|
+
id: "auraface-r100",
|
|
38201
|
+
label: "AuraFace R100"
|
|
38202
|
+
},
|
|
38203
|
+
{
|
|
38204
|
+
id: "inception-resnet-v1",
|
|
38205
|
+
label: "Inception ResNet V1"
|
|
38206
|
+
}
|
|
38207
|
+
]
|
|
38208
|
+
}, {
|
|
38209
|
+
stepId: "clip-embedding",
|
|
38210
|
+
label: "Semantic search model",
|
|
38211
|
+
defaultModelId: "mobileclip-s1",
|
|
38212
|
+
indexName: "the object semantic-search index",
|
|
38213
|
+
options: [{
|
|
38214
|
+
id: "mobileclip-s1",
|
|
38215
|
+
label: "MobileCLIP S1"
|
|
38216
|
+
}, {
|
|
38217
|
+
id: "mobileclip-s2",
|
|
38218
|
+
label: "MobileCLIP S2"
|
|
38219
|
+
}]
|
|
38220
|
+
}].map((s) => [s.stepId, s.defaultModelId])));
|
|
38221
|
+
string().min(1);
|
|
38003
38222
|
object({
|
|
38004
38223
|
/**
|
|
38005
38224
|
* Fraction of the box's own size added on EACH side before cutting.
|
|
@@ -38020,10 +38239,21 @@ object({
|
|
|
38020
38239
|
*/
|
|
38021
38240
|
square: boolean()
|
|
38022
38241
|
});
|
|
38023
|
-
|
|
38242
|
+
/**
|
|
38243
|
+
* The convention in force when nobody has configured one — byte-for-byte the
|
|
38244
|
+
* behaviour of the pre-unification LIVE path (`DETAIL_CROP_PADDING_RATIO`).
|
|
38245
|
+
*/
|
|
38246
|
+
var DEFAULT_DETAIL_CROP_CONVENTION = {
|
|
38024
38247
|
paddingRatio: .15,
|
|
38025
38248
|
square: false
|
|
38026
|
-
}
|
|
38249
|
+
};
|
|
38250
|
+
Object.freeze({
|
|
38251
|
+
x: 0,
|
|
38252
|
+
y: 0,
|
|
38253
|
+
w: 1,
|
|
38254
|
+
h: 1
|
|
38255
|
+
});
|
|
38256
|
+
DEFAULT_DETAIL_CROP_CONVENTION.paddingRatio;
|
|
38027
38257
|
/**
|
|
38028
38258
|
* WHICH delivered frames the decode worker retains a native copy of.
|
|
38029
38259
|
*
|
|
@@ -232293,6 +232523,34 @@ var ReolinkCamera = class ReolinkCamera extends BaseDevice {
|
|
|
232293
232523
|
* within a day while removing the per-poll round-trip entirely. */
|
|
232294
232524
|
static CAP_OPTIONS_TTL_MS = 360 * 6e4;
|
|
232295
232525
|
/**
|
|
232526
|
+
* Per-cap epoch-ms until which a FAILED options probe is not re-attempted.
|
|
232527
|
+
*
|
|
232528
|
+
* The same discipline `createRuntimeStateBridge` applies to a refresh that
|
|
232529
|
+
* did not land, for the same reason and against the same measurement. A probe
|
|
232530
|
+
* that fails persists nothing, so without this the next caller probes again —
|
|
232531
|
+
* and on a camera that is merely unreachable each attempt burns the full
|
|
232532
|
+
* connect give-up. Measured on the live hub (2026-08-21, device 3629, a
|
|
232533
|
+
* battery Reolink the registry already calls offline): `getOptions` took
|
|
232534
|
+
* **23.1s on every single call**, while its `getStatus` sibling answered in
|
|
232535
|
+
* 8ms because the bridge had already opened its cooldown. The status side was
|
|
232536
|
+
* bounded and the options side was not, and only one of them was visible.
|
|
232537
|
+
*/
|
|
232538
|
+
capOptionsMissCooldownUntil = /* @__PURE__ */ new Map();
|
|
232539
|
+
/**
|
|
232540
|
+
* Per-cap in-flight options probe, so concurrent callers share ONE.
|
|
232541
|
+
*
|
|
232542
|
+
* The cooldown above only opens once a probe has FAILED — and on an
|
|
232543
|
+
* unreachable camera that takes the full connect give-up (23.1s measured on
|
|
232544
|
+
* device 3629). Every poll arriving inside that window used to start its own
|
|
232545
|
+
* probe, so the cooldown never got a chance to bound anything: the fix for
|
|
232546
|
+
* "one dial per poll" has to cover the dial that is still in the air.
|
|
232547
|
+
*
|
|
232548
|
+
* A caller that finds one here is SERVED, not queued behind it — this method
|
|
232549
|
+
* sits on a polled path, and waiting for a probe that is not coming back is
|
|
232550
|
+
* how one unreachable camera stalls every poll it overlaps.
|
|
232551
|
+
*/
|
|
232552
|
+
capOptionsInFlight = /* @__PURE__ */ new Map();
|
|
232553
|
+
/**
|
|
232296
232554
|
* Read a persisted `getOptions` descriptor, validated with the cap's
|
|
232297
232555
|
* OWN Zod schema. Validation (not a cast) is what makes the
|
|
232298
232556
|
* `z.unknown()` blob type-safe: an entry written by an older addon
|
|
@@ -232350,7 +232608,11 @@ var ReolinkCamera = class ReolinkCamera extends BaseDevice {
|
|
|
232350
232608
|
async resolveCapOptions(params) {
|
|
232351
232609
|
const { capName, schema, probe, fallback } = params;
|
|
232352
232610
|
const cached = this.readCapOptionsCache(capName, schema);
|
|
232353
|
-
if (
|
|
232611
|
+
if (this.capOptionsInFlight.has(capName)) return cached?.value ?? fallback();
|
|
232612
|
+
if (cached && Date.now() - cached.fetchedAt < ReolinkCamera.CAP_OPTIONS_TTL_MS) {
|
|
232613
|
+
this.capOptionsMissCooldownUntil.delete(capName);
|
|
232614
|
+
return cached.value;
|
|
232615
|
+
}
|
|
232354
232616
|
if (this.isBattery && this.sleeping) {
|
|
232355
232617
|
this.ctx.logger.debug("cap options: battery cam sleeping — serving cache, not probing", {
|
|
232356
232618
|
tags: { deviceId: this.id },
|
|
@@ -232361,19 +232623,29 @@ var ReolinkCamera = class ReolinkCamera extends BaseDevice {
|
|
|
232361
232623
|
});
|
|
232362
232624
|
return cached?.value ?? fallback();
|
|
232363
232625
|
}
|
|
232626
|
+
const cooldownUntil = this.capOptionsMissCooldownUntil.get(capName) ?? 0;
|
|
232627
|
+
if (Date.now() < cooldownUntil) return cached?.value ?? fallback();
|
|
232628
|
+
const started = probe();
|
|
232629
|
+
this.capOptionsInFlight.set(capName, started);
|
|
232364
232630
|
try {
|
|
232365
|
-
const value = await
|
|
232631
|
+
const value = await started;
|
|
232366
232632
|
await this.persistCapOptions(capName, value);
|
|
232633
|
+
this.capOptionsMissCooldownUntil.delete(capName);
|
|
232367
232634
|
return value;
|
|
232368
232635
|
} catch (err) {
|
|
232369
|
-
this.
|
|
232636
|
+
this.capOptionsMissCooldownUntil.set(capName, Date.now() + RUNTIME_STATE_REFRESH_MISS_COOLDOWN_MS);
|
|
232637
|
+
this.ctx.logger.warn("cap options probe failed — serving cache/fallback and not re-probing for a minute", {
|
|
232370
232638
|
tags: { deviceId: this.id },
|
|
232371
232639
|
meta: {
|
|
232372
232640
|
capName,
|
|
232641
|
+
cooldownMs: RUNTIME_STATE_REFRESH_MISS_COOLDOWN_MS,
|
|
232642
|
+
hasCache: cached !== null,
|
|
232373
232643
|
error: err instanceof Error ? err.message : String(err)
|
|
232374
232644
|
}
|
|
232375
232645
|
});
|
|
232376
232646
|
return cached?.value ?? fallback();
|
|
232647
|
+
} finally {
|
|
232648
|
+
if (this.capOptionsInFlight.get(capName) === started) this.capOptionsInFlight.delete(capName);
|
|
232377
232649
|
}
|
|
232378
232650
|
}
|
|
232379
232651
|
/**
|
|
@@ -232814,7 +233086,6 @@ var ReolinkCamera = class ReolinkCamera extends BaseDevice {
|
|
|
232814
233086
|
registerPrivacyMaskCap() {
|
|
232815
233087
|
const channel = this.getChannel();
|
|
232816
233088
|
const CAP_NAME = "privacy-mask";
|
|
232817
|
-
const STALE_MS = 1e4;
|
|
232818
233089
|
const refreshFromCamera = async () => {
|
|
232819
233090
|
if (this.privacyMaskRefreshInFlight) return this.privacyMaskRefreshInFlight;
|
|
232820
233091
|
const promise = (async () => {
|
|
@@ -232867,7 +233138,8 @@ var ReolinkCamera = class ReolinkCamera extends BaseDevice {
|
|
|
232867
233138
|
cap: privacyMaskCapability,
|
|
232868
233139
|
ownDeviceId: this.id,
|
|
232869
233140
|
refresh: this.sleepGatedRefresh(CAP_NAME, refreshFromCamera),
|
|
232870
|
-
staleMs:
|
|
233141
|
+
staleMs: OPERATOR_WRITTEN_STALE_MS,
|
|
233142
|
+
staleRead: "serve-and-revalidate",
|
|
232871
233143
|
empty: () => ({
|
|
232872
233144
|
enabled: false,
|
|
232873
233145
|
regions: [],
|