iobroker.agent-dvr 0.0.1
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/LICENSE +21 -0
- package/README.md +159 -0
- package/admin/agent-dvr.png +0 -0
- package/admin/i18n/de.json +40 -0
- package/admin/i18n/en.json +40 -0
- package/admin/i18n/es.json +40 -0
- package/admin/i18n/fr.json +40 -0
- package/admin/i18n/it.json +40 -0
- package/admin/i18n/nl.json +40 -0
- package/admin/i18n/pl.json +40 -0
- package/admin/i18n/pt.json +40 -0
- package/admin/i18n/ru.json +40 -0
- package/admin/i18n/uk.json +40 -0
- package/admin/i18n/zh-cn.json +40 -0
- package/admin/jsonConfig.json +205 -0
- package/build/lib/widget-i18n.js +118 -0
- package/build/lib/widget-i18n.js.map +7 -0
- package/build/main.js +991 -0
- package/build/main.js.map +7 -0
- package/io-package.json +131 -0
- package/package.json +73 -0
package/build/main.js
ADDED
|
@@ -0,0 +1,991 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __create = Object.create;
|
|
3
|
+
var __defProp = Object.defineProperty;
|
|
4
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
5
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
6
|
+
var __getProtoOf = Object.getPrototypeOf;
|
|
7
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
8
|
+
var __copyProps = (to, from, except, desc) => {
|
|
9
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
10
|
+
for (let key of __getOwnPropNames(from))
|
|
11
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
12
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
13
|
+
}
|
|
14
|
+
return to;
|
|
15
|
+
};
|
|
16
|
+
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
|
|
17
|
+
// If the importer is in node compatibility mode or this is not an ESM
|
|
18
|
+
// file that has been converted to a CommonJS file using a Babel-
|
|
19
|
+
// compatible transform (i.e. "__esModule" has not been set), then set
|
|
20
|
+
// "default" to the CommonJS "module.exports" for node compatibility.
|
|
21
|
+
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
|
|
22
|
+
mod
|
|
23
|
+
));
|
|
24
|
+
var utils = __toESM(require("@iobroker/adapter-core"));
|
|
25
|
+
var http = __toESM(require("node:http"));
|
|
26
|
+
var import_widget_i18n = require("./lib/widget-i18n");
|
|
27
|
+
const CAM_COMMANDS = [
|
|
28
|
+
{ id: "record", path: "/command/record", params: ["oid", "ot"], name: "Start recording" },
|
|
29
|
+
{ id: "recordStop", path: "/command/recordStop", params: ["oid", "ot"], name: "Stop recording" },
|
|
30
|
+
{ id: "recordRestart", path: "/command/recordRestart", params: ["oid", "ot"], name: "Restart recording" },
|
|
31
|
+
{ id: "triggerRecord", path: "/command/triggerRecord", params: ["oid", "ot"], name: "Trigger recording (timeout)" },
|
|
32
|
+
{ id: "snapshot", path: "/command/snapshot", params: ["oid"], name: "Take snapshot" },
|
|
33
|
+
{ id: "detect", path: "/command/detect", params: ["oid", "ot"], name: "Trigger motion detection" },
|
|
34
|
+
{ id: "alertOn", path: "/command/alertOn", params: ["oid", "ot"], name: "Arm alerts" },
|
|
35
|
+
{ id: "alertOff", path: "/command/alertOff", params: ["oid", "ot"], name: "Disarm alerts" },
|
|
36
|
+
{ id: "switchOn", path: "/command/switchOn", params: ["oid", "ot"], name: "Switch device on" },
|
|
37
|
+
{ id: "switchOff", path: "/command/switchOff", params: ["oid", "ot"], name: "Switch device off" },
|
|
38
|
+
{ id: "objectDetectOn", path: "/command/objectdetecton", params: ["oid", "ot"], name: "Enable object detection" },
|
|
39
|
+
{
|
|
40
|
+
id: "objectDetectOff",
|
|
41
|
+
path: "/command/objectdetectoff",
|
|
42
|
+
params: ["oid", "ot"],
|
|
43
|
+
name: "Disable object detection"
|
|
44
|
+
},
|
|
45
|
+
{ id: "recOnAlert", path: "/command/recordOnAlertOn", params: ["oid", "ot"], name: "Record on alert: on" },
|
|
46
|
+
{ id: "recOnDetect", path: "/command/recordOnDetectOn", params: ["oid", "ot"], name: "Record on detect: on" },
|
|
47
|
+
{ id: "purge", path: "/command/purge", params: ["oid", "ot"], name: "Purge folder (caution!)" }
|
|
48
|
+
];
|
|
49
|
+
const SYS_COMMANDS = [
|
|
50
|
+
{ id: "arm", path: "/command/arm", name: "Arm system" },
|
|
51
|
+
{ id: "disarm", path: "/command/disarm", name: "Disarm system" },
|
|
52
|
+
{ id: "allOn", path: "/command/allOn", name: "All devices on" },
|
|
53
|
+
{ id: "allOff", path: "/command/allOff", name: "All devices off" },
|
|
54
|
+
{ id: "reloadConfig", path: "/command/reloadConfig", name: "Reload config" },
|
|
55
|
+
{ id: "reloadObjects", path: "/command/reloadObjects", name: "Reload objects" },
|
|
56
|
+
{ id: "runStorageMgmt", path: "/command/runStorageMgmt", name: "Run storage management" },
|
|
57
|
+
{ id: "blockExternal", path: "/command/blockExternal", name: "Block external access" },
|
|
58
|
+
{ id: "unblockExternal", path: "/command/unblockExternal", name: "Unblock external access" },
|
|
59
|
+
{ id: "restart", path: "/command/restart", name: "Restart Agent DVR" },
|
|
60
|
+
{ id: "refresh", path: null, name: "Refresh data now" }
|
|
61
|
+
];
|
|
62
|
+
const PTZ_DIRS = [
|
|
63
|
+
{ id: "left", dir: 1, hold: true },
|
|
64
|
+
{ id: "upLeft", dir: 2, hold: true },
|
|
65
|
+
{ id: "up", dir: 3, hold: true },
|
|
66
|
+
{ id: "upRight", dir: 4, hold: true },
|
|
67
|
+
{ id: "right", dir: 5, hold: true },
|
|
68
|
+
{ id: "downRight", dir: 6, hold: true },
|
|
69
|
+
{ id: "down", dir: 7, hold: true },
|
|
70
|
+
{ id: "downLeft", dir: 8, hold: true },
|
|
71
|
+
{ id: "zoomIn", dir: 9, hold: true },
|
|
72
|
+
{ id: "zoomOut", dir: 10, hold: true },
|
|
73
|
+
{ id: "stop", dir: 11, hold: false },
|
|
74
|
+
{ id: "center", dir: 0, hold: false }
|
|
75
|
+
];
|
|
76
|
+
const SKIP_KEYS = ["icons", "image", "thumbnail", "base64"];
|
|
77
|
+
function sanitize(s) {
|
|
78
|
+
return String(s).replace(/[\s.[\]*?"'`,;:/\\]+/g, "_").replace(/[^A-Za-z0-9_-]/g, "_").replace(/_+/g, "_").replace(/^_+|_+$/g, "") || "x";
|
|
79
|
+
}
|
|
80
|
+
function asJson(data) {
|
|
81
|
+
if (data == null) {
|
|
82
|
+
return null;
|
|
83
|
+
}
|
|
84
|
+
if (typeof data === "object") {
|
|
85
|
+
return data;
|
|
86
|
+
}
|
|
87
|
+
if (typeof data === "string") {
|
|
88
|
+
try {
|
|
89
|
+
return JSON.parse(data);
|
|
90
|
+
} catch {
|
|
91
|
+
return null;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
return null;
|
|
95
|
+
}
|
|
96
|
+
function toStr(v) {
|
|
97
|
+
if (v == null) {
|
|
98
|
+
return "";
|
|
99
|
+
}
|
|
100
|
+
if (typeof v === "string" || typeof v === "number" || typeof v === "boolean") {
|
|
101
|
+
return String(v);
|
|
102
|
+
}
|
|
103
|
+
return Object.prototype.toString.call(v);
|
|
104
|
+
}
|
|
105
|
+
function escHtml(s) {
|
|
106
|
+
return toStr(s).replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """);
|
|
107
|
+
}
|
|
108
|
+
function parseSizeGb(s) {
|
|
109
|
+
if (s == null) {
|
|
110
|
+
return null;
|
|
111
|
+
}
|
|
112
|
+
const m = toStr(s).trim().match(/([\d.,]+)\s*([KMGT]?B)?/i);
|
|
113
|
+
if (!m) {
|
|
114
|
+
return null;
|
|
115
|
+
}
|
|
116
|
+
let v = parseFloat(m[1].replace(",", "."));
|
|
117
|
+
if (isNaN(v)) {
|
|
118
|
+
return null;
|
|
119
|
+
}
|
|
120
|
+
const u = (m[2] || "GB").toUpperCase();
|
|
121
|
+
if (u === "TB") {
|
|
122
|
+
v *= 1024;
|
|
123
|
+
} else if (u === "MB") {
|
|
124
|
+
v /= 1024;
|
|
125
|
+
} else if (u === "KB") {
|
|
126
|
+
v /= 1048576;
|
|
127
|
+
} else if (u === "B") {
|
|
128
|
+
v /= 1073741824;
|
|
129
|
+
}
|
|
130
|
+
return Math.round(v * 100) / 100;
|
|
131
|
+
}
|
|
132
|
+
function findWH(obj, depth) {
|
|
133
|
+
if (!obj || typeof obj !== "object" || depth > 5) {
|
|
134
|
+
return null;
|
|
135
|
+
}
|
|
136
|
+
const o = obj;
|
|
137
|
+
for (const [kw, kh] of [
|
|
138
|
+
["width", "height"],
|
|
139
|
+
["w", "h"],
|
|
140
|
+
["sourceWidth", "sourceHeight"],
|
|
141
|
+
["videoWidth", "videoHeight"],
|
|
142
|
+
["imageWidth", "imageHeight"]
|
|
143
|
+
]) {
|
|
144
|
+
const w = o[kw], h = o[kh];
|
|
145
|
+
if (typeof w === "number" && typeof h === "number" && w >= 100 && h >= 100 && w <= 12e3 && h <= 12e3) {
|
|
146
|
+
const r = w / h;
|
|
147
|
+
if (r >= 0.3 && r <= 4) {
|
|
148
|
+
return [w, h];
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
for (const k of Object.keys(o)) {
|
|
153
|
+
const g = findWH(o[k], depth + 1);
|
|
154
|
+
if (g) {
|
|
155
|
+
return g;
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
return null;
|
|
159
|
+
}
|
|
160
|
+
function detectOt(elem) {
|
|
161
|
+
var _a, _b, _c;
|
|
162
|
+
const v = (_c = (_b = (_a = elem.typeID) != null ? _a : elem.ot) != null ? _b : elem.objectTypeID) != null ? _c : elem.type;
|
|
163
|
+
return v === 1 || v === 2 ? v : 2;
|
|
164
|
+
}
|
|
165
|
+
function findDevices(json) {
|
|
166
|
+
var _a;
|
|
167
|
+
const list = Array.isArray(json.objectList) ? json.objectList : [];
|
|
168
|
+
const out = [];
|
|
169
|
+
for (const e of list) {
|
|
170
|
+
if (!e || typeof e !== "object") {
|
|
171
|
+
continue;
|
|
172
|
+
}
|
|
173
|
+
const el = e;
|
|
174
|
+
const rawId = (_a = el.id) != null ? _a : el.oid;
|
|
175
|
+
if (rawId === void 0 || rawId === null) {
|
|
176
|
+
continue;
|
|
177
|
+
}
|
|
178
|
+
const id = rawId;
|
|
179
|
+
const name = typeof el.name === "string" ? el.name : `obj_${id}`;
|
|
180
|
+
out.push({ oid: id, ot: detectOt(el), name, raw: el });
|
|
181
|
+
}
|
|
182
|
+
return out;
|
|
183
|
+
}
|
|
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}`;
|
|
186
|
+
}
|
|
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}`;
|
|
189
|
+
}
|
|
190
|
+
const ADV_CLIENT_CODE = `
|
|
191
|
+
(function(){
|
|
192
|
+
function esc(s){return String(s==null?'':s).replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>').replace(/"/g,'"');}
|
|
193
|
+
var MODAL_CSS='.advmodaljs{display:none;position:fixed;inset:0;z-index:99999;align-items:center;justify-content:center}.advbackdropjs{position:absolute;inset:0;background:transparent;cursor:pointer}.advboxjs{position:relative;z-index:1;display:flex;flex-direction:column;gap:8px;max-width:min(92vw,900px);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)}.advvideojs{width:100%;max-height:80vh;border-radius:8px;background:#000;display:block}.advclosejs{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)}.advinfojs{color:#fff;font-size:.8rem;text-align:center}';
|
|
194
|
+
var modalEl=null,modalVideo=null,modalInfo=null,modalBox=null;
|
|
195
|
+
function getModal(){
|
|
196
|
+
if(modalEl&&document.body.contains(modalEl))return modalEl;
|
|
197
|
+
if(!document.getElementById('advmodalcss')){var st=document.createElement('style');st.id='advmodalcss';st.textContent=MODAL_CSS;document.head.appendChild(st);}
|
|
198
|
+
modalEl=document.createElement('div');modalEl.className='advmodaljs';
|
|
199
|
+
var bd=document.createElement('div');bd.className='advbackdropjs';
|
|
200
|
+
modalBox=document.createElement('div');modalBox.className='advboxjs';
|
|
201
|
+
var cl=document.createElement('span');cl.className='advclosejs';cl.textContent='\u2715';
|
|
202
|
+
modalVideo=document.createElement('video');modalVideo.className='advvideojs';
|
|
203
|
+
modalVideo.setAttribute('controls','');modalVideo.setAttribute('preload','none');modalVideo.setAttribute('playsinline','');
|
|
204
|
+
modalInfo=document.createElement('div');modalInfo.className='advinfojs';
|
|
205
|
+
modalBox.appendChild(cl);modalBox.appendChild(modalVideo);modalBox.appendChild(modalInfo);
|
|
206
|
+
modalEl.appendChild(bd);modalEl.appendChild(modalBox);
|
|
207
|
+
document.body.appendChild(modalEl);
|
|
208
|
+
bd.addEventListener('click',closeModal);cl.addEventListener('click',closeModal);
|
|
209
|
+
return modalEl;
|
|
210
|
+
}
|
|
211
|
+
function openSrc(src,label,maxW){
|
|
212
|
+
var m=getModal();
|
|
213
|
+
if(maxW)modalBox.style.maxWidth='min(92vw,'+maxW+'px)';
|
|
214
|
+
modalVideo.setAttribute('src',src);modalInfo.textContent=label;m.style.display='flex';
|
|
215
|
+
}
|
|
216
|
+
function closeModal(){
|
|
217
|
+
if(!modalEl)return;
|
|
218
|
+
try{modalVideo.pause();}catch(e){}
|
|
219
|
+
modalVideo.removeAttribute('src');try{modalVideo.load();}catch(e){}
|
|
220
|
+
modalEl.style.display='none';
|
|
221
|
+
}
|
|
222
|
+
document.addEventListener('keydown',function(e){if(e.key==='Escape'||e.keyCode===27)closeModal();});
|
|
223
|
+
function initRoot(root){
|
|
224
|
+
var dataEl=root.querySelector('script.advdata');
|
|
225
|
+
if(!dataEl)return;
|
|
226
|
+
var D;try{D=JSON.parse(dataEl.textContent);}catch(e){return;}
|
|
227
|
+
var cfg=D.cfg||{},items=D.items||[],live=D.live||null;
|
|
228
|
+
var grid=root.querySelector('.advgridjs'),search=root.querySelector('.advsearchjs'),tagsBox=root.querySelector('.advtagsjs');
|
|
229
|
+
var state={q:'',tag:''};
|
|
230
|
+
var tagset={};
|
|
231
|
+
items.forEach(function(it){String(it.tag||'').split(/[,\\s]+/).forEach(function(t){if(t)tagset[t]=1;});});
|
|
232
|
+
var tags=Object.keys(tagset);
|
|
233
|
+
if(cfg.show_tags&&tags.length){
|
|
234
|
+
var mk=function(label,val){
|
|
235
|
+
var b=document.createElement('button');b.className='advtagbtn';b.textContent=label;b.setAttribute('data-tag',val);
|
|
236
|
+
b.addEventListener('click',function(){state.tag=state.tag===val?'':val;updateTagBtns();render();});
|
|
237
|
+
tagsBox.appendChild(b);
|
|
238
|
+
};
|
|
239
|
+
mk(cfg.labels&&cfg.labels.all||'All','');tags.forEach(function(t){mk(t,t);});
|
|
240
|
+
}
|
|
241
|
+
function updateTagBtns(){var bs=tagsBox.querySelectorAll('.advtagbtn');for(var i=0;i<bs.length;i++)bs[i].classList.toggle('on',bs[i].getAttribute('data-tag')===state.tag);}
|
|
242
|
+
updateTagBtns();
|
|
243
|
+
if(search)search.addEventListener('input',function(){state.q=this.value.toLowerCase();render();});
|
|
244
|
+
function badge(it){if(!(cfg.show_tags&&it.tag))return '';var pos=String(cfg.tag_position||'bottom-left').split('-');return '<span class="advtagjs" style="'+pos[0]+':5px;'+pos[1]+':5px">'+esc(it.tag)+'</span>';}
|
|
245
|
+
function render(){
|
|
246
|
+
var html='';
|
|
247
|
+
if(live){var ar=cfg.live_aspect||'',arStyle=ar?' style="aspect-ratio:'+ar.replace('/',' / ')+'"':'',arCls=ar?' advimgfix':'';
|
|
248
|
+
html+='<div class="advcelljs" data-live="1"><span class="advimgjs'+arCls+'"'+arStyle+'><img loading="lazy" src="'+esc(live.thumb)+'"><span class="advtagjs" style="top:5px;left:5px">● '+(cfg.labels&&cfg.labels.live||'LIVE')+'</span><span class="advplayjs"></span></span><span class="advcapjs">'+esc(live.name)+'</span></div>';}
|
|
249
|
+
items.forEach(function(it,i){
|
|
250
|
+
if(state.tag&&String(it.tag||'').toLowerCase().indexOf(state.tag.toLowerCase())<0)return;
|
|
251
|
+
if(state.q){var hay=((it.date||'')+' '+(it.time||'')+' '+(it.tag||'')).toLowerCase();if(hay.indexOf(state.q)<0)return;}
|
|
252
|
+
html+='<div class="advcelljs" data-i="'+i+'"><span class="advimgjs"><img loading="lazy" src="'+esc(it.thumb)+'">'+badge(it)+'<span class="advplayjs"></span></span><span class="advcapjs">'+esc(it.date)+' '+esc(it.time)+'<br>'+esc(it.dur)+'s · '+esc(it.size)+' MB</span></div>';
|
|
253
|
+
});
|
|
254
|
+
grid.innerHTML=html||'<div class="advemptyjs">'+(cfg.labels&&cfg.labels.noRecordings||'No recordings')+'</div>';
|
|
255
|
+
var cells=grid.querySelectorAll('.advcelljs');
|
|
256
|
+
for(var k=0;k<cells.length;k++)(function(cell){
|
|
257
|
+
cell.addEventListener('click',function(){
|
|
258
|
+
if(cell.getAttribute('data-live')==='1'){openSrc(live.video,(live.name||'')+' \xB7 Live',cfg.max_modal_width);return;}
|
|
259
|
+
var it=items[parseInt(cell.getAttribute('data-i'),10)],maxW=Number(cfg.max_modal_width)||900;
|
|
260
|
+
if(cfg.player_url){var u=cfg.player_url.replace(/{video}/g,encodeURIComponent(it.video)).replace(/{videoRaw}/g,it.video).replace(/{fn}/g,encodeURIComponent(it.fn||'')).replace(/{date}/g,encodeURIComponent(it.date)).replace(/{time}/g,encodeURIComponent(it.time)).replace(/{duration}/g,encodeURIComponent(it.dur)).replace(/{size}/g,encodeURIComponent(it.size)).replace(/{tags}/g,encodeURIComponent(it.tag));window.open(u,'_blank');}
|
|
261
|
+
else{openSrc(it.video,esc(it.date)+' '+esc(it.time)+(it.dur!=null?' \xB7 '+it.dur+'s':'')+(it.size!=null?' \xB7 '+it.size+' MB':'')+(it.tag?' \xB7 '+it.tag:''),maxW);}
|
|
262
|
+
});
|
|
263
|
+
})(cells[k]);
|
|
264
|
+
}
|
|
265
|
+
render();
|
|
266
|
+
}
|
|
267
|
+
function scan(){var roots=document.querySelectorAll('.advroot');for(var i=0;i<roots.length;i++)if(!roots[i].__adv){roots[i].__adv=1;initRoot(roots[i]);}}
|
|
268
|
+
window.ADVscan=scan;scan();
|
|
269
|
+
})();
|
|
270
|
+
`.trim();
|
|
271
|
+
class AgentDvr extends utils.Adapter {
|
|
272
|
+
pollTimer = void 0;
|
|
273
|
+
refreshTimer = void 0;
|
|
274
|
+
authHeader = null;
|
|
275
|
+
baseUrl = "";
|
|
276
|
+
wt = (0, import_widget_i18n.getWidgetLabels)("en");
|
|
277
|
+
ensuredFolders = /* @__PURE__ */ new Set();
|
|
278
|
+
registry = /* @__PURE__ */ new Map();
|
|
279
|
+
ptzActive = /* @__PURE__ */ new Map();
|
|
280
|
+
widgetSig = {};
|
|
281
|
+
lastEventFn = {};
|
|
282
|
+
camAspect = {};
|
|
283
|
+
devById = /* @__PURE__ */ new Map();
|
|
284
|
+
eventBusy = {};
|
|
285
|
+
constructor(options = {}) {
|
|
286
|
+
super({ ...options, name: "agent-dvr" });
|
|
287
|
+
this.on("ready", this.onReady.bind(this));
|
|
288
|
+
this.on("stateChange", this.onStateChange.bind(this));
|
|
289
|
+
this.on("unload", this.onUnload.bind(this));
|
|
290
|
+
}
|
|
291
|
+
// ---- lifecycle ----
|
|
292
|
+
async onReady() {
|
|
293
|
+
var _a;
|
|
294
|
+
void this.setState("info.connection", false, true);
|
|
295
|
+
const sysConfig = await this.getForeignObjectAsync("system.config");
|
|
296
|
+
const rawLang = (_a = sysConfig == null ? void 0 : sysConfig.common) == null ? void 0 : _a.language;
|
|
297
|
+
this.wt = (0, import_widget_i18n.getWidgetLabels)(rawLang != null ? rawLang : "en");
|
|
298
|
+
const pollSeconds = Math.max(5, Math.min(3600, this.config.pollSeconds || 30));
|
|
299
|
+
this.baseUrl = `http://${this.config.ip}:${this.config.port || 8090}`;
|
|
300
|
+
if (this.config.user || this.config.pass) {
|
|
301
|
+
this.authHeader = `Basic ${Buffer.from(`${this.config.user}:${this.config.pass}`).toString("base64")}`;
|
|
302
|
+
}
|
|
303
|
+
await this.buildSystem();
|
|
304
|
+
try {
|
|
305
|
+
await this.poll();
|
|
306
|
+
} catch (e) {
|
|
307
|
+
this.log.warn(`First poll error: ${e.message}`);
|
|
308
|
+
}
|
|
309
|
+
this.pollTimer = this.setInterval(
|
|
310
|
+
() => this.poll().catch((e) => this.log.warn(`Poll error: ${e.message}`)),
|
|
311
|
+
pollSeconds * 1e3
|
|
312
|
+
);
|
|
313
|
+
this.subscribeStates("*");
|
|
314
|
+
this.log.info(`Agent DVR adapter ready. Polling ${this.baseUrl} every ${pollSeconds}s.`);
|
|
315
|
+
}
|
|
316
|
+
onUnload(callback) {
|
|
317
|
+
try {
|
|
318
|
+
if (this.pollTimer !== void 0) {
|
|
319
|
+
this.clearInterval(this.pollTimer);
|
|
320
|
+
this.pollTimer = void 0;
|
|
321
|
+
}
|
|
322
|
+
if (this.refreshTimer !== void 0) {
|
|
323
|
+
this.clearTimeout(this.refreshTimer);
|
|
324
|
+
this.refreshTimer = void 0;
|
|
325
|
+
}
|
|
326
|
+
} catch {
|
|
327
|
+
}
|
|
328
|
+
callback();
|
|
329
|
+
}
|
|
330
|
+
onStateChange(id, state) {
|
|
331
|
+
if (!state || state.ack !== false) {
|
|
332
|
+
return;
|
|
333
|
+
}
|
|
334
|
+
const relId = this.getRelId(id);
|
|
335
|
+
const entry = this.registry.get(relId);
|
|
336
|
+
if (entry) {
|
|
337
|
+
this.runCommand(relId, entry, state.val).catch(
|
|
338
|
+
(e) => this.log.warn(`Command error: ${e.message}`)
|
|
339
|
+
);
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
// ---- helpers ----
|
|
343
|
+
getRelId(fullId) {
|
|
344
|
+
return fullId.startsWith(`${this.namespace}.`) ? fullId.slice(this.namespace.length + 1) : fullId;
|
|
345
|
+
}
|
|
346
|
+
apiGet(path) {
|
|
347
|
+
return new Promise((resolve) => {
|
|
348
|
+
const timeout = Math.max(1e3, Math.min(3e4, this.config.httpTimeoutMs || 8e3));
|
|
349
|
+
const opts = {
|
|
350
|
+
hostname: this.config.ip,
|
|
351
|
+
port: this.config.port || 8090,
|
|
352
|
+
path,
|
|
353
|
+
method: "GET",
|
|
354
|
+
timeout
|
|
355
|
+
};
|
|
356
|
+
if (this.authHeader) {
|
|
357
|
+
opts.headers = { Authorization: this.authHeader };
|
|
358
|
+
}
|
|
359
|
+
try {
|
|
360
|
+
const req = http.request(opts, (res) => {
|
|
361
|
+
let data = "";
|
|
362
|
+
res.on("data", (c) => {
|
|
363
|
+
data += c.toString();
|
|
364
|
+
});
|
|
365
|
+
res.on("end", () => {
|
|
366
|
+
if (res.statusCode && res.statusCode >= 400) {
|
|
367
|
+
resolve({ ok: false, error: `HTTP ${res.statusCode}` });
|
|
368
|
+
} else {
|
|
369
|
+
resolve({ ok: true, data });
|
|
370
|
+
}
|
|
371
|
+
});
|
|
372
|
+
res.on("error", (e) => resolve({ ok: false, error: e.message }));
|
|
373
|
+
});
|
|
374
|
+
req.on("error", (e) => resolve({ ok: false, error: e.message }));
|
|
375
|
+
req.on("timeout", () => {
|
|
376
|
+
req.destroy();
|
|
377
|
+
resolve({ ok: false, error: "timeout" });
|
|
378
|
+
});
|
|
379
|
+
req.end();
|
|
380
|
+
} catch (e) {
|
|
381
|
+
resolve({ ok: false, error: String(e.message || e) });
|
|
382
|
+
}
|
|
383
|
+
});
|
|
384
|
+
}
|
|
385
|
+
async ensureFolder(id, name, type = "channel") {
|
|
386
|
+
if (this.ensuredFolders.has(id)) {
|
|
387
|
+
return;
|
|
388
|
+
}
|
|
389
|
+
await this.setObjectNotExistsAsync(id, { type, common: { name }, native: {} });
|
|
390
|
+
this.ensuredFolders.add(id);
|
|
391
|
+
}
|
|
392
|
+
async ensurePath(fullId) {
|
|
393
|
+
const parts = fullId.split(".");
|
|
394
|
+
let acc = parts[0];
|
|
395
|
+
for (let i = 1; i < parts.length - 1; i++) {
|
|
396
|
+
acc += `.${parts[i]}`;
|
|
397
|
+
if (!this.ensuredFolders.has(acc)) {
|
|
398
|
+
await this.setObjectNotExistsAsync(acc, { type: "channel", common: { name: parts[i] }, native: {} });
|
|
399
|
+
this.ensuredFolders.add(acc);
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
guessRole(id, val) {
|
|
404
|
+
if (typeof val === "string" && /^https?:\/\//i.test(val)) {
|
|
405
|
+
return "string";
|
|
406
|
+
}
|
|
407
|
+
return "string";
|
|
408
|
+
}
|
|
409
|
+
async writeLeaf(id, val) {
|
|
410
|
+
const t = typeof val;
|
|
411
|
+
let common;
|
|
412
|
+
if (t === "boolean") {
|
|
413
|
+
common = { name: id.split(".").pop() || id, type: "boolean", role: "indicator", read: true, write: false };
|
|
414
|
+
} else if (t === "number") {
|
|
415
|
+
common = { name: id.split(".").pop() || id, type: "number", role: "value", read: true, write: false };
|
|
416
|
+
} else {
|
|
417
|
+
const role = typeof val === "string" && /^https?:\/\//i.test(val) ? "text.url" : "text";
|
|
418
|
+
common = { name: id.split(".").pop() || id, type: "string", role, read: true, write: false };
|
|
419
|
+
}
|
|
420
|
+
if (!this.ensuredFolders.has(id)) {
|
|
421
|
+
await this.ensurePath(id);
|
|
422
|
+
await this.setObjectNotExistsAsync(id, { type: "state", common, native: {} });
|
|
423
|
+
this.ensuredFolders.add(id);
|
|
424
|
+
}
|
|
425
|
+
await this.setStateAsync(id, { val: val === void 0 ? null : val, ack: true });
|
|
426
|
+
}
|
|
427
|
+
async flattenWrite(obj, prefix, depth) {
|
|
428
|
+
const maxDepth = Math.max(1, Math.min(10, this.config.maxDepth || 6));
|
|
429
|
+
const maxArray = Math.max(1, Math.min(500, this.config.maxArray || 30));
|
|
430
|
+
if (depth > maxDepth) {
|
|
431
|
+
return;
|
|
432
|
+
}
|
|
433
|
+
if (obj === null || obj === void 0) {
|
|
434
|
+
await this.writeLeaf(prefix, obj);
|
|
435
|
+
return;
|
|
436
|
+
}
|
|
437
|
+
if (typeof obj === "object") {
|
|
438
|
+
if (Array.isArray(obj)) {
|
|
439
|
+
if (obj.length > maxArray) {
|
|
440
|
+
await this.writeLeaf(`${prefix}.count`, obj.length);
|
|
441
|
+
return;
|
|
442
|
+
}
|
|
443
|
+
for (let i = 0; i < obj.length; i++) {
|
|
444
|
+
await this.flattenWrite(obj[i], `${prefix}.${i}`, depth + 1);
|
|
445
|
+
}
|
|
446
|
+
} else {
|
|
447
|
+
for (const k of Object.keys(obj)) {
|
|
448
|
+
if (SKIP_KEYS.includes(k)) {
|
|
449
|
+
continue;
|
|
450
|
+
}
|
|
451
|
+
await this.flattenWrite(obj[k], `${prefix}.${sanitize(k)}`, depth + 1);
|
|
452
|
+
}
|
|
453
|
+
}
|
|
454
|
+
} else {
|
|
455
|
+
await this.writeLeaf(prefix, obj);
|
|
456
|
+
}
|
|
457
|
+
}
|
|
458
|
+
async ensureControl(id, name, entry, role) {
|
|
459
|
+
if (!this.ensuredFolders.has(id)) {
|
|
460
|
+
await this.ensurePath(id);
|
|
461
|
+
await this.setObjectNotExistsAsync(id, {
|
|
462
|
+
type: "state",
|
|
463
|
+
common: { name, type: "boolean", role, read: role === "switch", write: true, def: false },
|
|
464
|
+
native: {}
|
|
465
|
+
});
|
|
466
|
+
await this.setStateAsync(id, { val: false, ack: true });
|
|
467
|
+
this.ensuredFolders.add(id);
|
|
468
|
+
}
|
|
469
|
+
this.registry.set(id, entry);
|
|
470
|
+
}
|
|
471
|
+
ensureButton(id, name, entry) {
|
|
472
|
+
return this.ensureControl(id, name, entry, "button");
|
|
473
|
+
}
|
|
474
|
+
async ensureFlag(id, name) {
|
|
475
|
+
if (this.ensuredFolders.has(id)) {
|
|
476
|
+
return;
|
|
477
|
+
}
|
|
478
|
+
await this.ensurePath(id);
|
|
479
|
+
await this.setObjectNotExistsAsync(id, {
|
|
480
|
+
type: "state",
|
|
481
|
+
common: { name, type: "boolean", role: "indicator", read: true, write: false, def: false },
|
|
482
|
+
native: {}
|
|
483
|
+
});
|
|
484
|
+
this.ensuredFolders.add(id);
|
|
485
|
+
await this.setStateAsync(id, { val: false, ack: true });
|
|
486
|
+
}
|
|
487
|
+
async pulse(id) {
|
|
488
|
+
await this.setStateAsync(id, { val: true, ack: true });
|
|
489
|
+
this.setTimeout(() => this.setStateAsync(id, { val: false, ack: true }).catch(() => {
|
|
490
|
+
}), 1500);
|
|
491
|
+
}
|
|
492
|
+
// ---- system structure ----
|
|
493
|
+
async buildSystem() {
|
|
494
|
+
await this.ensureFolder("system", "System", "channel");
|
|
495
|
+
await this.writeLeaf("system.online", false);
|
|
496
|
+
await this.writeLeaf("system.lastUpdate", "");
|
|
497
|
+
await this.writeLeaf("system.lastPoll", 0);
|
|
498
|
+
await this.writeLeaf("system.cameraCount", 0);
|
|
499
|
+
if (this.config.enableSystemControls) {
|
|
500
|
+
await this.ensureFolder("system.control", "Control", "channel");
|
|
501
|
+
for (const c of SYS_COMMANDS) {
|
|
502
|
+
await this.ensureButton(`system.control.${c.id}`, c.name, { kind: "sys", path: c.path });
|
|
503
|
+
}
|
|
504
|
+
}
|
|
505
|
+
}
|
|
506
|
+
// ---- camera aspect detection ----
|
|
507
|
+
async detectAspect(d) {
|
|
508
|
+
if (this.camAspect[d.oid] !== void 0) {
|
|
509
|
+
return;
|
|
510
|
+
}
|
|
511
|
+
let wh = findWH(d.raw, 0);
|
|
512
|
+
if (!wh) {
|
|
513
|
+
const res = await this.apiGet(`/command/getObject?oid=${d.oid}&ot=2`);
|
|
514
|
+
const j = asJson(res.data);
|
|
515
|
+
if (j) {
|
|
516
|
+
wh = findWH(j, 0);
|
|
517
|
+
}
|
|
518
|
+
}
|
|
519
|
+
this.camAspect[d.oid] = wh ? `${wh[0]}/${wh[1]}` : "";
|
|
520
|
+
if (wh) {
|
|
521
|
+
this.log.debug(`cam ${d.oid} resolution ${wh[0]}x${wh[1]} -> AR ${this.camAspect[d.oid]}`);
|
|
522
|
+
}
|
|
523
|
+
}
|
|
524
|
+
// ---- device folder name ----
|
|
525
|
+
deviceFolder(d) {
|
|
526
|
+
return `${d.ot === 1 ? "mic" : "cam"}_${sanitize(d.oid)}_${sanitize(d.name)}`;
|
|
527
|
+
}
|
|
528
|
+
// ---- build device data points ----
|
|
529
|
+
async buildDevice(d) {
|
|
530
|
+
const fid = this.deviceFolder(d);
|
|
531
|
+
this.devById.set(d.oid, d);
|
|
532
|
+
await this.ensureFolder(fid, d.name, "device");
|
|
533
|
+
if (d.ot === 2) {
|
|
534
|
+
await this.detectAspect(d);
|
|
535
|
+
}
|
|
536
|
+
await this.flattenWrite(d.raw, fid, 0);
|
|
537
|
+
await this.ensureFolder(`${fid}.control`, "Control", "channel");
|
|
538
|
+
for (const c of CAM_COMMANDS) {
|
|
539
|
+
if (c.id === "snapshot" && d.ot !== 2) {
|
|
540
|
+
continue;
|
|
541
|
+
}
|
|
542
|
+
await this.ensureButton(`${fid}.control.${c.id}`, c.name, {
|
|
543
|
+
kind: "cam",
|
|
544
|
+
path: c.path,
|
|
545
|
+
params: [...c.params],
|
|
546
|
+
oid: d.oid,
|
|
547
|
+
ot: d.ot
|
|
548
|
+
});
|
|
549
|
+
}
|
|
550
|
+
if (this.config.enablePtz && d.ot === 2) {
|
|
551
|
+
await this.ensureFolder(`${fid}.control.ptz`, "PTZ", "channel");
|
|
552
|
+
for (const p of PTZ_DIRS) {
|
|
553
|
+
const cid = `${fid}.control.ptz.${p.id}`;
|
|
554
|
+
if (p.hold) {
|
|
555
|
+
await this.ensureControl(
|
|
556
|
+
cid,
|
|
557
|
+
`PTZ ${p.id} (hold)`,
|
|
558
|
+
{ kind: "ptzHold", oid: d.oid, dir: p.dir },
|
|
559
|
+
"switch"
|
|
560
|
+
);
|
|
561
|
+
} else {
|
|
562
|
+
await this.ensureButton(cid, `PTZ ${p.id}`, { kind: "ptz", oid: d.oid, dir: p.dir });
|
|
563
|
+
}
|
|
564
|
+
}
|
|
565
|
+
}
|
|
566
|
+
if (this.config.enableUrls && d.ot === 2) {
|
|
567
|
+
await this.ensureFolder(`${fid}.urls`, "URLs", "channel");
|
|
568
|
+
await this.writeLeaf(`${fid}.urls.snapshot`, `${this.baseUrl}/grab.jpg?oid=${d.oid}`);
|
|
569
|
+
await this.writeLeaf(`${fid}.urls.photo`, `${this.baseUrl}/photo.jpg?oid=${d.oid}`);
|
|
570
|
+
await this.writeLeaf(`${fid}.urls.mjpeg`, `${this.baseUrl}/video.mjpg?oids=${d.oid}`);
|
|
571
|
+
await this.writeLeaf(`${fid}.urls.mp4`, `${this.baseUrl}/video.mp4?oids=${d.oid}`);
|
|
572
|
+
}
|
|
573
|
+
await this.updateCameraEvents(d, fid);
|
|
574
|
+
}
|
|
575
|
+
// ---- event formatting ----
|
|
576
|
+
fmtEvent(ev, oid) {
|
|
577
|
+
var _a, _b, _c;
|
|
578
|
+
const e = ev;
|
|
579
|
+
const fn = (_a = e.fn) != null ? _a : "";
|
|
580
|
+
const dot = fn.lastIndexOf(".");
|
|
581
|
+
const jpg = `${dot > 0 ? fn.slice(0, dot) : fn}_large.jpg`;
|
|
582
|
+
const sizeMB = e.sb ? Math.round(Number(e.sb) / 1048576 * 100) / 100 : 0;
|
|
583
|
+
const dur = e.d != null ? String(e.d) : "";
|
|
584
|
+
const tag = (_b = e.tg) != null ? _b : "";
|
|
585
|
+
const ms = (parseFloat(String((_c = e.c) != null ? _c : 0)) - 621355968e9) / 1e4;
|
|
586
|
+
const dt = new Date(ms);
|
|
587
|
+
const two = (n) => String(n).padStart(2, "0");
|
|
588
|
+
const date = `${two(dt.getDate())}.${two(dt.getMonth() + 1)}.`;
|
|
589
|
+
const time = `${two(dt.getHours())}:${two(dt.getMinutes())}`;
|
|
590
|
+
const thumb = `${this.baseUrl}/fileThumb.jpg?oid=${oid}&fn=${encodeURIComponent(jpg)}`;
|
|
591
|
+
const video = `${this.baseUrl}/streamFile.cgi?oid=${oid}&ot=2&fn=${encodeURIComponent(fn)}`;
|
|
592
|
+
return { fn, sizeMB, dur, tag, date, time, thumb, video };
|
|
593
|
+
}
|
|
594
|
+
// ---- gallery HTML builders ----
|
|
595
|
+
buildGalleryHtml(d, events) {
|
|
596
|
+
return (this.config.widgetMode || "nojs") === "js" ? this.buildGalleryHtmlJs(d, events) : this.buildGalleryHtmlNojs(d, events);
|
|
597
|
+
}
|
|
598
|
+
buildGalleryHtmlNojs(d, events) {
|
|
599
|
+
const oid = d.oid;
|
|
600
|
+
const minCol = this.config.widgetMinCol || 150;
|
|
601
|
+
const maxW = this.config.widgetMaxModalWidth || 900;
|
|
602
|
+
const showTags = this.config.widgetShowTags;
|
|
603
|
+
const pos = (this.config.widgetTagPosition || "bottom-left").split("-");
|
|
604
|
+
const tagStyle = `${pos[0]}:5px;${pos[1]}:5px`;
|
|
605
|
+
const playerUrl = (this.config.widgetPlayerUrl || "").trim();
|
|
606
|
+
const PAUSE_ATTR = ` onchange="if(!this.checked){var m=this.nextElementSibling.nextElementSibling,v=m&&m.querySelector('video');if(v){v.pause();}}"`;
|
|
607
|
+
const items = events.map((ev, i) => {
|
|
608
|
+
const p = this.fmtEvent(ev, oid);
|
|
609
|
+
const id = `adv${sanitize(oid)}_${i}`;
|
|
610
|
+
const badge = showTags && p.tag ? `<span class="advtag" style="${tagStyle}">${escHtml(p.tag)}</span>` : "";
|
|
611
|
+
const inner = `<span class="advimg"><img src="${p.thumb}" loading="lazy" alt="">${badge}<span class="advplay"></span></span><span class="advcap">${escHtml(p.date)} ${escHtml(p.time)}<br>${escHtml(p.dur)}s · ${escHtml(p.sizeMB)} MB</span>`;
|
|
612
|
+
if (playerUrl) {
|
|
613
|
+
const href = playerUrl.replace(/{video}/g, encodeURIComponent(p.video)).replace(/{videoRaw}/g, p.video).replace(/{fn}/g, encodeURIComponent(p.fn)).replace(/{date}/g, encodeURIComponent(p.date)).replace(/{time}/g, encodeURIComponent(p.time)).replace(/{duration}/g, encodeURIComponent(p.dur)).replace(/{size}/g, encodeURIComponent(String(p.sizeMB))).replace(/{tags}/g, encodeURIComponent(p.tag));
|
|
614
|
+
return `<a class="advcell" href="${escHtml(href)}" target="_blank" rel="noopener">${inner}</a>`;
|
|
615
|
+
}
|
|
616
|
+
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}">✕</label><video class="advvideo" controls preload="none" playsinline src="${p.video}"></video><div class="advinfo">${escHtml(p.date)} ${escHtml(p.time)} · ${escHtml(p.dur)}s · ${escHtml(p.sizeMB)} MB${p.tag ? ` · ${escHtml(p.tag)}` : ""} · <a href="${p.video}" download target="_blank">${this.wt.download}</a></div></div></div>`;
|
|
617
|
+
}).join("");
|
|
618
|
+
const grid = items ? `<div class="advgrid">${items}</div>` : `<div class="advempty">${this.wt.noRecordings}</div>`;
|
|
619
|
+
return `<style>${galleryCss(minCol, maxW)}</style>${grid}`;
|
|
620
|
+
}
|
|
621
|
+
buildGalleryHtmlJs(d, events) {
|
|
622
|
+
const oid = d.oid;
|
|
623
|
+
const minCol = this.config.widgetMinCol || 150;
|
|
624
|
+
const maxW = this.config.widgetMaxModalWidth || 900;
|
|
625
|
+
const items = events.map((ev) => {
|
|
626
|
+
const p = this.fmtEvent(ev, oid);
|
|
627
|
+
return {
|
|
628
|
+
fn: p.fn,
|
|
629
|
+
date: p.date,
|
|
630
|
+
time: p.time,
|
|
631
|
+
dur: p.dur,
|
|
632
|
+
size: p.sizeMB,
|
|
633
|
+
tag: p.tag,
|
|
634
|
+
thumb: p.thumb,
|
|
635
|
+
video: p.video
|
|
636
|
+
};
|
|
637
|
+
});
|
|
638
|
+
const cfg = {
|
|
639
|
+
min_col: minCol,
|
|
640
|
+
show_tags: !!this.config.widgetShowTags,
|
|
641
|
+
max_modal_width: maxW,
|
|
642
|
+
live_aspect: this.camAspect[oid] || this.config.widgetLiveAspect || "",
|
|
643
|
+
player_url: (this.config.widgetPlayerUrl || "").trim(),
|
|
644
|
+
tag_position: this.config.widgetTagPosition || "bottom-left",
|
|
645
|
+
labels: { noRecordings: this.wt.noRecordings, all: this.wt.all, live: this.wt.live }
|
|
646
|
+
};
|
|
647
|
+
const data = JSON.stringify({ items, live: null, cfg }).replace(/</g, "\\u003c");
|
|
648
|
+
const boot = "data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7";
|
|
649
|
+
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);})()">`;
|
|
650
|
+
}
|
|
651
|
+
buildOverviewHtml(cams) {
|
|
652
|
+
const ts = Date.now();
|
|
653
|
+
const minCol = this.config.widgetMinCol || 150;
|
|
654
|
+
const maxW = this.config.widgetMaxModalWidth || 900;
|
|
655
|
+
const PAUSE_ATTR = ` onchange="if(!this.checked){var m=this.nextElementSibling.nextElementSibling,v=m&&m.querySelector('video');if(v){v.pause();}}"`;
|
|
656
|
+
const tiles = cams.map((d) => {
|
|
657
|
+
const oid = d.oid;
|
|
658
|
+
const id = `advlive${sanitize(oid)}`;
|
|
659
|
+
const grab = `${this.baseUrl}/grab.jpg?oid=${oid}&ot=2&maintainAR=1&ts=${ts}`;
|
|
660
|
+
const webm = `${this.baseUrl}/video.webm?oid=${oid}&ot=2`;
|
|
661
|
+
const name = escHtml(d.name || `Camera ${oid}`);
|
|
662
|
+
const arRaw = this.camAspect[oid] || this.config.widgetLiveAspect || "";
|
|
663
|
+
const ar = arRaw ? String(arRaw).replace("/", " / ") : "";
|
|
664
|
+
const fix = ar ? " advimgfix" : "";
|
|
665
|
+
const arStyle = ar ? ` style="aspect-ratio:${ar}"` : "";
|
|
666
|
+
const inner = `<span class="advimg${fix}"${arStyle}><img src="${grab}" loading="lazy" alt=""><span class="advtag" style="top:5px;left:5px">● ${escHtml(this.wt.live)}</span><span class="advplay"></span></span><span class="advcap">${name}</span>`;
|
|
667
|
+
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}">✕</label><video class="advvideo" controls preload="none" playsinline src="${webm}"></video><div class="advinfo">${name} · Live</div></div></div>`;
|
|
668
|
+
}).join("");
|
|
669
|
+
const grid = tiles ? `<div class="advgrid">${tiles}</div>` : `<div class="advempty">No cameras</div>`;
|
|
670
|
+
return `<style>${galleryCss(minCol, maxW)}</style>${grid}`;
|
|
671
|
+
}
|
|
672
|
+
// ---- event data points ----
|
|
673
|
+
async writeEventDps(d, fid, events) {
|
|
674
|
+
var _a, _b, _c, _d;
|
|
675
|
+
const oid = d.oid;
|
|
676
|
+
await this.ensureFolder(`${fid}.events`, "Events", "channel");
|
|
677
|
+
await this.ensureFolder(`${fid}.events.last`, "Last event", "channel");
|
|
678
|
+
const two = (n) => String(n).padStart(2, "0");
|
|
679
|
+
const now = /* @__PURE__ */ new Date();
|
|
680
|
+
const td = `${two(now.getDate())}.${two(now.getMonth() + 1)}.`;
|
|
681
|
+
const todayCount = events.filter((ev) => this.fmtEvent(ev, oid).date === td).length;
|
|
682
|
+
await this.writeLeaf(`${fid}.events.count`, events.length);
|
|
683
|
+
await this.writeLeaf(`${fid}.events.today`, todayCount);
|
|
684
|
+
if (events.length) {
|
|
685
|
+
const p = this.fmtEvent(events[0], oid);
|
|
686
|
+
const tsMs = (parseFloat(String((_a = events[0].c) != null ? _a : 0)) - 621355968e9) / 1e4;
|
|
687
|
+
await this.writeLeaf(`${fid}.events.last.time`, `${p.date} ${p.time}`);
|
|
688
|
+
await this.writeLeaf(`${fid}.events.last.timestamp`, Math.round(tsMs));
|
|
689
|
+
await this.writeLeaf(`${fid}.events.last.tag`, p.tag);
|
|
690
|
+
await this.writeLeaf(`${fid}.events.last.duration`, p.dur ? parseFloat(p.dur) : 0);
|
|
691
|
+
await this.writeLeaf(`${fid}.events.last.size_mb`, p.sizeMB);
|
|
692
|
+
await this.writeLeaf(`${fid}.events.last.thumb`, p.thumb);
|
|
693
|
+
await this.writeLeaf(`${fid}.events.last.video`, p.video);
|
|
694
|
+
}
|
|
695
|
+
await this.ensureFlag(`${fid}.events.new`, "New event");
|
|
696
|
+
if (this.config.enablePush) {
|
|
697
|
+
const pid = `${fid}.events.pushTrigger`;
|
|
698
|
+
if (!this.ensuredFolders.has(pid)) {
|
|
699
|
+
await this.ensurePath(pid);
|
|
700
|
+
await this.setObjectNotExistsAsync(pid, {
|
|
701
|
+
type: "state",
|
|
702
|
+
common: {
|
|
703
|
+
name: "Push trigger (AgentDVR action)",
|
|
704
|
+
type: "string",
|
|
705
|
+
role: "text",
|
|
706
|
+
read: true,
|
|
707
|
+
write: true,
|
|
708
|
+
def: ""
|
|
709
|
+
},
|
|
710
|
+
native: {}
|
|
711
|
+
});
|
|
712
|
+
await this.setStateAsync(pid, { val: "", ack: true });
|
|
713
|
+
this.ensuredFolders.add(pid);
|
|
714
|
+
}
|
|
715
|
+
this.registry.set(pid, { kind: "push", oid, fid });
|
|
716
|
+
}
|
|
717
|
+
const ignore = (this.config.eventTagsIgnore || "detected").split(",").map((s) => s.trim().toLowerCase()).filter(Boolean);
|
|
718
|
+
const tokensOf = (s) => s.toLowerCase().split(/[,\s]+/).filter(Boolean);
|
|
719
|
+
for (const kw of (this.config.eventTags || "").split(",").map((s) => s.trim()).filter(Boolean)) {
|
|
720
|
+
await this.ensureFlag(`${fid}.events.detected_${sanitize(kw)}`, `Detected: ${kw}`);
|
|
721
|
+
}
|
|
722
|
+
if (this.config.eventTagsDynamic) {
|
|
723
|
+
const seen = /* @__PURE__ */ new Set();
|
|
724
|
+
for (const ev of events) {
|
|
725
|
+
tokensOf((_b = ev.tg) != null ? _b : "").forEach((t) => seen.add(t));
|
|
726
|
+
}
|
|
727
|
+
for (const t of seen) {
|
|
728
|
+
if (ignore.includes(t)) {
|
|
729
|
+
continue;
|
|
730
|
+
}
|
|
731
|
+
await this.ensureFlag(`${fid}.events.detected_${sanitize(t)}`, `Detected: ${t}`);
|
|
732
|
+
}
|
|
733
|
+
}
|
|
734
|
+
let newCount = 0;
|
|
735
|
+
if (events.length) {
|
|
736
|
+
const prev = this.lastEventFn[oid];
|
|
737
|
+
if (prev !== void 0) {
|
|
738
|
+
const i = events.findIndex((e) => e.fn === prev);
|
|
739
|
+
newCount = i === -1 ? events.length : i;
|
|
740
|
+
}
|
|
741
|
+
this.lastEventFn[oid] = (_c = events[0].fn) != null ? _c : "";
|
|
742
|
+
}
|
|
743
|
+
if (newCount > 0) {
|
|
744
|
+
await this.pulse(`${fid}.events.new`);
|
|
745
|
+
const tags = /* @__PURE__ */ new Set();
|
|
746
|
+
for (let i = 0; i < newCount && i < events.length; i++) {
|
|
747
|
+
tokensOf((_d = events[i].tg) != null ? _d : "").forEach((t) => tags.add(t));
|
|
748
|
+
}
|
|
749
|
+
for (const t of tags) {
|
|
750
|
+
if (ignore.includes(t)) {
|
|
751
|
+
continue;
|
|
752
|
+
}
|
|
753
|
+
const tid = `${fid}.events.detected_${sanitize(t)}`;
|
|
754
|
+
await this.ensureFlag(tid, `Detected: ${t}`);
|
|
755
|
+
await this.pulse(tid);
|
|
756
|
+
}
|
|
757
|
+
this.log.debug(`${newCount} new event(s) cam ${oid} [${[...tags].join(",")}]`);
|
|
758
|
+
}
|
|
759
|
+
}
|
|
760
|
+
// ---- camera events + widget ----
|
|
761
|
+
async updateCameraEvents(d, fid) {
|
|
762
|
+
var _a, _b, _c, _d;
|
|
763
|
+
if (d.ot !== 2) {
|
|
764
|
+
return;
|
|
765
|
+
}
|
|
766
|
+
if (!this.config.enableWidget && !this.config.enableEventDps) {
|
|
767
|
+
return;
|
|
768
|
+
}
|
|
769
|
+
if (this.eventBusy[d.oid]) {
|
|
770
|
+
return;
|
|
771
|
+
}
|
|
772
|
+
this.eventBusy[d.oid] = true;
|
|
773
|
+
try {
|
|
774
|
+
const evRes = await this.apiGet(`/q/getEvents?oid=${d.oid}&ot=2`);
|
|
775
|
+
const evJson = asJson(evRes.data);
|
|
776
|
+
if (!evJson) {
|
|
777
|
+
return;
|
|
778
|
+
}
|
|
779
|
+
const events = Array.isArray(evJson.events) ? evJson.events : Array.isArray(evJson) ? evJson : [];
|
|
780
|
+
if (this.config.enableEventDps) {
|
|
781
|
+
await this.writeEventDps(d, fid, events);
|
|
782
|
+
}
|
|
783
|
+
if (this.config.enableWidget) {
|
|
784
|
+
const wId = `${fid}.widget`;
|
|
785
|
+
if (!this.ensuredFolders.has(wId)) {
|
|
786
|
+
await this.ensurePath(wId);
|
|
787
|
+
await this.setObjectNotExistsAsync(wId, {
|
|
788
|
+
type: "state",
|
|
789
|
+
common: {
|
|
790
|
+
name: "HTML gallery widget",
|
|
791
|
+
type: "string",
|
|
792
|
+
role: "html",
|
|
793
|
+
read: true,
|
|
794
|
+
write: false,
|
|
795
|
+
def: ""
|
|
796
|
+
},
|
|
797
|
+
native: {}
|
|
798
|
+
});
|
|
799
|
+
this.ensuredFolders.add(wId);
|
|
800
|
+
}
|
|
801
|
+
const ev = events.slice(0, Math.max(1, this.config.widgetAnzahl || 50));
|
|
802
|
+
const f = (_a = ev[0]) != null ? _a : {};
|
|
803
|
+
const l = (_b = ev[ev.length - 1]) != null ? _b : {};
|
|
804
|
+
const sig = `${ev.length}|${(_c = f.fn) != null ? _c : ""}|${(_d = l.fn) != null ? _d : ""}`;
|
|
805
|
+
if (this.widgetSig[wId] !== sig) {
|
|
806
|
+
await this.setStateAsync(wId, { val: this.buildGalleryHtml(d, ev), ack: true });
|
|
807
|
+
this.widgetSig[wId] = sig;
|
|
808
|
+
}
|
|
809
|
+
}
|
|
810
|
+
} finally {
|
|
811
|
+
this.eventBusy[d.oid] = false;
|
|
812
|
+
}
|
|
813
|
+
}
|
|
814
|
+
pushRefresh(oid) {
|
|
815
|
+
const d = this.devById.get(oid);
|
|
816
|
+
if (!d) {
|
|
817
|
+
this.log.warn(`Push: camera ${oid} unknown (available after first poll)`);
|
|
818
|
+
return;
|
|
819
|
+
}
|
|
820
|
+
const fid = this.deviceFolder(d);
|
|
821
|
+
for (const ms of [0, 1500, 3500, 6e3]) {
|
|
822
|
+
this.setTimeout(
|
|
823
|
+
() => this.updateCameraEvents(d, fid).catch(
|
|
824
|
+
(e) => this.log.warn(`Push refresh error: ${e.message}`)
|
|
825
|
+
),
|
|
826
|
+
ms
|
|
827
|
+
);
|
|
828
|
+
}
|
|
829
|
+
}
|
|
830
|
+
// ---- main poll ----
|
|
831
|
+
async poll() {
|
|
832
|
+
const res = await this.apiGet("/command/getObjects");
|
|
833
|
+
const json = asJson(res.data);
|
|
834
|
+
if (!res.ok || !json) {
|
|
835
|
+
await this.setStateAsync("info.connection", false, true);
|
|
836
|
+
await this.setStateAsync("system.online", { val: false, ack: true });
|
|
837
|
+
this.log.warn(`AgentDVR unreachable: ${res.error || "invalid response"}`);
|
|
838
|
+
return;
|
|
839
|
+
}
|
|
840
|
+
await this.setStateAsync("info.connection", true, true);
|
|
841
|
+
await this.setStateAsync("system.online", { val: true, ack: true });
|
|
842
|
+
if (json.settings) {
|
|
843
|
+
await this.flattenWrite(json.settings, "system.settings", 0);
|
|
844
|
+
}
|
|
845
|
+
if (this.config.storeRawJson) {
|
|
846
|
+
const clean = { ...json };
|
|
847
|
+
delete clean.icons;
|
|
848
|
+
await this.writeLeaf("system.raw_getObjects", JSON.stringify(clean).slice(0, 6e4));
|
|
849
|
+
}
|
|
850
|
+
const devices = findDevices(json);
|
|
851
|
+
await this.setStateAsync("system.cameraCount", { val: devices.filter((d) => d.ot === 2).length, ack: true });
|
|
852
|
+
for (const d of devices) {
|
|
853
|
+
await this.buildDevice(d);
|
|
854
|
+
}
|
|
855
|
+
const stats = asJson((await this.apiGet("/command/getSystemStats")).data);
|
|
856
|
+
if (stats) {
|
|
857
|
+
await this.flattenWrite(stats, "system.stats", 0);
|
|
858
|
+
const gb = parseSizeGb(stats.disk_free);
|
|
859
|
+
if (gb !== null) {
|
|
860
|
+
await this.writeLeaf("system.disk_free_gb", gb);
|
|
861
|
+
}
|
|
862
|
+
}
|
|
863
|
+
const status = asJson((await this.apiGet("/command/getStatus")).data);
|
|
864
|
+
if (status) {
|
|
865
|
+
await this.flattenWrite(status, "system.status", 0);
|
|
866
|
+
}
|
|
867
|
+
if (this.config.enableOverview) {
|
|
868
|
+
const cams = devices.filter((d) => d.ot === 2);
|
|
869
|
+
const ovId = "overview";
|
|
870
|
+
if (!this.ensuredFolders.has(ovId)) {
|
|
871
|
+
await this.setObjectNotExistsAsync(ovId, {
|
|
872
|
+
type: "state",
|
|
873
|
+
common: {
|
|
874
|
+
name: "Overview (all cameras)",
|
|
875
|
+
type: "string",
|
|
876
|
+
role: "html",
|
|
877
|
+
read: true,
|
|
878
|
+
write: false,
|
|
879
|
+
def: ""
|
|
880
|
+
},
|
|
881
|
+
native: {}
|
|
882
|
+
});
|
|
883
|
+
this.ensuredFolders.add(ovId);
|
|
884
|
+
}
|
|
885
|
+
await this.setStateAsync(ovId, { val: this.buildOverviewHtml(cams), ack: true });
|
|
886
|
+
}
|
|
887
|
+
await this.setStateAsync("system.lastUpdate", { val: (/* @__PURE__ */ new Date()).toISOString(), ack: true });
|
|
888
|
+
await this.setStateAsync("system.lastPoll", { val: Date.now(), ack: true });
|
|
889
|
+
}
|
|
890
|
+
scheduleRefresh() {
|
|
891
|
+
if (this.refreshTimer !== void 0) {
|
|
892
|
+
this.clearTimeout(this.refreshTimer);
|
|
893
|
+
this.refreshTimer = void 0;
|
|
894
|
+
}
|
|
895
|
+
this.refreshTimer = this.setTimeout(
|
|
896
|
+
() => this.poll().catch((e) => this.log.warn(`Refresh error: ${e.message}`)),
|
|
897
|
+
1500
|
|
898
|
+
);
|
|
899
|
+
}
|
|
900
|
+
// ---- command execution ----
|
|
901
|
+
ptzUrl(oid, dir) {
|
|
902
|
+
return `/ptz.cgi?oid=${encodeURIComponent(String(oid))}&dir=${dir}`;
|
|
903
|
+
}
|
|
904
|
+
buildCommandUrl(entry) {
|
|
905
|
+
if (entry.kind === "ptz") {
|
|
906
|
+
return this.ptzUrl(entry.oid, entry.dir);
|
|
907
|
+
}
|
|
908
|
+
if (entry.kind === "sys") {
|
|
909
|
+
return entry.path;
|
|
910
|
+
}
|
|
911
|
+
const qs = [];
|
|
912
|
+
for (const p of entry.params || []) {
|
|
913
|
+
if (p === "oid") {
|
|
914
|
+
qs.push(`oid=${encodeURIComponent(String(entry.oid))}`);
|
|
915
|
+
} else if (p === "ot") {
|
|
916
|
+
qs.push(`ot=${encodeURIComponent(String(entry.ot))}`);
|
|
917
|
+
}
|
|
918
|
+
}
|
|
919
|
+
return (entry.path || "") + (qs.length ? `?${qs.join("&")}` : "");
|
|
920
|
+
}
|
|
921
|
+
async clearPtzSiblings(stateId) {
|
|
922
|
+
const prefix = stateId.slice(0, stateId.lastIndexOf(".") + 1);
|
|
923
|
+
for (const p of PTZ_DIRS) {
|
|
924
|
+
if (!p.hold) {
|
|
925
|
+
continue;
|
|
926
|
+
}
|
|
927
|
+
const sid = prefix + p.id;
|
|
928
|
+
if (sid !== stateId) {
|
|
929
|
+
await this.setStateAsync(sid, { val: false, ack: true });
|
|
930
|
+
}
|
|
931
|
+
}
|
|
932
|
+
}
|
|
933
|
+
async runCommand(relId, entry, val) {
|
|
934
|
+
if (entry.kind === "push") {
|
|
935
|
+
this.log.info(`Push trigger cam ${entry.oid}`);
|
|
936
|
+
await this.setStateAsync(relId, { val: "", ack: true });
|
|
937
|
+
this.pushRefresh(entry.oid);
|
|
938
|
+
return;
|
|
939
|
+
}
|
|
940
|
+
if (entry.kind === "ptzHold") {
|
|
941
|
+
const oid = entry.oid;
|
|
942
|
+
const startMove = async () => {
|
|
943
|
+
await this.clearPtzSiblings(relId);
|
|
944
|
+
await this.apiGet(this.ptzUrl(oid, entry.dir));
|
|
945
|
+
this.ptzActive.set(oid, relId);
|
|
946
|
+
await this.setStateAsync(relId, { val: true, ack: true });
|
|
947
|
+
};
|
|
948
|
+
const stopMove = async () => {
|
|
949
|
+
await this.apiGet(this.ptzUrl(oid, 11));
|
|
950
|
+
if (this.ptzActive.get(oid) === relId) {
|
|
951
|
+
this.ptzActive.delete(oid);
|
|
952
|
+
}
|
|
953
|
+
await this.setStateAsync(relId, { val: false, ack: true });
|
|
954
|
+
};
|
|
955
|
+
if (val) {
|
|
956
|
+
if (this.ptzActive.get(oid) === relId) {
|
|
957
|
+
await stopMove();
|
|
958
|
+
} else {
|
|
959
|
+
await startMove();
|
|
960
|
+
}
|
|
961
|
+
} else {
|
|
962
|
+
await stopMove();
|
|
963
|
+
}
|
|
964
|
+
this.scheduleRefresh();
|
|
965
|
+
return;
|
|
966
|
+
}
|
|
967
|
+
if (!val) {
|
|
968
|
+
return;
|
|
969
|
+
}
|
|
970
|
+
if (entry.kind === "sys" && entry.path === null) {
|
|
971
|
+
await this.poll().catch((e) => this.log.warn(`Manual refresh error: ${e.message}`));
|
|
972
|
+
await this.setStateAsync(relId, { val: false, ack: true });
|
|
973
|
+
return;
|
|
974
|
+
}
|
|
975
|
+
const url = this.buildCommandUrl(entry);
|
|
976
|
+
const cmdRes = await this.apiGet(url);
|
|
977
|
+
if (cmdRes.ok) {
|
|
978
|
+
this.log.debug(`OK: ${url}`);
|
|
979
|
+
} else {
|
|
980
|
+
this.log.warn(`Command failed (${url}): ${cmdRes.error}`);
|
|
981
|
+
}
|
|
982
|
+
await this.setStateAsync(relId, { val: false, ack: true });
|
|
983
|
+
this.scheduleRefresh();
|
|
984
|
+
}
|
|
985
|
+
}
|
|
986
|
+
if (require.main !== module) {
|
|
987
|
+
module.exports = (options) => new AgentDvr(options);
|
|
988
|
+
} else {
|
|
989
|
+
(() => new AgentDvr())();
|
|
990
|
+
}
|
|
991
|
+
//# sourceMappingURL=main.js.map
|