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