iobroker.poolcontrol 0.5.2 → 0.5.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +3 -0
- package/admin/jsonConfig.json +24 -1
- package/io-package.json +27 -13
- package/lib/helpers/speechHelper.js +18 -2
- package/lib/helpers/statisticsHelper.js +71 -26
- package/lib/helpers/statisticsHelperMonth.js +83 -52
- package/lib/helpers/statisticsHelperWeek.js +84 -52
- package/lib/stateDefinitions/statisticsStates.js +4 -3
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -121,6 +121,9 @@ Funktionen können sich ändern – bitte regelmäßig den Changelog beachten.
|
|
|
121
121
|
## Changelog
|
|
122
122
|
### **WORK IN PROGRESS**
|
|
123
123
|
|
|
124
|
+
## v0.5.3 (2025-10-30)
|
|
125
|
+
- Telegram-Benutzerwahl hinzugefügt
|
|
126
|
+
|
|
124
127
|
## v0.5.2 (2025-10-30)
|
|
125
128
|
- Erweitertes Helper-Vorrangssystem: Konflikte zwischen Zeit- und Solarsteuerung behoben
|
|
126
129
|
- Frostschutz pausiert während Zeitfenster. Nun stabiles Pumpenverhalten und Verbesserte
|
package/admin/jsonConfig.json
CHANGED
|
@@ -849,7 +849,30 @@
|
|
|
849
849
|
"lg": 6,
|
|
850
850
|
"xl": 6
|
|
851
851
|
},
|
|
852
|
-
|
|
852
|
+
"speech_telegram_placeholder": {
|
|
853
|
+
"type": "staticText",
|
|
854
|
+
"label": "",
|
|
855
|
+
"text": "",
|
|
856
|
+
"xs": 12,
|
|
857
|
+
"sm": 3,
|
|
858
|
+
"md": 3,
|
|
859
|
+
"lg": 3,
|
|
860
|
+
"xl": 3,
|
|
861
|
+
"newLine": true
|
|
862
|
+
},
|
|
863
|
+
"speech_telegram_users": {
|
|
864
|
+
"type": "text",
|
|
865
|
+
"label": "Telegram-Empfänger (Benutzernamen, Komma-getrennt / leer = an alle Benutzer)",
|
|
866
|
+
"attr": "speech_telegram_users",
|
|
867
|
+
"tooltip": "Beispiel: Dirk,Dennis (leer = an alle Benutzer senden)",
|
|
868
|
+
"default": "",
|
|
869
|
+
"xs": 12,
|
|
870
|
+
"sm": 6,
|
|
871
|
+
"md": 6,
|
|
872
|
+
"lg": 6,
|
|
873
|
+
"xl": 6
|
|
874
|
+
},
|
|
875
|
+
"divider_speech_email": {
|
|
853
876
|
"type": "divider",
|
|
854
877
|
"newLine": true
|
|
855
878
|
},
|
package/io-package.json
CHANGED
|
@@ -1,8 +1,34 @@
|
|
|
1
1
|
{
|
|
2
2
|
"common": {
|
|
3
3
|
"name": "poolcontrol",
|
|
4
|
-
"version": "0.5.
|
|
4
|
+
"version": "0.5.4",
|
|
5
5
|
"news": {
|
|
6
|
+
"0.5.4": {
|
|
7
|
+
"en": "Fixed a rare infinite loop during weekly and monthly statistics reset that could cause Redis overload. Added timer protection and improved stability.",
|
|
8
|
+
"de": "Selten auftretende Endlosschleife beim Wochen- und Monatsreset der Statistik behoben, die zu Redis-Überlastung führen konnte. Timer-Schutz und Stabilität verbessert.",
|
|
9
|
+
"ru": "Исправлена редкая бесконечная петля при сбросе еженедельной и ежемесячной статистики, вызывавшая перегрузку Redis. Добавлена защита таймера и улучшена стабильность.",
|
|
10
|
+
"pt": "Corrigido um loop infinito raro durante a redefinição das estatísticas semanais e mensais que podia causar sobrecarga do Redis. Adicionada proteção de temporizador e melhorada a estabilidade.",
|
|
11
|
+
"nl": "Zeldzame oneindige lus opgelost tijdens de reset van wekelijkse en maandelijkse statistieken die Redis kon overbelasten. Timerbescherming en stabiliteit verbeterd.",
|
|
12
|
+
"fr": "Correction d'une boucle infinie rare lors de la réinitialisation des statistiques hebdomadaires et mensuelles pouvant provoquer une surcharge de Redis. Protection du minuteur et stabilité améliorées.",
|
|
13
|
+
"it": "Corretto un raro ciclo infinito durante il reset delle statistiche settimanali e mensili che poteva causare un sovraccarico di Redis. Migliorata la protezione del timer e la stabilità.",
|
|
14
|
+
"es": "Se corrigió un bucle infinito poco frecuente durante el reinicio de las estadísticas semanales y mensuales que podía causar sobrecarga de Redis. Protección de temporizador y estabilidad mejoradas.",
|
|
15
|
+
"pl": "Naprawiono rzadką nieskończoną pętlę podczas resetowania statystyk tygodniowych i miesięcznych, która mogła powodować przeciążenie Redis. Dodano ochronę timera i poprawiono stabilność.",
|
|
16
|
+
"uk": "Виправлено рідкісну нескінченну петлю під час скидання тижневої та місячної статистики, що могла спричинити перевантаження Redis. Додано захист таймера та покращено стабільність.",
|
|
17
|
+
"zh-cn": "修复了每周和每月统计重置期间可能导致 Redis 过载的罕见无限循环。改进了计时器保护和系统稳定性。"
|
|
18
|
+
},
|
|
19
|
+
"0.5.3": {
|
|
20
|
+
"en": "Added user selection for Telegram notifications. If no user is selected, messages are sent globally as before; if one or more usernames are specified, only those users receive the messages. Admin UI visually improved (recipient field indented under the instance). speechHelper and jsonConfig.json updated.",
|
|
21
|
+
"de": "Benutzerauswahl für Telegram-Benachrichtigungen hinzugefügt. Wenn kein Benutzer ausgewählt ist, werden Nachrichten wie bisher global gesendet; bei einem oder mehreren Benutzernamen erhalten nur diese die Nachrichten. Admin-UI optisch verbessert (Empfängerfeld unter der Instanz eingerückt). speechHelper und jsonConfig.json aktualisiert.",
|
|
22
|
+
"ru": "Добавлен выбор пользователей для уведомлений Telegram. Если пользователь не выбран, сообщения отправляются глобально, как и раньше; при указании одного или нескольких имен сообщения получают только эти пользователи. Улучшен интерфейс админки (поле получателей расположено под экземпляром). Обновлены speechHelper и jsonConfig.json.",
|
|
23
|
+
"pt": "Adicionada seleção de usuários para notificações do Telegram. Se nenhum usuário for selecionado, as mensagens serão enviadas globalmente como antes; se um ou mais nomes forem informados, apenas esses usuários receberão as mensagens. UI de administração melhorada visualmente (campo de destinatários recuado sob a instância). speechHelper e jsonConfig.json atualizados.",
|
|
24
|
+
"nl": "Gebruikersselectie toegevoegd voor Telegram-meldingen. Als er geen gebruiker is geselecteerd, worden berichten zoals voorheen wereldwijd verzonden; bij één of meer opgegeven gebruikersnamen ontvangen alleen die gebruikers de berichten. Admin-UI visueel verbeterd (ontvanger-veld ingesprongen onder de instantie). speechHelper en jsonConfig.json bijgewerkt.",
|
|
25
|
+
"fr": "Ajout de la sélection d’utilisateurs pour les notifications Telegram. Si aucun utilisateur n’est sélectionné, les messages sont envoyés globalement comme auparavant ; si un ou plusieurs noms sont spécifiés, seuls ces utilisateurs reçoivent les messages. Amélioration visuelle de l’interface d’admin (champ destinataire indenté sous l’instance). speechHelper et jsonConfig.json mis à jour.",
|
|
26
|
+
"it": "Aggiunta la selezione degli utenti per le notifiche Telegram. Se non viene selezionato alcun utente, i messaggi vengono inviati globalmente come prima; indicando uno o più nomi utente, solo questi riceveranno i messaggi. Migliorata l’UI di amministrazione (campo destinatari rientrato sotto l’istanza). Aggiornati speechHelper e jsonConfig.json.",
|
|
27
|
+
"es": "Se añadió la selección de usuarios para notificaciones de Telegram. Si no se selecciona ningún usuario, los mensajes se envían globalmente como antes; si se especifican uno o más nombres de usuario, solo esos usuarios reciben los mensajes. Mejora visual en la interfaz de administración (campo de destinatarios sangrado bajo la instancia). speechHelper y jsonConfig.json actualizados.",
|
|
28
|
+
"pl": "Dodano wybór użytkowników dla powiadomień Telegram. Gdy nie wybrano żadnego użytkownika, wiadomości są wysyłane globalnie jak wcześniej; po podaniu jednego lub kilku nazw użytkowników wiadomości otrzymują tylko oni. Ulepszono UI administracyjne (pole odbiorców pod instancją). Zaktualizowano speechHelper i jsonConfig.json.",
|
|
29
|
+
"uk": "Додано вибір користувачів для сповіщень Telegram. Якщо користувача не вибрано, повідомлення надсилаються глобально, як і раніше; якщо вказано одне чи кілька імен, їх отримують лише ці користувачі. Візуально покращено інтерфейс адміністрування (поле одержувача під екземпляром). Оновлено speechHelper та jsonConfig.json.",
|
|
30
|
+
"zh-cn": "新增 Telegram 通知的用户选择功能。未选择用户时消息将像以前一样全局发送;指定一个或多个用户名时,仅这些用户会收到消息。优化了管理界面显示(收件人字段缩进至实例下方)。已更新 speechHelper 和 jsonConfig.json。"
|
|
31
|
+
},
|
|
6
32
|
"0.5.2": {
|
|
7
33
|
"en": "Extended helper priority system: fixed time/solar conflicts, frost pauses during time windows. Stable pump behavior and improved coordination between helpers.",
|
|
8
34
|
"de": "Erweitertes Helper-Vorrangsystem: Konflikte zwischen Zeit- und Solarsteuerung behoben, Frostschutz pausiert während Zeitfenstern. Stabiles Pumpenverhalten und verbesserte Koordination zwischen den Helpern.",
|
|
@@ -65,18 +91,6 @@
|
|
|
65
91
|
"pl": "Ustabilizowano logikę ochrony przed zamarzaniem: stała histereza +2 °C i zaokrąglone wartości temperatury, aby uniknąć wahań przełączania pompy w okolicach 3 °C.",
|
|
66
92
|
"uk": "Стабілізовано логіку захисту від замерзання: фіксована гістерезис +2 °C і округлені значення температури, щоб уникнути коливань увімкнення насоса біля 3 °C.",
|
|
67
93
|
"zh-cn": "防冻逻辑稳定:固定 +2 °C 滞后并四舍五入温度值,以避免泵在 3 °C 附近频繁切换。"
|
|
68
|
-
},
|
|
69
|
-
"0.3.0": {
|
|
70
|
-
"en": "Added real pump flow calculation, live monitoring, and self-learning normal range system for pump analysis.",
|
|
71
|
-
"de": "Reelle Durchflussberechnung, Liveüberwachung und selbstlernendes Normalbereich-System zur Pumpenanalyse hinzugefügt.",
|
|
72
|
-
"ru": "Добавлен расчет реального расхода насоса, живой мониторинг и самообучающаяся система нормальных диапазонов для анализа насоса.",
|
|
73
|
-
"pt": "Adicionada cálculo de fluxo real da bomba, monitoramento ao vivo e sistema autoaprendente de faixa normal para análise da bomba.",
|
|
74
|
-
"nl": "Reële pompdebietberekening, livebewaking en zelflerend normaalbereiksysteem voor pompanalyse toegevoegd.",
|
|
75
|
-
"fr": "Ajout du calcul du débit réel de la pompe, de la surveillance en direct et d’un système de plage normale auto-apprenant pour l’analyse de la pompe.",
|
|
76
|
-
"it": "Aggiunto calcolo del flusso reale della pompa, monitoraggio live e sistema autoapprendente di intervallo normale per l'analisi della pompa.",
|
|
77
|
-
"es": "Se añadió el cálculo del caudal real de la bomba, la monitorización en vivo y un sistema de rango normal autoaprendente para el análisis de la bomba.",
|
|
78
|
-
"pl": "Dodano obliczanie rzeczywistego przepływu pompy, monitorowanie na żywo i samouczący się system normalnego zakresu do analizy pompy.",
|
|
79
|
-
"zh-cn": "添加了实际泵流量计算、实时监控以及用于泵分析的自学习正常范围系统。"
|
|
80
94
|
}
|
|
81
95
|
},
|
|
82
96
|
"titleLang": {
|
|
@@ -148,8 +148,24 @@ const speechHelper = {
|
|
|
148
148
|
if (this.adapter.config.speech_telegram_enabled && this.adapter.config.speech_telegram_instance) {
|
|
149
149
|
const instance = this.adapter.config.speech_telegram_instance;
|
|
150
150
|
try {
|
|
151
|
-
|
|
152
|
-
this.adapter.
|
|
151
|
+
// NEU: Benutzerliste aus Admin lesen (Komma-getrennte Namen)
|
|
152
|
+
const rawUsers = this.adapter.config.speech_telegram_users || '';
|
|
153
|
+
const users = rawUsers
|
|
154
|
+
.split(',')
|
|
155
|
+
.map(u => u.trim())
|
|
156
|
+
.filter(Boolean);
|
|
157
|
+
|
|
158
|
+
// Wenn keine Benutzer angegeben → Standard: global an alle senden
|
|
159
|
+
if (users.length === 0) {
|
|
160
|
+
await this.adapter.sendToAsync(instance, { text, parse_mode: 'Markdown' });
|
|
161
|
+
this.adapter.log.info(`[speechHelper] Telegram (global): ${text}`);
|
|
162
|
+
} else {
|
|
163
|
+
// Nur an ausgewählte Benutzer senden
|
|
164
|
+
for (const user of users) {
|
|
165
|
+
await this.adapter.sendToAsync(instance, { user, text, parse_mode: 'Markdown' });
|
|
166
|
+
this.adapter.log.info(`[speechHelper] Telegram an ${user}: ${text}`);
|
|
167
|
+
}
|
|
168
|
+
}
|
|
153
169
|
} catch (err) {
|
|
154
170
|
this.adapter.log.warn(
|
|
155
171
|
`[speechHelper] Telegram-Versand fehlgeschlagen (${instance}): ${err.message}`,
|
|
@@ -35,7 +35,6 @@ const statisticsHelper = {
|
|
|
35
35
|
|
|
36
36
|
// --- Überinstallationsschutz ---
|
|
37
37
|
try {
|
|
38
|
-
// Prüft, ob alle States vorhanden sind, und legt fehlende still neu an
|
|
39
38
|
await this._verifyStructure();
|
|
40
39
|
} catch {
|
|
41
40
|
// keine Log-Ausgabe – stiller Schutz
|
|
@@ -44,6 +43,22 @@ const statisticsHelper = {
|
|
|
44
43
|
try {
|
|
45
44
|
await this._createTemperatureStatistics();
|
|
46
45
|
await this._subscribeActiveSensors();
|
|
46
|
+
|
|
47
|
+
// 🟢 NEU: Listener für Reset-Button (Einzelsensor)
|
|
48
|
+
adapter.subscribeStates('analytics.statistics.temperature.today.*.reset_today');
|
|
49
|
+
adapter.on('stateChange', async (id, state) => {
|
|
50
|
+
if (!state || state.ack === true) {
|
|
51
|
+
return;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
if (id.includes('analytics.statistics.temperature.today.') && id.endsWith('.reset_today')) {
|
|
55
|
+
const sensorId = id.split('.').slice(-2, -1)[0];
|
|
56
|
+
adapter.log.info(`[statisticsHelper] Manueller Reset für Sensor "${sensorId}" ausgelöst.`);
|
|
57
|
+
await this._resetSingleSensor(sensorId);
|
|
58
|
+
}
|
|
59
|
+
});
|
|
60
|
+
// 🔵 ENDE NEU
|
|
61
|
+
|
|
47
62
|
await this._scheduleMidnightReset();
|
|
48
63
|
adapter.log.debug('statisticsHelper: Initialisierung abgeschlossen (Sensorüberwachung aktiv).');
|
|
49
64
|
} catch (err) {
|
|
@@ -51,6 +66,55 @@ const statisticsHelper = {
|
|
|
51
66
|
}
|
|
52
67
|
},
|
|
53
68
|
|
|
69
|
+
/**
|
|
70
|
+
* Setzt alle Statistikwerte eines einzelnen Sensors (manueller Reset-Button).
|
|
71
|
+
*
|
|
72
|
+
* @param {string} sensorId - ID des Sensors, z. B. "surface" oder "flow".
|
|
73
|
+
*/
|
|
74
|
+
async _resetSingleSensor(sensorId) {
|
|
75
|
+
const adapter = this.adapter;
|
|
76
|
+
const resetDate = new Date().toISOString().slice(0, 19).replace('T', ' ');
|
|
77
|
+
const basePath = `analytics.statistics.temperature.today.${sensorId}`;
|
|
78
|
+
adapter.log.debug(`[statisticsHelper] Starte Einzelreset für ${sensorId}.`);
|
|
79
|
+
|
|
80
|
+
const stateList = [
|
|
81
|
+
'temp_min',
|
|
82
|
+
'temp_max',
|
|
83
|
+
'temp_min_time',
|
|
84
|
+
'temp_max_time',
|
|
85
|
+
'temp_avg',
|
|
86
|
+
'data_points_count',
|
|
87
|
+
'last_update',
|
|
88
|
+
];
|
|
89
|
+
|
|
90
|
+
for (const state of stateList) {
|
|
91
|
+
let defValue = null;
|
|
92
|
+
if (state.includes('time')) {
|
|
93
|
+
defValue = '';
|
|
94
|
+
}
|
|
95
|
+
if (state === 'data_points_count') {
|
|
96
|
+
defValue = 0;
|
|
97
|
+
}
|
|
98
|
+
if (state === 'last_update') {
|
|
99
|
+
defValue = resetDate;
|
|
100
|
+
}
|
|
101
|
+
await adapter.setStateAsync(`${basePath}.${state}`, { val: defValue, ack: true });
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
await adapter.setStateAsync(`${basePath}.summary_json`, {
|
|
105
|
+
val: JSON.stringify({ date_reset: resetDate, status: 'Tageswerte zurückgesetzt' }),
|
|
106
|
+
ack: true,
|
|
107
|
+
});
|
|
108
|
+
await adapter.setStateAsync(`${basePath}.summary_html`, {
|
|
109
|
+
val: `<div style="color:gray;">Tageswerte zurückgesetzt (${resetDate})</div>`,
|
|
110
|
+
ack: true,
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
await this._updateOverallSummary();
|
|
114
|
+
adapter.log.info(`[statisticsHelper] Einzelreset für Sensor "${sensorId}" abgeschlossen.`);
|
|
115
|
+
},
|
|
116
|
+
// 🔵 ENDE NEU
|
|
117
|
+
|
|
54
118
|
/**
|
|
55
119
|
* Erstellt States, falls sie fehlen (Überinstallationsschutz)
|
|
56
120
|
*/
|
|
@@ -72,7 +136,6 @@ const statisticsHelper = {
|
|
|
72
136
|
const summaryHtmlPath = `${basePath}.summary_html`;
|
|
73
137
|
|
|
74
138
|
if (!isActive) {
|
|
75
|
-
// Leere JSONs vorbereiten, damit spätere Temperaturwerte sie befüllen können
|
|
76
139
|
await adapter.setStateAsync(summaryJsonPath, { val: '[]', ack: true });
|
|
77
140
|
await adapter.setStateAsync(summaryHtmlPath, { val: '', ack: true });
|
|
78
141
|
continue;
|
|
@@ -184,11 +247,10 @@ const statisticsHelper = {
|
|
|
184
247
|
},
|
|
185
248
|
|
|
186
249
|
/**
|
|
187
|
-
*
|
|
188
|
-
* Aktualisiert Min-, Max- und Durchschnittswerte sowie die Zusammenfassungen.
|
|
250
|
+
* Verarbeitet Temperaturänderungen und aktualisiert Statistikdaten.
|
|
189
251
|
*
|
|
190
|
-
* @param {string} sensorId -
|
|
191
|
-
* @param {number} newValue -
|
|
252
|
+
* @param {string} sensorId - ID des betroffenen Sensors.
|
|
253
|
+
* @param {number} newValue - Neuer gemessener Temperaturwert in °C.
|
|
192
254
|
*/
|
|
193
255
|
async _processTemperatureChange(sensorId, newValue) {
|
|
194
256
|
const adapter = this.adapter;
|
|
@@ -199,7 +261,6 @@ const statisticsHelper = {
|
|
|
199
261
|
return;
|
|
200
262
|
}
|
|
201
263
|
|
|
202
|
-
// NEU: Rundung auf 1 Nachkommastelle
|
|
203
264
|
newValue = Math.round(newValue * 10) / 10;
|
|
204
265
|
|
|
205
266
|
const tempMin = (await adapter.getStateAsync(`${basePath}.temp_min`))?.val;
|
|
@@ -220,11 +281,9 @@ const statisticsHelper = {
|
|
|
220
281
|
await adapter.setStateAsync(`${basePath}.temp_max_time`, { val: now, ack: true });
|
|
221
282
|
}
|
|
222
283
|
|
|
223
|
-
// Durchschnitt (gleitend)
|
|
224
284
|
const newCount = count + 1;
|
|
225
285
|
newAvg = tempAvg === null ? newValue : (tempAvg * count + newValue) / newCount;
|
|
226
286
|
|
|
227
|
-
// NEU: Alle Temperaturwerte auf 1 Nachkommastelle runden
|
|
228
287
|
newMin = Math.round(newMin * 10) / 10;
|
|
229
288
|
newMax = Math.round(newMax * 10) / 10;
|
|
230
289
|
newAvg = Math.round(newAvg * 10) / 10;
|
|
@@ -235,7 +294,6 @@ const statisticsHelper = {
|
|
|
235
294
|
await adapter.setStateAsync(`${basePath}.data_points_count`, { val: newCount, ack: true });
|
|
236
295
|
await adapter.setStateAsync(`${basePath}.last_update`, { val: now, ack: true });
|
|
237
296
|
|
|
238
|
-
// Summary aktualisieren – erweitert um Datum, Zeitpunkte, Messanzahl, Name
|
|
239
297
|
const summary = {
|
|
240
298
|
name: 'Tagesstatistik',
|
|
241
299
|
date: new Date().toISOString().slice(0, 10),
|
|
@@ -249,7 +307,6 @@ const statisticsHelper = {
|
|
|
249
307
|
};
|
|
250
308
|
await adapter.setStateAsync(`${basePath}.summary_json`, { val: JSON.stringify(summary), ack: true });
|
|
251
309
|
await adapter.setStateAsync(`${basePath}.summary_html`, {
|
|
252
|
-
// NEU: HTML-Ausgabe mit gerundeten Werten
|
|
253
310
|
val: `<div><b>Min:</b> ${newMin} °C / <b>Max:</b> ${newMax} °C / <b>Ø:</b> ${newAvg} °C</div>`,
|
|
254
311
|
ack: true,
|
|
255
312
|
});
|
|
@@ -258,16 +315,13 @@ const statisticsHelper = {
|
|
|
258
315
|
},
|
|
259
316
|
|
|
260
317
|
/**
|
|
261
|
-
* Gesamt-HTML/JSON-Ausgabe aktualisieren
|
|
262
|
-
* Liest die fertigen summary_json-Werte aller aktiven Sensoren aus
|
|
263
|
-
* und erstellt daraus eine zusammengefasste HTML- und JSON-Ausgabe.
|
|
318
|
+
* Gesamt-HTML/JSON-Ausgabe aktualisieren
|
|
264
319
|
*/
|
|
265
320
|
async _updateOverallSummary() {
|
|
266
321
|
const adapter = this.adapter;
|
|
267
322
|
const allData = [];
|
|
268
323
|
|
|
269
324
|
try {
|
|
270
|
-
// Alle Sensoren durchlaufen
|
|
271
325
|
for (const sensor of this.sensors) {
|
|
272
326
|
const summaryState = await adapter.getStateAsync(
|
|
273
327
|
`analytics.statistics.temperature.today.${sensor.id}.summary_json`,
|
|
@@ -277,7 +331,6 @@ const statisticsHelper = {
|
|
|
277
331
|
continue;
|
|
278
332
|
}
|
|
279
333
|
|
|
280
|
-
// JSON-Inhalt parsen
|
|
281
334
|
let parsed;
|
|
282
335
|
try {
|
|
283
336
|
parsed = JSON.parse(summaryState.val);
|
|
@@ -293,17 +346,14 @@ const statisticsHelper = {
|
|
|
293
346
|
const maxTime = parsed.temp_max_time || '';
|
|
294
347
|
const count = parsed.data_points_count || 0;
|
|
295
348
|
|
|
296
|
-
// Falls gar keine Werte vorhanden, überspringen
|
|
297
349
|
if (min == null && max == null && avg == null) {
|
|
298
350
|
continue;
|
|
299
351
|
}
|
|
300
352
|
|
|
301
|
-
// Werte runden
|
|
302
353
|
const rMin = typeof min === 'number' ? Math.round(min * 10) / 10 : min;
|
|
303
354
|
const rMax = typeof max === 'number' ? Math.round(max * 10) / 10 : max;
|
|
304
355
|
const rAvg = typeof avg === 'number' ? Math.round(avg * 10) / 10 : avg;
|
|
305
356
|
|
|
306
|
-
// Erweiterte Datensammlung
|
|
307
357
|
allData.push({
|
|
308
358
|
name: sensor.name,
|
|
309
359
|
date,
|
|
@@ -316,7 +366,6 @@ const statisticsHelper = {
|
|
|
316
366
|
});
|
|
317
367
|
}
|
|
318
368
|
|
|
319
|
-
// Wenn noch keine Daten vorliegen → leer lassen
|
|
320
369
|
if (allData.length === 0) {
|
|
321
370
|
await adapter.setStateChangedAsync('analytics.statistics.temperature.today.outputs.summary_all_json', {
|
|
322
371
|
val: '[]',
|
|
@@ -329,10 +378,8 @@ const statisticsHelper = {
|
|
|
329
378
|
return;
|
|
330
379
|
}
|
|
331
380
|
|
|
332
|
-
// JSON-Ausgabe erstellen (komplett erweitert)
|
|
333
381
|
const jsonOutput = JSON.stringify(allData, null, 2);
|
|
334
382
|
|
|
335
|
-
// HTML-Ausgabe erstellen (mit Datum & Anzahl Messwerte)
|
|
336
383
|
let html = '<table style="width:100%;border-collapse:collapse;">';
|
|
337
384
|
html +=
|
|
338
385
|
'<tr><th style="text-align:left;">Sensor</th><th>Datum</th><th>Min</th><th>Zeit</th><th>Max</th><th>Zeit</th><th>Ø</th><th>Anz.</th></tr>';
|
|
@@ -350,7 +397,6 @@ const statisticsHelper = {
|
|
|
350
397
|
}
|
|
351
398
|
html += '</table>';
|
|
352
399
|
|
|
353
|
-
// States setzen (auch bei gleichen Werten → Timestamp aktualisieren)
|
|
354
400
|
await adapter.setStateChangedAsync('analytics.statistics.temperature.today.outputs.summary_all_json', {
|
|
355
401
|
val: jsonOutput,
|
|
356
402
|
ack: true,
|
|
@@ -389,7 +435,7 @@ const statisticsHelper = {
|
|
|
389
435
|
},
|
|
390
436
|
|
|
391
437
|
/**
|
|
392
|
-
* Tagesstatistik zurücksetzen
|
|
438
|
+
* Tagesstatistik zurücksetzen (automatischer Reset)
|
|
393
439
|
*/
|
|
394
440
|
async _resetDailyTemperatureStats() {
|
|
395
441
|
const adapter = this.adapter;
|
|
@@ -465,8 +511,7 @@ const statisticsHelper = {
|
|
|
465
511
|
},
|
|
466
512
|
|
|
467
513
|
/**
|
|
468
|
-
*
|
|
469
|
-
* Prüft und legt fehlende States erneut an, ohne bestehende Werte zu überschreiben.
|
|
514
|
+
* Überinstallationsschutz
|
|
470
515
|
*/
|
|
471
516
|
async _verifyStructure() {
|
|
472
517
|
try {
|
|
@@ -18,6 +18,7 @@
|
|
|
18
18
|
const statisticsHelperMonth = {
|
|
19
19
|
adapter: null,
|
|
20
20
|
monthResetTimer: null,
|
|
21
|
+
isResetting: false,
|
|
21
22
|
sensors: [
|
|
22
23
|
{ id: 'outside', name: 'Außentemperatur' },
|
|
23
24
|
{ id: 'ground', name: 'Bodentemperatur' },
|
|
@@ -348,6 +349,21 @@ const statisticsHelperMonth = {
|
|
|
348
349
|
const nextReset = new Date(now.getFullYear(), now.getMonth() + 1, 1, 0, 5, 0, 0);
|
|
349
350
|
const msUntilReset = nextReset.getTime() - now.getTime();
|
|
350
351
|
|
|
352
|
+
// 🟢 NEU: Schutz – falls Resetzeitpunkt in der Vergangenheit liegt
|
|
353
|
+
if (msUntilReset < 60 * 1000) {
|
|
354
|
+
// unter 1 Minute Differenz
|
|
355
|
+
adapter.log.warn(
|
|
356
|
+
'statisticsHelperMonth: Berechneter Resetzeitpunkt liegt in der Vergangenheit – Korrigiere auf nächsten Monat.',
|
|
357
|
+
);
|
|
358
|
+
const corrected = new Date(now.getFullYear(), now.getMonth() + 1, 1, 0, 5, 0, 0);
|
|
359
|
+
const diff = corrected.getTime() - now.getTime();
|
|
360
|
+
this.monthResetTimer = setTimeout(async () => {
|
|
361
|
+
await this._resetMonthlyTemperatureStats();
|
|
362
|
+
await this._scheduleMonthReset();
|
|
363
|
+
}, diff);
|
|
364
|
+
return;
|
|
365
|
+
}
|
|
366
|
+
|
|
351
367
|
this.monthResetTimer = setTimeout(async () => {
|
|
352
368
|
await this._resetMonthlyTemperatureStats();
|
|
353
369
|
await this._scheduleMonthReset();
|
|
@@ -363,75 +379,90 @@ const statisticsHelperMonth = {
|
|
|
363
379
|
*/
|
|
364
380
|
async _resetMonthlyTemperatureStats() {
|
|
365
381
|
const adapter = this.adapter;
|
|
366
|
-
adapter.log.info('statisticsHelperMonth: Monatsstatistik wird zurückgesetzt.');
|
|
367
382
|
|
|
368
|
-
|
|
383
|
+
// 🟢 NEU: Schutz vor Endlosschleifen und Mehrfachausführung
|
|
384
|
+
if (this.isResetting) {
|
|
385
|
+
adapter.log.debug('statisticsHelperMonth: Reset bereits aktiv – übersprungen.');
|
|
386
|
+
return;
|
|
387
|
+
}
|
|
388
|
+
this.isResetting = true;
|
|
369
389
|
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
const activeState = `temperature.${sensor.id}.active`;
|
|
373
|
-
const isActive = (await adapter.getStateAsync(activeState))?.val === true;
|
|
390
|
+
try {
|
|
391
|
+
adapter.log.info('statisticsHelperMonth: Monatsstatistik wird zurückgesetzt.');
|
|
374
392
|
|
|
375
|
-
|
|
376
|
-
const
|
|
393
|
+
// 🟢 NEU: fehlende Zeile wieder einfügen
|
|
394
|
+
const resetDate = new Date().toISOString().slice(0, 19).replace('T', ' ');
|
|
395
|
+
|
|
396
|
+
for (const sensor of this.sensors) {
|
|
397
|
+
const basePath = `analytics.statistics.temperature.month.${sensor.id}`;
|
|
398
|
+
const activeState = `temperature.${sensor.id}.active`;
|
|
399
|
+
const isActive = (await adapter.getStateAsync(activeState))?.val === true;
|
|
400
|
+
|
|
401
|
+
const summaryJsonPath = `${basePath}.summary_json`;
|
|
402
|
+
const summaryHtmlPath = `${basePath}.summary_html`;
|
|
403
|
+
|
|
404
|
+
if (!isActive) {
|
|
405
|
+
await adapter.setStateAsync(summaryJsonPath, {
|
|
406
|
+
val: JSON.stringify({ status: 'kein Sensor aktiv' }),
|
|
407
|
+
ack: true,
|
|
408
|
+
});
|
|
409
|
+
await adapter.setStateAsync(summaryHtmlPath, {
|
|
410
|
+
val: '<div style="color:gray;">kein Sensor aktiv</div>',
|
|
411
|
+
ack: true,
|
|
412
|
+
});
|
|
413
|
+
continue;
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
const stateList = [
|
|
417
|
+
'temp_min',
|
|
418
|
+
'temp_max',
|
|
419
|
+
'temp_min_time',
|
|
420
|
+
'temp_max_time',
|
|
421
|
+
'temp_avg',
|
|
422
|
+
'data_points_count',
|
|
423
|
+
'last_update',
|
|
424
|
+
];
|
|
425
|
+
|
|
426
|
+
for (const state of stateList) {
|
|
427
|
+
const fullPath = `${basePath}.${state}`;
|
|
428
|
+
let defValue = null;
|
|
429
|
+
if (state.includes('time')) {
|
|
430
|
+
defValue = '';
|
|
431
|
+
}
|
|
432
|
+
if (state === 'data_points_count') {
|
|
433
|
+
defValue = 0;
|
|
434
|
+
}
|
|
435
|
+
if (state === 'last_update') {
|
|
436
|
+
defValue = resetDate;
|
|
437
|
+
}
|
|
438
|
+
await adapter.setStateAsync(fullPath, { val: defValue, ack: true });
|
|
439
|
+
}
|
|
377
440
|
|
|
378
|
-
if (!isActive) {
|
|
379
441
|
await adapter.setStateAsync(summaryJsonPath, {
|
|
380
|
-
val: JSON.stringify({ status: '
|
|
442
|
+
val: JSON.stringify({ date_reset: resetDate, status: 'Monatswerte zurückgesetzt' }),
|
|
381
443
|
ack: true,
|
|
382
444
|
});
|
|
383
445
|
await adapter.setStateAsync(summaryHtmlPath, {
|
|
384
|
-
val:
|
|
446
|
+
val: `<div style="color:gray;">Monatswerte zurückgesetzt (${resetDate})</div>`,
|
|
385
447
|
ack: true,
|
|
386
448
|
});
|
|
387
|
-
continue;
|
|
388
449
|
}
|
|
389
450
|
|
|
390
|
-
|
|
391
|
-
'
|
|
392
|
-
'temp_max',
|
|
393
|
-
'temp_min_time',
|
|
394
|
-
'temp_max_time',
|
|
395
|
-
'temp_avg',
|
|
396
|
-
'data_points_count',
|
|
397
|
-
'last_update',
|
|
398
|
-
];
|
|
399
|
-
|
|
400
|
-
for (const state of stateList) {
|
|
401
|
-
const fullPath = `${basePath}.${state}`;
|
|
402
|
-
let defValue = null;
|
|
403
|
-
if (state.includes('time')) {
|
|
404
|
-
defValue = '';
|
|
405
|
-
}
|
|
406
|
-
if (state === 'data_points_count') {
|
|
407
|
-
defValue = 0;
|
|
408
|
-
}
|
|
409
|
-
if (state === 'last_update') {
|
|
410
|
-
defValue = resetDate;
|
|
411
|
-
}
|
|
412
|
-
await adapter.setStateAsync(fullPath, { val: defValue, ack: true });
|
|
413
|
-
}
|
|
414
|
-
|
|
415
|
-
await adapter.setStateAsync(summaryJsonPath, {
|
|
416
|
-
val: JSON.stringify({ date_reset: resetDate, status: 'Monatswerte zurückgesetzt' }),
|
|
451
|
+
await adapter.setStateAsync('analytics.statistics.temperature.month.outputs.summary_all_json', {
|
|
452
|
+
val: '{}',
|
|
417
453
|
ack: true,
|
|
418
454
|
});
|
|
419
|
-
await adapter.setStateAsync(
|
|
420
|
-
val:
|
|
455
|
+
await adapter.setStateAsync('analytics.statistics.temperature.month.outputs.summary_all_html', {
|
|
456
|
+
val: '',
|
|
421
457
|
ack: true,
|
|
422
458
|
});
|
|
423
|
-
}
|
|
424
459
|
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
}
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
ack: true,
|
|
432
|
-
});
|
|
433
|
-
|
|
434
|
-
adapter.log.debug('statisticsHelperMonth: Monatsstatistik zurückgesetzt.');
|
|
460
|
+
adapter.log.debug('statisticsHelperMonth: Monatsstatistik zurückgesetzt.');
|
|
461
|
+
} catch (err) {
|
|
462
|
+
adapter.log.warn(`statisticsHelperMonth: Fehler beim Monatsreset: ${err.message}`);
|
|
463
|
+
} finally {
|
|
464
|
+
this.isResetting = false; // 🟢 NEU: Flag wieder freigeben
|
|
465
|
+
}
|
|
435
466
|
},
|
|
436
467
|
|
|
437
468
|
/**
|
|
@@ -18,6 +18,7 @@
|
|
|
18
18
|
const statisticsHelperWeek = {
|
|
19
19
|
adapter: null,
|
|
20
20
|
weekResetTimer: null,
|
|
21
|
+
isResetting: false, // 🟢 NEU
|
|
21
22
|
sensors: [
|
|
22
23
|
{ id: 'outside', name: 'Außentemperatur' },
|
|
23
24
|
{ id: 'ground', name: 'Bodentemperatur' },
|
|
@@ -368,6 +369,24 @@ const statisticsHelperWeek = {
|
|
|
368
369
|
nextReset.setHours(0, 5, 0, 0);
|
|
369
370
|
|
|
370
371
|
const msUntilReset = nextReset.getTime() - now.getTime();
|
|
372
|
+
|
|
373
|
+
// 🟢 NEU: Schutz – falls Resetzeitpunkt in der Vergangenheit liegt
|
|
374
|
+
if (msUntilReset < 60 * 1000) {
|
|
375
|
+
// unter 1 Minute Differenz
|
|
376
|
+
adapter.log.warn(
|
|
377
|
+
'statisticsHelperWeek: Berechneter Resetzeitpunkt liegt in der Vergangenheit – Korrigiere auf nächste Woche.',
|
|
378
|
+
);
|
|
379
|
+
const corrected = new Date(now);
|
|
380
|
+
corrected.setDate(now.getDate() + 7);
|
|
381
|
+
corrected.setHours(0, 5, 0, 0);
|
|
382
|
+
const diff = corrected.getTime() - now.getTime();
|
|
383
|
+
this.weekResetTimer = setTimeout(async () => {
|
|
384
|
+
await this._resetWeeklyTemperatureStats();
|
|
385
|
+
await this._scheduleWeekReset();
|
|
386
|
+
}, diff);
|
|
387
|
+
return;
|
|
388
|
+
}
|
|
389
|
+
|
|
371
390
|
this.weekResetTimer = setTimeout(async () => {
|
|
372
391
|
await this._resetWeeklyTemperatureStats();
|
|
373
392
|
await this._scheduleWeekReset();
|
|
@@ -381,75 +400,88 @@ const statisticsHelperWeek = {
|
|
|
381
400
|
*/
|
|
382
401
|
async _resetWeeklyTemperatureStats() {
|
|
383
402
|
const adapter = this.adapter;
|
|
384
|
-
adapter.log.info('statisticsHelperWeek: Wochenstatistik wird zurückgesetzt.');
|
|
385
403
|
|
|
386
|
-
|
|
404
|
+
// 🟢 NEU: Schutz vor Endlosschleifen und Mehrfachausführung
|
|
405
|
+
if (this.isResetting) {
|
|
406
|
+
adapter.log.debug('statisticsHelperWeek: Reset bereits aktiv – übersprungen.');
|
|
407
|
+
return;
|
|
408
|
+
}
|
|
409
|
+
this.isResetting = true;
|
|
387
410
|
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
const
|
|
391
|
-
const isActive = (await adapter.getStateAsync(activeState))?.val === true;
|
|
411
|
+
try {
|
|
412
|
+
adapter.log.info('statisticsHelperWeek: Wochenstatistik wird zurückgesetzt.');
|
|
413
|
+
const resetDate = new Date().toISOString().slice(0, 19).replace('T', ' ');
|
|
392
414
|
|
|
393
|
-
const
|
|
394
|
-
|
|
415
|
+
for (const sensor of this.sensors) {
|
|
416
|
+
const basePath = `analytics.statistics.temperature.week.${sensor.id}`;
|
|
417
|
+
const activeState = `temperature.${sensor.id}.active`;
|
|
418
|
+
const isActive = (await adapter.getStateAsync(activeState))?.val === true;
|
|
419
|
+
|
|
420
|
+
const summaryJsonPath = `${basePath}.summary_json`;
|
|
421
|
+
const summaryHtmlPath = `${basePath}.summary_html`;
|
|
422
|
+
|
|
423
|
+
if (!isActive) {
|
|
424
|
+
await adapter.setStateAsync(summaryJsonPath, {
|
|
425
|
+
val: JSON.stringify({ status: 'kein Sensor aktiv' }),
|
|
426
|
+
ack: true,
|
|
427
|
+
});
|
|
428
|
+
await adapter.setStateAsync(summaryHtmlPath, {
|
|
429
|
+
val: '<div style="color:gray;">kein Sensor aktiv</div>',
|
|
430
|
+
ack: true,
|
|
431
|
+
});
|
|
432
|
+
continue;
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
const stateList = [
|
|
436
|
+
'temp_min',
|
|
437
|
+
'temp_max',
|
|
438
|
+
'temp_min_time',
|
|
439
|
+
'temp_max_time',
|
|
440
|
+
'temp_avg',
|
|
441
|
+
'data_points_count',
|
|
442
|
+
'last_update',
|
|
443
|
+
];
|
|
444
|
+
|
|
445
|
+
for (const state of stateList) {
|
|
446
|
+
const fullPath = `${basePath}.${state}`;
|
|
447
|
+
let defValue = null;
|
|
448
|
+
if (state.includes('time')) {
|
|
449
|
+
defValue = '';
|
|
450
|
+
}
|
|
451
|
+
if (state === 'data_points_count') {
|
|
452
|
+
defValue = 0;
|
|
453
|
+
}
|
|
454
|
+
if (state === 'last_update') {
|
|
455
|
+
defValue = resetDate;
|
|
456
|
+
}
|
|
457
|
+
await adapter.setStateAsync(fullPath, { val: defValue, ack: true });
|
|
458
|
+
}
|
|
395
459
|
|
|
396
|
-
if (!isActive) {
|
|
397
460
|
await adapter.setStateAsync(summaryJsonPath, {
|
|
398
|
-
val: JSON.stringify({ status: '
|
|
461
|
+
val: JSON.stringify({ date_reset: resetDate, status: 'Wochenwerte zurückgesetzt' }),
|
|
399
462
|
ack: true,
|
|
400
463
|
});
|
|
401
464
|
await adapter.setStateAsync(summaryHtmlPath, {
|
|
402
|
-
val:
|
|
465
|
+
val: `<div style="color:gray;">Wochenwerte zurückgesetzt (${resetDate})</div>`,
|
|
403
466
|
ack: true,
|
|
404
467
|
});
|
|
405
|
-
continue;
|
|
406
468
|
}
|
|
407
469
|
|
|
408
|
-
|
|
409
|
-
'
|
|
410
|
-
'temp_max',
|
|
411
|
-
'temp_min_time',
|
|
412
|
-
'temp_max_time',
|
|
413
|
-
'temp_avg',
|
|
414
|
-
'data_points_count',
|
|
415
|
-
'last_update',
|
|
416
|
-
];
|
|
417
|
-
|
|
418
|
-
for (const state of stateList) {
|
|
419
|
-
const fullPath = `${basePath}.${state}`;
|
|
420
|
-
let defValue = null;
|
|
421
|
-
if (state.includes('time')) {
|
|
422
|
-
defValue = '';
|
|
423
|
-
}
|
|
424
|
-
if (state === 'data_points_count') {
|
|
425
|
-
defValue = 0;
|
|
426
|
-
}
|
|
427
|
-
if (state === 'last_update') {
|
|
428
|
-
defValue = resetDate;
|
|
429
|
-
}
|
|
430
|
-
await adapter.setStateAsync(fullPath, { val: defValue, ack: true });
|
|
431
|
-
}
|
|
432
|
-
|
|
433
|
-
await adapter.setStateAsync(summaryJsonPath, {
|
|
434
|
-
val: JSON.stringify({ date_reset: resetDate, status: 'Wochenwerte zurückgesetzt' }),
|
|
470
|
+
await adapter.setStateAsync('analytics.statistics.temperature.week.outputs.summary_all_json', {
|
|
471
|
+
val: '{}',
|
|
435
472
|
ack: true,
|
|
436
473
|
});
|
|
437
|
-
await adapter.setStateAsync(
|
|
438
|
-
val:
|
|
474
|
+
await adapter.setStateAsync('analytics.statistics.temperature.week.outputs.summary_all_html', {
|
|
475
|
+
val: '',
|
|
439
476
|
ack: true,
|
|
440
477
|
});
|
|
441
|
-
}
|
|
442
478
|
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
}
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
ack: true,
|
|
450
|
-
});
|
|
451
|
-
|
|
452
|
-
adapter.log.debug('statisticsHelperWeek: Wochenstatistik zurückgesetzt.');
|
|
479
|
+
adapter.log.debug('statisticsHelperWeek: Wochenstatistik zurückgesetzt.');
|
|
480
|
+
} catch (err) {
|
|
481
|
+
adapter.log.warn(`statisticsHelperWeek: Fehler beim Wochenreset: ${err.message}`);
|
|
482
|
+
} finally {
|
|
483
|
+
this.isResetting = false; // 🟢 NEU: Flag wieder freigeben
|
|
484
|
+
}
|
|
453
485
|
},
|
|
454
486
|
|
|
455
487
|
/**
|
|
@@ -100,6 +100,7 @@ async function _createTemperatureStatsGroup(adapter, periodId, displayName) {
|
|
|
100
100
|
{ id: 'temp_avg', name: 'Durchschnittstemperatur', type: 'number', role: 'value.temperature', unit: '°C' },
|
|
101
101
|
{ id: 'data_points_count', name: 'Anzahl Messwerte', type: 'number', role: 'value' },
|
|
102
102
|
{ id: 'last_update', name: 'Letzte Aktualisierung', type: 'string', role: 'value.time' },
|
|
103
|
+
{ id: 'reset_today', name: 'Tagesstatistik zurücksetzen', type: 'boolean', role: 'button' },
|
|
103
104
|
{
|
|
104
105
|
id: 'summary_json',
|
|
105
106
|
name: `${displayName} (JSON)`,
|
|
@@ -122,9 +123,9 @@ async function _createTemperatureStatsGroup(adapter, periodId, displayName) {
|
|
|
122
123
|
type: def.type,
|
|
123
124
|
role: def.role,
|
|
124
125
|
unit: def.unit || undefined,
|
|
125
|
-
read: true,
|
|
126
|
-
write: false,
|
|
127
|
-
def: def.type === 'number' ? null : '',
|
|
126
|
+
read: def.type === 'boolean' && def.role === 'button' ? false : true,
|
|
127
|
+
write: def.type === 'boolean' && def.role === 'button' ? true : false,
|
|
128
|
+
def: def.type === 'number' ? null : def.type === 'boolean' ? false : '',
|
|
128
129
|
persist: true,
|
|
129
130
|
},
|
|
130
131
|
native: {},
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "iobroker.poolcontrol",
|
|
3
|
-
"version": "0.5.
|
|
3
|
+
"version": "0.5.4",
|
|
4
4
|
"description": "Steuerung & Automatisierung für den Pool (Pumpe, Heizung, Ventile, Sensoren).",
|
|
5
5
|
"author": "DasBo1975 <dasbo1975@outlook.de>",
|
|
6
6
|
"homepage": "https://github.com/DasBo1975/ioBroker.poolcontrol",
|