@camstack/addon-provider-frigate 0.1.3 → 0.1.4

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 ADDED
@@ -0,0 +1,1000 @@
1
+ "use strict";
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
+ var __getOwnPropNames = Object.getOwnPropertyNames;
5
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
6
+ var __export = (target, all) => {
7
+ for (var name in all)
8
+ __defProp(target, name, { get: all[name], enumerable: true });
9
+ };
10
+ var __copyProps = (to, from, except, desc) => {
11
+ if (from && typeof from === "object" || typeof from === "function") {
12
+ for (let key of __getOwnPropNames(from))
13
+ if (!__hasOwnProp.call(to, key) && key !== except)
14
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
15
+ }
16
+ return to;
17
+ };
18
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
+
20
+ // src/addon.ts
21
+ var addon_exports = {};
22
+ __export(addon_exports, {
23
+ FrigateProviderAddon: () => FrigateProviderAddon
24
+ });
25
+ module.exports = __toCommonJS(addon_exports);
26
+
27
+ // src/frigate-api.ts
28
+ var FrigateApiClient = class {
29
+ constructor(config) {
30
+ this.config = config;
31
+ this.baseUrl = (config.baseUrl ?? "").replace(/\/+$/, "");
32
+ }
33
+ baseUrl;
34
+ authToken = null;
35
+ authHeader = null;
36
+ async authenticate() {
37
+ const { username, password } = this.config;
38
+ if (!username || !password) {
39
+ return;
40
+ }
41
+ try {
42
+ const response = await fetch(`${this.baseUrl}/api/login`, {
43
+ method: "POST",
44
+ headers: { "Content-Type": "application/json" },
45
+ body: JSON.stringify({ user: username, password })
46
+ });
47
+ if (response.ok) {
48
+ const cookies = response.headers.get("set-cookie");
49
+ if (cookies) {
50
+ this.authToken = cookies;
51
+ return;
52
+ }
53
+ }
54
+ } catch {
55
+ }
56
+ this.authHeader = `Basic ${Buffer.from(`${username}:${password}`).toString("base64")}`;
57
+ }
58
+ buildHeaders() {
59
+ const headers = {};
60
+ if (this.authToken) {
61
+ headers["Cookie"] = this.authToken;
62
+ } else if (this.authHeader) {
63
+ headers["Authorization"] = this.authHeader;
64
+ }
65
+ return headers;
66
+ }
67
+ async request(path, options) {
68
+ if ((this.config.username || this.config.password) && !this.authToken && !this.authHeader) {
69
+ await this.authenticate();
70
+ }
71
+ const url = `${this.baseUrl}${path}`;
72
+ const response = await fetch(url, {
73
+ ...options,
74
+ headers: {
75
+ ...this.buildHeaders(),
76
+ ...options?.headers
77
+ }
78
+ });
79
+ if (!response.ok) {
80
+ throw new Error(`Frigate API error: ${response.status} ${response.statusText} for ${path}`);
81
+ }
82
+ return response.json();
83
+ }
84
+ async requestBuffer(path) {
85
+ if ((this.config.username || this.config.password) && !this.authToken && !this.authHeader) {
86
+ await this.authenticate();
87
+ }
88
+ const url = `${this.baseUrl}${path}`;
89
+ const response = await fetch(url, {
90
+ headers: this.buildHeaders()
91
+ });
92
+ if (!response.ok) {
93
+ throw new Error(`Frigate API error: ${response.status} ${response.statusText} for ${path}`);
94
+ }
95
+ const arrayBuffer = await response.arrayBuffer();
96
+ return Buffer.from(arrayBuffer);
97
+ }
98
+ async requestText(path, options) {
99
+ if ((this.config.username || this.config.password) && !this.authToken && !this.authHeader) {
100
+ await this.authenticate();
101
+ }
102
+ const url = `${this.baseUrl}${path}`;
103
+ const response = await fetch(url, {
104
+ ...options,
105
+ headers: {
106
+ ...this.buildHeaders(),
107
+ ...options?.headers
108
+ }
109
+ });
110
+ if (!response.ok) {
111
+ throw new Error(`Frigate API error: ${response.status} ${response.statusText} for ${path}`);
112
+ }
113
+ return response.text();
114
+ }
115
+ // --- Config & Discovery ---
116
+ async getConfig() {
117
+ return this.request("/api/config");
118
+ }
119
+ async getGo2rtcStreams() {
120
+ return this.request("/api/go2rtc/streams");
121
+ }
122
+ // --- Events ---
123
+ async getEvents(params) {
124
+ const query = buildQueryString(params);
125
+ return this.request(`/api/events${query}`);
126
+ }
127
+ async getReviewItems(params) {
128
+ const query = buildQueryString(params);
129
+ return this.request(`/api/review${query}`);
130
+ }
131
+ // --- Recordings ---
132
+ async getRecordings(camera, after, before) {
133
+ const query = buildQueryString({ after, before });
134
+ return this.request(`/api/${encodeURIComponent(camera)}/recordings${query}`);
135
+ }
136
+ async getMotionActivity(params) {
137
+ const query = buildQueryString(params);
138
+ return this.request(`/api/motion_activity${query}`);
139
+ }
140
+ // --- Media ---
141
+ async getLatestSnapshot(camera) {
142
+ return this.requestBuffer(`/api/${encodeURIComponent(camera)}/latest.jpg`);
143
+ }
144
+ async getEventThumbnail(eventId) {
145
+ return this.requestBuffer(`/api/events/${encodeURIComponent(eventId)}/thumbnail.jpg`);
146
+ }
147
+ async getEventSnapshot(eventId) {
148
+ return this.requestBuffer(`/api/events/${encodeURIComponent(eventId)}/snapshot.jpg`);
149
+ }
150
+ async getEventClip(eventId) {
151
+ return this.requestBuffer(`/api/events/${encodeURIComponent(eventId)}/clip.mp4`);
152
+ }
153
+ async getRecordingThumbnail(camera, timestampSec) {
154
+ return this.requestBuffer(
155
+ `/api/${encodeURIComponent(camera)}/recordings/thumbnail-${timestampSec}.jpg`
156
+ );
157
+ }
158
+ // --- WebRTC ---
159
+ async proxyWhepSdp(streamName, sdpOffer) {
160
+ return this.requestText(`/api/go2rtc/webrtc?src=${encodeURIComponent(streamName)}`, {
161
+ method: "POST",
162
+ headers: { "Content-Type": "application/sdp" },
163
+ body: sdpOffer
164
+ });
165
+ }
166
+ // --- PTZ ---
167
+ async getPtzInfo(camera) {
168
+ try {
169
+ return await this.request(
170
+ `/api/${encodeURIComponent(camera)}/ptz/info`
171
+ );
172
+ } catch {
173
+ return null;
174
+ }
175
+ }
176
+ async ptzCommand(camera, command, params) {
177
+ const query = buildQueryString({ ...params, command });
178
+ await this.request(`/api/${encodeURIComponent(camera)}/ptz${query}`);
179
+ }
180
+ // --- Test Connection ---
181
+ async testConnection() {
182
+ try {
183
+ const config = await this.getConfig();
184
+ const cameraNames = Object.keys(config.cameras ?? {});
185
+ return {
186
+ success: true,
187
+ version: config.version,
188
+ cameraCount: cameraNames.length
189
+ };
190
+ } catch (error) {
191
+ return {
192
+ success: false,
193
+ error: error instanceof Error ? error.message : String(error)
194
+ };
195
+ }
196
+ }
197
+ };
198
+ function buildQueryString(params) {
199
+ const entries = Object.entries(params).filter(
200
+ ([, v]) => v !== void 0 && v !== null
201
+ );
202
+ if (entries.length === 0) {
203
+ return "";
204
+ }
205
+ const searchParams = new URLSearchParams();
206
+ for (const [key, value] of entries) {
207
+ searchParams.set(key, String(value));
208
+ }
209
+ return `?${searchParams.toString()}`;
210
+ }
211
+
212
+ // src/frigate-mqtt.ts
213
+ var import_mqtt = require("mqtt");
214
+ var FrigateMqttClient = class {
215
+ constructor(config, cameraResolutions) {
216
+ this.config = config;
217
+ this.cameraResolutions = cameraResolutions;
218
+ }
219
+ client = null;
220
+ listeners = /* @__PURE__ */ new Set();
221
+ eventCount = 0;
222
+ async connect() {
223
+ return new Promise((resolve, reject) => {
224
+ const { brokerUrl, username, password, topicPrefix } = this.config;
225
+ this.client = (0, import_mqtt.connect)(brokerUrl, {
226
+ username,
227
+ password,
228
+ reconnectPeriod: 5e3,
229
+ connectTimeout: 1e4
230
+ });
231
+ this.client.on("connect", () => {
232
+ const prefix = topicPrefix;
233
+ this.client.subscribe([
234
+ `${prefix}/events`,
235
+ `${prefix}/reviews`,
236
+ `${prefix}/+/motion/state`,
237
+ `${prefix}/+/audio/+`
238
+ ]);
239
+ resolve();
240
+ });
241
+ this.client.on("error", (err) => {
242
+ reject(err);
243
+ });
244
+ this.client.on("message", (topic, payload) => {
245
+ const event = parseMqttMessage(
246
+ topic,
247
+ payload,
248
+ topicPrefix,
249
+ this.cameraResolutions
250
+ );
251
+ if (event) {
252
+ this.eventCount++;
253
+ for (const listener of this.listeners) {
254
+ listener(event);
255
+ }
256
+ }
257
+ });
258
+ });
259
+ }
260
+ async disconnect() {
261
+ if (this.client) {
262
+ await this.client.endAsync();
263
+ this.client = null;
264
+ }
265
+ this.listeners.clear();
266
+ }
267
+ subscribe(callback) {
268
+ this.listeners.add(callback);
269
+ return () => {
270
+ this.listeners.delete(callback);
271
+ };
272
+ }
273
+ get status() {
274
+ return {
275
+ connected: this.client?.connected ?? false,
276
+ eventCount: this.eventCount
277
+ };
278
+ }
279
+ };
280
+ function parseMqttMessage(topic, payload, prefix, resolutions) {
281
+ const topicParts = topic.split("/");
282
+ const prefixParts = prefix.split("/");
283
+ const relative = topicParts.slice(prefixParts.length);
284
+ if (relative.length === 1 && relative[0] === "events") {
285
+ return parseDetectionEvent(payload, resolutions);
286
+ }
287
+ if (relative.length === 1 && relative[0] === "reviews") {
288
+ return parseReviewEvent(payload);
289
+ }
290
+ if (relative.length === 3 && relative[1] === "motion" && relative[2] === "state") {
291
+ const camera = relative[0];
292
+ return parseMotionEvent(camera, payload);
293
+ }
294
+ if (relative.length === 3 && relative[1] === "audio") {
295
+ const camera = relative[0];
296
+ const metric = relative[2];
297
+ return parseAudioEvent(camera, metric, payload);
298
+ }
299
+ return null;
300
+ }
301
+ function parseDetectionEvent(payload, resolutions) {
302
+ try {
303
+ const parsed = JSON.parse(payload.toString());
304
+ const state = parsed.after ?? parsed.before;
305
+ if (!state?.camera || !state?.label) {
306
+ return null;
307
+ }
308
+ const data = {
309
+ eventType: parsed.type ?? "update",
310
+ id: state.id,
311
+ label: state.label,
312
+ subLabel: state.sub_label,
313
+ score: state.top_score,
314
+ startTime: state.start_time,
315
+ endTime: state.end_time,
316
+ zones: state.current_zones,
317
+ hasSnapshot: state.has_snapshot,
318
+ hasClip: state.has_clip
319
+ };
320
+ if (state.box) {
321
+ const resolution = resolutions.get(state.camera);
322
+ if (resolution) {
323
+ data.boundingBox = normalizeBoundingBox(state.box, resolution.width, resolution.height);
324
+ } else {
325
+ data.rawBox = state.box;
326
+ }
327
+ }
328
+ return {
329
+ type: "detection",
330
+ camera: state.camera,
331
+ timestamp: Date.now(),
332
+ data
333
+ };
334
+ } catch {
335
+ return null;
336
+ }
337
+ }
338
+ function parseReviewEvent(payload) {
339
+ try {
340
+ const parsed = JSON.parse(payload.toString());
341
+ const state = parsed.after ?? parsed.before;
342
+ if (!state?.camera) {
343
+ return null;
344
+ }
345
+ return {
346
+ type: "review",
347
+ camera: state.camera,
348
+ timestamp: Date.now(),
349
+ data: {
350
+ eventType: parsed.type ?? "update",
351
+ id: state.id,
352
+ severity: state.severity,
353
+ startTime: state.start_time,
354
+ endTime: state.end_time,
355
+ objects: state.data?.objects,
356
+ zones: state.data?.zones,
357
+ subLabels: state.data?.sub_labels
358
+ }
359
+ };
360
+ } catch {
361
+ return null;
362
+ }
363
+ }
364
+ function parseMotionEvent(camera, payload) {
365
+ const value = payload.toString().trim();
366
+ const active = value === "ON";
367
+ return {
368
+ type: "motion",
369
+ camera,
370
+ timestamp: Date.now(),
371
+ data: { active }
372
+ };
373
+ }
374
+ function parseAudioEvent(camera, metric, payload) {
375
+ const raw = payload.toString().trim();
376
+ const value = parseFloat(raw);
377
+ if (isNaN(value)) {
378
+ return null;
379
+ }
380
+ return {
381
+ type: "audio",
382
+ camera,
383
+ timestamp: Date.now(),
384
+ data: { metric, value }
385
+ };
386
+ }
387
+ function normalizeBoundingBox(box, width, height) {
388
+ const [yMin, xMin, yMax, xMax] = box;
389
+ return {
390
+ x: xMin / width,
391
+ y: yMin / height,
392
+ w: (xMax - xMin) / width,
393
+ h: (yMax - yMin) / height
394
+ };
395
+ }
396
+
397
+ // src/frigate-device.ts
398
+ var import_types = require("@camstack/types");
399
+ var FrigateDevice = class {
400
+ constructor(config, api, ctx) {
401
+ this.config = config;
402
+ this.api = api;
403
+ this.id = `${config.providerId}/${config.cameraName}`;
404
+ this.name = config.cameraName;
405
+ this.providerId = config.providerId;
406
+ this.ctx = ctx;
407
+ const caps = ["camera", "events", "recording", "motionSensor", "objectDetector"];
408
+ if (config.audioEnabled) caps.push("audioDetector");
409
+ this.capabilities = caps;
410
+ this.capabilityMap.set("camera", this.createCamera());
411
+ this.capabilityMap.set("motionSensor", this.createMotionSensor());
412
+ this.capabilityMap.set("objectDetector", this.createObjectDetector());
413
+ this.capabilityMap.set("events", this.createEvents());
414
+ this.capabilityMap.set("recording", this.createRecording());
415
+ if (config.audioEnabled) {
416
+ this.capabilityMap.set("audioDetector", this.createAudioDetector());
417
+ }
418
+ }
419
+ id;
420
+ name;
421
+ providerId;
422
+ type = import_types.DeviceType.Camera;
423
+ capabilities;
424
+ capabilityMap = /* @__PURE__ */ new Map();
425
+ ctx;
426
+ getCapability(cap) {
427
+ return this.capabilityMap.get(cap) ?? null;
428
+ }
429
+ hasCapability(cap) {
430
+ return this.capabilityMap.has(cap);
431
+ }
432
+ getState() {
433
+ return { online: true };
434
+ }
435
+ getMetadata() {
436
+ return { manufacturer: "Frigate NVR" };
437
+ }
438
+ // --- Capability Factories ---
439
+ createCamera() {
440
+ const { api, config } = this;
441
+ return {
442
+ kind: "camera",
443
+ async getSnapshot() {
444
+ return api.getLatestSnapshot(config.cameraName);
445
+ },
446
+ async getStreamOptions() {
447
+ return config.streams.map((s) => ({
448
+ ...s,
449
+ url: s.url ?? `rtsp://${config.frigateHost}:8554/${s.id}`
450
+ }));
451
+ },
452
+ async getStreamUrl(option) {
453
+ return `/api/go2rtc/webrtc?src=${encodeURIComponent(option.id)}`;
454
+ },
455
+ getConnectionMode() {
456
+ return "on-demand";
457
+ },
458
+ async setConnectionMode() {
459
+ }
460
+ };
461
+ }
462
+ createMotionSensor() {
463
+ let motionActive = false;
464
+ return {
465
+ kind: "motionSensor",
466
+ isMotionDetected() {
467
+ return motionActive;
468
+ },
469
+ subscribe() {
470
+ return () => {
471
+ };
472
+ }
473
+ // real-time via MQTT through provider
474
+ };
475
+ }
476
+ createObjectDetector() {
477
+ const zones = [];
478
+ const lines = [];
479
+ return {
480
+ kind: "objectDetector",
481
+ getLabels() {
482
+ return [];
483
+ },
484
+ onDetections() {
485
+ return () => {
486
+ };
487
+ },
488
+ // Zone management
489
+ async getZones() {
490
+ return zones;
491
+ },
492
+ async addZone(zone) {
493
+ zones.push(zone);
494
+ },
495
+ async removeZone(id) {
496
+ const i = zones.findIndex((z) => z.id === id);
497
+ if (i >= 0) zones.splice(i, 1);
498
+ },
499
+ async updateZone(id, partial) {
500
+ const z = zones.find((z2) => z2.id === id);
501
+ if (z) Object.assign(z, partial);
502
+ },
503
+ // Line management
504
+ async getLines() {
505
+ return lines;
506
+ },
507
+ async addLine(line) {
508
+ lines.push(line);
509
+ },
510
+ async removeLine(id) {
511
+ const i = lines.findIndex((l) => l.id === id);
512
+ if (i >= 0) lines.splice(i, 1);
513
+ },
514
+ // Active tracking (populated by MQTT events in real-time)
515
+ getActiveDetections() {
516
+ return [];
517
+ },
518
+ getZoneDetections() {
519
+ return [];
520
+ },
521
+ getStationaryDetections() {
522
+ return [];
523
+ },
524
+ getMovingDetections() {
525
+ return [];
526
+ }
527
+ };
528
+ }
529
+ createEvents() {
530
+ const { api, config } = this;
531
+ return {
532
+ kind: "events",
533
+ async getEvents(query) {
534
+ const raw = await api.getEvents({
535
+ cameras: config.cameraName,
536
+ after: query.since,
537
+ before: query.until,
538
+ limit: query.limit,
539
+ labels: query.detectionClasses?.join(",")
540
+ });
541
+ const events = raw.map((e) => ({
542
+ id: e.id,
543
+ type: "detection",
544
+ timestamp: e.start_time * 1e3,
545
+ // Frigate uses seconds, we use ms
546
+ endTimestamp: e.end_time ? e.end_time * 1e3 : void 0,
547
+ label: e.label,
548
+ score: e.top_score,
549
+ hasClip: e.has_clip,
550
+ hasSnapshot: e.has_snapshot,
551
+ thumbnailUrl: `/api/events/${e.id}/thumbnail.jpg`
552
+ }));
553
+ return { events, total: events.length };
554
+ },
555
+ async getEventThumbnail(eventId) {
556
+ try {
557
+ return await api.getEventThumbnail(eventId);
558
+ } catch {
559
+ return null;
560
+ }
561
+ },
562
+ async getEventSnapshot(eventId) {
563
+ try {
564
+ return await api.getEventSnapshot(eventId);
565
+ } catch {
566
+ return null;
567
+ }
568
+ },
569
+ async getEventClipUrl(eventId) {
570
+ return `/api/events/${encodeURIComponent(eventId)}/clip.mp4`;
571
+ }
572
+ };
573
+ }
574
+ createRecording() {
575
+ const { api, config } = this;
576
+ return {
577
+ kind: "recording",
578
+ async getSegments(range) {
579
+ const recs = await api.getRecordings(config.cameraName, range.since, range.until);
580
+ return recs.map((r) => ({
581
+ id: r.id ?? `${config.cameraName}-${r.start_time}`,
582
+ startTime: r.start_time * 1e3,
583
+ endTime: r.end_time * 1e3,
584
+ duration: r.duration
585
+ }));
586
+ },
587
+ async getPlaybackUrl(startTime, endTime) {
588
+ const startSec = Math.floor(startTime / 1e3);
589
+ const endSec = Math.floor(endTime / 1e3);
590
+ return `/api/${encodeURIComponent(config.cameraName)}/start/${startSec}/end/${endSec}/clip.mp4`;
591
+ },
592
+ async getThumbnailAt(timestampMs) {
593
+ try {
594
+ return await api.getRecordingThumbnail(config.cameraName, Math.floor(timestampMs / 1e3));
595
+ } catch {
596
+ return null;
597
+ }
598
+ }
599
+ };
600
+ }
601
+ createAudioDetector() {
602
+ return {
603
+ kind: "audioDetector",
604
+ getAudioLevel() {
605
+ return 0;
606
+ },
607
+ subscribe() {
608
+ return () => {
609
+ };
610
+ }
611
+ // real-time via MQTT through provider
612
+ };
613
+ }
614
+ };
615
+
616
+ // src/element-config-store.ts
617
+ var ElementConfigStore = class {
618
+ constructor(elementId, storage) {
619
+ this.elementId = elementId;
620
+ this.storage = storage;
621
+ }
622
+ cache = {};
623
+ listeners = /* @__PURE__ */ new Set();
624
+ loaded = false;
625
+ /** Load config from storage into cache. Called once on first access. */
626
+ async ensureLoaded() {
627
+ if (this.loaded) return;
628
+ if (!this.storage.structured) {
629
+ this.loaded = true;
630
+ return;
631
+ }
632
+ try {
633
+ const records = await this.storage.structured.query("config", {
634
+ where: { id: this.elementId },
635
+ limit: 1
636
+ });
637
+ if (records.length > 0) {
638
+ this.cache = records[0].data ?? {};
639
+ }
640
+ } catch {
641
+ }
642
+ this.loaded = true;
643
+ }
644
+ getAll() {
645
+ return { ...this.cache };
646
+ }
647
+ get(key) {
648
+ const parts = key.split(".");
649
+ let current = this.cache;
650
+ for (const part of parts) {
651
+ if (current == null || typeof current !== "object") return void 0;
652
+ current = current[part];
653
+ }
654
+ return current;
655
+ }
656
+ async set(key, value) {
657
+ await this.ensureLoaded();
658
+ setNestedValue(this.cache, key, value);
659
+ await this.persist();
660
+ this.notifyListeners();
661
+ }
662
+ async setAll(config) {
663
+ await this.ensureLoaded();
664
+ this.cache = { ...config };
665
+ await this.persist();
666
+ this.notifyListeners();
667
+ }
668
+ onChange(callback) {
669
+ this.listeners.add(callback);
670
+ return () => {
671
+ this.listeners.delete(callback);
672
+ };
673
+ }
674
+ /** Initialize from storage — called by ContextFactory after creation */
675
+ async load() {
676
+ await this.ensureLoaded();
677
+ }
678
+ /** Initialize with default values (doesn't overwrite existing) */
679
+ async loadDefaults(defaults) {
680
+ await this.ensureLoaded();
681
+ if (Object.keys(this.cache).length === 0) {
682
+ this.cache = { ...defaults };
683
+ await this.persist();
684
+ }
685
+ }
686
+ async persist() {
687
+ if (!this.storage.structured) return;
688
+ try {
689
+ const existing = await this.storage.structured.query("config", {
690
+ where: { id: this.elementId },
691
+ limit: 1
692
+ });
693
+ if (existing.length > 0) {
694
+ await this.storage.structured.update("config", this.elementId, this.cache);
695
+ } else {
696
+ await this.storage.structured.insert({
697
+ collection: "config",
698
+ id: this.elementId,
699
+ data: this.cache
700
+ });
701
+ }
702
+ } catch {
703
+ }
704
+ }
705
+ notifyListeners() {
706
+ const snapshot = this.getAll();
707
+ for (const listener of this.listeners) {
708
+ try {
709
+ listener(snapshot);
710
+ } catch {
711
+ }
712
+ }
713
+ }
714
+ };
715
+ function setNestedValue(obj, path, value) {
716
+ const parts = path.split(".");
717
+ let current = obj;
718
+ for (let i = 0; i < parts.length - 1; i++) {
719
+ const part = parts[i];
720
+ if (!(part in current) || typeof current[part] !== "object" || current[part] === null) {
721
+ current[part] = {};
722
+ }
723
+ current = current[part];
724
+ }
725
+ current[parts[parts.length - 1]] = value;
726
+ }
727
+
728
+ // src/frigate-provider.ts
729
+ var ADOPTED_DEVICES_KEY = "adoptedDevices";
730
+ var FrigateProvider = class {
731
+ constructor(config, ctx) {
732
+ this.config = config;
733
+ this.id = config.id;
734
+ this.name = config.name;
735
+ this.ctx = ctx;
736
+ this.api = new FrigateApiClient({
737
+ baseUrl: config.url,
738
+ username: config.username,
739
+ password: config.password
740
+ });
741
+ this.frigateHost = new URL(config.url).hostname;
742
+ }
743
+ id;
744
+ type = "frigate";
745
+ name;
746
+ discoveryMode = "auto";
747
+ ctx;
748
+ api;
749
+ mqtt = null;
750
+ devices = [];
751
+ liveEventListeners = /* @__PURE__ */ new Set();
752
+ mqttUnsubscribe;
753
+ /** Cached Frigate config, refreshed on start() and discoverDevices() */
754
+ frigateConfig = null;
755
+ go2rtcStreams = {};
756
+ frigateHost;
757
+ async start() {
758
+ this.ctx.logger.info(`Starting Frigate provider: ${this.config.url}`);
759
+ this.frigateConfig = await this.api.getConfig();
760
+ this.go2rtcStreams = await this.api.getGo2rtcStreams();
761
+ const resolutions = /* @__PURE__ */ new Map();
762
+ for (const [name, cam] of Object.entries(this.frigateConfig.cameras)) {
763
+ if (cam.detect) {
764
+ resolutions.set(name, { width: cam.detect.width, height: cam.detect.height });
765
+ }
766
+ }
767
+ const adoptedIds = this.getAdoptedDeviceIds();
768
+ if (adoptedIds.length > 0) {
769
+ const devices = [];
770
+ for (const cameraName of adoptedIds) {
771
+ const cam = this.frigateConfig.cameras[cameraName];
772
+ if (!cam || cam.enabled === false) {
773
+ this.ctx.logger.warn(`Adopted camera "${cameraName}" no longer available in Frigate config, skipping`);
774
+ continue;
775
+ }
776
+ devices.push(this.createDeviceFromCamera(cameraName, cam));
777
+ }
778
+ this.devices = devices;
779
+ this.ctx.logger.info(`Loaded ${devices.length} adopted cameras`);
780
+ } else {
781
+ this.ctx.logger.info("No adopted cameras yet \u2014 use discoverDevices() + adoptDevice() to import cameras");
782
+ }
783
+ if (this.config.mqtt?.brokerUrl) {
784
+ this.mqtt = new FrigateMqttClient(
785
+ {
786
+ brokerUrl: this.config.mqtt.brokerUrl,
787
+ username: this.config.mqtt.username,
788
+ password: this.config.mqtt.password,
789
+ topicPrefix: this.config.mqtt.topicPrefix ?? "frigate"
790
+ },
791
+ resolutions
792
+ );
793
+ await this.mqtt.connect();
794
+ this.mqttUnsubscribe = this.mqtt.subscribe((event) => {
795
+ for (const listener of this.liveEventListeners) {
796
+ listener(event);
797
+ }
798
+ });
799
+ this.ctx.logger.info("MQTT connected");
800
+ }
801
+ }
802
+ async stop() {
803
+ this.ctx.logger.info("Stopping Frigate provider");
804
+ this.mqttUnsubscribe?.();
805
+ this.mqttUnsubscribe = void 0;
806
+ await this.mqtt?.disconnect();
807
+ this.mqtt = null;
808
+ this.devices = [];
809
+ this.frigateConfig = null;
810
+ this.go2rtcStreams = {};
811
+ }
812
+ getStatus() {
813
+ return {
814
+ connected: this.mqtt?.status.connected ?? false,
815
+ deviceCount: this.devices.length
816
+ };
817
+ }
818
+ async discoverDevices() {
819
+ this.frigateConfig = await this.api.getConfig();
820
+ this.go2rtcStreams = await this.api.getGo2rtcStreams();
821
+ const adoptedIds = this.getAdoptedDeviceIds();
822
+ const discovered = [];
823
+ for (const [name, cam] of Object.entries(this.frigateConfig.cameras)) {
824
+ if (cam.enabled === false) continue;
825
+ const caps = ["camera", "events", "recording", "motionSensor", "objectDetector"];
826
+ if (cam.audio?.enabled) caps.push("audioDetector");
827
+ discovered.push({
828
+ externalId: name,
829
+ name,
830
+ type: "camera",
831
+ capabilities: caps,
832
+ metadata: { manufacturer: "Frigate NVR" }
833
+ });
834
+ }
835
+ return discovered;
836
+ }
837
+ async adoptDevice(externalId, _config) {
838
+ if (!this.frigateConfig) {
839
+ this.frigateConfig = await this.api.getConfig();
840
+ this.go2rtcStreams = await this.api.getGo2rtcStreams();
841
+ }
842
+ const cam = this.frigateConfig.cameras[externalId];
843
+ if (!cam) {
844
+ throw new Error(`Camera "${externalId}" not found in Frigate config`);
845
+ }
846
+ if (cam.enabled === false) {
847
+ throw new Error(`Camera "${externalId}" is disabled in Frigate config`);
848
+ }
849
+ const existing = this.devices.find((d) => d.name === externalId);
850
+ if (existing) {
851
+ this.ctx.logger.info(`Camera "${externalId}" is already adopted`);
852
+ return existing;
853
+ }
854
+ const device = this.createDeviceFromCamera(externalId, cam);
855
+ this.devices = [...this.devices, device];
856
+ await this.persistAdoptedDeviceId(externalId);
857
+ this.ctx.logger.info(`Adopted camera "${externalId}" (total: ${this.devices.length})`);
858
+ return device;
859
+ }
860
+ getDevices() {
861
+ return [...this.devices];
862
+ }
863
+ subscribeLiveEvents(callback) {
864
+ this.liveEventListeners.add(callback);
865
+ return () => {
866
+ this.liveEventListeners.delete(callback);
867
+ };
868
+ }
869
+ // --- Private helpers ---
870
+ createDeviceFromCamera(cameraName, cam) {
871
+ const streams = buildStreamOptions(cameraName, this.go2rtcStreams);
872
+ const deviceId = `${this.id}/${cameraName}`;
873
+ const deviceCtx = {
874
+ id: `device:${deviceId}`,
875
+ logger: this.ctx.logger.child(cameraName),
876
+ eventBus: this.ctx.eventBus,
877
+ storage: this.ctx.storage,
878
+ config: new ElementConfigStore(`device:${deviceId}`, this.ctx.storage)
879
+ };
880
+ return new FrigateDevice(
881
+ {
882
+ cameraName,
883
+ providerId: this.id,
884
+ detectWidth: cam.detect?.width ?? 1920,
885
+ detectHeight: cam.detect?.height ?? 1080,
886
+ audioEnabled: cam.audio?.enabled ?? false,
887
+ recordEnabled: cam.record?.enabled ?? false,
888
+ ptzEnabled: false,
889
+ streams,
890
+ frigateHost: this.frigateHost
891
+ },
892
+ this.api,
893
+ deviceCtx
894
+ );
895
+ }
896
+ getAdoptedDeviceIds() {
897
+ return this.ctx.config.get(ADOPTED_DEVICES_KEY) ?? [];
898
+ }
899
+ async persistAdoptedDeviceId(cameraName) {
900
+ const current = this.getAdoptedDeviceIds();
901
+ if (current.includes(cameraName)) return;
902
+ await this.ctx.config.set(ADOPTED_DEVICES_KEY, [...current, cameraName]);
903
+ }
904
+ };
905
+ function buildStreamOptions(cameraName, go2rtcStreams) {
906
+ const streams = [];
907
+ for (const streamName of Object.keys(go2rtcStreams)) {
908
+ if (!streamName.startsWith(cameraName)) continue;
909
+ const isSub = streamName.includes("sub") || streamName.includes("ext") || streamName.includes("medium");
910
+ streams.push({ id: streamName, label: streamName, protocol: "rtsp", quality: isSub ? "sub" : "main" });
911
+ }
912
+ return streams;
913
+ }
914
+
915
+ // src/addon.ts
916
+ var FrigateProviderAddon = class {
917
+ manifest = {
918
+ id: "provider-frigate",
919
+ name: "Frigate NVR Provider",
920
+ version: "0.1.0",
921
+ description: "Integrazione con Frigate NVR per camere e detection",
922
+ capabilities: ["device-provider"]
923
+ };
924
+ provider = null;
925
+ async initialize(context) {
926
+ const config = context.addonConfig;
927
+ if (!config.url) {
928
+ context.logger.warn("Frigate provider: no URL configured, skipping initialization");
929
+ return;
930
+ }
931
+ const providerConfig = {
932
+ id: config.id ?? "frigate-default",
933
+ name: config.name ?? "Frigate NVR",
934
+ url: config.url,
935
+ username: config.username,
936
+ password: config.password,
937
+ mqtt: config.mqtt
938
+ };
939
+ this.provider = new FrigateProvider(providerConfig, {
940
+ id: context.id,
941
+ logger: context.logger,
942
+ eventBus: context.eventBus,
943
+ storage: context.storage,
944
+ config: context.config
945
+ });
946
+ context.logger.info("Frigate provider addon initialized");
947
+ }
948
+ async shutdown() {
949
+ await this.provider?.stop();
950
+ this.provider = null;
951
+ }
952
+ getCapabilityProvider(name) {
953
+ if (name === "device-provider" && this.provider) {
954
+ return this.provider;
955
+ }
956
+ return null;
957
+ }
958
+ getConfigSchema() {
959
+ return {
960
+ sections: [
961
+ {
962
+ id: "connection",
963
+ title: "Frigate Connection",
964
+ description: "Configure the connection to your Frigate NVR instance",
965
+ columns: 2,
966
+ fields: [
967
+ { type: "text", key: "url", label: "Frigate URL", required: true, placeholder: "http://frigate:5000" },
968
+ { type: "text", key: "name", label: "Display Name", placeholder: "Frigate NVR" },
969
+ { type: "text", key: "username", label: "Username" },
970
+ { type: "password", key: "password", label: "Password", showToggle: true }
971
+ ]
972
+ },
973
+ {
974
+ id: "mqtt",
975
+ title: "MQTT Settings",
976
+ description: "Optional: Connect to Frigate MQTT for real-time events",
977
+ style: "accordion",
978
+ defaultCollapsed: true,
979
+ columns: 2,
980
+ fields: [
981
+ { type: "text", key: "mqtt.brokerUrl", label: "MQTT Broker URL", placeholder: "mqtt://broker:1883" },
982
+ { type: "text", key: "mqtt.topicPrefix", label: "Topic Prefix", placeholder: "frigate" },
983
+ { type: "text", key: "mqtt.username", label: "MQTT Username" },
984
+ { type: "password", key: "mqtt.password", label: "MQTT Password", showToggle: true }
985
+ ]
986
+ }
987
+ ]
988
+ };
989
+ }
990
+ getConfig() {
991
+ return {};
992
+ }
993
+ async onConfigChange(_config) {
994
+ }
995
+ };
996
+ // Annotate the CommonJS export names for ESM import in node:
997
+ 0 && (module.exports = {
998
+ FrigateProviderAddon
999
+ });
1000
+ //# sourceMappingURL=addon.js.map