@camstack/addon-provider-hikvision 1.2.24 → 1.2.27
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/README.md +0 -1
- package/dist/addon.js +180 -39
- package/dist/addon.mjs +180 -39
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -35,4 +35,3 @@ The probe runs once per camera lifetime (after the first successful `publishToBr
|
|
|
35
35
|
## References
|
|
36
36
|
|
|
37
37
|
- ISAPI Service v2 & v3 specs (Hikvision ISAPI doc package — vendor download).
|
|
38
|
-
- Scrypted Hikvision plugin — `plugins/hikvision/src/` in the [Scrypted](https://github.com/koush/scrypted) repository (port reference for endpoint patterns + alarm-stream parsing).
|
package/dist/addon.js
CHANGED
|
@@ -6,7 +6,7 @@ let node_crypto = require("node:crypto");
|
|
|
6
6
|
let node_http = require("node:http");
|
|
7
7
|
let node_https = require("node:https");
|
|
8
8
|
let node_os = require("node:os");
|
|
9
|
-
//#region ../types/dist/event-category-
|
|
9
|
+
//#region ../types/dist/event-category-C0lyLd5U.mjs
|
|
10
10
|
var EventCategory = /* @__PURE__ */ function(EventCategory) {
|
|
11
11
|
EventCategory["SystemBoot"] = "system.boot";
|
|
12
12
|
EventCategory["SystemAddonsReady"] = "system.addons-ready";
|
|
@@ -23,9 +23,10 @@ var EventCategory = /* @__PURE__ */ function(EventCategory) {
|
|
|
23
23
|
EventCategory["SystemRestartCompleted"] = "system.restart-completed";
|
|
24
24
|
/**
|
|
25
25
|
* A newer addon or server-root package version was found by the
|
|
26
|
-
* authoritative registry check. Emitted once
|
|
27
|
-
* `(
|
|
28
|
-
*
|
|
26
|
+
* authoritative registry check. Emitted once when any observed
|
|
27
|
+
* `latestVersion` changes (or a package/node first appears behind);
|
|
28
|
+
* the payload carries the full currently-available list. Repeated
|
|
29
|
+
* polling of the same latests is silent.
|
|
29
30
|
*/
|
|
30
31
|
EventCategory["UpdateAvailable"] = "update.available";
|
|
31
32
|
/**
|
|
@@ -7543,6 +7544,10 @@ var RecordingBandSchema = object({
|
|
|
7543
7544
|
preBufferSec: number().min(0).optional(),
|
|
7544
7545
|
postBufferSec: number().min(0).optional()
|
|
7545
7546
|
});
|
|
7547
|
+
({
|
|
7548
|
+
preBufferSec: 10,
|
|
7549
|
+
postBufferSec: 30
|
|
7550
|
+
}).postBufferSec * 1e3;
|
|
7546
7551
|
/**
|
|
7547
7552
|
* Per-device retention overrides. Every field is optional; an unset or `0`
|
|
7548
7553
|
* value inherits the node-wide recorder default. Only footage-lifetime limits
|
|
@@ -7590,6 +7595,12 @@ var RecordingConfigSchema = object({
|
|
|
7590
7595
|
/** DERIVED summary of `bands`, stamped by the recorder on every save.
|
|
7591
7596
|
* Authoring it has no effect — see {@link RecordingStorageModeSchema}. */
|
|
7592
7597
|
mode: RecordingStorageModeSchema.optional(),
|
|
7598
|
+
/**
|
|
7599
|
+
* Which assigned broker slots to record. Absent / empty = {@link
|
|
7600
|
+
* DEFAULT_RECORDING_PROFILES} (`high`+`low`) intersected with the
|
|
7601
|
+
* camera's currently assigned slots — never `mid` unless the operator
|
|
7602
|
+
* picks it, and never a slot the broker has not assigned.
|
|
7603
|
+
*/
|
|
7593
7604
|
profiles: array(CamProfileSchema).optional(),
|
|
7594
7605
|
segmentSeconds: number().int().positive().optional(),
|
|
7595
7606
|
/**
|
|
@@ -7670,7 +7681,11 @@ var RelocateFootageInputSchema = object({
|
|
|
7670
7681
|
profiles: array(string()).optional(),
|
|
7671
7682
|
/** Copy throttle in MB/s (default 40) — the drain is a background chore,
|
|
7672
7683
|
* never allowed to starve live writers. */
|
|
7673
|
-
throttleMbps: number().min(1).max(1e3).optional()
|
|
7684
|
+
throttleMbps: number().min(1).max(1e3).optional(),
|
|
7685
|
+
/** Move only segments whose startMs is >= this. Absent = the whole source
|
|
7686
|
+
* pile. Used when a full drain is too expensive and the operator only
|
|
7687
|
+
* wants the recent window on the new disk. */
|
|
7688
|
+
sinceMs: number().int().optional()
|
|
7674
7689
|
});
|
|
7675
7690
|
/** Internal, lease-scoped participant operation. It is intentionally separate
|
|
7676
7691
|
* from persistent recording settings: a migration never changes
|
|
@@ -14729,6 +14744,9 @@ var NcSystemEventKindSchema = _enum([
|
|
|
14729
14744
|
"alarm-disarmed",
|
|
14730
14745
|
"alarm-arming",
|
|
14731
14746
|
"alarm-arm-refused",
|
|
14747
|
+
"addon-updated",
|
|
14748
|
+
"server-updated",
|
|
14749
|
+
"export-completed",
|
|
14732
14750
|
"camera-online",
|
|
14733
14751
|
"camera-offline",
|
|
14734
14752
|
"camera-disabled",
|
|
@@ -16622,13 +16640,19 @@ var RetrainStatusSchema = _enum([
|
|
|
16622
16640
|
* "never marked" from "already trained" must read `retrainStatus`.
|
|
16623
16641
|
*
|
|
16624
16642
|
* `debug` does NOT pin; it is attention, not durability.
|
|
16643
|
+
*
|
|
16644
|
+
* `favourited` IS a BOOLEAN pin (durability bit), not a retrain lifecycle.
|
|
16645
|
+
* A favourited track is skipped by retention the same way `staging` is, but
|
|
16646
|
+
* it does not enter `none|staging|trained` and has no staging budget.
|
|
16625
16647
|
*/
|
|
16626
16648
|
var TrackFlagFields = {
|
|
16627
16649
|
/** Operator marked this track as training material — i.e. `retrainStatus` is
|
|
16628
16650
|
* `'staging'`. */
|
|
16629
16651
|
markForTrain: boolean().optional(),
|
|
16630
16652
|
/** Operator marked this track for diagnostic attention. */
|
|
16631
|
-
debug: boolean().optional()
|
|
16653
|
+
debug: boolean().optional(),
|
|
16654
|
+
/** Operator favourited this track. Pins it against pruning. */
|
|
16655
|
+
favourited: boolean().optional()
|
|
16632
16656
|
};
|
|
16633
16657
|
/**
|
|
16634
16658
|
* The lifecycle field itself, on the READ surfaces only (`Track`, `KeyEvent`).
|
|
@@ -16653,6 +16677,7 @@ var TrackFlagsSchema = object({
|
|
|
16653
16677
|
trackId: string(),
|
|
16654
16678
|
markForTrain: boolean(),
|
|
16655
16679
|
debug: boolean(),
|
|
16680
|
+
favourited: boolean(),
|
|
16656
16681
|
/** The lifecycle state the boolean was derived from. Required here (unlike on
|
|
16657
16682
|
* a track row) because this shape is only ever produced by the write body,
|
|
16658
16683
|
* which always knows it — and a surface that has just written needs to render
|
|
@@ -17285,6 +17310,12 @@ var TrackCascadeCountsSchema = object({
|
|
|
17285
17310
|
/** Per-track CLIP search vectors removed (best-effort). */
|
|
17286
17311
|
embeddings: number().int()
|
|
17287
17312
|
});
|
|
17313
|
+
/** Disk-wins reconcile: media rows whose blobs are gone, then empty tracks. */
|
|
17314
|
+
var DiskReconcileCountsSchema = object({
|
|
17315
|
+
mediaDropped: number().int(),
|
|
17316
|
+
tracks: number().int(),
|
|
17317
|
+
events: number().int()
|
|
17318
|
+
});
|
|
17288
17319
|
/** Event-store footprint for one camera. */
|
|
17289
17320
|
var EventStoreDeviceFootprintSchema = object({
|
|
17290
17321
|
deviceId: number(),
|
|
@@ -17461,6 +17492,9 @@ DeviceType.Camera, method(object({ deviceId: number() }), array(TrackSchema).rea
|
|
|
17461
17492
|
}), method(object({ deviceId: number() }), TrackCascadeCountsSchema, {
|
|
17462
17493
|
kind: "mutation",
|
|
17463
17494
|
auth: "admin"
|
|
17495
|
+
}), method(object({ deviceId: number() }), DiskReconcileCountsSchema, {
|
|
17496
|
+
kind: "mutation",
|
|
17497
|
+
auth: "admin"
|
|
17464
17498
|
}), method(object({
|
|
17465
17499
|
deviceId: number(),
|
|
17466
17500
|
trackIds: array(string()).min(1)
|
|
@@ -19011,6 +19045,24 @@ var CameraStatusDegradationSchema = object({
|
|
|
19011
19045
|
*
|
|
19012
19046
|
* See spec: `docs/superpowers/specs/2026-06-24-camera-status-aggregator-cap.md`
|
|
19013
19047
|
*/
|
|
19048
|
+
var DiskReconcileJobSchema = object({
|
|
19049
|
+
state: _enum([
|
|
19050
|
+
"idle",
|
|
19051
|
+
"running",
|
|
19052
|
+
"done",
|
|
19053
|
+
"error"
|
|
19054
|
+
]),
|
|
19055
|
+
total: number().int().nonnegative(),
|
|
19056
|
+
completed: number().int().nonnegative(),
|
|
19057
|
+
currentDeviceId: number().int().nullable(),
|
|
19058
|
+
failed: array(number().int()).readonly(),
|
|
19059
|
+
mediaDropped: number().int().nonnegative(),
|
|
19060
|
+
tracks: number().int().nonnegative(),
|
|
19061
|
+
events: number().int().nonnegative(),
|
|
19062
|
+
startedAtMs: number().int().nullable(),
|
|
19063
|
+
finishedAtMs: number().int().nullable(),
|
|
19064
|
+
error: string().nullable()
|
|
19065
|
+
});
|
|
19014
19066
|
var CameraStatusSchema = object({
|
|
19015
19067
|
deviceId: number(),
|
|
19016
19068
|
assignment: CameraAssignmentStatusSchema,
|
|
@@ -19242,7 +19294,10 @@ method(StorageMigrationLeaseInputSchema, object({ paused: literal(true) }), {
|
|
|
19242
19294
|
}), CameraSwitchGroupSchema, {
|
|
19243
19295
|
kind: "mutation",
|
|
19244
19296
|
auth: "admin"
|
|
19245
|
-
}), method(object({ deviceId: number() }), CameraStatusSchema), method(object({ deviceIds: array(number()).optional() }), array(CameraStatusSchema).readonly()), method(_void(),
|
|
19297
|
+
}), method(object({ deviceId: number() }), CameraStatusSchema), method(object({ deviceIds: array(number()).optional() }), array(CameraStatusSchema).readonly()), method(_void(), DiskReconcileJobSchema, {
|
|
19298
|
+
kind: "mutation",
|
|
19299
|
+
auth: "admin"
|
|
19300
|
+
}), method(_void(), DiskReconcileJobSchema, { auth: "admin" }), method(_void(), array(PipelineTemplateSchema).readonly()), method(object({
|
|
19246
19301
|
name: string(),
|
|
19247
19302
|
description: string().optional(),
|
|
19248
19303
|
config: CameraPipelineConfigSchema
|
|
@@ -19745,7 +19800,14 @@ targets: array(object({
|
|
|
19745
19800
|
"sleeping",
|
|
19746
19801
|
"unreachable",
|
|
19747
19802
|
"waking"
|
|
19748
|
-
]).nullable()
|
|
19803
|
+
]).nullable(),
|
|
19804
|
+
/** A battery camera (whatever its current state). An AWAKE battery
|
|
19805
|
+
* camera is deliberately NOT recaptured on the poll cadence — every
|
|
19806
|
+
* capture is a camera hit that would keep it out of sleep — so its
|
|
19807
|
+
* cached frame legitimately ages past the currency ceiling while
|
|
19808
|
+
* nothing is streaming. A surface must keep painting it (a fresh
|
|
19809
|
+
* frame is captured at each wake), not blank to "unavailable". */
|
|
19810
|
+
battery: boolean()
|
|
19749
19811
|
})))
|
|
19750
19812
|
},
|
|
19751
19813
|
status: {
|
|
@@ -20442,8 +20504,28 @@ var ClipSchema = object({
|
|
|
20442
20504
|
startMs: number(),
|
|
20443
20505
|
endMs: number()
|
|
20444
20506
|
}),
|
|
20445
|
-
/**
|
|
20446
|
-
thumbnail
|
|
20507
|
+
/**
|
|
20508
|
+
* Lazy thumbnail URL, never inlined.
|
|
20509
|
+
*
|
|
20510
|
+
* Recording-derived clips (events-mode keep-window, and the prepared
|
|
20511
|
+
* continuous event+fragment visit) MUST use the snapshot of the **main
|
|
20512
|
+
* event of the interval** — `getEventMedia({ eventId, kind: 'snapshot' })`
|
|
20513
|
+
* of the event that owns `kind` (object > motion > audio). Do not extract
|
|
20514
|
+
* a keyframe from the recorded segments. Other providers (onboard, HKSV)
|
|
20515
|
+
* mint their own stills.
|
|
20516
|
+
*/
|
|
20517
|
+
thumbnail: string().optional(),
|
|
20518
|
+
/** Analytics event ids that overlap this visit. Empty on footage-only clips.
|
|
20519
|
+
* The default provider's visit grain puts many motion heartbeats on one clip
|
|
20520
|
+
* instead of minting one clip per marker. */
|
|
20521
|
+
eventIds: array(string()).optional(),
|
|
20522
|
+
/** Intra-visit footage holes (GOP rolls, discarded segments) still shorter
|
|
20523
|
+
* than `VISIT_MERGE_GAP_MS`. Playback concatenates around them; the timeline
|
|
20524
|
+
* bar keeps showing them via `recording.getAvailability`. */
|
|
20525
|
+
holes: array(object({
|
|
20526
|
+
startMs: number(),
|
|
20527
|
+
endMs: number()
|
|
20528
|
+
})).optional()
|
|
20447
20529
|
});
|
|
20448
20530
|
var ClipPlaybackSchema = object({
|
|
20449
20531
|
/** HLS master URL through the hub data-plane (Range + token in path). */
|
|
@@ -26862,7 +26944,9 @@ var ExportOptionsSchema = object({
|
|
|
26862
26944
|
includeAudio: boolean(),
|
|
26863
26945
|
maxLifeMs: number().int().positive(),
|
|
26864
26946
|
deleteAfterDownload: boolean(),
|
|
26865
|
-
title: string().max(200).optional()
|
|
26947
|
+
title: string().max(200).optional(),
|
|
26948
|
+
/** Notification-output target ids to ping when this export becomes ready. */
|
|
26949
|
+
notifyTargetIds: array(string().min(1)).max(20).optional()
|
|
26866
26950
|
}).superRefine((v, ctx) => {
|
|
26867
26951
|
if (v.speed !== void 0 && v.timelapse !== void 0) ctx.addIssue({
|
|
26868
26952
|
code: ZodIssueCode.custom,
|
|
@@ -26921,10 +27005,18 @@ var ExportBytesSchema = object({
|
|
|
26921
27005
|
});
|
|
26922
27006
|
method(object({
|
|
26923
27007
|
deviceId: number(),
|
|
26924
|
-
|
|
27008
|
+
/** @deprecated Prefer `profiles`. Kept so timelapse/notifiers keep working. */
|
|
27009
|
+
profile: string().optional(),
|
|
27010
|
+
profiles: array(string()).min(1).optional(),
|
|
26925
27011
|
fromMs: number(),
|
|
26926
27012
|
toMs: number(),
|
|
26927
27013
|
options: ExportOptionsSchema
|
|
27014
|
+
}).superRefine((v, ctx) => {
|
|
27015
|
+
if ((v.profiles !== void 0 && v.profiles.length > 0 ? v.profiles : v.profile !== void 0 ? [v.profile] : []).length < 1) ctx.addIssue({
|
|
27016
|
+
code: ZodIssueCode.custom,
|
|
27017
|
+
message: "pass profiles[] (min 1) or legacy profile",
|
|
27018
|
+
path: ["profiles"]
|
|
27019
|
+
});
|
|
26928
27020
|
}), ExportRecordSchema, {
|
|
26929
27021
|
kind: "mutation",
|
|
26930
27022
|
auth: "protected"
|
|
@@ -30079,15 +30171,6 @@ var BaseDeviceProvider = class extends BaseAddon {
|
|
|
30079
30171
|
labels: ["probe not implemented"]
|
|
30080
30172
|
};
|
|
30081
30173
|
}
|
|
30082
|
-
/**
|
|
30083
|
-
* Top-level devices restored at once in {@link onRestoreDevices}.
|
|
30084
|
-
*
|
|
30085
|
-
* Four covers the fleets this ships to without turning a boot into a burst a
|
|
30086
|
-
* camera NVR answers with a refusal. A provider whose upstream is a single
|
|
30087
|
-
* session with a serial command channel (a Baichuan hub, an NVR that
|
|
30088
|
-
* serialises ISAPI) should lower it; nothing needs to raise it.
|
|
30089
|
-
*/
|
|
30090
|
-
restoreConcurrency = 4;
|
|
30091
30174
|
async restoreDevices(savedDevices) {
|
|
30092
30175
|
await this.onRestoreDevices(savedDevices);
|
|
30093
30176
|
if (savedDevices.length > 0) this.ctx.logger.info(`Restored ${savedDevices.length} ${this.providerName} device(s)`);
|
|
@@ -30142,14 +30225,7 @@ var BaseDeviceProvider = class extends BaseAddon {
|
|
|
30142
30225
|
});
|
|
30143
30226
|
}
|
|
30144
30227
|
};
|
|
30145
|
-
|
|
30146
|
-
await Promise.all(Array.from({ length: Math.min(Math.max(1, this.restoreConcurrency), topLevel.length) }, async () => {
|
|
30147
|
-
for (;;) {
|
|
30148
|
-
const saved = topLevel[nextTopLevel++];
|
|
30149
|
-
if (saved === void 0) return;
|
|
30150
|
-
await restoreOne(saved);
|
|
30151
|
-
}
|
|
30152
|
-
}));
|
|
30228
|
+
await Promise.all(topLevel.map((saved) => restoreOne(saved)));
|
|
30153
30229
|
const childRows = savedDevices.filter((s) => s.parentDeviceId !== null);
|
|
30154
30230
|
for (const saved of childRows) {
|
|
30155
30231
|
const Class = this.deviceClasses[saved.type];
|
|
@@ -33553,6 +33629,12 @@ Object.freeze({
|
|
|
33553
33629
|
addonId: null,
|
|
33554
33630
|
access: "create"
|
|
33555
33631
|
},
|
|
33632
|
+
"pipelineAnalytics.reconcileFromDisk": {
|
|
33633
|
+
capName: "pipeline-analytics",
|
|
33634
|
+
capScope: "device",
|
|
33635
|
+
addonId: null,
|
|
33636
|
+
access: "create"
|
|
33637
|
+
},
|
|
33556
33638
|
"pipelineAnalytics.refreshStorageLocationsForMigration": {
|
|
33557
33639
|
capName: "pipeline-analytics",
|
|
33558
33640
|
capScope: "device",
|
|
@@ -33955,6 +34037,12 @@ Object.freeze({
|
|
|
33955
34037
|
addonId: null,
|
|
33956
34038
|
access: "view"
|
|
33957
34039
|
},
|
|
34040
|
+
"pipelineOrchestrator.getReconcileFromDiskStatus": {
|
|
34041
|
+
capName: "pipeline-orchestrator",
|
|
34042
|
+
capScope: "system",
|
|
34043
|
+
addonId: null,
|
|
34044
|
+
access: "view"
|
|
34045
|
+
},
|
|
33958
34046
|
"pipelineOrchestrator.listAgentSettings": {
|
|
33959
34047
|
capName: "pipeline-orchestrator",
|
|
33960
34048
|
capScope: "system",
|
|
@@ -33979,6 +34067,12 @@ Object.freeze({
|
|
|
33979
34067
|
addonId: null,
|
|
33980
34068
|
access: "create"
|
|
33981
34069
|
},
|
|
34070
|
+
"pipelineOrchestrator.reconcileFromDisk": {
|
|
34071
|
+
capName: "pipeline-orchestrator",
|
|
34072
|
+
capScope: "system",
|
|
34073
|
+
addonId: null,
|
|
34074
|
+
access: "create"
|
|
34075
|
+
},
|
|
33982
34076
|
"pipelineOrchestrator.removeAgentSettings": {
|
|
33983
34077
|
capName: "pipeline-orchestrator",
|
|
33984
34078
|
capScope: "system",
|
|
@@ -36970,6 +37064,11 @@ Object.freeze({
|
|
|
36970
37064
|
form: "single",
|
|
36971
37065
|
optional: true
|
|
36972
37066
|
}],
|
|
37067
|
+
"pipelineAnalytics.reconcileFromDisk": [{
|
|
37068
|
+
name: "deviceId",
|
|
37069
|
+
form: "single",
|
|
37070
|
+
optional: false
|
|
37071
|
+
}],
|
|
36973
37072
|
"pipelineAnalytics.restageRetrainTrack": [{
|
|
36974
37073
|
name: "deviceId",
|
|
36975
37074
|
form: "single",
|
|
@@ -40690,7 +40789,7 @@ async function pumpAlarmStream(stream, boundary, handlers, idleTimeoutMs = ALARM
|
|
|
40690
40789
|
for (const block of parts.blocks) {
|
|
40691
40790
|
const parsed = parseMultipartBlock(block);
|
|
40692
40791
|
if (!parsed) continue;
|
|
40693
|
-
if (parsed.contentType.startsWith("application/xml") || parsed.contentType.startsWith("text/xml")) {
|
|
40792
|
+
if (parsed.contentType.startsWith("application/xml") || parsed.contentType.startsWith("text/xml") || looksLikeAlertXml(parsed.bodyText)) {
|
|
40694
40793
|
const ev = parseAlertXml(parsed.bodyText);
|
|
40695
40794
|
if (ev) handlers.onEvent(ev);
|
|
40696
40795
|
}
|
|
@@ -40722,14 +40821,24 @@ function splitOnBoundary(buf, dashBoundary) {
|
|
|
40722
40821
|
tail
|
|
40723
40822
|
};
|
|
40724
40823
|
}
|
|
40824
|
+
function looksLikeAlertXml(text) {
|
|
40825
|
+
return text.includes("<eventType>") || text.includes("EventNotificationAlert");
|
|
40826
|
+
}
|
|
40725
40827
|
function parseMultipartBlock(block) {
|
|
40726
|
-
|
|
40727
|
-
|
|
40728
|
-
|
|
40729
|
-
if (headerEnd
|
|
40730
|
-
|
|
40731
|
-
|
|
40732
|
-
|
|
40828
|
+
const headerEnd = findHeaderEnd(block, 0);
|
|
40829
|
+
let headerBlob = "";
|
|
40830
|
+
let bodySlice;
|
|
40831
|
+
if (headerEnd >= 0) {
|
|
40832
|
+
headerBlob = Buffer.from(block.subarray(0, headerEnd)).toString("utf8");
|
|
40833
|
+
bodySlice = block.subarray(headerEnd + 4);
|
|
40834
|
+
} else {
|
|
40835
|
+
const raw = Buffer.from(block).toString("utf8");
|
|
40836
|
+
if (!looksLikeAlertXml(raw)) return null;
|
|
40837
|
+
return {
|
|
40838
|
+
contentType: "",
|
|
40839
|
+
bodyText: raw.replace(/\r?\n--$/m, "").trim()
|
|
40840
|
+
};
|
|
40841
|
+
}
|
|
40733
40842
|
const headers = {};
|
|
40734
40843
|
for (const line of headerBlob.split(/\r?\n/)) {
|
|
40735
40844
|
const idx = line.indexOf(":");
|
|
@@ -40745,6 +40854,28 @@ function findHeaderEnd(buf, from) {
|
|
|
40745
40854
|
for (let i = from; i + 3 < buf.length; i++) if (buf[i] === 13 && buf[i + 1] === 10 && buf[i + 2] === 13 && buf[i + 3] === 10) return i;
|
|
40746
40855
|
return -1;
|
|
40747
40856
|
}
|
|
40857
|
+
/**
|
|
40858
|
+
* Event types that imply onboard motion. Compared after `eventType`
|
|
40859
|
+
* is lowercased. Some firmwares emit `regionExit`, others
|
|
40860
|
+
* `regionexiting` — accept both.
|
|
40861
|
+
*/
|
|
40862
|
+
function isHikvisionMotionAlarmType(type) {
|
|
40863
|
+
switch (type) {
|
|
40864
|
+
case "vmd":
|
|
40865
|
+
case "motiondetection":
|
|
40866
|
+
case "linedetection":
|
|
40867
|
+
case "fielddetection":
|
|
40868
|
+
case "regionentrance":
|
|
40869
|
+
case "regionexit":
|
|
40870
|
+
case "regionexiting":
|
|
40871
|
+
case "attendedbaggage":
|
|
40872
|
+
case "unattendedbaggage": return true;
|
|
40873
|
+
default: return false;
|
|
40874
|
+
}
|
|
40875
|
+
}
|
|
40876
|
+
function isHikvisionSmartAlarmType(type) {
|
|
40877
|
+
return type !== "vmd" && type !== "motiondetection" && isHikvisionMotionAlarmType(type);
|
|
40878
|
+
}
|
|
40748
40879
|
function parseAlertXml(xml) {
|
|
40749
40880
|
const type = extractTag(xml, "eventType");
|
|
40750
40881
|
if (!type) return null;
|
|
@@ -44405,7 +44536,7 @@ var HikvisionCamera = class HikvisionCamera extends BaseDevice {
|
|
|
44405
44536
|
handleAlarmEvent(ev) {
|
|
44406
44537
|
const camStreamId = this.resolveAlarmCamStreamId(ev.channelId);
|
|
44407
44538
|
const isMotion = ev.type === "vmd" || ev.type === "motiondetection";
|
|
44408
|
-
const isSmart = ev.type
|
|
44539
|
+
const isSmart = isHikvisionSmartAlarmType(ev.type);
|
|
44409
44540
|
if (isMotion) {
|
|
44410
44541
|
this.handleMotionEvent(ev, camStreamId);
|
|
44411
44542
|
return;
|
|
@@ -44415,7 +44546,17 @@ var HikvisionCamera = class HikvisionCamera extends BaseDevice {
|
|
|
44415
44546
|
this.handleMotionEvent(ev, camStreamId);
|
|
44416
44547
|
return;
|
|
44417
44548
|
}
|
|
44418
|
-
|
|
44549
|
+
if (ev.type === "videoloss") {
|
|
44550
|
+
this.ctx.logger.debug("hikvision: unhandled alarm event", {
|
|
44551
|
+
tags: { deviceId: this.id },
|
|
44552
|
+
meta: {
|
|
44553
|
+
type: ev.type,
|
|
44554
|
+
state: ev.state
|
|
44555
|
+
}
|
|
44556
|
+
});
|
|
44557
|
+
return;
|
|
44558
|
+
}
|
|
44559
|
+
this.ctx.logger.info("hikvision: unhandled alarm event", {
|
|
44419
44560
|
tags: { deviceId: this.id },
|
|
44420
44561
|
meta: {
|
|
44421
44562
|
type: ev.type,
|
package/dist/addon.mjs
CHANGED
|
@@ -7,7 +7,7 @@ import { networkInterfaces } from "node:os";
|
|
|
7
7
|
var __commonJSMin = (cb, mod) => () => (mod || (cb((mod = { exports: {} }).exports, mod), cb = null), mod.exports);
|
|
8
8
|
var __require = /* @__PURE__ */ createRequire(import.meta.url);
|
|
9
9
|
//#endregion
|
|
10
|
-
//#region ../types/dist/event-category-
|
|
10
|
+
//#region ../types/dist/event-category-C0lyLd5U.mjs
|
|
11
11
|
var EventCategory = /* @__PURE__ */ function(EventCategory) {
|
|
12
12
|
EventCategory["SystemBoot"] = "system.boot";
|
|
13
13
|
EventCategory["SystemAddonsReady"] = "system.addons-ready";
|
|
@@ -24,9 +24,10 @@ var EventCategory = /* @__PURE__ */ function(EventCategory) {
|
|
|
24
24
|
EventCategory["SystemRestartCompleted"] = "system.restart-completed";
|
|
25
25
|
/**
|
|
26
26
|
* A newer addon or server-root package version was found by the
|
|
27
|
-
* authoritative registry check. Emitted once
|
|
28
|
-
* `(
|
|
29
|
-
*
|
|
27
|
+
* authoritative registry check. Emitted once when any observed
|
|
28
|
+
* `latestVersion` changes (or a package/node first appears behind);
|
|
29
|
+
* the payload carries the full currently-available list. Repeated
|
|
30
|
+
* polling of the same latests is silent.
|
|
30
31
|
*/
|
|
31
32
|
EventCategory["UpdateAvailable"] = "update.available";
|
|
32
33
|
/**
|
|
@@ -7544,6 +7545,10 @@ var RecordingBandSchema = object({
|
|
|
7544
7545
|
preBufferSec: number().min(0).optional(),
|
|
7545
7546
|
postBufferSec: number().min(0).optional()
|
|
7546
7547
|
});
|
|
7548
|
+
({
|
|
7549
|
+
preBufferSec: 10,
|
|
7550
|
+
postBufferSec: 30
|
|
7551
|
+
}).postBufferSec * 1e3;
|
|
7547
7552
|
/**
|
|
7548
7553
|
* Per-device retention overrides. Every field is optional; an unset or `0`
|
|
7549
7554
|
* value inherits the node-wide recorder default. Only footage-lifetime limits
|
|
@@ -7591,6 +7596,12 @@ var RecordingConfigSchema = object({
|
|
|
7591
7596
|
/** DERIVED summary of `bands`, stamped by the recorder on every save.
|
|
7592
7597
|
* Authoring it has no effect — see {@link RecordingStorageModeSchema}. */
|
|
7593
7598
|
mode: RecordingStorageModeSchema.optional(),
|
|
7599
|
+
/**
|
|
7600
|
+
* Which assigned broker slots to record. Absent / empty = {@link
|
|
7601
|
+
* DEFAULT_RECORDING_PROFILES} (`high`+`low`) intersected with the
|
|
7602
|
+
* camera's currently assigned slots — never `mid` unless the operator
|
|
7603
|
+
* picks it, and never a slot the broker has not assigned.
|
|
7604
|
+
*/
|
|
7594
7605
|
profiles: array(CamProfileSchema).optional(),
|
|
7595
7606
|
segmentSeconds: number().int().positive().optional(),
|
|
7596
7607
|
/**
|
|
@@ -7671,7 +7682,11 @@ var RelocateFootageInputSchema = object({
|
|
|
7671
7682
|
profiles: array(string()).optional(),
|
|
7672
7683
|
/** Copy throttle in MB/s (default 40) — the drain is a background chore,
|
|
7673
7684
|
* never allowed to starve live writers. */
|
|
7674
|
-
throttleMbps: number().min(1).max(1e3).optional()
|
|
7685
|
+
throttleMbps: number().min(1).max(1e3).optional(),
|
|
7686
|
+
/** Move only segments whose startMs is >= this. Absent = the whole source
|
|
7687
|
+
* pile. Used when a full drain is too expensive and the operator only
|
|
7688
|
+
* wants the recent window on the new disk. */
|
|
7689
|
+
sinceMs: number().int().optional()
|
|
7675
7690
|
});
|
|
7676
7691
|
/** Internal, lease-scoped participant operation. It is intentionally separate
|
|
7677
7692
|
* from persistent recording settings: a migration never changes
|
|
@@ -14730,6 +14745,9 @@ var NcSystemEventKindSchema = _enum([
|
|
|
14730
14745
|
"alarm-disarmed",
|
|
14731
14746
|
"alarm-arming",
|
|
14732
14747
|
"alarm-arm-refused",
|
|
14748
|
+
"addon-updated",
|
|
14749
|
+
"server-updated",
|
|
14750
|
+
"export-completed",
|
|
14733
14751
|
"camera-online",
|
|
14734
14752
|
"camera-offline",
|
|
14735
14753
|
"camera-disabled",
|
|
@@ -16623,13 +16641,19 @@ var RetrainStatusSchema = _enum([
|
|
|
16623
16641
|
* "never marked" from "already trained" must read `retrainStatus`.
|
|
16624
16642
|
*
|
|
16625
16643
|
* `debug` does NOT pin; it is attention, not durability.
|
|
16644
|
+
*
|
|
16645
|
+
* `favourited` IS a BOOLEAN pin (durability bit), not a retrain lifecycle.
|
|
16646
|
+
* A favourited track is skipped by retention the same way `staging` is, but
|
|
16647
|
+
* it does not enter `none|staging|trained` and has no staging budget.
|
|
16626
16648
|
*/
|
|
16627
16649
|
var TrackFlagFields = {
|
|
16628
16650
|
/** Operator marked this track as training material — i.e. `retrainStatus` is
|
|
16629
16651
|
* `'staging'`. */
|
|
16630
16652
|
markForTrain: boolean().optional(),
|
|
16631
16653
|
/** Operator marked this track for diagnostic attention. */
|
|
16632
|
-
debug: boolean().optional()
|
|
16654
|
+
debug: boolean().optional(),
|
|
16655
|
+
/** Operator favourited this track. Pins it against pruning. */
|
|
16656
|
+
favourited: boolean().optional()
|
|
16633
16657
|
};
|
|
16634
16658
|
/**
|
|
16635
16659
|
* The lifecycle field itself, on the READ surfaces only (`Track`, `KeyEvent`).
|
|
@@ -16654,6 +16678,7 @@ var TrackFlagsSchema = object({
|
|
|
16654
16678
|
trackId: string(),
|
|
16655
16679
|
markForTrain: boolean(),
|
|
16656
16680
|
debug: boolean(),
|
|
16681
|
+
favourited: boolean(),
|
|
16657
16682
|
/** The lifecycle state the boolean was derived from. Required here (unlike on
|
|
16658
16683
|
* a track row) because this shape is only ever produced by the write body,
|
|
16659
16684
|
* which always knows it — and a surface that has just written needs to render
|
|
@@ -17286,6 +17311,12 @@ var TrackCascadeCountsSchema = object({
|
|
|
17286
17311
|
/** Per-track CLIP search vectors removed (best-effort). */
|
|
17287
17312
|
embeddings: number().int()
|
|
17288
17313
|
});
|
|
17314
|
+
/** Disk-wins reconcile: media rows whose blobs are gone, then empty tracks. */
|
|
17315
|
+
var DiskReconcileCountsSchema = object({
|
|
17316
|
+
mediaDropped: number().int(),
|
|
17317
|
+
tracks: number().int(),
|
|
17318
|
+
events: number().int()
|
|
17319
|
+
});
|
|
17289
17320
|
/** Event-store footprint for one camera. */
|
|
17290
17321
|
var EventStoreDeviceFootprintSchema = object({
|
|
17291
17322
|
deviceId: number(),
|
|
@@ -17462,6 +17493,9 @@ DeviceType.Camera, method(object({ deviceId: number() }), array(TrackSchema).rea
|
|
|
17462
17493
|
}), method(object({ deviceId: number() }), TrackCascadeCountsSchema, {
|
|
17463
17494
|
kind: "mutation",
|
|
17464
17495
|
auth: "admin"
|
|
17496
|
+
}), method(object({ deviceId: number() }), DiskReconcileCountsSchema, {
|
|
17497
|
+
kind: "mutation",
|
|
17498
|
+
auth: "admin"
|
|
17465
17499
|
}), method(object({
|
|
17466
17500
|
deviceId: number(),
|
|
17467
17501
|
trackIds: array(string()).min(1)
|
|
@@ -19012,6 +19046,24 @@ var CameraStatusDegradationSchema = object({
|
|
|
19012
19046
|
*
|
|
19013
19047
|
* See spec: `docs/superpowers/specs/2026-06-24-camera-status-aggregator-cap.md`
|
|
19014
19048
|
*/
|
|
19049
|
+
var DiskReconcileJobSchema = object({
|
|
19050
|
+
state: _enum([
|
|
19051
|
+
"idle",
|
|
19052
|
+
"running",
|
|
19053
|
+
"done",
|
|
19054
|
+
"error"
|
|
19055
|
+
]),
|
|
19056
|
+
total: number().int().nonnegative(),
|
|
19057
|
+
completed: number().int().nonnegative(),
|
|
19058
|
+
currentDeviceId: number().int().nullable(),
|
|
19059
|
+
failed: array(number().int()).readonly(),
|
|
19060
|
+
mediaDropped: number().int().nonnegative(),
|
|
19061
|
+
tracks: number().int().nonnegative(),
|
|
19062
|
+
events: number().int().nonnegative(),
|
|
19063
|
+
startedAtMs: number().int().nullable(),
|
|
19064
|
+
finishedAtMs: number().int().nullable(),
|
|
19065
|
+
error: string().nullable()
|
|
19066
|
+
});
|
|
19015
19067
|
var CameraStatusSchema = object({
|
|
19016
19068
|
deviceId: number(),
|
|
19017
19069
|
assignment: CameraAssignmentStatusSchema,
|
|
@@ -19243,7 +19295,10 @@ method(StorageMigrationLeaseInputSchema, object({ paused: literal(true) }), {
|
|
|
19243
19295
|
}), CameraSwitchGroupSchema, {
|
|
19244
19296
|
kind: "mutation",
|
|
19245
19297
|
auth: "admin"
|
|
19246
|
-
}), method(object({ deviceId: number() }), CameraStatusSchema), method(object({ deviceIds: array(number()).optional() }), array(CameraStatusSchema).readonly()), method(_void(),
|
|
19298
|
+
}), method(object({ deviceId: number() }), CameraStatusSchema), method(object({ deviceIds: array(number()).optional() }), array(CameraStatusSchema).readonly()), method(_void(), DiskReconcileJobSchema, {
|
|
19299
|
+
kind: "mutation",
|
|
19300
|
+
auth: "admin"
|
|
19301
|
+
}), method(_void(), DiskReconcileJobSchema, { auth: "admin" }), method(_void(), array(PipelineTemplateSchema).readonly()), method(object({
|
|
19247
19302
|
name: string(),
|
|
19248
19303
|
description: string().optional(),
|
|
19249
19304
|
config: CameraPipelineConfigSchema
|
|
@@ -19746,7 +19801,14 @@ targets: array(object({
|
|
|
19746
19801
|
"sleeping",
|
|
19747
19802
|
"unreachable",
|
|
19748
19803
|
"waking"
|
|
19749
|
-
]).nullable()
|
|
19804
|
+
]).nullable(),
|
|
19805
|
+
/** A battery camera (whatever its current state). An AWAKE battery
|
|
19806
|
+
* camera is deliberately NOT recaptured on the poll cadence — every
|
|
19807
|
+
* capture is a camera hit that would keep it out of sleep — so its
|
|
19808
|
+
* cached frame legitimately ages past the currency ceiling while
|
|
19809
|
+
* nothing is streaming. A surface must keep painting it (a fresh
|
|
19810
|
+
* frame is captured at each wake), not blank to "unavailable". */
|
|
19811
|
+
battery: boolean()
|
|
19750
19812
|
})))
|
|
19751
19813
|
},
|
|
19752
19814
|
status: {
|
|
@@ -20443,8 +20505,28 @@ var ClipSchema = object({
|
|
|
20443
20505
|
startMs: number(),
|
|
20444
20506
|
endMs: number()
|
|
20445
20507
|
}),
|
|
20446
|
-
/**
|
|
20447
|
-
thumbnail
|
|
20508
|
+
/**
|
|
20509
|
+
* Lazy thumbnail URL, never inlined.
|
|
20510
|
+
*
|
|
20511
|
+
* Recording-derived clips (events-mode keep-window, and the prepared
|
|
20512
|
+
* continuous event+fragment visit) MUST use the snapshot of the **main
|
|
20513
|
+
* event of the interval** — `getEventMedia({ eventId, kind: 'snapshot' })`
|
|
20514
|
+
* of the event that owns `kind` (object > motion > audio). Do not extract
|
|
20515
|
+
* a keyframe from the recorded segments. Other providers (onboard, HKSV)
|
|
20516
|
+
* mint their own stills.
|
|
20517
|
+
*/
|
|
20518
|
+
thumbnail: string().optional(),
|
|
20519
|
+
/** Analytics event ids that overlap this visit. Empty on footage-only clips.
|
|
20520
|
+
* The default provider's visit grain puts many motion heartbeats on one clip
|
|
20521
|
+
* instead of minting one clip per marker. */
|
|
20522
|
+
eventIds: array(string()).optional(),
|
|
20523
|
+
/** Intra-visit footage holes (GOP rolls, discarded segments) still shorter
|
|
20524
|
+
* than `VISIT_MERGE_GAP_MS`. Playback concatenates around them; the timeline
|
|
20525
|
+
* bar keeps showing them via `recording.getAvailability`. */
|
|
20526
|
+
holes: array(object({
|
|
20527
|
+
startMs: number(),
|
|
20528
|
+
endMs: number()
|
|
20529
|
+
})).optional()
|
|
20448
20530
|
});
|
|
20449
20531
|
var ClipPlaybackSchema = object({
|
|
20450
20532
|
/** HLS master URL through the hub data-plane (Range + token in path). */
|
|
@@ -26863,7 +26945,9 @@ var ExportOptionsSchema = object({
|
|
|
26863
26945
|
includeAudio: boolean(),
|
|
26864
26946
|
maxLifeMs: number().int().positive(),
|
|
26865
26947
|
deleteAfterDownload: boolean(),
|
|
26866
|
-
title: string().max(200).optional()
|
|
26948
|
+
title: string().max(200).optional(),
|
|
26949
|
+
/** Notification-output target ids to ping when this export becomes ready. */
|
|
26950
|
+
notifyTargetIds: array(string().min(1)).max(20).optional()
|
|
26867
26951
|
}).superRefine((v, ctx) => {
|
|
26868
26952
|
if (v.speed !== void 0 && v.timelapse !== void 0) ctx.addIssue({
|
|
26869
26953
|
code: ZodIssueCode.custom,
|
|
@@ -26922,10 +27006,18 @@ var ExportBytesSchema = object({
|
|
|
26922
27006
|
});
|
|
26923
27007
|
method(object({
|
|
26924
27008
|
deviceId: number(),
|
|
26925
|
-
|
|
27009
|
+
/** @deprecated Prefer `profiles`. Kept so timelapse/notifiers keep working. */
|
|
27010
|
+
profile: string().optional(),
|
|
27011
|
+
profiles: array(string()).min(1).optional(),
|
|
26926
27012
|
fromMs: number(),
|
|
26927
27013
|
toMs: number(),
|
|
26928
27014
|
options: ExportOptionsSchema
|
|
27015
|
+
}).superRefine((v, ctx) => {
|
|
27016
|
+
if ((v.profiles !== void 0 && v.profiles.length > 0 ? v.profiles : v.profile !== void 0 ? [v.profile] : []).length < 1) ctx.addIssue({
|
|
27017
|
+
code: ZodIssueCode.custom,
|
|
27018
|
+
message: "pass profiles[] (min 1) or legacy profile",
|
|
27019
|
+
path: ["profiles"]
|
|
27020
|
+
});
|
|
26929
27021
|
}), ExportRecordSchema, {
|
|
26930
27022
|
kind: "mutation",
|
|
26931
27023
|
auth: "protected"
|
|
@@ -30080,15 +30172,6 @@ var BaseDeviceProvider = class extends BaseAddon {
|
|
|
30080
30172
|
labels: ["probe not implemented"]
|
|
30081
30173
|
};
|
|
30082
30174
|
}
|
|
30083
|
-
/**
|
|
30084
|
-
* Top-level devices restored at once in {@link onRestoreDevices}.
|
|
30085
|
-
*
|
|
30086
|
-
* Four covers the fleets this ships to without turning a boot into a burst a
|
|
30087
|
-
* camera NVR answers with a refusal. A provider whose upstream is a single
|
|
30088
|
-
* session with a serial command channel (a Baichuan hub, an NVR that
|
|
30089
|
-
* serialises ISAPI) should lower it; nothing needs to raise it.
|
|
30090
|
-
*/
|
|
30091
|
-
restoreConcurrency = 4;
|
|
30092
30175
|
async restoreDevices(savedDevices) {
|
|
30093
30176
|
await this.onRestoreDevices(savedDevices);
|
|
30094
30177
|
if (savedDevices.length > 0) this.ctx.logger.info(`Restored ${savedDevices.length} ${this.providerName} device(s)`);
|
|
@@ -30143,14 +30226,7 @@ var BaseDeviceProvider = class extends BaseAddon {
|
|
|
30143
30226
|
});
|
|
30144
30227
|
}
|
|
30145
30228
|
};
|
|
30146
|
-
|
|
30147
|
-
await Promise.all(Array.from({ length: Math.min(Math.max(1, this.restoreConcurrency), topLevel.length) }, async () => {
|
|
30148
|
-
for (;;) {
|
|
30149
|
-
const saved = topLevel[nextTopLevel++];
|
|
30150
|
-
if (saved === void 0) return;
|
|
30151
|
-
await restoreOne(saved);
|
|
30152
|
-
}
|
|
30153
|
-
}));
|
|
30229
|
+
await Promise.all(topLevel.map((saved) => restoreOne(saved)));
|
|
30154
30230
|
const childRows = savedDevices.filter((s) => s.parentDeviceId !== null);
|
|
30155
30231
|
for (const saved of childRows) {
|
|
30156
30232
|
const Class = this.deviceClasses[saved.type];
|
|
@@ -33554,6 +33630,12 @@ Object.freeze({
|
|
|
33554
33630
|
addonId: null,
|
|
33555
33631
|
access: "create"
|
|
33556
33632
|
},
|
|
33633
|
+
"pipelineAnalytics.reconcileFromDisk": {
|
|
33634
|
+
capName: "pipeline-analytics",
|
|
33635
|
+
capScope: "device",
|
|
33636
|
+
addonId: null,
|
|
33637
|
+
access: "create"
|
|
33638
|
+
},
|
|
33557
33639
|
"pipelineAnalytics.refreshStorageLocationsForMigration": {
|
|
33558
33640
|
capName: "pipeline-analytics",
|
|
33559
33641
|
capScope: "device",
|
|
@@ -33956,6 +34038,12 @@ Object.freeze({
|
|
|
33956
34038
|
addonId: null,
|
|
33957
34039
|
access: "view"
|
|
33958
34040
|
},
|
|
34041
|
+
"pipelineOrchestrator.getReconcileFromDiskStatus": {
|
|
34042
|
+
capName: "pipeline-orchestrator",
|
|
34043
|
+
capScope: "system",
|
|
34044
|
+
addonId: null,
|
|
34045
|
+
access: "view"
|
|
34046
|
+
},
|
|
33959
34047
|
"pipelineOrchestrator.listAgentSettings": {
|
|
33960
34048
|
capName: "pipeline-orchestrator",
|
|
33961
34049
|
capScope: "system",
|
|
@@ -33980,6 +34068,12 @@ Object.freeze({
|
|
|
33980
34068
|
addonId: null,
|
|
33981
34069
|
access: "create"
|
|
33982
34070
|
},
|
|
34071
|
+
"pipelineOrchestrator.reconcileFromDisk": {
|
|
34072
|
+
capName: "pipeline-orchestrator",
|
|
34073
|
+
capScope: "system",
|
|
34074
|
+
addonId: null,
|
|
34075
|
+
access: "create"
|
|
34076
|
+
},
|
|
33983
34077
|
"pipelineOrchestrator.removeAgentSettings": {
|
|
33984
34078
|
capName: "pipeline-orchestrator",
|
|
33985
34079
|
capScope: "system",
|
|
@@ -36971,6 +37065,11 @@ Object.freeze({
|
|
|
36971
37065
|
form: "single",
|
|
36972
37066
|
optional: true
|
|
36973
37067
|
}],
|
|
37068
|
+
"pipelineAnalytics.reconcileFromDisk": [{
|
|
37069
|
+
name: "deviceId",
|
|
37070
|
+
form: "single",
|
|
37071
|
+
optional: false
|
|
37072
|
+
}],
|
|
36974
37073
|
"pipelineAnalytics.restageRetrainTrack": [{
|
|
36975
37074
|
name: "deviceId",
|
|
36976
37075
|
form: "single",
|
|
@@ -40691,7 +40790,7 @@ async function pumpAlarmStream(stream, boundary, handlers, idleTimeoutMs = ALARM
|
|
|
40691
40790
|
for (const block of parts.blocks) {
|
|
40692
40791
|
const parsed = parseMultipartBlock(block);
|
|
40693
40792
|
if (!parsed) continue;
|
|
40694
|
-
if (parsed.contentType.startsWith("application/xml") || parsed.contentType.startsWith("text/xml")) {
|
|
40793
|
+
if (parsed.contentType.startsWith("application/xml") || parsed.contentType.startsWith("text/xml") || looksLikeAlertXml(parsed.bodyText)) {
|
|
40695
40794
|
const ev = parseAlertXml(parsed.bodyText);
|
|
40696
40795
|
if (ev) handlers.onEvent(ev);
|
|
40697
40796
|
}
|
|
@@ -40723,14 +40822,24 @@ function splitOnBoundary(buf, dashBoundary) {
|
|
|
40723
40822
|
tail
|
|
40724
40823
|
};
|
|
40725
40824
|
}
|
|
40825
|
+
function looksLikeAlertXml(text) {
|
|
40826
|
+
return text.includes("<eventType>") || text.includes("EventNotificationAlert");
|
|
40827
|
+
}
|
|
40726
40828
|
function parseMultipartBlock(block) {
|
|
40727
|
-
|
|
40728
|
-
|
|
40729
|
-
|
|
40730
|
-
if (headerEnd
|
|
40731
|
-
|
|
40732
|
-
|
|
40733
|
-
|
|
40829
|
+
const headerEnd = findHeaderEnd(block, 0);
|
|
40830
|
+
let headerBlob = "";
|
|
40831
|
+
let bodySlice;
|
|
40832
|
+
if (headerEnd >= 0) {
|
|
40833
|
+
headerBlob = Buffer.from(block.subarray(0, headerEnd)).toString("utf8");
|
|
40834
|
+
bodySlice = block.subarray(headerEnd + 4);
|
|
40835
|
+
} else {
|
|
40836
|
+
const raw = Buffer.from(block).toString("utf8");
|
|
40837
|
+
if (!looksLikeAlertXml(raw)) return null;
|
|
40838
|
+
return {
|
|
40839
|
+
contentType: "",
|
|
40840
|
+
bodyText: raw.replace(/\r?\n--$/m, "").trim()
|
|
40841
|
+
};
|
|
40842
|
+
}
|
|
40734
40843
|
const headers = {};
|
|
40735
40844
|
for (const line of headerBlob.split(/\r?\n/)) {
|
|
40736
40845
|
const idx = line.indexOf(":");
|
|
@@ -40746,6 +40855,28 @@ function findHeaderEnd(buf, from) {
|
|
|
40746
40855
|
for (let i = from; i + 3 < buf.length; i++) if (buf[i] === 13 && buf[i + 1] === 10 && buf[i + 2] === 13 && buf[i + 3] === 10) return i;
|
|
40747
40856
|
return -1;
|
|
40748
40857
|
}
|
|
40858
|
+
/**
|
|
40859
|
+
* Event types that imply onboard motion. Compared after `eventType`
|
|
40860
|
+
* is lowercased. Some firmwares emit `regionExit`, others
|
|
40861
|
+
* `regionexiting` — accept both.
|
|
40862
|
+
*/
|
|
40863
|
+
function isHikvisionMotionAlarmType(type) {
|
|
40864
|
+
switch (type) {
|
|
40865
|
+
case "vmd":
|
|
40866
|
+
case "motiondetection":
|
|
40867
|
+
case "linedetection":
|
|
40868
|
+
case "fielddetection":
|
|
40869
|
+
case "regionentrance":
|
|
40870
|
+
case "regionexit":
|
|
40871
|
+
case "regionexiting":
|
|
40872
|
+
case "attendedbaggage":
|
|
40873
|
+
case "unattendedbaggage": return true;
|
|
40874
|
+
default: return false;
|
|
40875
|
+
}
|
|
40876
|
+
}
|
|
40877
|
+
function isHikvisionSmartAlarmType(type) {
|
|
40878
|
+
return type !== "vmd" && type !== "motiondetection" && isHikvisionMotionAlarmType(type);
|
|
40879
|
+
}
|
|
40749
40880
|
function parseAlertXml(xml) {
|
|
40750
40881
|
const type = extractTag(xml, "eventType");
|
|
40751
40882
|
if (!type) return null;
|
|
@@ -44406,7 +44537,7 @@ var HikvisionCamera = class HikvisionCamera extends BaseDevice {
|
|
|
44406
44537
|
handleAlarmEvent(ev) {
|
|
44407
44538
|
const camStreamId = this.resolveAlarmCamStreamId(ev.channelId);
|
|
44408
44539
|
const isMotion = ev.type === "vmd" || ev.type === "motiondetection";
|
|
44409
|
-
const isSmart = ev.type
|
|
44540
|
+
const isSmart = isHikvisionSmartAlarmType(ev.type);
|
|
44410
44541
|
if (isMotion) {
|
|
44411
44542
|
this.handleMotionEvent(ev, camStreamId);
|
|
44412
44543
|
return;
|
|
@@ -44416,7 +44547,17 @@ var HikvisionCamera = class HikvisionCamera extends BaseDevice {
|
|
|
44416
44547
|
this.handleMotionEvent(ev, camStreamId);
|
|
44417
44548
|
return;
|
|
44418
44549
|
}
|
|
44419
|
-
|
|
44550
|
+
if (ev.type === "videoloss") {
|
|
44551
|
+
this.ctx.logger.debug("hikvision: unhandled alarm event", {
|
|
44552
|
+
tags: { deviceId: this.id },
|
|
44553
|
+
meta: {
|
|
44554
|
+
type: ev.type,
|
|
44555
|
+
state: ev.state
|
|
44556
|
+
}
|
|
44557
|
+
});
|
|
44558
|
+
return;
|
|
44559
|
+
}
|
|
44560
|
+
this.ctx.logger.info("hikvision: unhandled alarm event", {
|
|
44420
44561
|
tags: { deviceId: this.id },
|
|
44421
44562
|
meta: {
|
|
44422
44563
|
type: ev.type,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@camstack/addon-provider-hikvision",
|
|
3
|
-
"version": "1.2.
|
|
3
|
+
"version": "1.2.27",
|
|
4
4
|
"description": "Hikvision camera device provider addon for CamStack — ISAPI over HTTP(S) with digest auth (snapshot, alarm stream, RTSP discovery)",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"camstack",
|