iobroker.agent-dvr 0.0.4 → 0.0.6

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/build/main.js CHANGED
@@ -23,6 +23,7 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
23
23
  ));
24
24
  var utils = __toESM(require("@iobroker/adapter-core"));
25
25
  var http = __toESM(require("node:http"));
26
+ var https = __toESM(require("node:https"));
26
27
  var import_widget_i18n = require("./lib/widget-i18n");
27
28
  const CAM_COMMANDS = [
28
29
  { id: "record", path: "/command/record", params: ["oid", "ot"], name: "Start recording" },
@@ -181,11 +182,22 @@ function findDevices(json) {
181
182
  }
182
183
  return out;
183
184
  }
184
- function galleryCss(minCol, maxW) {
185
- return `.advgrid{display:grid;grid-template-columns:repeat(auto-fill,minmax(${minCol}px,1fr));gap:12px;max-width:100%}.advgrid .advlb{display:none}.advcell{display:flex;flex-direction:column;cursor:pointer;text-decoration:none;color:inherit;font-family:inherit}.advimg{position:relative;display:block;border-radius:8px;overflow:hidden;background:rgba(127,127,127,.15)}.advimg img{width:100%;height:auto;display:block}.advimgfix img{height:100%;object-fit:cover}.advtag{position:absolute;background:rgba(0,0,0,.55);color:#fff;font-size:.6rem;padding:2px 4px;border-radius:3px;line-height:1;max-width:85%;overflow:hidden;white-space:nowrap;text-overflow:ellipsis}.advplay{position:absolute;inset:0;display:flex;align-items:center;justify-content:center;opacity:0;transition:opacity .15s;background:rgba(0,0,0,.2)}.advplay::before{content:"";border-style:solid;border-width:11px 0 11px 18px;border-color:transparent transparent transparent #fff;filter:drop-shadow(0 1px 2px rgba(0,0,0,.6))}.advcell:hover .advplay{opacity:1}.advcap{font-size:.72rem;margin-top:4px;line-height:1.25;opacity:.85}.advmodal{display:none;position:fixed;inset:0;z-index:99999;align-items:center;justify-content:center}.advgrid .advlb:checked + .advthumb + .advmodal{display:flex}.advbackdrop{position:absolute;inset:0;background:transparent;cursor:pointer}.advbox{position:relative;z-index:1;display:flex;flex-direction:column;gap:8px;max-width:min(92vw,${maxW}px);background:#1c1c1e;padding:12px;border-radius:14px;box-shadow:0 16px 48px rgba(0,0,0,.55),0 0 0 1px rgba(255,255,255,.08)}.advvideo{width:100%;max-height:80vh;border-radius:8px;background:#000;display:block}.advclose{position:absolute;top:18px;right:18px;width:32px;height:32px;border-radius:50%;background:rgba(0,0,0,.65);color:#fff;display:flex;align-items:center;justify-content:center;cursor:pointer;font-size:15px;z-index:2;border:2px solid rgba(255,255,255,.35)}.advinfo{color:#fff;font-size:.8rem;text-align:center}.advinfo a{color:#8ab4f8}.advempty{padding:24px;text-align:center;opacity:.7;font-family:inherit}`;
185
+ function advCssVars(c) {
186
+ const cardBg = (c == null ? void 0 : c.cardBg) || "rgba(127,127,127,.15)";
187
+ const tagBg = (c == null ? void 0 : c.tagBg) || "rgba(0,0,0,.55)";
188
+ const tagText = (c == null ? void 0 : c.tagText) || "#fff";
189
+ const accent = (c == null ? void 0 : c.accent) || "#8ab4f8";
190
+ const modalBg = (c == null ? void 0 : c.modalBg) || "#1c1c1e";
191
+ const radius = (c == null ? void 0 : c.radius) != null ? `${c.radius}px` : "8px";
192
+ return `--adv-card-bg:${cardBg};--adv-tag-bg:${tagBg};--adv-tag-color:${tagText};--adv-accent:${accent};--adv-modal-bg:${modalBg};--adv-radius:${radius};`;
186
193
  }
187
- function galleryCssJs(minCol) {
188
- return `.advroot{font-family:inherit;color:inherit}.advbar{display:flex;flex-wrap:wrap;gap:8px;align-items:center;margin-bottom:10px}.advsearchjs{flex:1 1 160px;min-width:120px;padding:6px 10px;border-radius:8px;border:1px solid rgba(127,127,127,.4);background:rgba(127,127,127,.08);color:inherit;font:inherit}.advtagsjs{display:flex;flex-wrap:wrap;gap:6px}.advtagbtn{padding:4px 10px;border-radius:999px;border:1px solid rgba(127,127,127,.4);background:transparent;color:inherit;cursor:pointer;font:inherit;font-size:.8rem}.advtagbtn.on{background:#2d6cdf;color:#fff;border-color:#2d6cdf}.advgridjs{display:grid;grid-template-columns:repeat(auto-fill,minmax(${minCol}px,1fr));gap:12px}.advcelljs{display:flex;flex-direction:column;cursor:pointer}.advimgjs{position:relative;display:block;border-radius:8px;overflow:hidden;background:rgba(127,127,127,.15)}.advimgjs img{width:100%;height:auto;display:block}.advimgjs.advimgfix img{height:100%;object-fit:cover}.advtagjs{position:absolute;background:rgba(0,0,0,.55);color:#fff;font-size:.6rem;padding:2px 4px;border-radius:3px;line-height:1;max-width:85%;overflow:hidden;white-space:nowrap;text-overflow:ellipsis}.advplayjs{position:absolute;inset:0;display:flex;align-items:center;justify-content:center;opacity:0;transition:opacity .15s;background:rgba(0,0,0,.2)}.advplayjs::before{content:"";border-style:solid;border-width:11px 0 11px 18px;border-color:transparent transparent transparent #fff;filter:drop-shadow(0 1px 2px rgba(0,0,0,.6))}.advcelljs:hover .advplayjs{opacity:1}.advcapjs{font-size:.72rem;margin-top:4px;line-height:1.25;opacity:.85}.advemptyjs{padding:24px;text-align:center;opacity:.7}`;
194
+ function galleryCss(minCol, maxW, compact = false, colors) {
195
+ const gap = compact ? 6 : 12;
196
+ return `.advgrid{${advCssVars(colors)}display:grid;grid-template-columns:repeat(auto-fill,minmax(${minCol}px,1fr));gap:${gap}px;max-width:100%}.advgrid .advlb{display:none}.advcell{display:flex;flex-direction:column;cursor:pointer;text-decoration:none;color:inherit;font-family:inherit}.advimg{position:relative;display:block;border-radius:var(--adv-radius);overflow:hidden;background:var(--adv-card-bg)}.advimg img{width:100%;height:auto;display:block}.advimgfix img{height:100%;object-fit:cover}.advtag{position:absolute;background:var(--adv-tag-bg);color:var(--adv-tag-color);font-size:.6rem;padding:2px 4px;border-radius:3px;line-height:1;max-width:85%;overflow:hidden;white-space:nowrap;text-overflow:ellipsis}.advplay{position:absolute;inset:0;display:flex;align-items:center;justify-content:center;opacity:0;transition:opacity .15s;background:rgba(0,0,0,.2)}.advplay::before{content:"";border-style:solid;border-width:11px 0 11px 18px;border-color:transparent transparent transparent #fff;filter:drop-shadow(0 1px 2px rgba(0,0,0,.6))}.advcell:hover .advplay{opacity:1}.advcap{font-size:${compact ? ".65rem" : ".72rem"};margin-top:${compact ? 2 : 4}px;line-height:1.25;opacity:.85}.advmodal{display:none;position:fixed;inset:0;z-index:99999;align-items:center;justify-content:center}.advgrid .advlb:checked + .advthumb + .advmodal{display:flex}.advbackdrop{position:absolute;inset:0;background:transparent;cursor:pointer}.advbox{position:relative;z-index:1;display:flex;flex-direction:column;gap:8px;max-width:min(92vw,${maxW}px);background:var(--adv-modal-bg);padding:12px;border-radius:14px;box-shadow:0 16px 48px rgba(0,0,0,.55),0 0 0 1px rgba(255,255,255,.08)}.advvideo{width:100%;max-height:80vh;border-radius:8px;background:#000;display:block}.advclose{position:absolute;top:18px;right:18px;width:32px;height:32px;border-radius:50%;background:rgba(0,0,0,.65);color:#fff;display:flex;align-items:center;justify-content:center;cursor:pointer;font-size:15px;z-index:2;border:2px solid rgba(255,255,255,.35)}.advinfo{color:#fff;font-size:.8rem;text-align:center}.advinfo a{color:var(--adv-accent)}.advempty{padding:24px;text-align:center;opacity:.7;font-family:inherit}`;
197
+ }
198
+ function galleryCssJs(minCol, compact = false, colors) {
199
+ const gap = compact ? 6 : 12;
200
+ return `.advroot{${advCssVars(colors)}font-family:inherit;color:inherit}.advbar{display:flex;flex-wrap:wrap;gap:8px;align-items:center;margin-bottom:10px}.advsearchjs{flex:1 1 160px;min-width:120px;padding:6px 10px;border-radius:8px;border:1px solid rgba(127,127,127,.4);background:rgba(127,127,127,.08);color:inherit;font:inherit}.advtagsjs{display:flex;flex-wrap:wrap;gap:6px}.advtagbtn{padding:4px 10px;border-radius:999px;border:1px solid rgba(127,127,127,.4);background:transparent;color:inherit;cursor:pointer;font:inherit;font-size:.8rem}.advtagbtn.on{background:var(--adv-accent);color:#fff;border-color:var(--adv-accent)}.advgridjs{display:grid;grid-template-columns:repeat(auto-fill,minmax(${minCol}px,1fr));gap:${gap}px}.advcelljs{display:flex;flex-direction:column;cursor:pointer}.advimgjs{position:relative;display:block;border-radius:var(--adv-radius);overflow:hidden;background:var(--adv-card-bg)}.advimgjs img{width:100%;height:auto;display:block}.advimgjs.advimgfix img{height:100%;object-fit:cover}.advtagjs{position:absolute;background:var(--adv-tag-bg);color:var(--adv-tag-color);font-size:.6rem;padding:2px 4px;border-radius:3px;line-height:1;max-width:85%;overflow:hidden;white-space:nowrap;text-overflow:ellipsis}.advplayjs{position:absolute;inset:0;display:flex;align-items:center;justify-content:center;opacity:0;transition:opacity .15s;background:rgba(0,0,0,.2)}.advplayjs::before{content:"";border-style:solid;border-width:11px 0 11px 18px;border-color:transparent transparent transparent #fff;filter:drop-shadow(0 1px 2px rgba(0,0,0,.6))}.advcelljs:hover .advplayjs{opacity:1}.advcapjs{font-size:${compact ? ".65rem" : ".72rem"};margin-top:${compact ? 2 : 4}px;line-height:1.25;opacity:.85}.advemptyjs{padding:24px;text-align:center;opacity:.7}`;
189
201
  }
190
202
  const ADV_CLIENT_CODE = `
191
203
  (function(){
@@ -226,7 +238,7 @@ function initRoot(root){
226
238
  var D;try{D=JSON.parse(dataEl.textContent);}catch(e){return;}
227
239
  var cfg=D.cfg||{},items=D.items||[],live=D.live||null;
228
240
  var grid=root.querySelector('.advgridjs'),search=root.querySelector('.advsearchjs'),tagsBox=root.querySelector('.advtagsjs');
229
- var state={q:'',tag:''};
241
+ var state={q:'',tag:cfg.default_tag||''};
230
242
  var tagset={};
231
243
  items.forEach(function(it){String(it.tag||'').split(/[,\\s]+/).forEach(function(t){if(t)tagset[t]=1;});});
232
244
  var tags=Object.keys(tagset);
@@ -287,12 +299,23 @@ class AgentDvr extends utils.Adapter {
287
299
  super({ ...options, name: "agent-dvr" });
288
300
  this.on("ready", this.onReady.bind(this));
289
301
  this.on("stateChange", this.onStateChange.bind(this));
302
+ this.on("message", this.onMessage.bind(this));
290
303
  this.on("unload", this.onUnload.bind(this));
291
304
  }
292
305
  // ---- lifecycle ----
306
+ async ensureWebInstance() {
307
+ var _a;
308
+ const instanceId = `system.adapter.${this.namespace}`;
309
+ const obj = await this.getForeignObjectAsync(instanceId);
310
+ if (obj && !((_a = obj.native) == null ? void 0 : _a.webInstance)) {
311
+ await this.extendForeignObjectAsync(instanceId, { native: { webInstance: "*" } });
312
+ this.log.info("webInstance set \u2014 web adapter will reload the extension");
313
+ }
314
+ }
293
315
  async onReady() {
294
316
  var _a;
295
317
  void this.setState("info.connection", false, true);
318
+ await this.ensureWebInstance();
296
319
  const sysConfig = await this.getForeignObjectAsync("system.config");
297
320
  const rawLang = (_a = sysConfig == null ? void 0 : sysConfig.common) == null ? void 0 : _a.language;
298
321
  this.wt = (0, import_widget_i18n.getWidgetLabels)(rawLang != null ? rawLang : "en");
@@ -328,6 +351,49 @@ class AgentDvr extends utils.Adapter {
328
351
  }
329
352
  callback();
330
353
  }
354
+ onMessage(obj) {
355
+ if (!obj || obj.command !== "getGo2rtcStreams") {
356
+ return;
357
+ }
358
+ void this.fetchGo2rtcStreams().then((streams) => {
359
+ this.sendTo(obj.from, obj.command, { streams }, obj.callback);
360
+ });
361
+ }
362
+ fetchGo2rtcStreams() {
363
+ return new Promise((resolve) => {
364
+ const url = this.config.go2rtcUrl;
365
+ if (!url) {
366
+ resolve([]);
367
+ return;
368
+ }
369
+ let target;
370
+ try {
371
+ target = new URL("/api/streams", url);
372
+ } catch {
373
+ resolve([]);
374
+ return;
375
+ }
376
+ const mod = target.protocol === "https:" ? https : http;
377
+ const req = mod.get(target.toString(), (res) => {
378
+ let body = "";
379
+ res.on("data", (c) => {
380
+ body += c.toString();
381
+ });
382
+ res.on("end", () => {
383
+ try {
384
+ resolve(Object.keys(JSON.parse(body) || {}));
385
+ } catch {
386
+ resolve([]);
387
+ }
388
+ });
389
+ res.on("error", () => resolve([]));
390
+ });
391
+ req.setTimeout(4e3, () => {
392
+ req.destroy();
393
+ });
394
+ req.on("error", () => resolve([]));
395
+ });
396
+ }
331
397
  onStateChange(id, state) {
332
398
  if (!state || state.ack !== false) {
333
399
  return;
@@ -607,6 +673,36 @@ class AgentDvr extends utils.Adapter {
607
673
  deviceFolder(d) {
608
674
  return `${d.ot === 1 ? "mic" : "cam"}_${sanitize(d.oid)}_${sanitize(d.name)}`;
609
675
  }
676
+ // ---- device status data points ----
677
+ STATUS_DPS = [
678
+ { key: "recording", role: "indicator.working", name: "Active recording" },
679
+ { key: "online", role: "indicator.reachable", name: "Online" },
680
+ { key: "connected", role: "indicator.connected", name: "Stream connected" },
681
+ { key: "detected", role: "indicator", name: "Motion detected" },
682
+ { key: "alerted", role: "indicator.alarm", name: "Alert active" }
683
+ ];
684
+ async writeStatusDps(d, fid) {
685
+ const data = d.raw.data;
686
+ if (!data || typeof data !== "object") {
687
+ return;
688
+ }
689
+ const sfid = `${fid}.status`;
690
+ if (!this.ensuredFolders.has(sfid)) {
691
+ await this.ensureFolder(sfid, "Status", "channel");
692
+ }
693
+ for (const dp of this.STATUS_DPS) {
694
+ if (!(dp.key in data)) {
695
+ continue;
696
+ }
697
+ const id = `${sfid}.${dp.key}`;
698
+ await this.setObjectNotExistsAsync(id, {
699
+ type: "state",
700
+ common: { name: dp.name, type: "boolean", role: dp.role, read: true, write: false, def: false },
701
+ native: {}
702
+ });
703
+ await this.setStateAsync(id, { val: !!data[dp.key], ack: true });
704
+ }
705
+ }
610
706
  // ---- build device data points ----
611
707
  async buildDevice(d) {
612
708
  const fid = this.deviceFolder(d);
@@ -616,6 +712,7 @@ class AgentDvr extends utils.Adapter {
616
712
  await this.detectAspect(d);
617
713
  }
618
714
  await this.flattenWrite(d.raw, fid, 0);
715
+ await this.writeStatusDps(d, fid);
619
716
  await this.ensureFolder(`${fid}.control`, "Control", "channel");
620
717
  for (const c of CAM_COMMANDS) {
621
718
  if (c.id === "snapshot" && d.ot !== 2) {
@@ -701,19 +798,52 @@ class AgentDvr extends utils.Adapter {
701
798
  return { fn, sizeMB, dur, tag, date, time, thumb, video };
702
799
  }
703
800
  // ---- gallery HTML builders ----
801
+ effectiveMinCol() {
802
+ const sizeMap = { small: 120, medium: 180, large: 240 };
803
+ const ts = this.config.widgetThumbSize;
804
+ return ts && sizeMap[ts] ? sizeMap[ts] : this.config.widgetMinCol || 150;
805
+ }
806
+ async pruneStaleDevices(activeFolders) {
807
+ const allObjs = await this.getAdapterObjectsAsync();
808
+ const stale = [];
809
+ for (const fullId of Object.keys(allObjs)) {
810
+ const obj = allObjs[fullId];
811
+ if (obj.type !== "device") {
812
+ continue;
813
+ }
814
+ const rel = fullId.slice(this.namespace.length + 1);
815
+ if (/^(cam|mic)_/.test(rel) && !activeFolders.has(rel)) {
816
+ stale.push(rel);
817
+ }
818
+ }
819
+ for (const rel of stale) {
820
+ this.log.info(`[agent-dvr] removing stale device: ${rel}`);
821
+ await this.delObjectAsync(rel, { recursive: true });
822
+ for (const [oid, d] of this.devById) {
823
+ if (this.deviceFolder(d) === rel) {
824
+ this.devById.delete(oid);
825
+ break;
826
+ }
827
+ }
828
+ }
829
+ }
830
+ sortedEvents(events) {
831
+ return this.config.widgetSortNewest === false ? [...events].reverse() : events;
832
+ }
704
833
  buildGalleryHtml(d, events) {
705
834
  return (this.config.widgetMode || "nojs") === "js" ? this.buildGalleryHtmlJs(d, events) : this.buildGalleryHtmlNojs(d, events);
706
835
  }
707
836
  buildGalleryHtmlNojs(d, events) {
708
837
  const oid = d.oid;
709
- const minCol = this.config.widgetMinCol || 150;
838
+ const minCol = this.effectiveMinCol();
710
839
  const maxW = this.config.widgetMaxModalWidth || 900;
711
840
  const showTags = this.config.widgetShowTags;
712
841
  const pos = (this.config.widgetTagPosition || "bottom-left").split("-");
713
842
  const tagStyle = `${pos[0]}:5px;${pos[1]}:5px`;
714
843
  const playerUrl = (this.config.widgetPlayerUrl || "").trim();
844
+ const compact = !!this.config.widgetCompact;
715
845
  const PAUSE_ATTR = ` onchange="if(!this.checked){var m=this.nextElementSibling.nextElementSibling,v=m&&m.querySelector('video');if(v){v.pause();}}"`;
716
- const items = events.map((ev, i) => {
846
+ const items = this.sortedEvents(events).map((ev, i) => {
717
847
  const p = this.fmtEvent(ev, oid);
718
848
  const id = `adv${sanitize(oid)}_${i}`;
719
849
  const badge = showTags && p.tag ? `<span class="advtag" style="${tagStyle}">${escHtml(p.tag)}</span>` : "";
@@ -724,14 +854,26 @@ class AgentDvr extends utils.Adapter {
724
854
  }
725
855
  return `<input class="advlb" type="checkbox" id="${id}"${PAUSE_ATTR}><label class="advcell advthumb" for="${id}">${inner}</label><div class="advmodal"><label class="advbackdrop" for="${id}"></label><div class="advbox"><label class="advclose" for="${id}">&#10005;</label><video class="advvideo" controls preload="none" playsinline src="${p.video}"></video><div class="advinfo">${escHtml(p.date)} ${escHtml(p.time)} &middot; ${escHtml(p.dur)}s &middot; ${escHtml(p.sizeMB)} MB${p.tag ? ` &middot; ${escHtml(p.tag)}` : ""} &middot; <a href="${p.video}" download target="_blank">${this.wt.download}</a></div></div></div>`;
726
856
  }).join("");
727
- const grid = items ? `<div class="advgrid">${items}</div>` : `<div class="advempty">${this.wt.noRecordings}</div>`;
728
- return `<style>${galleryCss(minCol, maxW)}</style>${grid}`;
857
+ const gridClass = compact ? "advgrid advcompact" : "advgrid";
858
+ const grid = items ? `<div class="${gridClass}">${items}</div>` : `<div class="advempty">${this.wt.noRecordings}</div>`;
859
+ const colors = {
860
+ cardBg: this.config.widgetColorCardBg || void 0,
861
+ tagBg: this.config.widgetColorTagBg || void 0,
862
+ tagText: this.config.widgetColorTagText || void 0,
863
+ accent: this.config.widgetColorAccent || void 0,
864
+ modalBg: this.config.widgetColorModalBg || void 0,
865
+ radius: this.config.widgetBorderRadius
866
+ };
867
+ return `<style>${galleryCss(minCol, maxW, compact, colors)}</style>${grid}`;
729
868
  }
730
869
  buildGalleryHtmlJs(d, events) {
731
870
  const oid = d.oid;
732
- const minCol = this.config.widgetMinCol || 150;
871
+ const minCol = this.effectiveMinCol();
733
872
  const maxW = this.config.widgetMaxModalWidth || 900;
734
- const items = events.map((ev) => {
873
+ const showSearch = this.config.widgetShowSearch !== false;
874
+ const compact = !!this.config.widgetCompact;
875
+ const defaultTag = (this.config.widgetDefaultTag || "").trim();
876
+ const items = this.sortedEvents(events).map((ev) => {
735
877
  const p = this.fmtEvent(ev, oid);
736
878
  return {
737
879
  fn: p.fn,
@@ -751,11 +893,22 @@ class AgentDvr extends utils.Adapter {
751
893
  live_aspect: this.camAspect[oid] || this.config.widgetLiveAspect || "",
752
894
  player_url: (this.config.widgetPlayerUrl || "").trim(),
753
895
  tag_position: this.config.widgetTagPosition || "bottom-left",
896
+ compact,
897
+ default_tag: defaultTag,
754
898
  labels: { noRecordings: this.wt.noRecordings, all: this.wt.all, live: this.wt.live }
755
899
  };
756
900
  const data = JSON.stringify({ items, live: null, cfg }).replace(/</g, "\\u003c");
757
901
  const boot = "data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7";
758
- return `<style>${galleryCssJs(minCol)}</style><div class="advroot"><script type="application/json" class="advdata">${data}</script><div class="advbar"><input class="advsearchjs" type="text" placeholder="${escHtml(this.wt.search)}"><div class="advtagsjs"></div></div><div class="advgridjs"></div></div><script type="text/plain" class="advcode">${ADV_CLIENT_CODE}</script><img alt="" src="${boot}" style="display:none" onload="(function(){if(window.ADVscan){window.ADVscan();return;}var c=document.querySelector('script.advcode');if(!c)return;var s=document.createElement('script');s.textContent=c.textContent;document.body.appendChild(s);})()">`;
902
+ const searchHtml = showSearch ? `<input class="advsearchjs" type="text" placeholder="${escHtml(this.wt.search)}">` : "";
903
+ const colors = {
904
+ cardBg: this.config.widgetColorCardBg || void 0,
905
+ tagBg: this.config.widgetColorTagBg || void 0,
906
+ tagText: this.config.widgetColorTagText || void 0,
907
+ accent: this.config.widgetColorAccent || void 0,
908
+ modalBg: this.config.widgetColorModalBg || void 0,
909
+ radius: this.config.widgetBorderRadius
910
+ };
911
+ return `<style>${galleryCssJs(minCol, compact, colors)}</style><div class="advroot"><script type="application/json" class="advdata">${data}</script><div class="advbar">${searchHtml}<div class="advtagsjs"></div></div><div class="advgridjs"></div></div><script type="text/plain" class="advcode">${ADV_CLIENT_CODE}</script><img alt="" src="${boot}" style="display:none" onload="(function(){if(window.ADVscan){window.ADVscan();return;}var c=document.querySelector('script.advcode');if(!c)return;var s=document.createElement('script');s.textContent=c.textContent;document.body.appendChild(s);})()">`;
759
912
  }
760
913
  buildOverviewHtml(cams) {
761
914
  const ts = Date.now();
@@ -801,6 +954,33 @@ class AgentDvr extends utils.Adapter {
801
954
  await this.writeLeaf(`${fid}.events.last.thumb`, p.thumb);
802
955
  await this.writeLeaf(`${fid}.events.last.video`, p.video);
803
956
  }
957
+ const jsonId = `${fid}.events.json`;
958
+ if (!this.ensuredFolders.has(jsonId)) {
959
+ await this.ensurePath(jsonId);
960
+ await this.setObjectNotExistsAsync(jsonId, {
961
+ type: "state",
962
+ common: { name: "Recordings list (web UI)", type: "string", role: "json", read: true, write: false },
963
+ native: {}
964
+ });
965
+ this.ensuredFolders.add(jsonId);
966
+ }
967
+ await this.setStateAsync(jsonId, {
968
+ val: JSON.stringify(
969
+ events.slice(0, Math.max(1, this.config.widgetAnzahl || 50)).map((ev) => {
970
+ const p = this.fmtEvent(ev, oid);
971
+ return {
972
+ date: p.date,
973
+ time: p.time,
974
+ dur: p.dur,
975
+ size: p.sizeMB,
976
+ tag: p.tag,
977
+ thumb: p.thumb,
978
+ video: p.video
979
+ };
980
+ })
981
+ ),
982
+ ack: true
983
+ });
804
984
  await this.ensureFlag(`${fid}.events.new`, "New event");
805
985
  if (this.config.enablePush) {
806
986
  const pid = `${fid}.events.pushTrigger`;
@@ -959,9 +1139,11 @@ class AgentDvr extends utils.Adapter {
959
1139
  }
960
1140
  const devices = findDevices(json);
961
1141
  await this.setStateAsync("system.cameraCount", { val: devices.filter((d) => d.ot === 2).length, ack: true });
1142
+ const activeFolders = new Set(devices.map((d) => this.deviceFolder(d)));
962
1143
  for (const d of devices) {
963
1144
  await this.buildDevice(d);
964
1145
  }
1146
+ await this.pruneStaleDevices(activeFolders);
965
1147
  const stats = asJson((await this.apiGet("/command/getSystemStats")).data);
966
1148
  if (stats) {
967
1149
  await this.flattenWrite(stats, "system.stats", 0);