@camstack/addon-provider-hikvision 1.2.142 → 1.2.144
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 +213 -58
- package/dist/addon.mjs +213 -58
- package/package.json +1 -1
package/dist/addon.js
CHANGED
|
@@ -10802,6 +10802,57 @@ var ClipSearchStrictnessSchema = _enum([
|
|
|
10802
10802
|
"balanced",
|
|
10803
10803
|
"strict"
|
|
10804
10804
|
]);
|
|
10805
|
+
new Set([
|
|
10806
|
+
{
|
|
10807
|
+
id: "person",
|
|
10808
|
+
name: "Person"
|
|
10809
|
+
},
|
|
10810
|
+
{
|
|
10811
|
+
id: "vehicle",
|
|
10812
|
+
name: "Vehicle"
|
|
10813
|
+
},
|
|
10814
|
+
{
|
|
10815
|
+
id: "animal",
|
|
10816
|
+
name: "Animal"
|
|
10817
|
+
},
|
|
10818
|
+
{
|
|
10819
|
+
id: "package",
|
|
10820
|
+
name: "Package"
|
|
10821
|
+
}
|
|
10822
|
+
].map((l) => l.id));
|
|
10823
|
+
var COCO_TO_MACRO = {
|
|
10824
|
+
mapping: {
|
|
10825
|
+
person: "person",
|
|
10826
|
+
bicycle: "vehicle",
|
|
10827
|
+
car: "vehicle",
|
|
10828
|
+
motorcycle: "vehicle",
|
|
10829
|
+
airplane: "vehicle",
|
|
10830
|
+
bus: "vehicle",
|
|
10831
|
+
train: "vehicle",
|
|
10832
|
+
truck: "vehicle",
|
|
10833
|
+
boat: "vehicle",
|
|
10834
|
+
bird: "animal",
|
|
10835
|
+
cat: "animal",
|
|
10836
|
+
dog: "animal",
|
|
10837
|
+
horse: "animal",
|
|
10838
|
+
sheep: "animal",
|
|
10839
|
+
cow: "animal",
|
|
10840
|
+
elephant: "animal",
|
|
10841
|
+
bear: "animal",
|
|
10842
|
+
zebra: "animal",
|
|
10843
|
+
giraffe: "animal",
|
|
10844
|
+
suitcase: "package",
|
|
10845
|
+
backpack: "package",
|
|
10846
|
+
handbag: "package"
|
|
10847
|
+
},
|
|
10848
|
+
preserveOriginal: false
|
|
10849
|
+
};
|
|
10850
|
+
/** Steps a root detector may declare it fills. */
|
|
10851
|
+
var FILLABLE_STEP_IDS = [
|
|
10852
|
+
"animal-classifier",
|
|
10853
|
+
"vehicle-classifier",
|
|
10854
|
+
"package-detection"
|
|
10855
|
+
];
|
|
10805
10856
|
var MODEL_FORMATS = [
|
|
10806
10857
|
"onnx",
|
|
10807
10858
|
"coreml",
|
|
@@ -10934,6 +10985,12 @@ var ModelLicenseSchema = preprocess((value) => typeof value === "string" ? { wei
|
|
|
10934
10985
|
/** True when the weights or their data forbid commercial use. */
|
|
10935
10986
|
nonCommercial: boolean().optional()
|
|
10936
10987
|
}));
|
|
10988
|
+
/** One `fillsClassifierEffects` entry — see `ModelCatalogEntry.fillsClassifierEffects`. */
|
|
10989
|
+
var FillsClassifierEffectSchema = object({
|
|
10990
|
+
stepId: _enum(FILLABLE_STEP_IDS),
|
|
10991
|
+
labelToEffect: record(string(), string()),
|
|
10992
|
+
uncovered: array(string()).readonly()
|
|
10993
|
+
});
|
|
10937
10994
|
var ModelCatalogEntrySchema = object({
|
|
10938
10995
|
id: string(),
|
|
10939
10996
|
name: string(),
|
|
@@ -11060,6 +11117,16 @@ var ModelCatalogEntrySchema = object({
|
|
|
11060
11117
|
*/
|
|
11061
11118
|
classMap: DetectionCatalogClassMapSchema.optional(),
|
|
11062
11119
|
/**
|
|
11120
|
+
* The pipeline steps this ROOT detector already does the work of (D715):
|
|
11121
|
+
* its labels name the effect the step exists to produce (`deer` for
|
|
11122
|
+
* `animal-classifier`, `package` for `package-detection`). Derived from the
|
|
11123
|
+
* labelmap by `deriveClassifierFills`, never hand-written; a COCO model
|
|
11124
|
+
* declares none. Whether a declaration is honoured is the step's own
|
|
11125
|
+
* `labelLink` policy (`resolveLabelLink`). `labelToEffect` values are in
|
|
11126
|
+
* `FILLABLE_STEP_VOCABULARIES` (guard `check-label-link-vocabulary`).
|
|
11127
|
+
*/
|
|
11128
|
+
fillsClassifierEffects: array(FillsClassifierEffectSchema).readonly().optional(),
|
|
11129
|
+
/**
|
|
11063
11130
|
* The model's CLIP feature-space contract (text encoder, dimension, vector
|
|
11064
11131
|
* index, token window, search floor) — see `clip-model.ts`. Present ONLY on a
|
|
11065
11132
|
* CLIP image encoder; its absence is what "not a CLIP model" means to the
|
|
@@ -18402,51 +18469,6 @@ method(object({}), array(TargetKindSchema)), method(object({}), array(TargetSche
|
|
|
18402
18469
|
kind: "mutation",
|
|
18403
18470
|
auth: "admin"
|
|
18404
18471
|
});
|
|
18405
|
-
new Set([
|
|
18406
|
-
{
|
|
18407
|
-
id: "person",
|
|
18408
|
-
name: "Person"
|
|
18409
|
-
},
|
|
18410
|
-
{
|
|
18411
|
-
id: "vehicle",
|
|
18412
|
-
name: "Vehicle"
|
|
18413
|
-
},
|
|
18414
|
-
{
|
|
18415
|
-
id: "animal",
|
|
18416
|
-
name: "Animal"
|
|
18417
|
-
},
|
|
18418
|
-
{
|
|
18419
|
-
id: "package",
|
|
18420
|
-
name: "Package"
|
|
18421
|
-
}
|
|
18422
|
-
].map((l) => l.id));
|
|
18423
|
-
var COCO_TO_MACRO = {
|
|
18424
|
-
mapping: {
|
|
18425
|
-
person: "person",
|
|
18426
|
-
bicycle: "vehicle",
|
|
18427
|
-
car: "vehicle",
|
|
18428
|
-
motorcycle: "vehicle",
|
|
18429
|
-
airplane: "vehicle",
|
|
18430
|
-
bus: "vehicle",
|
|
18431
|
-
train: "vehicle",
|
|
18432
|
-
truck: "vehicle",
|
|
18433
|
-
boat: "vehicle",
|
|
18434
|
-
bird: "animal",
|
|
18435
|
-
cat: "animal",
|
|
18436
|
-
dog: "animal",
|
|
18437
|
-
horse: "animal",
|
|
18438
|
-
sheep: "animal",
|
|
18439
|
-
cow: "animal",
|
|
18440
|
-
elephant: "animal",
|
|
18441
|
-
bear: "animal",
|
|
18442
|
-
zebra: "animal",
|
|
18443
|
-
giraffe: "animal",
|
|
18444
|
-
suitcase: "package",
|
|
18445
|
-
backpack: "package",
|
|
18446
|
-
handbag: "package"
|
|
18447
|
-
},
|
|
18448
|
-
preserveOriginal: false
|
|
18449
|
-
};
|
|
18450
18472
|
var AUDIO_MACRO_LABELS = [
|
|
18451
18473
|
{
|
|
18452
18474
|
id: "speech",
|
|
@@ -19242,6 +19264,8 @@ var NcSystemEventKindSchema = _enum([
|
|
|
19242
19264
|
"stream-offline",
|
|
19243
19265
|
"node-online",
|
|
19244
19266
|
"node-offline",
|
|
19267
|
+
"node-restarting",
|
|
19268
|
+
"node-updating",
|
|
19245
19269
|
"node-inference-unavailable",
|
|
19246
19270
|
"detection-blind",
|
|
19247
19271
|
"addon-crash-loop",
|
|
@@ -23888,7 +23912,8 @@ var PipelineModelOptionSchema = object({
|
|
|
23888
23912
|
group: ModelVariantGroupSchema.optional(),
|
|
23889
23913
|
legacy: boolean().optional(),
|
|
23890
23914
|
provider: ModelProviderIdSchema.optional(),
|
|
23891
|
-
defaultConfidence: number().optional()
|
|
23915
|
+
defaultConfidence: number().optional(),
|
|
23916
|
+
fillsClassifierEffects: array(FillsClassifierEffectSchema).readonly().optional()
|
|
23892
23917
|
});
|
|
23893
23918
|
var ConfigFieldBridge = custom();
|
|
23894
23919
|
var PipelineAddonSchemaSchema = object({
|
|
@@ -33567,7 +33592,8 @@ var MotionStatusSchema = object({
|
|
|
33567
33592
|
* writes this slice (not even a boot seed), and neither does the runner's
|
|
33568
33593
|
* phase machine: its phase is "a session is attached" for every on-motion
|
|
33569
33594
|
* session, not motion, and it now has its own `DetectionPhaseTransition`.
|
|
33570
|
-
* A provider's `isDetected`
|
|
33595
|
+
* A provider's `isDetected` and `getStatus` answer from the hub's slice
|
|
33596
|
+
* (`hubMotionNativeProvider`).
|
|
33571
33597
|
* - **Sensors** (Home Assistant binary sensors, Homematic, Matter, Alexa
|
|
33572
33598
|
* imports) — the provider pushes both edges itself, from the upstream
|
|
33573
33599
|
* system's own state change. That is the sensor's own fact.
|
|
@@ -53831,6 +53857,8 @@ new Map(Object.entries({
|
|
|
53831
53857
|
"detection-blind": "device-events",
|
|
53832
53858
|
"node-online": "system",
|
|
53833
53859
|
"node-offline": "system",
|
|
53860
|
+
"node-restarting": "system",
|
|
53861
|
+
"node-updating": "system",
|
|
53834
53862
|
"node-inference-unavailable": "system",
|
|
53835
53863
|
"addon-crash-loop": "system",
|
|
53836
53864
|
"addon-update-available": "system",
|
|
@@ -53908,6 +53936,7 @@ var NC_SYSTEM_EVENT_KIND_GROUP_SPECS = [
|
|
|
53908
53936
|
kinds: [
|
|
53909
53937
|
"node-offline",
|
|
53910
53938
|
"node-online",
|
|
53939
|
+
"node-restarting",
|
|
53911
53940
|
"node-inference-unavailable"
|
|
53912
53941
|
]
|
|
53913
53942
|
},
|
|
@@ -53929,6 +53958,7 @@ var NC_SYSTEM_EVENT_KIND_GROUP_SPECS = [
|
|
|
53929
53958
|
label: "Server & container updates",
|
|
53930
53959
|
kinds: [
|
|
53931
53960
|
"server-update-available",
|
|
53961
|
+
"node-updating",
|
|
53932
53962
|
"server-updated",
|
|
53933
53963
|
"wrapper-update-available"
|
|
53934
53964
|
]
|
|
@@ -54076,6 +54106,7 @@ var STATE_SUMMARY_SECTION_VARS = [
|
|
|
54076
54106
|
kinds: [
|
|
54077
54107
|
"node-offline",
|
|
54078
54108
|
"node-online",
|
|
54109
|
+
"node-restarting",
|
|
54079
54110
|
"node-inference-unavailable"
|
|
54080
54111
|
]
|
|
54081
54112
|
},
|
|
@@ -54097,6 +54128,7 @@ var STATE_SUMMARY_SECTION_VARS = [
|
|
|
54097
54128
|
example: "server 1.2.377 → 1.2.378",
|
|
54098
54129
|
kinds: [
|
|
54099
54130
|
"server-update-available",
|
|
54131
|
+
"node-updating",
|
|
54100
54132
|
"server-updated",
|
|
54101
54133
|
"wrapper-update-available"
|
|
54102
54134
|
]
|
|
@@ -54552,10 +54584,9 @@ TimelapseRuleInputSchema.extend({
|
|
|
54552
54584
|
createdAt: number(),
|
|
54553
54585
|
updatedAt: number()
|
|
54554
54586
|
});
|
|
54555
|
-
async function
|
|
54556
|
-
let slice;
|
|
54587
|
+
async function readSlice(deviceState, deviceId) {
|
|
54557
54588
|
try {
|
|
54558
|
-
|
|
54589
|
+
return await deviceState.getCapSlice.query({
|
|
54559
54590
|
deviceId,
|
|
54560
54591
|
capName: "motion"
|
|
54561
54592
|
});
|
|
@@ -54563,10 +54594,44 @@ async function readHubMotionDetected(deviceState, deviceId) {
|
|
|
54563
54594
|
const reason = err instanceof Error ? err.message : String(err);
|
|
54564
54595
|
throw new Error(`motion state of device ${deviceId} is unreadable: ${reason}`, { cause: err });
|
|
54565
54596
|
}
|
|
54566
|
-
|
|
54597
|
+
}
|
|
54598
|
+
/**
|
|
54599
|
+
* The hub's `motion` slice as a `MotionStatus`, or `null` when nobody has
|
|
54600
|
+
* published it since the hub started. Throws when the hub does not answer or
|
|
54601
|
+
* the slice is malformed — never a made-up "no motion".
|
|
54602
|
+
*/
|
|
54603
|
+
async function readHubMotionStatus(deviceState, deviceId) {
|
|
54604
|
+
const slice = await readSlice(deviceState, deviceId);
|
|
54605
|
+
if (slice === null) return null;
|
|
54606
|
+
const parsed = MotionStatusSchema.safeParse(slice);
|
|
54607
|
+
if (!parsed.success) throw new Error(`motion state of device ${deviceId} is malformed on the hub: ${parsed.error.message}`);
|
|
54608
|
+
return parsed.data;
|
|
54609
|
+
}
|
|
54610
|
+
async function readHubMotionDetected(deviceState, deviceId) {
|
|
54611
|
+
const detected = (await readSlice(deviceState, deviceId))?.["detected"];
|
|
54567
54612
|
if (typeof detected !== "boolean") throw new Error(`motion state of device ${deviceId} is not published yet (unknown)`);
|
|
54568
54613
|
return detected;
|
|
54569
54614
|
}
|
|
54615
|
+
/**
|
|
54616
|
+
* The native `motion` provider of a camera whose slice the orchestrator writes
|
|
54617
|
+
* (D696): `isDetected` AND `getStatus`, both from the hub. `owner` names the
|
|
54618
|
+
* provider class in the deviceId-mismatch error.
|
|
54619
|
+
*/
|
|
54620
|
+
function hubMotionNativeProvider(deviceState, deviceId, owner) {
|
|
54621
|
+
const ownDevice = (asked) => {
|
|
54622
|
+
if (asked !== deviceId) throw new Error(`${owner}: motion deviceId mismatch, expected ${deviceId}, got ${asked}`);
|
|
54623
|
+
};
|
|
54624
|
+
return {
|
|
54625
|
+
isDetected: async ({ deviceId: asked }) => {
|
|
54626
|
+
ownDevice(asked);
|
|
54627
|
+
return readHubMotionDetected(deviceState, deviceId);
|
|
54628
|
+
},
|
|
54629
|
+
getStatus: async ({ deviceId: asked }) => {
|
|
54630
|
+
ownDevice(asked);
|
|
54631
|
+
return readHubMotionStatus(deviceState, deviceId);
|
|
54632
|
+
}
|
|
54633
|
+
};
|
|
54634
|
+
}
|
|
54570
54635
|
MotionOnMotionChangedDataSchema.omit({ triggeredBy: true });
|
|
54571
54636
|
function providerMotionEvent(source, data) {
|
|
54572
54637
|
return createEvent(EventCategory.ProviderMotion, source, data);
|
|
@@ -59911,6 +59976,75 @@ function createAccessoryDevice(kind, ctx, parent) {
|
|
|
59911
59976
|
default: throw new Error(`Hikvision provider does not implement accessory kind "${String(kind)}"`);
|
|
59912
59977
|
}
|
|
59913
59978
|
}
|
|
59979
|
+
var ALARM_INGRESS_MESSAGE = "hikvision: alarm event received";
|
|
59980
|
+
var countsRecord = (counts) => Object.fromEntries(counts);
|
|
59981
|
+
function createAlarmIngressLog(deps) {
|
|
59982
|
+
const intervalMs = deps.intervalMs ?? 6e4;
|
|
59983
|
+
const bursts = /* @__PURE__ */ new Map();
|
|
59984
|
+
const tags = { deviceId: deps.deviceId };
|
|
59985
|
+
const received = (ev) => {
|
|
59986
|
+
const existing = bursts.get(ev.camStreamId);
|
|
59987
|
+
const burst = existing ?? {
|
|
59988
|
+
startedAt: ev.at,
|
|
59989
|
+
lastLineAt: ev.at,
|
|
59990
|
+
counts: /* @__PURE__ */ new Map(),
|
|
59991
|
+
types: /* @__PURE__ */ new Set(),
|
|
59992
|
+
ticks: 0
|
|
59993
|
+
};
|
|
59994
|
+
if (!existing) bursts.set(ev.camStreamId, burst);
|
|
59995
|
+
burst.ticks += 1;
|
|
59996
|
+
if (!burst.types.has(ev.type)) {
|
|
59997
|
+
burst.types.add(ev.type);
|
|
59998
|
+
deps.logger.info(ALARM_INGRESS_MESSAGE, {
|
|
59999
|
+
tags,
|
|
60000
|
+
meta: {
|
|
60001
|
+
edge: "start",
|
|
60002
|
+
type: ev.type,
|
|
60003
|
+
state: ev.state,
|
|
60004
|
+
camStreamId: ev.camStreamId,
|
|
60005
|
+
...ev.aiClass === null ? {} : { aiClass: ev.aiClass },
|
|
60006
|
+
...existing ? { burstAgeMs: ev.at - burst.startedAt } : {}
|
|
60007
|
+
}
|
|
60008
|
+
});
|
|
60009
|
+
burst.lastLineAt = Math.max(burst.lastLineAt, ev.at);
|
|
60010
|
+
return;
|
|
60011
|
+
}
|
|
60012
|
+
burst.counts.set(ev.type, (burst.counts.get(ev.type) ?? 0) + 1);
|
|
60013
|
+
if (ev.at - burst.lastLineAt < intervalMs) return;
|
|
60014
|
+
deps.logger.info(ALARM_INGRESS_MESSAGE, {
|
|
60015
|
+
tags,
|
|
60016
|
+
meta: {
|
|
60017
|
+
edge: "ongoing",
|
|
60018
|
+
camStreamId: ev.camStreamId,
|
|
60019
|
+
counts: countsRecord(burst.counts),
|
|
60020
|
+
windowMs: ev.at - burst.lastLineAt,
|
|
60021
|
+
burstAgeMs: ev.at - burst.startedAt
|
|
60022
|
+
}
|
|
60023
|
+
});
|
|
60024
|
+
burst.counts.clear();
|
|
60025
|
+
burst.lastLineAt = ev.at;
|
|
60026
|
+
};
|
|
60027
|
+
const ended = (camStreamId, at) => {
|
|
60028
|
+
const burst = bursts.get(camStreamId);
|
|
60029
|
+
if (!burst) return;
|
|
60030
|
+
bursts.delete(camStreamId);
|
|
60031
|
+
deps.logger.info(ALARM_INGRESS_MESSAGE, {
|
|
60032
|
+
tags,
|
|
60033
|
+
meta: {
|
|
60034
|
+
edge: "end",
|
|
60035
|
+
camStreamId,
|
|
60036
|
+
counts: countsRecord(burst.counts),
|
|
60037
|
+
ticks: burst.ticks,
|
|
60038
|
+
types: [...burst.types],
|
|
60039
|
+
durationMs: at - burst.startedAt
|
|
60040
|
+
}
|
|
60041
|
+
});
|
|
60042
|
+
};
|
|
60043
|
+
return {
|
|
60044
|
+
received,
|
|
60045
|
+
ended
|
|
60046
|
+
};
|
|
60047
|
+
}
|
|
59914
60048
|
function createAlarmTypeTally(deps) {
|
|
59915
60049
|
const intervalMs = deps.intervalMs ?? 3e5;
|
|
59916
60050
|
const lastLoggedAt = /* @__PURE__ */ new Map();
|
|
@@ -64297,6 +64431,20 @@ var HikvisionCamera = class HikvisionCamera extends BaseDevice {
|
|
|
64297
64431
|
});
|
|
64298
64432
|
return this.unhandledAlarmsTally;
|
|
64299
64433
|
}
|
|
64434
|
+
/**
|
|
64435
|
+
* Sampled info-level ingress line for the motion-bearing alarms (VMD and
|
|
64436
|
+
* smart events): one at each burst's start, one per minute while it lasts,
|
|
64437
|
+
* one at its end — never one per ~1 Hz tick. Lazy for the same reason as
|
|
64438
|
+
* `unhandledAlarms`.
|
|
64439
|
+
*/
|
|
64440
|
+
motionIngressLog = null;
|
|
64441
|
+
get motionIngress() {
|
|
64442
|
+
this.motionIngressLog ??= createAlarmIngressLog({
|
|
64443
|
+
logger: this.ctx.logger,
|
|
64444
|
+
deviceId: this.id
|
|
64445
|
+
});
|
|
64446
|
+
return this.motionIngressLog;
|
|
64447
|
+
}
|
|
64300
64448
|
/** Hysteresis: drop motion-active back to inactive after this many ms with
|
|
64301
64449
|
* no fresh `VMD` event. Hikvision fires VMD every ~1s while motion is
|
|
64302
64450
|
* active; missing 2 ticks signals the burst is over. */
|
|
@@ -66859,19 +67007,25 @@ var HikvisionCamera = class HikvisionCamera extends BaseDevice {
|
|
|
66859
67007
|
* The native `motion` cap. Its presence is what defaults the camera to
|
|
66860
67008
|
* `motionSources: ['onboard']`. This provider writes NO `motion` slice (D696):
|
|
66861
67009
|
* the orchestrator is its only writer, with the EFFECTIVE motion, on the hub.
|
|
66862
|
-
* `isDetected` therefore
|
|
66863
|
-
*
|
|
66864
|
-
*
|
|
67010
|
+
* `isDetected` AND `getStatus` therefore answer from the hub's slice
|
|
67011
|
+
* (`hubMotionNativeProvider`; without its own `getStatus` the framework's
|
|
67012
|
+
* default reads this process's empty local state and answers `null`). An
|
|
67013
|
+
* unreadable hub fails both; never `false` for "unknown" (D49, D393).
|
|
66865
67014
|
*/
|
|
66866
67015
|
registerMotionCap() {
|
|
66867
|
-
this.ctx.
|
|
66868
|
-
|
|
66869
|
-
return readHubMotionDetected(this.ctx.api.deviceState, this.id);
|
|
66870
|
-
} });
|
|
67016
|
+
const motionProvider = hubMotionNativeProvider(this.ctx.api.deviceState, this.id, "HikvisionCamera");
|
|
67017
|
+
this.ctx.registerNativeCap(motionCapability, motionProvider);
|
|
66871
67018
|
}
|
|
66872
67019
|
handleMotionEvent(ev, camStreamId) {
|
|
66873
67020
|
const now = ev.observedAt;
|
|
66874
67021
|
this.lastMotionAt.set(camStreamId, now);
|
|
67022
|
+
this.motionIngress.received({
|
|
67023
|
+
type: ev.type,
|
|
67024
|
+
state: ev.state,
|
|
67025
|
+
aiClass: ev.aiClass,
|
|
67026
|
+
camStreamId,
|
|
67027
|
+
at: now
|
|
67028
|
+
});
|
|
66875
67029
|
this.ctx.eventBus.emit(providerMotionEvent(this.eventSource, {
|
|
66876
67030
|
deviceId: this.id,
|
|
66877
67031
|
detected: true,
|
|
@@ -66884,6 +67038,7 @@ var HikvisionCamera = class HikvisionCamera extends BaseDevice {
|
|
|
66884
67038
|
const clearedAt = Date.now();
|
|
66885
67039
|
this.lastMotionAt.set(camStreamId, 0);
|
|
66886
67040
|
this.motionInactiveTimers.delete(camStreamId);
|
|
67041
|
+
this.motionIngress.ended(camStreamId, clearedAt);
|
|
66887
67042
|
this.ctx.eventBus.emit(providerMotionEvent(this.eventSource, {
|
|
66888
67043
|
deviceId: this.id,
|
|
66889
67044
|
detected: false,
|
package/dist/addon.mjs
CHANGED
|
@@ -10781,6 +10781,57 @@ var ClipSearchStrictnessSchema = _enum([
|
|
|
10781
10781
|
"balanced",
|
|
10782
10782
|
"strict"
|
|
10783
10783
|
]);
|
|
10784
|
+
new Set([
|
|
10785
|
+
{
|
|
10786
|
+
id: "person",
|
|
10787
|
+
name: "Person"
|
|
10788
|
+
},
|
|
10789
|
+
{
|
|
10790
|
+
id: "vehicle",
|
|
10791
|
+
name: "Vehicle"
|
|
10792
|
+
},
|
|
10793
|
+
{
|
|
10794
|
+
id: "animal",
|
|
10795
|
+
name: "Animal"
|
|
10796
|
+
},
|
|
10797
|
+
{
|
|
10798
|
+
id: "package",
|
|
10799
|
+
name: "Package"
|
|
10800
|
+
}
|
|
10801
|
+
].map((l) => l.id));
|
|
10802
|
+
var COCO_TO_MACRO = {
|
|
10803
|
+
mapping: {
|
|
10804
|
+
person: "person",
|
|
10805
|
+
bicycle: "vehicle",
|
|
10806
|
+
car: "vehicle",
|
|
10807
|
+
motorcycle: "vehicle",
|
|
10808
|
+
airplane: "vehicle",
|
|
10809
|
+
bus: "vehicle",
|
|
10810
|
+
train: "vehicle",
|
|
10811
|
+
truck: "vehicle",
|
|
10812
|
+
boat: "vehicle",
|
|
10813
|
+
bird: "animal",
|
|
10814
|
+
cat: "animal",
|
|
10815
|
+
dog: "animal",
|
|
10816
|
+
horse: "animal",
|
|
10817
|
+
sheep: "animal",
|
|
10818
|
+
cow: "animal",
|
|
10819
|
+
elephant: "animal",
|
|
10820
|
+
bear: "animal",
|
|
10821
|
+
zebra: "animal",
|
|
10822
|
+
giraffe: "animal",
|
|
10823
|
+
suitcase: "package",
|
|
10824
|
+
backpack: "package",
|
|
10825
|
+
handbag: "package"
|
|
10826
|
+
},
|
|
10827
|
+
preserveOriginal: false
|
|
10828
|
+
};
|
|
10829
|
+
/** Steps a root detector may declare it fills. */
|
|
10830
|
+
var FILLABLE_STEP_IDS = [
|
|
10831
|
+
"animal-classifier",
|
|
10832
|
+
"vehicle-classifier",
|
|
10833
|
+
"package-detection"
|
|
10834
|
+
];
|
|
10784
10835
|
var MODEL_FORMATS = [
|
|
10785
10836
|
"onnx",
|
|
10786
10837
|
"coreml",
|
|
@@ -10913,6 +10964,12 @@ var ModelLicenseSchema = preprocess((value) => typeof value === "string" ? { wei
|
|
|
10913
10964
|
/** True when the weights or their data forbid commercial use. */
|
|
10914
10965
|
nonCommercial: boolean().optional()
|
|
10915
10966
|
}));
|
|
10967
|
+
/** One `fillsClassifierEffects` entry — see `ModelCatalogEntry.fillsClassifierEffects`. */
|
|
10968
|
+
var FillsClassifierEffectSchema = object({
|
|
10969
|
+
stepId: _enum(FILLABLE_STEP_IDS),
|
|
10970
|
+
labelToEffect: record(string(), string()),
|
|
10971
|
+
uncovered: array(string()).readonly()
|
|
10972
|
+
});
|
|
10916
10973
|
var ModelCatalogEntrySchema = object({
|
|
10917
10974
|
id: string(),
|
|
10918
10975
|
name: string(),
|
|
@@ -11039,6 +11096,16 @@ var ModelCatalogEntrySchema = object({
|
|
|
11039
11096
|
*/
|
|
11040
11097
|
classMap: DetectionCatalogClassMapSchema.optional(),
|
|
11041
11098
|
/**
|
|
11099
|
+
* The pipeline steps this ROOT detector already does the work of (D715):
|
|
11100
|
+
* its labels name the effect the step exists to produce (`deer` for
|
|
11101
|
+
* `animal-classifier`, `package` for `package-detection`). Derived from the
|
|
11102
|
+
* labelmap by `deriveClassifierFills`, never hand-written; a COCO model
|
|
11103
|
+
* declares none. Whether a declaration is honoured is the step's own
|
|
11104
|
+
* `labelLink` policy (`resolveLabelLink`). `labelToEffect` values are in
|
|
11105
|
+
* `FILLABLE_STEP_VOCABULARIES` (guard `check-label-link-vocabulary`).
|
|
11106
|
+
*/
|
|
11107
|
+
fillsClassifierEffects: array(FillsClassifierEffectSchema).readonly().optional(),
|
|
11108
|
+
/**
|
|
11042
11109
|
* The model's CLIP feature-space contract (text encoder, dimension, vector
|
|
11043
11110
|
* index, token window, search floor) — see `clip-model.ts`. Present ONLY on a
|
|
11044
11111
|
* CLIP image encoder; its absence is what "not a CLIP model" means to the
|
|
@@ -18381,51 +18448,6 @@ method(object({}), array(TargetKindSchema)), method(object({}), array(TargetSche
|
|
|
18381
18448
|
kind: "mutation",
|
|
18382
18449
|
auth: "admin"
|
|
18383
18450
|
});
|
|
18384
|
-
new Set([
|
|
18385
|
-
{
|
|
18386
|
-
id: "person",
|
|
18387
|
-
name: "Person"
|
|
18388
|
-
},
|
|
18389
|
-
{
|
|
18390
|
-
id: "vehicle",
|
|
18391
|
-
name: "Vehicle"
|
|
18392
|
-
},
|
|
18393
|
-
{
|
|
18394
|
-
id: "animal",
|
|
18395
|
-
name: "Animal"
|
|
18396
|
-
},
|
|
18397
|
-
{
|
|
18398
|
-
id: "package",
|
|
18399
|
-
name: "Package"
|
|
18400
|
-
}
|
|
18401
|
-
].map((l) => l.id));
|
|
18402
|
-
var COCO_TO_MACRO = {
|
|
18403
|
-
mapping: {
|
|
18404
|
-
person: "person",
|
|
18405
|
-
bicycle: "vehicle",
|
|
18406
|
-
car: "vehicle",
|
|
18407
|
-
motorcycle: "vehicle",
|
|
18408
|
-
airplane: "vehicle",
|
|
18409
|
-
bus: "vehicle",
|
|
18410
|
-
train: "vehicle",
|
|
18411
|
-
truck: "vehicle",
|
|
18412
|
-
boat: "vehicle",
|
|
18413
|
-
bird: "animal",
|
|
18414
|
-
cat: "animal",
|
|
18415
|
-
dog: "animal",
|
|
18416
|
-
horse: "animal",
|
|
18417
|
-
sheep: "animal",
|
|
18418
|
-
cow: "animal",
|
|
18419
|
-
elephant: "animal",
|
|
18420
|
-
bear: "animal",
|
|
18421
|
-
zebra: "animal",
|
|
18422
|
-
giraffe: "animal",
|
|
18423
|
-
suitcase: "package",
|
|
18424
|
-
backpack: "package",
|
|
18425
|
-
handbag: "package"
|
|
18426
|
-
},
|
|
18427
|
-
preserveOriginal: false
|
|
18428
|
-
};
|
|
18429
18451
|
var AUDIO_MACRO_LABELS = [
|
|
18430
18452
|
{
|
|
18431
18453
|
id: "speech",
|
|
@@ -19221,6 +19243,8 @@ var NcSystemEventKindSchema = _enum([
|
|
|
19221
19243
|
"stream-offline",
|
|
19222
19244
|
"node-online",
|
|
19223
19245
|
"node-offline",
|
|
19246
|
+
"node-restarting",
|
|
19247
|
+
"node-updating",
|
|
19224
19248
|
"node-inference-unavailable",
|
|
19225
19249
|
"detection-blind",
|
|
19226
19250
|
"addon-crash-loop",
|
|
@@ -23867,7 +23891,8 @@ var PipelineModelOptionSchema = object({
|
|
|
23867
23891
|
group: ModelVariantGroupSchema.optional(),
|
|
23868
23892
|
legacy: boolean().optional(),
|
|
23869
23893
|
provider: ModelProviderIdSchema.optional(),
|
|
23870
|
-
defaultConfidence: number().optional()
|
|
23894
|
+
defaultConfidence: number().optional(),
|
|
23895
|
+
fillsClassifierEffects: array(FillsClassifierEffectSchema).readonly().optional()
|
|
23871
23896
|
});
|
|
23872
23897
|
var ConfigFieldBridge = custom();
|
|
23873
23898
|
var PipelineAddonSchemaSchema = object({
|
|
@@ -33546,7 +33571,8 @@ var MotionStatusSchema = object({
|
|
|
33546
33571
|
* writes this slice (not even a boot seed), and neither does the runner's
|
|
33547
33572
|
* phase machine: its phase is "a session is attached" for every on-motion
|
|
33548
33573
|
* session, not motion, and it now has its own `DetectionPhaseTransition`.
|
|
33549
|
-
* A provider's `isDetected`
|
|
33574
|
+
* A provider's `isDetected` and `getStatus` answer from the hub's slice
|
|
33575
|
+
* (`hubMotionNativeProvider`).
|
|
33550
33576
|
* - **Sensors** (Home Assistant binary sensors, Homematic, Matter, Alexa
|
|
33551
33577
|
* imports) — the provider pushes both edges itself, from the upstream
|
|
33552
33578
|
* system's own state change. That is the sensor's own fact.
|
|
@@ -53810,6 +53836,8 @@ new Map(Object.entries({
|
|
|
53810
53836
|
"detection-blind": "device-events",
|
|
53811
53837
|
"node-online": "system",
|
|
53812
53838
|
"node-offline": "system",
|
|
53839
|
+
"node-restarting": "system",
|
|
53840
|
+
"node-updating": "system",
|
|
53813
53841
|
"node-inference-unavailable": "system",
|
|
53814
53842
|
"addon-crash-loop": "system",
|
|
53815
53843
|
"addon-update-available": "system",
|
|
@@ -53887,6 +53915,7 @@ var NC_SYSTEM_EVENT_KIND_GROUP_SPECS = [
|
|
|
53887
53915
|
kinds: [
|
|
53888
53916
|
"node-offline",
|
|
53889
53917
|
"node-online",
|
|
53918
|
+
"node-restarting",
|
|
53890
53919
|
"node-inference-unavailable"
|
|
53891
53920
|
]
|
|
53892
53921
|
},
|
|
@@ -53908,6 +53937,7 @@ var NC_SYSTEM_EVENT_KIND_GROUP_SPECS = [
|
|
|
53908
53937
|
label: "Server & container updates",
|
|
53909
53938
|
kinds: [
|
|
53910
53939
|
"server-update-available",
|
|
53940
|
+
"node-updating",
|
|
53911
53941
|
"server-updated",
|
|
53912
53942
|
"wrapper-update-available"
|
|
53913
53943
|
]
|
|
@@ -54055,6 +54085,7 @@ var STATE_SUMMARY_SECTION_VARS = [
|
|
|
54055
54085
|
kinds: [
|
|
54056
54086
|
"node-offline",
|
|
54057
54087
|
"node-online",
|
|
54088
|
+
"node-restarting",
|
|
54058
54089
|
"node-inference-unavailable"
|
|
54059
54090
|
]
|
|
54060
54091
|
},
|
|
@@ -54076,6 +54107,7 @@ var STATE_SUMMARY_SECTION_VARS = [
|
|
|
54076
54107
|
example: "server 1.2.377 → 1.2.378",
|
|
54077
54108
|
kinds: [
|
|
54078
54109
|
"server-update-available",
|
|
54110
|
+
"node-updating",
|
|
54079
54111
|
"server-updated",
|
|
54080
54112
|
"wrapper-update-available"
|
|
54081
54113
|
]
|
|
@@ -54531,10 +54563,9 @@ TimelapseRuleInputSchema.extend({
|
|
|
54531
54563
|
createdAt: number(),
|
|
54532
54564
|
updatedAt: number()
|
|
54533
54565
|
});
|
|
54534
|
-
async function
|
|
54535
|
-
let slice;
|
|
54566
|
+
async function readSlice(deviceState, deviceId) {
|
|
54536
54567
|
try {
|
|
54537
|
-
|
|
54568
|
+
return await deviceState.getCapSlice.query({
|
|
54538
54569
|
deviceId,
|
|
54539
54570
|
capName: "motion"
|
|
54540
54571
|
});
|
|
@@ -54542,10 +54573,44 @@ async function readHubMotionDetected(deviceState, deviceId) {
|
|
|
54542
54573
|
const reason = err instanceof Error ? err.message : String(err);
|
|
54543
54574
|
throw new Error(`motion state of device ${deviceId} is unreadable: ${reason}`, { cause: err });
|
|
54544
54575
|
}
|
|
54545
|
-
|
|
54576
|
+
}
|
|
54577
|
+
/**
|
|
54578
|
+
* The hub's `motion` slice as a `MotionStatus`, or `null` when nobody has
|
|
54579
|
+
* published it since the hub started. Throws when the hub does not answer or
|
|
54580
|
+
* the slice is malformed — never a made-up "no motion".
|
|
54581
|
+
*/
|
|
54582
|
+
async function readHubMotionStatus(deviceState, deviceId) {
|
|
54583
|
+
const slice = await readSlice(deviceState, deviceId);
|
|
54584
|
+
if (slice === null) return null;
|
|
54585
|
+
const parsed = MotionStatusSchema.safeParse(slice);
|
|
54586
|
+
if (!parsed.success) throw new Error(`motion state of device ${deviceId} is malformed on the hub: ${parsed.error.message}`);
|
|
54587
|
+
return parsed.data;
|
|
54588
|
+
}
|
|
54589
|
+
async function readHubMotionDetected(deviceState, deviceId) {
|
|
54590
|
+
const detected = (await readSlice(deviceState, deviceId))?.["detected"];
|
|
54546
54591
|
if (typeof detected !== "boolean") throw new Error(`motion state of device ${deviceId} is not published yet (unknown)`);
|
|
54547
54592
|
return detected;
|
|
54548
54593
|
}
|
|
54594
|
+
/**
|
|
54595
|
+
* The native `motion` provider of a camera whose slice the orchestrator writes
|
|
54596
|
+
* (D696): `isDetected` AND `getStatus`, both from the hub. `owner` names the
|
|
54597
|
+
* provider class in the deviceId-mismatch error.
|
|
54598
|
+
*/
|
|
54599
|
+
function hubMotionNativeProvider(deviceState, deviceId, owner) {
|
|
54600
|
+
const ownDevice = (asked) => {
|
|
54601
|
+
if (asked !== deviceId) throw new Error(`${owner}: motion deviceId mismatch, expected ${deviceId}, got ${asked}`);
|
|
54602
|
+
};
|
|
54603
|
+
return {
|
|
54604
|
+
isDetected: async ({ deviceId: asked }) => {
|
|
54605
|
+
ownDevice(asked);
|
|
54606
|
+
return readHubMotionDetected(deviceState, deviceId);
|
|
54607
|
+
},
|
|
54608
|
+
getStatus: async ({ deviceId: asked }) => {
|
|
54609
|
+
ownDevice(asked);
|
|
54610
|
+
return readHubMotionStatus(deviceState, deviceId);
|
|
54611
|
+
}
|
|
54612
|
+
};
|
|
54613
|
+
}
|
|
54549
54614
|
MotionOnMotionChangedDataSchema.omit({ triggeredBy: true });
|
|
54550
54615
|
function providerMotionEvent(source, data) {
|
|
54551
54616
|
return createEvent(EventCategory.ProviderMotion, source, data);
|
|
@@ -59890,6 +59955,75 @@ function createAccessoryDevice(kind, ctx, parent) {
|
|
|
59890
59955
|
default: throw new Error(`Hikvision provider does not implement accessory kind "${String(kind)}"`);
|
|
59891
59956
|
}
|
|
59892
59957
|
}
|
|
59958
|
+
var ALARM_INGRESS_MESSAGE = "hikvision: alarm event received";
|
|
59959
|
+
var countsRecord = (counts) => Object.fromEntries(counts);
|
|
59960
|
+
function createAlarmIngressLog(deps) {
|
|
59961
|
+
const intervalMs = deps.intervalMs ?? 6e4;
|
|
59962
|
+
const bursts = /* @__PURE__ */ new Map();
|
|
59963
|
+
const tags = { deviceId: deps.deviceId };
|
|
59964
|
+
const received = (ev) => {
|
|
59965
|
+
const existing = bursts.get(ev.camStreamId);
|
|
59966
|
+
const burst = existing ?? {
|
|
59967
|
+
startedAt: ev.at,
|
|
59968
|
+
lastLineAt: ev.at,
|
|
59969
|
+
counts: /* @__PURE__ */ new Map(),
|
|
59970
|
+
types: /* @__PURE__ */ new Set(),
|
|
59971
|
+
ticks: 0
|
|
59972
|
+
};
|
|
59973
|
+
if (!existing) bursts.set(ev.camStreamId, burst);
|
|
59974
|
+
burst.ticks += 1;
|
|
59975
|
+
if (!burst.types.has(ev.type)) {
|
|
59976
|
+
burst.types.add(ev.type);
|
|
59977
|
+
deps.logger.info(ALARM_INGRESS_MESSAGE, {
|
|
59978
|
+
tags,
|
|
59979
|
+
meta: {
|
|
59980
|
+
edge: "start",
|
|
59981
|
+
type: ev.type,
|
|
59982
|
+
state: ev.state,
|
|
59983
|
+
camStreamId: ev.camStreamId,
|
|
59984
|
+
...ev.aiClass === null ? {} : { aiClass: ev.aiClass },
|
|
59985
|
+
...existing ? { burstAgeMs: ev.at - burst.startedAt } : {}
|
|
59986
|
+
}
|
|
59987
|
+
});
|
|
59988
|
+
burst.lastLineAt = Math.max(burst.lastLineAt, ev.at);
|
|
59989
|
+
return;
|
|
59990
|
+
}
|
|
59991
|
+
burst.counts.set(ev.type, (burst.counts.get(ev.type) ?? 0) + 1);
|
|
59992
|
+
if (ev.at - burst.lastLineAt < intervalMs) return;
|
|
59993
|
+
deps.logger.info(ALARM_INGRESS_MESSAGE, {
|
|
59994
|
+
tags,
|
|
59995
|
+
meta: {
|
|
59996
|
+
edge: "ongoing",
|
|
59997
|
+
camStreamId: ev.camStreamId,
|
|
59998
|
+
counts: countsRecord(burst.counts),
|
|
59999
|
+
windowMs: ev.at - burst.lastLineAt,
|
|
60000
|
+
burstAgeMs: ev.at - burst.startedAt
|
|
60001
|
+
}
|
|
60002
|
+
});
|
|
60003
|
+
burst.counts.clear();
|
|
60004
|
+
burst.lastLineAt = ev.at;
|
|
60005
|
+
};
|
|
60006
|
+
const ended = (camStreamId, at) => {
|
|
60007
|
+
const burst = bursts.get(camStreamId);
|
|
60008
|
+
if (!burst) return;
|
|
60009
|
+
bursts.delete(camStreamId);
|
|
60010
|
+
deps.logger.info(ALARM_INGRESS_MESSAGE, {
|
|
60011
|
+
tags,
|
|
60012
|
+
meta: {
|
|
60013
|
+
edge: "end",
|
|
60014
|
+
camStreamId,
|
|
60015
|
+
counts: countsRecord(burst.counts),
|
|
60016
|
+
ticks: burst.ticks,
|
|
60017
|
+
types: [...burst.types],
|
|
60018
|
+
durationMs: at - burst.startedAt
|
|
60019
|
+
}
|
|
60020
|
+
});
|
|
60021
|
+
};
|
|
60022
|
+
return {
|
|
60023
|
+
received,
|
|
60024
|
+
ended
|
|
60025
|
+
};
|
|
60026
|
+
}
|
|
59893
60027
|
function createAlarmTypeTally(deps) {
|
|
59894
60028
|
const intervalMs = deps.intervalMs ?? 3e5;
|
|
59895
60029
|
const lastLoggedAt = /* @__PURE__ */ new Map();
|
|
@@ -64276,6 +64410,20 @@ var HikvisionCamera = class HikvisionCamera extends BaseDevice {
|
|
|
64276
64410
|
});
|
|
64277
64411
|
return this.unhandledAlarmsTally;
|
|
64278
64412
|
}
|
|
64413
|
+
/**
|
|
64414
|
+
* Sampled info-level ingress line for the motion-bearing alarms (VMD and
|
|
64415
|
+
* smart events): one at each burst's start, one per minute while it lasts,
|
|
64416
|
+
* one at its end — never one per ~1 Hz tick. Lazy for the same reason as
|
|
64417
|
+
* `unhandledAlarms`.
|
|
64418
|
+
*/
|
|
64419
|
+
motionIngressLog = null;
|
|
64420
|
+
get motionIngress() {
|
|
64421
|
+
this.motionIngressLog ??= createAlarmIngressLog({
|
|
64422
|
+
logger: this.ctx.logger,
|
|
64423
|
+
deviceId: this.id
|
|
64424
|
+
});
|
|
64425
|
+
return this.motionIngressLog;
|
|
64426
|
+
}
|
|
64279
64427
|
/** Hysteresis: drop motion-active back to inactive after this many ms with
|
|
64280
64428
|
* no fresh `VMD` event. Hikvision fires VMD every ~1s while motion is
|
|
64281
64429
|
* active; missing 2 ticks signals the burst is over. */
|
|
@@ -66838,19 +66986,25 @@ var HikvisionCamera = class HikvisionCamera extends BaseDevice {
|
|
|
66838
66986
|
* The native `motion` cap. Its presence is what defaults the camera to
|
|
66839
66987
|
* `motionSources: ['onboard']`. This provider writes NO `motion` slice (D696):
|
|
66840
66988
|
* the orchestrator is its only writer, with the EFFECTIVE motion, on the hub.
|
|
66841
|
-
* `isDetected` therefore
|
|
66842
|
-
*
|
|
66843
|
-
*
|
|
66989
|
+
* `isDetected` AND `getStatus` therefore answer from the hub's slice
|
|
66990
|
+
* (`hubMotionNativeProvider`; without its own `getStatus` the framework's
|
|
66991
|
+
* default reads this process's empty local state and answers `null`). An
|
|
66992
|
+
* unreadable hub fails both; never `false` for "unknown" (D49, D393).
|
|
66844
66993
|
*/
|
|
66845
66994
|
registerMotionCap() {
|
|
66846
|
-
this.ctx.
|
|
66847
|
-
|
|
66848
|
-
return readHubMotionDetected(this.ctx.api.deviceState, this.id);
|
|
66849
|
-
} });
|
|
66995
|
+
const motionProvider = hubMotionNativeProvider(this.ctx.api.deviceState, this.id, "HikvisionCamera");
|
|
66996
|
+
this.ctx.registerNativeCap(motionCapability, motionProvider);
|
|
66850
66997
|
}
|
|
66851
66998
|
handleMotionEvent(ev, camStreamId) {
|
|
66852
66999
|
const now = ev.observedAt;
|
|
66853
67000
|
this.lastMotionAt.set(camStreamId, now);
|
|
67001
|
+
this.motionIngress.received({
|
|
67002
|
+
type: ev.type,
|
|
67003
|
+
state: ev.state,
|
|
67004
|
+
aiClass: ev.aiClass,
|
|
67005
|
+
camStreamId,
|
|
67006
|
+
at: now
|
|
67007
|
+
});
|
|
66854
67008
|
this.ctx.eventBus.emit(providerMotionEvent(this.eventSource, {
|
|
66855
67009
|
deviceId: this.id,
|
|
66856
67010
|
detected: true,
|
|
@@ -66863,6 +67017,7 @@ var HikvisionCamera = class HikvisionCamera extends BaseDevice {
|
|
|
66863
67017
|
const clearedAt = Date.now();
|
|
66864
67018
|
this.lastMotionAt.set(camStreamId, 0);
|
|
66865
67019
|
this.motionInactiveTimers.delete(camStreamId);
|
|
67020
|
+
this.motionIngress.ended(camStreamId, clearedAt);
|
|
66866
67021
|
this.ctx.eventBus.emit(providerMotionEvent(this.eventSource, {
|
|
66867
67022
|
deviceId: this.id,
|
|
66868
67023
|
detected: false,
|
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.144",
|
|
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",
|