iobroker.metermaster 0.9.7 → 0.9.9
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/README.md +4 -0
- package/io-package.json +27 -1
- package/main.js +301 -16
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -56,6 +56,8 @@ Open the Web UI anytime at `http://{ioBroker-IP}:8089/` (no password required fo
|
|
|
56
56
|
- **Full history** — every meter keeps a `readings.history` JSON array
|
|
57
57
|
- **Basic Auth** — optional username/password protection for write endpoints
|
|
58
58
|
- **Built-in Web UI** — Data, Import, Logs and System tabs in DE/EN
|
|
59
|
+
- **Delete in Web UI** — remove apartments/meters from ioBroker (password confirm)
|
|
60
|
+
- **Collapsible sections** — fold house/apartment blocks in the Data tab
|
|
59
61
|
- **Charts & CSV** — history charts, monthly consumption and per-meter CSV export
|
|
60
62
|
- **Backup import** — restore MeterMaster app backups (schema 2.0) via drag & drop
|
|
61
63
|
|
|
@@ -212,6 +214,8 @@ metermaster.0.
|
|
|
212
214
|
| POST | `/api/reading` | Store a single reading |
|
|
213
215
|
| POST | `/api/readings` | Store batch readings |
|
|
214
216
|
| POST | `/api/import` | Import app backup |
|
|
217
|
+
| DELETE | `/api/apartment/{house}/{apartment}` | Delete apartment channel tree |
|
|
218
|
+
| DELETE | `/api/meter/{house}/{apartment}/{meter}` | Delete single meter |
|
|
215
219
|
| GET/POST | `/api/nodes/{MAC}/config` | Get / set ESP32 config |
|
|
216
220
|
| POST | `/api/nodes/{MAC}/configAck` | Config acknowledgement |
|
|
217
221
|
| POST | `/api/nodes/{MAC}/cmd` | Immediate command (e.g. LED) |
|
package/io-package.json
CHANGED
|
@@ -1,8 +1,34 @@
|
|
|
1
1
|
{
|
|
2
2
|
"common": {
|
|
3
3
|
"name": "metermaster",
|
|
4
|
-
"version": "0.9.
|
|
4
|
+
"version": "0.9.9",
|
|
5
5
|
"news": {
|
|
6
|
+
"0.9.9": {
|
|
7
|
+
"en": "Web UI: delete apartment/meter (password confirm); collapsible house/apartment sections",
|
|
8
|
+
"de": "Web-UI: Wohnung/Zähler löschen (Passwort-Bestätigung); Haus/Wohnung einklappbar",
|
|
9
|
+
"ru": "Web UI: delete apartment/meter; collapsible sections",
|
|
10
|
+
"pt": "Web UI: delete apartment/meter; collapsible sections",
|
|
11
|
+
"nl": "Web-UI: appartement/meter verwijderen; inklapbare secties",
|
|
12
|
+
"fr": "UI web : supprimer logement/compteur ; sections repliables",
|
|
13
|
+
"it": "Web UI: elimina appartamento/contatore; sezioni comprimibili",
|
|
14
|
+
"es": "Web UI: eliminar vivienda/contador; secciones plegables",
|
|
15
|
+
"pl": "Web UI: usuwanie mieszkania/licznika; zwijane sekcje",
|
|
16
|
+
"uk": "Web UI: delete apartment/meter; collapsible sections",
|
|
17
|
+
"zh-cn": "Web UI:删除公寓/表计;可折叠分区"
|
|
18
|
+
},
|
|
19
|
+
"0.9.8": {
|
|
20
|
+
"en": "Log MeterMaster app connection tests from User-Agent at info level",
|
|
21
|
+
"de": "Verbindungstests der MeterMaster-App per User-Agent auf Info-Level loggen",
|
|
22
|
+
"ru": "Log app connection tests via User-Agent",
|
|
23
|
+
"pt": "Log app connection tests via User-Agent",
|
|
24
|
+
"nl": "Log app connection tests via User-Agent",
|
|
25
|
+
"fr": "Log app connection tests via User-Agent",
|
|
26
|
+
"it": "Log app connection tests via User-Agent",
|
|
27
|
+
"es": "Log app connection tests via User-Agent",
|
|
28
|
+
"pl": "Log app connection tests via User-Agent",
|
|
29
|
+
"uk": "Log app connection tests via User-Agent",
|
|
30
|
+
"zh-cn": "通过 User-Agent 记录应用连接测试"
|
|
31
|
+
},
|
|
6
32
|
"0.9.7": {
|
|
7
33
|
"en": "Print fix (Blob URL); ESP32 discover proxy (getStates/node-discover); node heartbeat/ack logs moved to debug",
|
|
8
34
|
"de": "Druck-Fix (Blob-URL); ESP32-Discover-Proxy (getStates/node-discover); Node-Heartbeat/Ack-Logs auf Debug-Level",
|
package/main.js
CHANGED
|
@@ -4,7 +4,7 @@ const utils = require('@iobroker/adapter-core');
|
|
|
4
4
|
const http = require('node:http');
|
|
5
5
|
const crypto = require('node:crypto');
|
|
6
6
|
const https = require('node:https');
|
|
7
|
-
const CURRENT_VERSION = '0.9.
|
|
7
|
+
const CURRENT_VERSION = '0.9.9';
|
|
8
8
|
const GITHUB_REPO = 'MPunktBPunkt/ioBroker.metermaster';
|
|
9
9
|
const GITHUB_URL = 'https://github.com/MPunktBPunkt/ioBroker.metermaster';
|
|
10
10
|
|
|
@@ -254,7 +254,7 @@ function startHttpServer() {
|
|
|
254
254
|
|
|
255
255
|
server = http.createServer((req, res) => {
|
|
256
256
|
res.setHeader('Access-Control-Allow-Origin', '*');
|
|
257
|
-
res.setHeader('Access-Control-Allow-Methods', 'POST, PUT, GET, OPTIONS');
|
|
257
|
+
res.setHeader('Access-Control-Allow-Methods', 'POST, PUT, GET, DELETE, OPTIONS');
|
|
258
258
|
res.setHeader('Access-Control-Allow-Headers', 'Content-Type, Authorization');
|
|
259
259
|
if (req.method === 'OPTIONS') { res.writeHead(204); res.end(); return; }
|
|
260
260
|
|
|
@@ -327,12 +327,28 @@ function startHttpServer() {
|
|
|
327
327
|
log(LVL.DEBUG, CAT.AUTH, `Auth OK`, `IP: ${clientIp} | User: "${reqUser}"`);
|
|
328
328
|
}
|
|
329
329
|
|
|
330
|
-
if (req.method === 'GET' && url === '/api/ping') { handlePing(res, clientIp); }
|
|
330
|
+
if (req.method === 'GET' && url === '/api/ping') { handlePing(req, res, clientIp); return; }
|
|
331
331
|
else if ((req.method === 'POST' || req.method === 'PUT') && url === '/api/reading') {
|
|
332
332
|
readBody(req, b => handleReading(b, res, clientIp));
|
|
333
333
|
}
|
|
334
334
|
else if (req.method === 'POST' && url === '/api/readings') { readBody(req, b => handleReadings(b, res, clientIp)); }
|
|
335
335
|
else if (req.method === 'POST' && url === '/api/import') { readBody(req, b => handleImport(b, res, clientIp)); }
|
|
336
|
+
else if (req.method === 'DELETE' && url.startsWith('/api/apartment/')) {
|
|
337
|
+
const parts = url.split('/').filter(Boolean); // api, apartment, house, apt
|
|
338
|
+
if (parts.length === 4) {
|
|
339
|
+
handleDeleteApartment(parts[2], parts[3], res, clientIp);
|
|
340
|
+
} else {
|
|
341
|
+
sendJson(res, 400, { error: 'Expected /api/apartment/{house}/{apartment}' });
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
else if (req.method === 'DELETE' && url.startsWith('/api/meter/')) {
|
|
345
|
+
const parts = url.split('/').filter(Boolean); // api, meter, house, apt, meter
|
|
346
|
+
if (parts.length === 5) {
|
|
347
|
+
handleDeleteMeter(parts[2], parts[3], parts[4], res, clientIp);
|
|
348
|
+
} else {
|
|
349
|
+
sendJson(res, 400, { error: 'Expected /api/meter/{house}/{apartment}/{meter}' });
|
|
350
|
+
}
|
|
351
|
+
}
|
|
336
352
|
else {
|
|
337
353
|
// Node-Config: POST /api/nodes/{MAC}/config
|
|
338
354
|
const nodeMatch = url.match(/^\/api\/nodes\/([A-Fa-f0-9]+)\/config$/);
|
|
@@ -362,8 +378,14 @@ function startHttpServer() {
|
|
|
362
378
|
}
|
|
363
379
|
|
|
364
380
|
// ─── Ping ─────────────────────────────────────────────────────────────────────
|
|
365
|
-
function handlePing(res, clientIp) {
|
|
366
|
-
|
|
381
|
+
function handlePing(req, res, clientIp) {
|
|
382
|
+
const ua = String(req.headers['user-agent'] || '');
|
|
383
|
+
const appMatch = ua.match(/MeterMaster\/([\d.]+)/i);
|
|
384
|
+
if (appMatch) {
|
|
385
|
+
log(LVL.INFO, CAT.CONNECT, `App connection test`, `MeterMaster ${appMatch[1]} | IP: ${clientIp}`);
|
|
386
|
+
} else {
|
|
387
|
+
log(LVL.DEBUG, CAT.CONNECT, `Ping`, `IP: ${clientIp}${ua ? ` | UA: ${ua.slice(0, 80)}` : ''}`);
|
|
388
|
+
}
|
|
367
389
|
sendJson(res, 200, { ok: true, adapter: 'metermaster', version: CURRENT_VERSION, received: readingsReceived });
|
|
368
390
|
}
|
|
369
391
|
|
|
@@ -543,6 +565,62 @@ async function storeReading(data) {
|
|
|
543
565
|
return { path: base, history: histResult };
|
|
544
566
|
}
|
|
545
567
|
|
|
568
|
+
// ─── Löschen (Wohnung / Zähler) ───────────────────────────────────────────────
|
|
569
|
+
async function handleDeleteApartment(houseRaw, aptRaw, res, clientIp) {
|
|
570
|
+
const house = sanitize(decodeURIComponent(houseRaw || ''));
|
|
571
|
+
const apt = sanitize(decodeURIComponent(aptRaw || ''));
|
|
572
|
+
if (!house || !apt || house === 'unknown' || apt === 'unknown') {
|
|
573
|
+
sendJson(res, 400, { error: 'Invalid house or apartment' });
|
|
574
|
+
return;
|
|
575
|
+
}
|
|
576
|
+
const relId = `${house}.${apt}`;
|
|
577
|
+
try {
|
|
578
|
+
const existed = await adapter.getObjectAsync(relId).catch(() => null);
|
|
579
|
+
await adapter.delObjectAsync(relId, { recursive: true });
|
|
580
|
+
if (receivedData[house] && receivedData[house][apt]) {
|
|
581
|
+
delete receivedData[house][apt];
|
|
582
|
+
if (!Object.keys(receivedData[house]).length) {
|
|
583
|
+
delete receivedData[house];
|
|
584
|
+
}
|
|
585
|
+
}
|
|
586
|
+
log(LVL.INFO, CAT.DATAPOINT, `Apartment deleted`,
|
|
587
|
+
`${adapter.namespace}.${relId} | IP: ${clientIp}` + (existed ? '' : ' (objects already gone)'));
|
|
588
|
+
sendJson(res, 200, { ok: true, deleted: relId });
|
|
589
|
+
} catch (e) {
|
|
590
|
+
log(LVL.ERROR, CAT.DATAPOINT, `Apartment delete failed`, `${relId}: ${e.message}`);
|
|
591
|
+
sendJson(res, 500, { error: e.message });
|
|
592
|
+
}
|
|
593
|
+
}
|
|
594
|
+
|
|
595
|
+
async function handleDeleteMeter(houseRaw, aptRaw, meterRaw, res, clientIp) {
|
|
596
|
+
const house = sanitize(decodeURIComponent(houseRaw || ''));
|
|
597
|
+
const apt = sanitize(decodeURIComponent(aptRaw || ''));
|
|
598
|
+
const meter = sanitize(decodeURIComponent(meterRaw || ''));
|
|
599
|
+
if (!house || !apt || !meter || house === 'unknown' || apt === 'unknown' || meter === 'unknown') {
|
|
600
|
+
sendJson(res, 400, { error: 'Invalid house, apartment or meter' });
|
|
601
|
+
return;
|
|
602
|
+
}
|
|
603
|
+
const relId = `${house}.${apt}.${meter}`;
|
|
604
|
+
try {
|
|
605
|
+
await adapter.delObjectAsync(relId, { recursive: true });
|
|
606
|
+
if (receivedData[house] && receivedData[house][apt] && receivedData[house][apt][meter]) {
|
|
607
|
+
delete receivedData[house][apt][meter];
|
|
608
|
+
if (!Object.keys(receivedData[house][apt]).length) {
|
|
609
|
+
delete receivedData[house][apt];
|
|
610
|
+
}
|
|
611
|
+
if (receivedData[house] && !Object.keys(receivedData[house]).length) {
|
|
612
|
+
delete receivedData[house];
|
|
613
|
+
}
|
|
614
|
+
}
|
|
615
|
+
log(LVL.INFO, CAT.DATAPOINT, `Meter deleted`,
|
|
616
|
+
`${adapter.namespace}.${relId} | IP: ${clientIp}`);
|
|
617
|
+
sendJson(res, 200, { ok: true, deleted: relId });
|
|
618
|
+
} catch (e) {
|
|
619
|
+
log(LVL.ERROR, CAT.DATAPOINT, `Meter delete failed`, `${relId}: ${e.message}`);
|
|
620
|
+
sendJson(res, 500, { error: e.message });
|
|
621
|
+
}
|
|
622
|
+
}
|
|
623
|
+
|
|
546
624
|
// ─── Historie ─────────────────────────────────────────────────────────────────
|
|
547
625
|
async function updateHistory(base, entry) {
|
|
548
626
|
const stateId = `${base}.readings.history`;
|
|
@@ -1130,6 +1208,9 @@ nav {
|
|
|
1130
1208
|
margin-bottom: 12px; display: flex; align-items: center; gap: 8px;
|
|
1131
1209
|
padding-bottom: 8px; border-bottom: 1px solid var(--border);
|
|
1132
1210
|
}
|
|
1211
|
+
.house-title .collapse-label { flex: 1; cursor: pointer; user-select: none; }
|
|
1212
|
+
.house-body { }
|
|
1213
|
+
.house-block.collapsed .house-body { display: none; }
|
|
1133
1214
|
.apt-block { margin-bottom: 14px; margin-left: 10px; }
|
|
1134
1215
|
.apt-title {
|
|
1135
1216
|
font-size: .88em; font-weight: 600; color: var(--text-dim);
|
|
@@ -1138,6 +1219,22 @@ nav {
|
|
|
1138
1219
|
border-radius: 0 6px 6px 0;
|
|
1139
1220
|
display: flex; align-items: center; justify-content: space-between; gap: 8px;
|
|
1140
1221
|
}
|
|
1222
|
+
.apt-title .collapse-label { flex: 1; cursor: pointer; user-select: none; }
|
|
1223
|
+
.apt-block.collapsed .meters-grid { display: none; }
|
|
1224
|
+
.collapse-toggle {
|
|
1225
|
+
background: transparent; border: none; color: var(--text-dim);
|
|
1226
|
+
cursor: pointer; font-size: .85em; padding: 2px 4px; line-height: 1;
|
|
1227
|
+
min-width: 1.4em;
|
|
1228
|
+
}
|
|
1229
|
+
.collapse-toggle:hover { color: var(--secondary); }
|
|
1230
|
+
.apt-actions { display: flex; align-items: center; gap: 6px; flex-shrink: 0; }
|
|
1231
|
+
.apt-delete-btn, .meter-delete-btn {
|
|
1232
|
+
background: transparent; border: 1px solid transparent; color: var(--text-muted);
|
|
1233
|
+
border-radius: 6px; padding: 2px 7px; cursor: pointer; font-size: .85em; line-height: 1.2;
|
|
1234
|
+
}
|
|
1235
|
+
.apt-delete-btn:hover, .meter-delete-btn:hover {
|
|
1236
|
+
color: #EF9A9A; border-color: rgba(244,67,54,.4); background: rgba(244,67,54,.1);
|
|
1237
|
+
}
|
|
1141
1238
|
.print-scope-btn {
|
|
1142
1239
|
background: transparent; border: 1px solid var(--border); color: var(--text-dim);
|
|
1143
1240
|
border-radius: 6px; padding: 2px 8px; cursor: pointer; font-size: .78em;
|
|
@@ -1707,6 +1804,25 @@ input.search {
|
|
|
1707
1804
|
</div>
|
|
1708
1805
|
</div>
|
|
1709
1806
|
</div>
|
|
1807
|
+
<!-- ══ DELETE-CONFIRM-MODAL ═══════════════════════════════════════════════════ -->
|
|
1808
|
+
<div id="delete-overlay" style="display:none;position:fixed;inset:0;background:rgba(15,11,26,.85);z-index:9999;align-items:center;justify-content:center;">
|
|
1809
|
+
<div style="background:var(--bg-surface);border:1px solid rgba(244,67,54,.45);border-radius:16px;padding:28px 32px;min-width:320px;max-width:440px;width:90%;">
|
|
1810
|
+
<div id="delete-title" style="font-size:1.1em;font-weight:700;color:#EF9A9A;margin-bottom:8px;">\uD83D\uDDD1 Delete</div>
|
|
1811
|
+
<div id="delete-msg" style="font-size:.86em;color:var(--text-dim);margin-bottom:14px;line-height:1.5;"></div>
|
|
1812
|
+
<div id="delete-pass-hint" style="font-size:.78em;color:var(--text-muted);margin-bottom:6px;"></div>
|
|
1813
|
+
<div style="margin-bottom:14px;">
|
|
1814
|
+
<label id="delete-user-label" style="font-size:.82em;color:var(--text-dim);display:block;margin-bottom:4px;">Username</label>
|
|
1815
|
+
<input id="delete-user" type="text" value="metermaster" style="width:100%;background:var(--bg-surface2);border:1px solid var(--border-light);color:var(--text);padding:8px 12px;border-radius:8px;font-size:.9em;outline:none;margin-bottom:10px;">
|
|
1816
|
+
<label id="delete-pass-label" style="font-size:.82em;color:var(--text-dim);display:block;margin-bottom:4px;">Password</label>
|
|
1817
|
+
<input id="delete-pass" type="password" style="width:100%;background:var(--bg-surface2);border:1px solid var(--border-light);color:var(--text);padding:8px 12px;border-radius:8px;font-size:.9em;outline:none;" onkeydown="if(event.key===\'Enter\')confirmDeleteAction()">
|
|
1818
|
+
</div>
|
|
1819
|
+
<div id="delete-err" style="font-size:.82em;color:var(--danger);min-height:18px;margin-bottom:10px;"></div>
|
|
1820
|
+
<div style="display:flex;gap:10px;">
|
|
1821
|
+
<button id="delete-confirm-btn" onclick="confirmDeleteAction()" style="flex:1;background:#C62828;color:#fff;border:none;padding:9px;border-radius:9px;cursor:pointer;font-size:.9em;font-weight:600;"></button>
|
|
1822
|
+
<button id="delete-cancel-btn" onclick="hideDeleteModal()" style="background:transparent;border:1px solid var(--border-light);color:var(--text-dim);padding:9px 16px;border-radius:9px;cursor:pointer;font-size:.9em;"></button>
|
|
1823
|
+
</div>
|
|
1824
|
+
</div>
|
|
1825
|
+
</div>
|
|
1710
1826
|
|
|
1711
1827
|
<script>
|
|
1712
1828
|
|
|
@@ -1749,6 +1865,106 @@ async function authFetch(url, opts) {
|
|
|
1749
1865
|
return r;
|
|
1750
1866
|
}
|
|
1751
1867
|
|
|
1868
|
+
// ── Collapse state ──────────────────────────────────────────────────────────
|
|
1869
|
+
function loadCollapsedState() {
|
|
1870
|
+
try { return JSON.parse(localStorage.getItem('mm-collapsed') || '{}') || {}; }
|
|
1871
|
+
catch { return {}; }
|
|
1872
|
+
}
|
|
1873
|
+
function saveCollapsedState(map) {
|
|
1874
|
+
try { localStorage.setItem('mm-collapsed', JSON.stringify(map)); } catch {}
|
|
1875
|
+
}
|
|
1876
|
+
function toggleCollapse(ckey) {
|
|
1877
|
+
if (!ckey) return;
|
|
1878
|
+
const map = loadCollapsedState();
|
|
1879
|
+
map[ckey] = !map[ckey];
|
|
1880
|
+
if (!map[ckey]) delete map[ckey];
|
|
1881
|
+
saveCollapsedState(map);
|
|
1882
|
+
const blocks = document.querySelectorAll('[data-ckey="'+CSS.escape(ckey)+'"]');
|
|
1883
|
+
blocks.forEach(el => {
|
|
1884
|
+
if (el.classList.contains('house-block') || el.classList.contains('apt-block')) {
|
|
1885
|
+
el.classList.toggle('collapsed', !!map[ckey]);
|
|
1886
|
+
}
|
|
1887
|
+
if (el.classList.contains('collapse-toggle')) {
|
|
1888
|
+
el.textContent = map[ckey] ? '\u25B6' : '\u25BC';
|
|
1889
|
+
el.title = t(map[ckey] ? 'collapse_expand' : 'collapse_collapse');
|
|
1890
|
+
}
|
|
1891
|
+
});
|
|
1892
|
+
}
|
|
1893
|
+
|
|
1894
|
+
// ── Delete apartment / meter ────────────────────────────────────────────────
|
|
1895
|
+
let _pendingDelete = null;
|
|
1896
|
+
|
|
1897
|
+
function hideDeleteModal() {
|
|
1898
|
+
document.getElementById('delete-overlay').style.display = 'none';
|
|
1899
|
+
_pendingDelete = null;
|
|
1900
|
+
const err = document.getElementById('delete-err');
|
|
1901
|
+
if (err) err.textContent = '';
|
|
1902
|
+
const pass = document.getElementById('delete-pass');
|
|
1903
|
+
if (pass) pass.value = '';
|
|
1904
|
+
}
|
|
1905
|
+
|
|
1906
|
+
function showDeleteModal(kind, house, apt, meter) {
|
|
1907
|
+
_pendingDelete = { kind, house, apt, meter: meter || '' };
|
|
1908
|
+
document.getElementById('delete-title').textContent =
|
|
1909
|
+
'\uD83D\uDDD1 ' + (kind === 'meter' ? t('delete_meter_title') : t('delete_apt_title'));
|
|
1910
|
+
document.getElementById('delete-msg').textContent = kind === 'meter'
|
|
1911
|
+
? t('delete_meter_msg', { meter })
|
|
1912
|
+
: t('delete_apt_msg', { apt });
|
|
1913
|
+
document.getElementById('delete-pass-hint').textContent = t('delete_pass_hint');
|
|
1914
|
+
document.getElementById('delete-user-label').textContent = t('login_user');
|
|
1915
|
+
document.getElementById('delete-pass-label').textContent = t('login_pass');
|
|
1916
|
+
document.getElementById('delete-confirm-btn').textContent = t('delete_confirm');
|
|
1917
|
+
document.getElementById('delete-cancel-btn').textContent = t('delete_cancel');
|
|
1918
|
+
document.getElementById('delete-err').textContent = '';
|
|
1919
|
+
const loginUser = document.getElementById('login-user');
|
|
1920
|
+
if (loginUser && loginUser.value) {
|
|
1921
|
+
document.getElementById('delete-user').value = loginUser.value;
|
|
1922
|
+
}
|
|
1923
|
+
document.getElementById('delete-overlay').style.display = 'flex';
|
|
1924
|
+
window.setTimeout(() => document.getElementById('delete-pass').focus(), 50);
|
|
1925
|
+
}
|
|
1926
|
+
|
|
1927
|
+
window.confirmDeleteAction = async function confirmDeleteAction() {
|
|
1928
|
+
if (!_pendingDelete) return;
|
|
1929
|
+
const user = (document.getElementById('delete-user').value || '').trim() || 'metermaster';
|
|
1930
|
+
const pass = document.getElementById('delete-pass').value || '';
|
|
1931
|
+
const { kind, house, apt, meter } = _pendingDelete;
|
|
1932
|
+
const url = kind === 'meter'
|
|
1933
|
+
? '/api/meter/' + encodeURIComponent(house) + '/' + encodeURIComponent(apt) + '/' + encodeURIComponent(meter)
|
|
1934
|
+
: '/api/apartment/' + encodeURIComponent(house) + '/' + encodeURIComponent(apt);
|
|
1935
|
+
const btn = document.getElementById('delete-confirm-btn');
|
|
1936
|
+
btn.disabled = true;
|
|
1937
|
+
document.getElementById('delete-err').textContent = '';
|
|
1938
|
+
try {
|
|
1939
|
+
let r;
|
|
1940
|
+
if (pass) {
|
|
1941
|
+
setAuth(user, pass);
|
|
1942
|
+
r = await authFetch(url, { method: 'DELETE' });
|
|
1943
|
+
if (!r) { btn.disabled = false; return; }
|
|
1944
|
+
} else {
|
|
1945
|
+
r = await fetch(url, { method: 'DELETE' });
|
|
1946
|
+
if (r.status === 401 || r.status === 403) {
|
|
1947
|
+
document.getElementById('delete-err').textContent = t('delete_pass_hint');
|
|
1948
|
+
btn.disabled = false;
|
|
1949
|
+
return;
|
|
1950
|
+
}
|
|
1951
|
+
}
|
|
1952
|
+
const d = await r.json().catch(() => ({}));
|
|
1953
|
+
if (!r.ok || !d.ok) {
|
|
1954
|
+
document.getElementById('delete-err').textContent =
|
|
1955
|
+
t('delete_failed') + ': ' + (d.error || ('HTTP ' + r.status));
|
|
1956
|
+
btn.disabled = false;
|
|
1957
|
+
return;
|
|
1958
|
+
}
|
|
1959
|
+
hideDeleteModal();
|
|
1960
|
+
await fetchData();
|
|
1961
|
+
} catch (e) {
|
|
1962
|
+
document.getElementById('delete-err').textContent = t('delete_failed') + ': ' + e.message;
|
|
1963
|
+
} finally {
|
|
1964
|
+
btn.disabled = false;
|
|
1965
|
+
}
|
|
1966
|
+
};
|
|
1967
|
+
|
|
1752
1968
|
const TYPE_ICONS = {
|
|
1753
1969
|
Electricity:'\u26A1', Gas:'\uD83D\uDD25', Water:'\uD83D\uDCA7', HotWater:'\uD83C\uDF21',
|
|
1754
1970
|
TotalWater:'\uD83D\uDCA7', ColdWater:'\u2744', Heat:'\uD83C\uDFE0', HeatMeter:'\uD83C\uDFE0',
|
|
@@ -1803,11 +2019,24 @@ const I18N = {
|
|
|
1803
2019
|
sys_tip:'Tipp: Alle drei Befehle nacheinander ausf\u00FChren \u2014 warten bis jeder abgeschlossen ist.',
|
|
1804
2020
|
copy:'Kopieren',
|
|
1805
2021
|
login_title:'Anmelden',
|
|
1806
|
-
login_desc:'Zugangsdaten f\u00FCr schreibende Aktionen (Z\u00E4hler zuweisen, Import).',
|
|
2022
|
+
login_desc:'Zugangsdaten f\u00FCr schreibende Aktionen (Z\u00E4hler zuweisen, Import, L\u00F6schen).',
|
|
1807
2023
|
login_user:'Benutzername', login_pass:'Passwort', login_submit:'Anmelden',
|
|
1808
2024
|
login_missing_user:'Benutzername fehlt', login_wrong:'Falsche Zugangsdaten',
|
|
1809
2025
|
login_conn_err:'Verbindungsfehler', login_expired:'Sitzung abgelaufen \u2013 bitte neu anmelden',
|
|
1810
|
-
invalid_json:'Ung\u00FCltige JSON-Datei', network_err:'Netzwerkfehler'
|
|
2026
|
+
invalid_json:'Ung\u00FCltige JSON-Datei', network_err:'Netzwerkfehler',
|
|
2027
|
+
delete_apt_title:'Wohnung l\u00F6schen',
|
|
2028
|
+
delete_apt_msg:'Wohnung \u201E{apt}\u201C inkl. aller Z\u00E4hler und Ablesungen in ioBroker unwiderruflich l\u00F6schen?',
|
|
2029
|
+
delete_meter_title:'Z\u00E4hler l\u00F6schen',
|
|
2030
|
+
delete_meter_msg:'Z\u00E4hler \u201E{meter}\u201C inkl. Verlauf in ioBroker unwiderruflich l\u00F6schen?',
|
|
2031
|
+
delete_pass_hint:'Passwort aus den Adapter-Einstellungen eingeben.',
|
|
2032
|
+
delete_confirm:'Endg\u00FCltig l\u00F6schen',
|
|
2033
|
+
delete_cancel:'Abbrechen',
|
|
2034
|
+
delete_ok:'Gel\u00F6scht.',
|
|
2035
|
+
delete_failed:'L\u00F6schen fehlgeschlagen',
|
|
2036
|
+
delete_apt_title_attr:'Wohnung in ioBroker l\u00F6schen',
|
|
2037
|
+
delete_meter_title_attr:'Z\u00E4hler in ioBroker l\u00F6schen',
|
|
2038
|
+
collapse_expand:'Aufklappen',
|
|
2039
|
+
collapse_collapse:'Einklappen'
|
|
1811
2040
|
},
|
|
1812
2041
|
en: {
|
|
1813
2042
|
readings:'Readings', nodes:'Nodes', uptime:'Uptime', live:'Live', disconnected:'Disconnected',
|
|
@@ -1856,11 +2085,24 @@ const I18N = {
|
|
|
1856
2085
|
sys_tip:'Tip: Run all three commands in sequence \u2014 wait for each to finish.',
|
|
1857
2086
|
copy:'Copy',
|
|
1858
2087
|
login_title:'Sign in',
|
|
1859
|
-
login_desc:'Credentials for write actions (assign meter, import).',
|
|
2088
|
+
login_desc:'Credentials for write actions (assign meter, import, delete).',
|
|
1860
2089
|
login_user:'Username', login_pass:'Password', login_submit:'Sign in',
|
|
1861
2090
|
login_missing_user:'Username required', login_wrong:'Invalid credentials',
|
|
1862
2091
|
login_conn_err:'Connection error', login_expired:'Session expired \u2013 please sign in again',
|
|
1863
|
-
invalid_json:'Invalid JSON file', network_err:'Network error'
|
|
2092
|
+
invalid_json:'Invalid JSON file', network_err:'Network error',
|
|
2093
|
+
delete_apt_title:'Delete apartment',
|
|
2094
|
+
delete_apt_msg:'Permanently delete apartment "{apt}" including all meters and readings in ioBroker?',
|
|
2095
|
+
delete_meter_title:'Delete meter',
|
|
2096
|
+
delete_meter_msg:'Permanently delete meter "{meter}" including history in ioBroker?',
|
|
2097
|
+
delete_pass_hint:'Enter the password from the adapter settings.',
|
|
2098
|
+
delete_confirm:'Delete permanently',
|
|
2099
|
+
delete_cancel:'Cancel',
|
|
2100
|
+
delete_ok:'Deleted.',
|
|
2101
|
+
delete_failed:'Delete failed',
|
|
2102
|
+
delete_apt_title_attr:'Delete apartment in ioBroker',
|
|
2103
|
+
delete_meter_title_attr:'Delete meter in ioBroker',
|
|
2104
|
+
collapse_expand:'Expand',
|
|
2105
|
+
collapse_collapse:'Collapse'
|
|
1864
2106
|
}
|
|
1865
2107
|
};
|
|
1866
2108
|
|
|
@@ -2401,12 +2643,29 @@ async function fetchData() {
|
|
|
2401
2643
|
}
|
|
2402
2644
|
let html = '';
|
|
2403
2645
|
let idx = 0;
|
|
2646
|
+
const collapsed = loadCollapsedState();
|
|
2404
2647
|
for (const [house, apts] of Object.entries(d.data)) {
|
|
2405
|
-
|
|
2406
|
-
|
|
2648
|
+
const houseKey = 'h:' + house;
|
|
2649
|
+
const houseCollapsed = !!collapsed[houseKey];
|
|
2650
|
+
html += '<div class="house-block'+(houseCollapsed ? ' collapsed' : '')+'" data-ckey="'+esc(houseKey)+'">'+
|
|
2651
|
+
'<div class="house-title">'+
|
|
2652
|
+
'<button type="button" class="collapse-toggle" data-ckey="'+esc(houseKey)+'" title="'+esc(t(houseCollapsed ? 'collapse_expand' : 'collapse_collapse'))+'">'+(houseCollapsed ? '\u25B6' : '\u25BC')+'</button>'+
|
|
2653
|
+
'<span class="collapse-label" data-ckey="'+esc(houseKey)+'">\uD83C\uDFE0 '+esc(house)+'</span>'+
|
|
2654
|
+
'<button type="button" class="print-scope-btn" data-print-house="'+esc(house)+'" title="'+esc(t('print_house'))+'">\uD83D\uDDA8</button>'+
|
|
2655
|
+
'</div><div class="house-body">';
|
|
2407
2656
|
for (const [apt, meters] of Object.entries(apts)) {
|
|
2408
|
-
|
|
2409
|
-
|
|
2657
|
+
const aptKey = 'a:' + house + '/' + apt;
|
|
2658
|
+
const aptCollapsed = !!collapsed[aptKey];
|
|
2659
|
+
const meterCount = Object.keys(meters).length;
|
|
2660
|
+
html += '<div class="apt-block'+(aptCollapsed ? ' collapsed' : '')+'" data-ckey="'+esc(aptKey)+'">'+
|
|
2661
|
+
'<div class="apt-title">'+
|
|
2662
|
+
'<button type="button" class="collapse-toggle" data-ckey="'+esc(aptKey)+'" title="'+esc(t(aptCollapsed ? 'collapse_expand' : 'collapse_collapse'))+'">'+(aptCollapsed ? '\u25B6' : '\u25BC')+'</button>'+
|
|
2663
|
+
'<span class="collapse-label" data-ckey="'+esc(aptKey)+'">\uD83C\uDFD8 '+esc(apt)+' <span style="opacity:.6;font-weight:500">('+meterCount+')</span></span>'+
|
|
2664
|
+
'<span class="apt-actions">'+
|
|
2665
|
+
'<button type="button" class="print-scope-btn" data-print-house="'+esc(house)+'" data-print-apt="'+esc(apt)+'" title="'+esc(t('print_apartment'))+'">\uD83D\uDDA8</button>'+
|
|
2666
|
+
'<button type="button" class="apt-delete-btn" data-house="'+esc(house)+'" data-apt="'+esc(apt)+'" title="'+esc(t('delete_apt_title_attr'))+'">\uD83D\uDDD1</button>'+
|
|
2667
|
+
'</span>'+
|
|
2668
|
+
'</div><div class="meters-grid">';
|
|
2410
2669
|
for (const [key, m] of Object.entries(meters)) {
|
|
2411
2670
|
const cacheIdx = idx++;
|
|
2412
2671
|
dataCacheList.push({ house, apartment: apt, meter: key, unit: m.unit, typeName: m.typeName, latest: m.latest, latestDate: m.latestDate, history: m.history || [] });
|
|
@@ -2439,7 +2698,10 @@ async function fetchData() {
|
|
|
2439
2698
|
'<div class="meter-card'+(assignedHere ? ' has-node' : '')+'">'+
|
|
2440
2699
|
'<div class="mc-head">'+
|
|
2441
2700
|
'<div class="mc-name">'+icon+' '+esc(key)+'</div>'+
|
|
2442
|
-
'<div
|
|
2701
|
+
'<div style="display:flex;align-items:center;gap:6px;">'+
|
|
2702
|
+
'<div class="mc-badge">'+esc(m.typeName||'?')+'</div>'+
|
|
2703
|
+
'<button type="button" class="meter-delete-btn" data-house="'+esc(house)+'" data-apt="'+esc(apt)+'" data-meter="'+esc(key)+'" title="'+esc(t('delete_meter_title_attr'))+'">\uD83D\uDDD1</button>'+
|
|
2704
|
+
'</div>'+
|
|
2443
2705
|
'</div>'+
|
|
2444
2706
|
'<div class="mc-value-row">'+
|
|
2445
2707
|
'<span class="mc-value">'+(m.latest !== undefined ? m.latest : '\u2013')+'</span>'+
|
|
@@ -2457,7 +2719,7 @@ async function fetchData() {
|
|
|
2457
2719
|
}
|
|
2458
2720
|
html += '</div></div>';
|
|
2459
2721
|
}
|
|
2460
|
-
html += '</div>';
|
|
2722
|
+
html += '</div></div>';
|
|
2461
2723
|
}
|
|
2462
2724
|
con.innerHTML = html;
|
|
2463
2725
|
} catch(e) {
|
|
@@ -2865,6 +3127,29 @@ function initTabs() {
|
|
|
2865
3127
|
if (btn && btn.dataset.hist) toggleHist(btn.dataset.hist);
|
|
2866
3128
|
});
|
|
2867
3129
|
|
|
3130
|
+
// Collapse house / apartment
|
|
3131
|
+
document.addEventListener('click', e => {
|
|
3132
|
+
const toggle = e.target.closest('.collapse-toggle, .collapse-label');
|
|
3133
|
+
if (!toggle || !toggle.dataset.ckey) return;
|
|
3134
|
+
e.preventDefault();
|
|
3135
|
+
toggleCollapse(toggle.dataset.ckey);
|
|
3136
|
+
});
|
|
3137
|
+
|
|
3138
|
+
// Delete apartment / meter
|
|
3139
|
+
document.addEventListener('click', e => {
|
|
3140
|
+
const aptDel = e.target.closest('.apt-delete-btn');
|
|
3141
|
+
if (aptDel) {
|
|
3142
|
+
e.stopPropagation();
|
|
3143
|
+
showDeleteModal('apartment', aptDel.dataset.house, aptDel.dataset.apt);
|
|
3144
|
+
return;
|
|
3145
|
+
}
|
|
3146
|
+
const meterDel = e.target.closest('.meter-delete-btn');
|
|
3147
|
+
if (meterDel) {
|
|
3148
|
+
e.stopPropagation();
|
|
3149
|
+
showDeleteModal('meter', meterDel.dataset.house, meterDel.dataset.apt, meterDel.dataset.meter);
|
|
3150
|
+
}
|
|
3151
|
+
});
|
|
3152
|
+
|
|
2868
3153
|
// History value edit
|
|
2869
3154
|
document.addEventListener('click', e => {
|
|
2870
3155
|
const editBtn = e.target.closest('.hist-edit-btn');
|
|
@@ -2922,7 +3207,7 @@ function initTabs() {
|
|
|
2922
3207
|
if (langSel) langSel.addEventListener('change', e => setLang(e.target.value));
|
|
2923
3208
|
|
|
2924
3209
|
document.addEventListener('keydown', e => {
|
|
2925
|
-
if (e.key === 'Escape') closeChartModal();
|
|
3210
|
+
if (e.key === 'Escape') { closeChartModal(); hideDeleteModal(); }
|
|
2926
3211
|
});
|
|
2927
3212
|
|
|
2928
3213
|
// Nodes-Speichern via Event Delegation (kein onclick-Attribut mit Escaping-Problemen)
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "iobroker.metermaster",
|
|
3
|
-
"version": "0.9.
|
|
4
|
-
"description": "ioBroker adapter for the MeterMaster Android app
|
|
3
|
+
"version": "0.9.9",
|
|
4
|
+
"description": "ioBroker adapter for the MeterMaster Android app – receives meter readings and manages ESP32 display nodes",
|
|
5
5
|
"author": {
|
|
6
6
|
"name": "MPunktBPunkt",
|
|
7
7
|
"url": "https://github.com/MPunktBPunkt"
|