iobroker.metermaster 0.9.8 → 0.9.10
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 +25 -0
- package/io-package.json +55 -68
- package/main.js +292 -13
- package/package.json +1 -1
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) |
|
|
@@ -267,6 +271,27 @@ iobroker restart metermaster.0
|
|
|
267
271
|
### **WORK IN PROGRESS**
|
|
268
272
|
- (ioBroker-Bot) Adapter requires admin >= 7.8.23 now.
|
|
269
273
|
|
|
274
|
+
### 0.9.10
|
|
275
|
+
- Repo checker (E2004/E6029): remove unpublished `0.9.5` from `common.news`
|
|
276
|
+
- Trim `common.news` to 7 entries
|
|
277
|
+
- Document releases 0.9.6–0.9.10 in README changelog
|
|
278
|
+
|
|
279
|
+
### 0.9.9
|
|
280
|
+
- Web UI: delete apartment/meter with password confirmation (DELETE API)
|
|
281
|
+
- Collapsible house/apartment sections in the Data tab (localStorage)
|
|
282
|
+
|
|
283
|
+
### 0.9.8
|
|
284
|
+
- Log MeterMaster app connection tests from User-Agent on `/api/ping` at info level
|
|
285
|
+
|
|
286
|
+
### 0.9.7
|
|
287
|
+
- Print fix (Blob URL revoke)
|
|
288
|
+
- ESP32 discover proxy (`getStates` / node-discover)
|
|
289
|
+
- Node heartbeat/ack logs moved to debug
|
|
290
|
+
|
|
291
|
+
### 0.9.6
|
|
292
|
+
- Assign display nodes via chips on meter cards in the Data tab
|
|
293
|
+
- Correct history on re-sync; edit values in Web UI; print chart and apartment/house latest readings
|
|
294
|
+
|
|
270
295
|
### 0.9.4
|
|
271
296
|
- All adapter log messages and API JSON error responses in English
|
|
272
297
|
- State common names and roles corrected (readings channel, date/text/json roles, info.firmware for nodes)
|
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.10",
|
|
5
5
|
"news": {
|
|
6
|
+
"0.9.10": {
|
|
7
|
+
"en": "Repo checker: remove unpublished 0.9.5 from news; add missing changelog entries for 0.9.6\u20130.9.10",
|
|
8
|
+
"de": "Repo-Checker: unver\u00f6ffentlichte 0.9.5 aus News entfernt; fehlende Changelog-Eintr\u00e4ge 0.9.6\u20130.9.10 erg\u00e4nzt",
|
|
9
|
+
"ru": "Repo checker: news/changelog cleanup",
|
|
10
|
+
"pt": "Repo checker: news/changelog cleanup",
|
|
11
|
+
"nl": "Repo-checker: news/changelog opgeschoond",
|
|
12
|
+
"fr": "Repo checker : nettoyage news/changelog",
|
|
13
|
+
"it": "Repo checker: pulizia news/changelog",
|
|
14
|
+
"es": "Repo checker: limpieza news/changelog",
|
|
15
|
+
"pl": "Repo checker: porz\u0105dki news/changelog",
|
|
16
|
+
"uk": "Repo checker: \u043e\u0447\u0438\u0449\u0435\u043d\u043d\u044f news/changelog",
|
|
17
|
+
"zh-cn": "\u4ed3\u5e93\u68c0\u67e5\u5668\uff1a\u6e05\u7406 news/changelog"
|
|
18
|
+
},
|
|
19
|
+
"0.9.9": {
|
|
20
|
+
"en": "Web UI: delete apartment/meter (password confirm); collapsible house/apartment sections",
|
|
21
|
+
"de": "Web-UI: Wohnung/Z\u00e4hler l\u00f6schen (Passwort-Best\u00e4tigung); Haus/Wohnung einklappbar",
|
|
22
|
+
"ru": "Web UI: delete apartment/meter; collapsible sections",
|
|
23
|
+
"pt": "Web UI: delete apartment/meter; collapsible sections",
|
|
24
|
+
"nl": "Web-UI: appartement/meter verwijderen; inklapbare secties",
|
|
25
|
+
"fr": "UI web : supprimer logement/compteur ; sections repliables",
|
|
26
|
+
"it": "Web UI: elimina appartamento/contatore; sezioni comprimibili",
|
|
27
|
+
"es": "Web UI: eliminar vivienda/contador; secciones plegables",
|
|
28
|
+
"pl": "Web UI: usuwanie mieszkania/licznika; zwijane sekcje",
|
|
29
|
+
"uk": "Web UI: delete apartment/meter; collapsible sections",
|
|
30
|
+
"zh-cn": "Web UI\uff1a\u5220\u9664\u516c\u5bd3/\u8868\u8ba1\uff1b\u53ef\u6298\u53e0\u5206\u533a"
|
|
31
|
+
},
|
|
6
32
|
"0.9.8": {
|
|
7
33
|
"en": "Log MeterMaster app connection tests from User-Agent at info level",
|
|
8
34
|
"de": "Verbindungstests der MeterMaster-App per User-Agent auf Info-Level loggen",
|
|
@@ -14,7 +40,7 @@
|
|
|
14
40
|
"es": "Log app connection tests via User-Agent",
|
|
15
41
|
"pl": "Log app connection tests via User-Agent",
|
|
16
42
|
"uk": "Log app connection tests via User-Agent",
|
|
17
|
-
"zh-cn": "
|
|
43
|
+
"zh-cn": "\u901a\u8fc7 User-Agent \u8bb0\u5f55\u5e94\u7528\u8fde\u63a5\u6d4b\u8bd5"
|
|
18
44
|
},
|
|
19
45
|
"0.9.7": {
|
|
20
46
|
"en": "Print fix (Blob URL); ESP32 discover proxy (getStates/node-discover); node heartbeat/ack logs moved to debug",
|
|
@@ -27,85 +53,46 @@
|
|
|
27
53
|
"es": "Print fix; ESP32 discover proxy; node logs debug",
|
|
28
54
|
"pl": "Print fix; ESP32 discover proxy; node logs debug",
|
|
29
55
|
"uk": "Print fix; ESP32 discover proxy; node logs debug",
|
|
30
|
-
"zh-cn": "
|
|
56
|
+
"zh-cn": "\u6253\u5370\u4fee\u590d\uff1bESP32 discover \u4ee3\u7406\uff1b\u8282\u70b9\u65e5\u5fd7\u6539\u4e3a debug"
|
|
31
57
|
},
|
|
32
58
|
"0.9.6": {
|
|
33
59
|
"en": "Assign display nodes via chips on meter cards in the Data tab",
|
|
34
|
-
"de": "Anzeige-Nodes per Chips auf den
|
|
60
|
+
"de": "Anzeige-Nodes per Chips auf den Z\u00e4hler-Kacheln im Daten-Tab zuweisen",
|
|
35
61
|
"ru": "Assign nodes via chips on meter cards",
|
|
36
62
|
"pt": "Assign nodes via chips on meter cards",
|
|
37
63
|
"nl": "Nodes toewijzen via chips op meterkarten",
|
|
38
64
|
"fr": "Assigner des nodes via des chips sur les cartes compteur",
|
|
39
65
|
"it": "Assegna node tramite chip sulle card contatore",
|
|
40
66
|
"es": "Asignar nodes mediante chips en las tarjetas de contador",
|
|
41
|
-
"pl": "Przypisywanie node przez chipy na kartach
|
|
42
|
-
"uk": "
|
|
43
|
-
"zh-cn": "
|
|
44
|
-
},
|
|
45
|
-
"0.9.5": {
|
|
46
|
-
"en": "Correct history on re-sync; edit values in Web UI; print chart and apartment/house latest readings",
|
|
47
|
-
"de": "Historie bei erneutem Sync korrigieren; Werte in der Web-UI bearbeiten; Chart sowie Wohnungs-/Haus-Stände drucken",
|
|
48
|
-
"ru": "Correction of history; Web UI edit; print chart and readings",
|
|
49
|
-
"pt": "Correct history; Web UI edit; print",
|
|
50
|
-
"nl": "History corrigeren; Web UI edit; printen",
|
|
51
|
-
"fr": "Corriger l'historique; édition Web UI; impression",
|
|
52
|
-
"it": "Correggi cronologia; modifica Web UI; stampa",
|
|
53
|
-
"es": "Corregir historial; edición Web UI; imprimir",
|
|
54
|
-
"pl": "Korekta historii; edycja Web UI; druk",
|
|
55
|
-
"uk": "Корекція історії; редагування Web UI; друк",
|
|
56
|
-
"zh-cn": "更正历史记录;Web UI 编辑;打印图表与读数"
|
|
67
|
+
"pl": "Przypisywanie node przez chipy na kartach licznik\u00f3w",
|
|
68
|
+
"uk": "\u041f\u0440\u0438\u0437\u043d\u0430\u0447\u0435\u043d\u043d\u044f node \u0447\u0435\u0440\u0435\u0437 \u0447\u0456\u043f\u0438 \u043d\u0430 \u043a\u0430\u0440\u0442\u043a\u0430\u0445 \u043b\u0456\u0447\u0438\u043b\u044c\u043d\u0438\u043a\u0456\u0432",
|
|
69
|
+
"zh-cn": "\u5728\u6570\u636e\u9875\u8ba1\u91cf\u8868\u5361\u7247\u4e0a\u901a\u8fc7\u82af\u7247\u5206\u914d\u663e\u793a\u8282\u70b9"
|
|
57
70
|
},
|
|
58
71
|
"0.9.4": {
|
|
59
72
|
"en": "Review fixes: English logs/states/API errors, Web UI i18n, config validation, removed /api/update",
|
|
60
73
|
"de": "Review-Fixes: Englische Logs/States/API-Fehler, Web-UI-i18n, Config-Validierung, /api/update entfernt",
|
|
61
|
-
"ru": "
|
|
62
|
-
"pt": "
|
|
74
|
+
"ru": "\u0418\u0441\u043f\u0440\u0430\u0432\u043b\u0435\u043d\u0438\u044f review: \u0430\u043d\u0433\u043b\u0438\u0439\u0441\u043a\u0438\u0435 \u043b\u043e\u0433\u0438/states/API, i18n Web UI, \u0432\u0430\u043b\u0438\u0434\u0430\u0446\u0438\u044f config, \u0443\u0434\u0430\u043b\u0451\u043d /api/update",
|
|
75
|
+
"pt": "Corre\u00e7\u00f5es de review: logs/states/erros API em ingl\u00eas, i18n Web UI, valida\u00e7\u00e3o config, removido /api/update",
|
|
63
76
|
"nl": "Review-fixes: Engelse logs/states/API-fouten, Web UI i18n, config-validatie, /api/update verwijderd",
|
|
64
|
-
"fr": "Corrections review : logs/states/erreurs API en anglais, i18n Web UI, validation config, /api/update
|
|
77
|
+
"fr": "Corrections review : logs/states/erreurs API en anglais, i18n Web UI, validation config, /api/update supprim\u00e9",
|
|
65
78
|
"it": "Correzioni review: log/states/errori API in inglese, i18n Web UI, validazione config, rimosso /api/update",
|
|
66
|
-
"es": "Correcciones de review: logs/states/errores API en
|
|
67
|
-
"pl": "Poprawki review: angielskie logi/states/
|
|
68
|
-
"uk": "
|
|
69
|
-
"zh-cn": "
|
|
79
|
+
"es": "Correcciones de review: logs/states/errores API en ingl\u00e9s, i18n Web UI, validaci\u00f3n config, eliminado /api/update",
|
|
80
|
+
"pl": "Poprawki review: angielskie logi/states/b\u0142\u0119dy API, i18n Web UI, walidacja config, usuni\u0119to /api/update",
|
|
81
|
+
"uk": "\u0412\u0438\u043f\u0440\u0430\u0432\u043b\u0435\u043d\u043d\u044f review: \u0430\u043d\u0433\u043b\u0456\u0439\u0441\u044c\u043a\u0456 \u043b\u043e\u0433\u0438/states/API, i18n Web UI, \u0432\u0430\u043b\u0456\u0434\u0430\u0446\u0456\u044f config, \u0432\u0438\u0434\u0430\u043b\u0435\u043d\u043e /api/update",
|
|
82
|
+
"zh-cn": "\u5ba1\u67e5\u4fee\u590d\uff1a\u82f1\u6587\u65e5\u5fd7/states/API \u9519\u8bef\u3001Web UI \u56fd\u9645\u5316\u3001\u914d\u7f6e\u9a8c\u8bc1\u3001\u79fb\u9664 /api/update"
|
|
70
83
|
},
|
|
71
84
|
"0.9.3": {
|
|
72
85
|
"en": "Fix state roles for ioBroker object structure check (date, text, json, info.firmware)",
|
|
73
|
-
"de": "State-Rollen
|
|
74
|
-
"ru": "
|
|
75
|
-
"pt": "Corrigidas roles de states para
|
|
86
|
+
"de": "State-Rollen f\u00fcr ioBroker-Objektstruktur-Pr\u00fcfung korrigiert (date, text, json, info.firmware)",
|
|
87
|
+
"ru": "\u0418\u0441\u043f\u0440\u0430\u0432\u043b\u0435\u043d\u044b \u0440\u043e\u043b\u0438 states \u0434\u043b\u044f \u043f\u0440\u043e\u0432\u0435\u0440\u043a\u0438 \u0441\u0442\u0440\u0443\u043a\u0442\u0443\u0440\u044b \u043e\u0431\u044a\u0435\u043a\u0442\u043e\u0432 ioBroker",
|
|
88
|
+
"pt": "Corrigidas roles de states para verifica\u00e7\u00e3o da estrutura de objetos ioBroker",
|
|
76
89
|
"nl": "State-rollen gecorrigeerd voor ioBroker objectstructuurcontrole",
|
|
77
|
-
"fr": "
|
|
90
|
+
"fr": "R\u00f4les des states corrig\u00e9s pour la v\u00e9rification de structure d'objets ioBroker",
|
|
78
91
|
"it": "Corrette le role degli state per il controllo struttura oggetti ioBroker",
|
|
79
|
-
"es": "Corregidos roles de states para
|
|
80
|
-
"pl": "Poprawiono role states dla kontroli struktury
|
|
81
|
-
"uk": "
|
|
82
|
-
"zh-cn": "
|
|
83
|
-
},
|
|
84
|
-
"0.9.2": {
|
|
85
|
-
"en": "Adapter checker compliance: npm news cleanup, devDependencies, trusted publishing",
|
|
86
|
-
"de": "Adapter-Checker-Konformität: npm-News bereinigt, devDependencies, Trusted Publishing",
|
|
87
|
-
"ru": "Соответствие adapter checker: очистка npm news, devDependencies, trusted publishing",
|
|
88
|
-
"pt": "Conformidade com adapter checker: limpeza npm news, devDependencies, trusted publishing",
|
|
89
|
-
"nl": "Adapter-checker conformiteit: npm news opgeschoond, devDependencies, trusted publishing",
|
|
90
|
-
"fr": "Conformité adapter checker : nettoyage npm news, devDependencies, trusted publishing",
|
|
91
|
-
"it": "Conformità adapter checker: pulizia npm news, devDependencies, trusted publishing",
|
|
92
|
-
"es": "Conformidad adapter checker: limpieza npm news, devDependencies, trusted publishing",
|
|
93
|
-
"pl": "Zgodność z adapter checker: czyszczenie npm news, devDependencies, trusted publishing",
|
|
94
|
-
"uk": "Відповідність adapter checker: очищення npm news, devDependencies, trusted publishing",
|
|
95
|
-
"zh-cn": "适配器检查器合规:清理 npm news、devDependencies、trusted publishing"
|
|
96
|
-
},
|
|
97
|
-
"0.9.1": {
|
|
98
|
-
"en": "Lowered admin dependency to >=7.6.20 (fixes startup with admin 7.7.x)",
|
|
99
|
-
"de": "Admin-Abhängigkeit auf >=7.6.20 gesenkt (behebt Start mit admin 7.7.x)",
|
|
100
|
-
"ru": "Снижена зависимость admin до >=7.6.20 (исправлен запуск с admin 7.7.x)",
|
|
101
|
-
"pt": "Dependência admin reduzida para >=7.6.20 (corrige inicialização com admin 7.7.x)",
|
|
102
|
-
"nl": "Admin-afhankelijkheid verlaagd naar >=7.6.20 (lost opstarten met admin 7.7.x op)",
|
|
103
|
-
"fr": "Dépendance admin abaissée à >=7.6.20 (corrige le démarrage avec admin 7.7.x)",
|
|
104
|
-
"it": "Dipendenza admin ridotta a >=7.6.20 (corregge avvio con admin 7.7.x)",
|
|
105
|
-
"es": "Dependencia admin reducida a >=7.6.20 (corrige inicio con admin 7.7.x)",
|
|
106
|
-
"pl": "Obniżono zależność admin do >=7.6.20 (naprawia start z admin 7.7.x)",
|
|
107
|
-
"uk": "Знижено залежність admin до >=7.6.20 (виправлено запуск з admin 7.7.x)",
|
|
108
|
-
"zh-cn": "降低 admin 依赖至 >=7.6.20(修复 admin 7.7.x 下无法启动)"
|
|
92
|
+
"es": "Corregidos roles de states para verificaci\u00f3n de estructura de objetos ioBroker",
|
|
93
|
+
"pl": "Poprawiono role states dla kontroli struktury obiekt\u00f3w ioBroker",
|
|
94
|
+
"uk": "\u0412\u0438\u043f\u0440\u0430\u0432\u043b\u0435\u043d\u043e \u0440\u043e\u043b\u0456 states \u0434\u043b\u044f \u043f\u0435\u0440\u0435\u0432\u0456\u0440\u043a\u0438 \u0441\u0442\u0440\u0443\u043a\u0442\u0443\u0440\u0438 \u043e\u0431'\u0454\u043a\u0442\u0456\u0432 ioBroker",
|
|
95
|
+
"zh-cn": "\u4fee\u590d state \u89d2\u8272\u4ee5\u901a\u8fc7 ioBroker \u5bf9\u8c61\u7ed3\u6784\u68c0\u67e5"
|
|
109
96
|
}
|
|
110
97
|
},
|
|
111
98
|
"titleLang": {
|
|
@@ -123,16 +110,16 @@
|
|
|
123
110
|
},
|
|
124
111
|
"desc": {
|
|
125
112
|
"en": "Receives meter readings from the MeterMaster Android app and manages ESP32 display nodes",
|
|
126
|
-
"de": "
|
|
127
|
-
"ru": "
|
|
128
|
-
"pt": "Recebe leituras de medidores do app Android MeterMaster e gerencia
|
|
113
|
+
"de": "Empf\u00e4ngt Z\u00e4hlerablesungen von der MeterMaster Android-App und verwaltet ESP32 Display-Nodes",
|
|
114
|
+
"ru": "\u041f\u0440\u0438\u043d\u0438\u043c\u0430\u0435\u0442 \u043f\u043e\u043a\u0430\u0437\u0430\u043d\u0438\u044f \u0441\u0447\u0451\u0442\u0447\u0438\u043a\u043e\u0432 \u0438\u0437 Android-\u043f\u0440\u0438\u043b\u043e\u0436\u0435\u043d\u0438\u044f MeterMaster \u0438 \u0443\u043f\u0440\u0430\u0432\u043b\u044f\u0435\u0442 ESP32 display nodes",
|
|
115
|
+
"pt": "Recebe leituras de medidores do app Android MeterMaster e gerencia n\u00f3s de display ESP32",
|
|
129
116
|
"nl": "Ontvangt meterstanden van de MeterMaster Android-app en beheert ESP32 display nodes",
|
|
130
|
-
"fr": "
|
|
117
|
+
"fr": "Re\u00e7oit les relev\u00e9s de compteurs de l'app Android MeterMaster et g\u00e8re les n\u0153uds d'affichage ESP32",
|
|
131
118
|
"it": "Riceve letture contatori dall'app Android MeterMaster e gestisce nodi display ESP32",
|
|
132
119
|
"es": "Recibe lecturas de contadores de la app Android MeterMaster y gestiona nodos de display ESP32",
|
|
133
|
-
"pl": "Odbiera odczyty
|
|
134
|
-
"uk": "
|
|
135
|
-
"zh-cn": "
|
|
120
|
+
"pl": "Odbiera odczyty licznik\u00f3w z aplikacji Android MeterMaster i zarz\u0105dza w\u0119z\u0142ami wy\u015bwietlacza ESP32",
|
|
121
|
+
"uk": "\u041e\u0442\u0440\u0438\u043c\u0443\u0454 \u043f\u043e\u043a\u0430\u0437\u043d\u0438\u043a\u0438 \u043b\u0456\u0447\u0438\u043b\u044c\u043d\u0438\u043a\u0456\u0432 \u0437 Android-\u0434\u043e\u0434\u0430\u0442\u043a\u0443 MeterMaster \u0442\u0430 \u043a\u0435\u0440\u0443\u0454 ESP32 display nodes",
|
|
122
|
+
"zh-cn": "\u63a5\u6536 MeterMaster Android \u5e94\u7528\u7684\u8868\u8ba1\u8bfb\u6570\u5e76\u7ba1\u7406 ESP32 \u663e\u793a\u8282\u70b9"
|
|
136
123
|
},
|
|
137
124
|
"authors": [
|
|
138
125
|
"MPunktBPunkt"
|
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.10';
|
|
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
|
|
|
@@ -333,6 +333,22 @@ function startHttpServer() {
|
|
|
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$/);
|
|
@@ -549,6 +565,62 @@ async function storeReading(data) {
|
|
|
549
565
|
return { path: base, history: histResult };
|
|
550
566
|
}
|
|
551
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
|
+
|
|
552
624
|
// ─── Historie ─────────────────────────────────────────────────────────────────
|
|
553
625
|
async function updateHistory(base, entry) {
|
|
554
626
|
const stateId = `${base}.readings.history`;
|
|
@@ -1136,6 +1208,9 @@ nav {
|
|
|
1136
1208
|
margin-bottom: 12px; display: flex; align-items: center; gap: 8px;
|
|
1137
1209
|
padding-bottom: 8px; border-bottom: 1px solid var(--border);
|
|
1138
1210
|
}
|
|
1211
|
+
.house-title .collapse-label { flex: 1; cursor: pointer; user-select: none; }
|
|
1212
|
+
.house-body { }
|
|
1213
|
+
.house-block.collapsed .house-body { display: none; }
|
|
1139
1214
|
.apt-block { margin-bottom: 14px; margin-left: 10px; }
|
|
1140
1215
|
.apt-title {
|
|
1141
1216
|
font-size: .88em; font-weight: 600; color: var(--text-dim);
|
|
@@ -1144,6 +1219,22 @@ nav {
|
|
|
1144
1219
|
border-radius: 0 6px 6px 0;
|
|
1145
1220
|
display: flex; align-items: center; justify-content: space-between; gap: 8px;
|
|
1146
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
|
+
}
|
|
1147
1238
|
.print-scope-btn {
|
|
1148
1239
|
background: transparent; border: 1px solid var(--border); color: var(--text-dim);
|
|
1149
1240
|
border-radius: 6px; padding: 2px 8px; cursor: pointer; font-size: .78em;
|
|
@@ -1713,6 +1804,25 @@ input.search {
|
|
|
1713
1804
|
</div>
|
|
1714
1805
|
</div>
|
|
1715
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>
|
|
1716
1826
|
|
|
1717
1827
|
<script>
|
|
1718
1828
|
|
|
@@ -1755,6 +1865,106 @@ async function authFetch(url, opts) {
|
|
|
1755
1865
|
return r;
|
|
1756
1866
|
}
|
|
1757
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
|
+
|
|
1758
1968
|
const TYPE_ICONS = {
|
|
1759
1969
|
Electricity:'\u26A1', Gas:'\uD83D\uDD25', Water:'\uD83D\uDCA7', HotWater:'\uD83C\uDF21',
|
|
1760
1970
|
TotalWater:'\uD83D\uDCA7', ColdWater:'\u2744', Heat:'\uD83C\uDFE0', HeatMeter:'\uD83C\uDFE0',
|
|
@@ -1809,11 +2019,24 @@ const I18N = {
|
|
|
1809
2019
|
sys_tip:'Tipp: Alle drei Befehle nacheinander ausf\u00FChren \u2014 warten bis jeder abgeschlossen ist.',
|
|
1810
2020
|
copy:'Kopieren',
|
|
1811
2021
|
login_title:'Anmelden',
|
|
1812
|
-
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).',
|
|
1813
2023
|
login_user:'Benutzername', login_pass:'Passwort', login_submit:'Anmelden',
|
|
1814
2024
|
login_missing_user:'Benutzername fehlt', login_wrong:'Falsche Zugangsdaten',
|
|
1815
2025
|
login_conn_err:'Verbindungsfehler', login_expired:'Sitzung abgelaufen \u2013 bitte neu anmelden',
|
|
1816
|
-
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'
|
|
1817
2040
|
},
|
|
1818
2041
|
en: {
|
|
1819
2042
|
readings:'Readings', nodes:'Nodes', uptime:'Uptime', live:'Live', disconnected:'Disconnected',
|
|
@@ -1862,11 +2085,24 @@ const I18N = {
|
|
|
1862
2085
|
sys_tip:'Tip: Run all three commands in sequence \u2014 wait for each to finish.',
|
|
1863
2086
|
copy:'Copy',
|
|
1864
2087
|
login_title:'Sign in',
|
|
1865
|
-
login_desc:'Credentials for write actions (assign meter, import).',
|
|
2088
|
+
login_desc:'Credentials for write actions (assign meter, import, delete).',
|
|
1866
2089
|
login_user:'Username', login_pass:'Password', login_submit:'Sign in',
|
|
1867
2090
|
login_missing_user:'Username required', login_wrong:'Invalid credentials',
|
|
1868
2091
|
login_conn_err:'Connection error', login_expired:'Session expired \u2013 please sign in again',
|
|
1869
|
-
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'
|
|
1870
2106
|
}
|
|
1871
2107
|
};
|
|
1872
2108
|
|
|
@@ -2407,12 +2643,29 @@ async function fetchData() {
|
|
|
2407
2643
|
}
|
|
2408
2644
|
let html = '';
|
|
2409
2645
|
let idx = 0;
|
|
2646
|
+
const collapsed = loadCollapsedState();
|
|
2410
2647
|
for (const [house, apts] of Object.entries(d.data)) {
|
|
2411
|
-
|
|
2412
|
-
|
|
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">';
|
|
2413
2656
|
for (const [apt, meters] of Object.entries(apts)) {
|
|
2414
|
-
|
|
2415
|
-
|
|
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">';
|
|
2416
2669
|
for (const [key, m] of Object.entries(meters)) {
|
|
2417
2670
|
const cacheIdx = idx++;
|
|
2418
2671
|
dataCacheList.push({ house, apartment: apt, meter: key, unit: m.unit, typeName: m.typeName, latest: m.latest, latestDate: m.latestDate, history: m.history || [] });
|
|
@@ -2445,7 +2698,10 @@ async function fetchData() {
|
|
|
2445
2698
|
'<div class="meter-card'+(assignedHere ? ' has-node' : '')+'">'+
|
|
2446
2699
|
'<div class="mc-head">'+
|
|
2447
2700
|
'<div class="mc-name">'+icon+' '+esc(key)+'</div>'+
|
|
2448
|
-
'<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>'+
|
|
2449
2705
|
'</div>'+
|
|
2450
2706
|
'<div class="mc-value-row">'+
|
|
2451
2707
|
'<span class="mc-value">'+(m.latest !== undefined ? m.latest : '\u2013')+'</span>'+
|
|
@@ -2463,7 +2719,7 @@ async function fetchData() {
|
|
|
2463
2719
|
}
|
|
2464
2720
|
html += '</div></div>';
|
|
2465
2721
|
}
|
|
2466
|
-
html += '</div>';
|
|
2722
|
+
html += '</div></div>';
|
|
2467
2723
|
}
|
|
2468
2724
|
con.innerHTML = html;
|
|
2469
2725
|
} catch(e) {
|
|
@@ -2871,6 +3127,29 @@ function initTabs() {
|
|
|
2871
3127
|
if (btn && btn.dataset.hist) toggleHist(btn.dataset.hist);
|
|
2872
3128
|
});
|
|
2873
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
|
+
|
|
2874
3153
|
// History value edit
|
|
2875
3154
|
document.addEventListener('click', e => {
|
|
2876
3155
|
const editBtn = e.target.closest('.hist-edit-btn');
|
|
@@ -2928,7 +3207,7 @@ function initTabs() {
|
|
|
2928
3207
|
if (langSel) langSel.addEventListener('change', e => setLang(e.target.value));
|
|
2929
3208
|
|
|
2930
3209
|
document.addEventListener('keydown', e => {
|
|
2931
|
-
if (e.key === 'Escape') closeChartModal();
|
|
3210
|
+
if (e.key === 'Escape') { closeChartModal(); hideDeleteModal(); }
|
|
2932
3211
|
});
|
|
2933
3212
|
|
|
2934
3213
|
// Nodes-Speichern via Event Delegation (kein onclick-Attribut mit Escaping-Problemen)
|
package/package.json
CHANGED