@camstack/addon-provider-frigate 0.1.0

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