iobroker.poolcontrol 0.4.0 → 0.5.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +8 -0
- package/io-package.json +26 -26
- package/lib/helpers/pumpHelper.js +7 -0
- package/lib/helpers/statisticsHelper.js +134 -40
- package/lib/helpers/statisticsHelperMonth.js +466 -0
- package/lib/helpers/statisticsHelperWeek.js +501 -0
- package/lib/helpers/timeHelper.js +13 -6
- package/lib/stateDefinitions/statisticsStates.js +52 -15
- package/main.js +4 -0
- package/package.json +5 -3
|
@@ -0,0 +1,501 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* statisticsHelperWeek.js
|
|
5
|
+
* -----------------------
|
|
6
|
+
* Vollständige Steuerung der Wochenstatistik (Temperatur)
|
|
7
|
+
* im Bereich analytics.statistics.temperature.week.*
|
|
8
|
+
*
|
|
9
|
+
* - Erkennt aktive Sensoren anhand temperature.<sensor>.active
|
|
10
|
+
* - Reagiert eventbasiert auf Änderungen der Temperaturwerte
|
|
11
|
+
* - Berechnet laufend Min/Max/Durchschnitt über 7 Tage
|
|
12
|
+
* - Aktualisiert JSON- und HTML-Ausgaben (pro Sensor & gesamt)
|
|
13
|
+
* - Führt automatischen Wochen-Reset (Sonntag 00:05 Uhr) durch
|
|
14
|
+
*
|
|
15
|
+
* @param {ioBroker.Adapter} adapter - Aktive ioBroker-Adapterinstanz
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
const statisticsHelperWeek = {
|
|
19
|
+
adapter: null,
|
|
20
|
+
weekResetTimer: null,
|
|
21
|
+
sensors: [
|
|
22
|
+
{ id: 'outside', name: 'Außentemperatur' },
|
|
23
|
+
{ id: 'ground', name: 'Bodentemperatur' },
|
|
24
|
+
{ id: 'surface', name: 'Pooloberfläche' },
|
|
25
|
+
{ id: 'flow', name: 'Vorlauf' },
|
|
26
|
+
{ id: 'return', name: 'Rücklauf' },
|
|
27
|
+
{ id: 'collector', name: 'Kollektor (Solar)' },
|
|
28
|
+
],
|
|
29
|
+
|
|
30
|
+
async init(adapter) {
|
|
31
|
+
this.adapter = adapter;
|
|
32
|
+
adapter.log.debug('statisticsHelperWeek: Initialisierung gestartet.');
|
|
33
|
+
|
|
34
|
+
// --- Überinstallationsschutz ---
|
|
35
|
+
try {
|
|
36
|
+
// Prüft, ob alle States vorhanden sind, und legt fehlende still neu an
|
|
37
|
+
await this._verifyStructure();
|
|
38
|
+
} catch {
|
|
39
|
+
// keine Log-Ausgabe – stiller Schutz
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
try {
|
|
43
|
+
await this._createTemperatureStatistics();
|
|
44
|
+
await this._subscribeActiveSensors();
|
|
45
|
+
await this._scheduleWeekReset();
|
|
46
|
+
adapter.log.debug('statisticsHelperWeek: Initialisierung abgeschlossen (Sensorüberwachung aktiv).');
|
|
47
|
+
} catch (err) {
|
|
48
|
+
adapter.log.warn(`statisticsHelperWeek: Fehler bei Initialisierung: ${err.message}`);
|
|
49
|
+
}
|
|
50
|
+
},
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Erstellt States, falls sie fehlen (Überinstallationsschutz)
|
|
54
|
+
*/
|
|
55
|
+
async _createTemperatureStatistics() {
|
|
56
|
+
const adapter = this.adapter;
|
|
57
|
+
|
|
58
|
+
for (const sensor of this.sensors) {
|
|
59
|
+
const basePath = `analytics.statistics.temperature.week.${sensor.id}`;
|
|
60
|
+
await adapter.setObjectNotExistsAsync(basePath, {
|
|
61
|
+
type: 'channel',
|
|
62
|
+
common: { name: `${sensor.name} (Wochenstatistik)` },
|
|
63
|
+
native: {},
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
const activeState = `temperature.${sensor.id}.active`;
|
|
67
|
+
const isActive = (await adapter.getStateAsync(activeState))?.val === true;
|
|
68
|
+
|
|
69
|
+
const summaryJsonPath = `${basePath}.summary_json`;
|
|
70
|
+
const summaryHtmlPath = `${basePath}.summary_html`;
|
|
71
|
+
|
|
72
|
+
if (!isActive) {
|
|
73
|
+
await adapter.setStateAsync(summaryJsonPath, { val: '[]', ack: true });
|
|
74
|
+
await adapter.setStateAsync(summaryHtmlPath, { val: '', ack: true });
|
|
75
|
+
continue;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
const stateDefs = [
|
|
79
|
+
{ id: 'temp_min', def: null },
|
|
80
|
+
{ id: 'temp_max', def: null },
|
|
81
|
+
{ id: 'temp_min_time', def: '' },
|
|
82
|
+
{ id: 'temp_max_time', def: '' },
|
|
83
|
+
{ id: 'temp_avg', def: null },
|
|
84
|
+
{ id: 'data_points_count', def: 0 },
|
|
85
|
+
{ id: 'last_update', def: '' },
|
|
86
|
+
{ id: 'summary_json', def: '' },
|
|
87
|
+
{ id: 'summary_html', def: '' },
|
|
88
|
+
];
|
|
89
|
+
|
|
90
|
+
for (const def of stateDefs) {
|
|
91
|
+
const fullPath = `${basePath}.${def.id}`;
|
|
92
|
+
const obj = await adapter.getObjectAsync(fullPath);
|
|
93
|
+
if (!obj) {
|
|
94
|
+
await adapter.setObjectNotExistsAsync(fullPath, {
|
|
95
|
+
type: 'state',
|
|
96
|
+
common: {
|
|
97
|
+
name: def.id,
|
|
98
|
+
type: typeof def.def === 'number' ? 'number' : 'string',
|
|
99
|
+
role: def.id.includes('time')
|
|
100
|
+
? 'value.time'
|
|
101
|
+
: def.id.includes('temp')
|
|
102
|
+
? 'value.temperature'
|
|
103
|
+
: 'value',
|
|
104
|
+
read: true,
|
|
105
|
+
write: false,
|
|
106
|
+
persist: true,
|
|
107
|
+
},
|
|
108
|
+
native: {},
|
|
109
|
+
});
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
const state = await adapter.getStateAsync(fullPath);
|
|
113
|
+
if (!state || state.val === null || state.val === undefined) {
|
|
114
|
+
await adapter.setStateAsync(fullPath, { val: def.def, ack: true });
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
const outputBase = 'analytics.statistics.temperature.week.outputs';
|
|
120
|
+
await adapter.setObjectNotExistsAsync(outputBase, {
|
|
121
|
+
type: 'channel',
|
|
122
|
+
common: { name: 'Gesamtausgaben (alle Sensoren – Woche)' },
|
|
123
|
+
native: {},
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
const outputStates = [
|
|
127
|
+
{ id: 'summary_all_json', def: '' },
|
|
128
|
+
{ id: 'summary_all_html', def: '' },
|
|
129
|
+
];
|
|
130
|
+
|
|
131
|
+
for (const out of outputStates) {
|
|
132
|
+
const fullPath = `${outputBase}.${out.id}`;
|
|
133
|
+
const obj = await adapter.getObjectAsync(fullPath);
|
|
134
|
+
if (!obj) {
|
|
135
|
+
await adapter.setObjectNotExistsAsync(fullPath, {
|
|
136
|
+
type: 'state',
|
|
137
|
+
common: {
|
|
138
|
+
name: out.id,
|
|
139
|
+
type: 'string',
|
|
140
|
+
role: out.id.endsWith('json') ? 'json' : 'html',
|
|
141
|
+
read: true,
|
|
142
|
+
write: false,
|
|
143
|
+
persist: true,
|
|
144
|
+
},
|
|
145
|
+
native: {},
|
|
146
|
+
});
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
const state = await adapter.getStateAsync(fullPath);
|
|
150
|
+
if (!state || state.val === null || state.val === undefined) {
|
|
151
|
+
await adapter.setStateAsync(fullPath, { val: out.id.endsWith('json') ? '{}' : '', ack: true });
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
},
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* Abonniert alle aktiven Temperatursensoren
|
|
158
|
+
*/
|
|
159
|
+
async _subscribeActiveSensors() {
|
|
160
|
+
const adapter = this.adapter;
|
|
161
|
+
for (const sensor of this.sensors) {
|
|
162
|
+
const activeState = `temperature.${sensor.id}.active`;
|
|
163
|
+
const isActive = (await adapter.getStateAsync(activeState))?.val === true;
|
|
164
|
+
if (isActive) {
|
|
165
|
+
const stateId = `temperature.${sensor.id}.current`;
|
|
166
|
+
adapter.subscribeStates(stateId);
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
adapter.on('stateChange', async (id, state) => {
|
|
171
|
+
if (!state || state.ack === false) {
|
|
172
|
+
return;
|
|
173
|
+
}
|
|
174
|
+
for (const sensor of this.sensors) {
|
|
175
|
+
if (id.endsWith(`temperature.${sensor.id}.current`)) {
|
|
176
|
+
await this._processTemperatureChange(sensor.id, state.val);
|
|
177
|
+
break;
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
});
|
|
181
|
+
},
|
|
182
|
+
|
|
183
|
+
/**
|
|
184
|
+
* Verarbeitung einer Temperaturänderung für einen Sensor.
|
|
185
|
+
* Aktualisiert Min-, Max- und Durchschnittswerte sowie die Zusammenfassungen.
|
|
186
|
+
*
|
|
187
|
+
* @param {string} sensorId - Die ID des Sensors (z. B. "outside", "flow", "collector" usw.)
|
|
188
|
+
* @param {number} newValue - Der neue gemessene Temperaturwert in °C
|
|
189
|
+
*/
|
|
190
|
+
async _processTemperatureChange(sensorId, newValue) {
|
|
191
|
+
const adapter = this.adapter;
|
|
192
|
+
const basePath = `analytics.statistics.temperature.week.${sensorId}`;
|
|
193
|
+
const now = new Date().toLocaleString('de-DE', {
|
|
194
|
+
weekday: 'short',
|
|
195
|
+
hour: '2-digit',
|
|
196
|
+
minute: '2-digit',
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
if (typeof newValue !== 'number') {
|
|
200
|
+
return;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
newValue = Math.round(newValue * 10) / 10;
|
|
204
|
+
|
|
205
|
+
const tempMin = (await adapter.getStateAsync(`${basePath}.temp_min`))?.val;
|
|
206
|
+
const tempMax = (await adapter.getStateAsync(`${basePath}.temp_max`))?.val;
|
|
207
|
+
const tempAvg = (await adapter.getStateAsync(`${basePath}.temp_avg`))?.val;
|
|
208
|
+
const count = (await adapter.getStateAsync(`${basePath}.data_points_count`))?.val || 0;
|
|
209
|
+
|
|
210
|
+
let newMin = tempMin;
|
|
211
|
+
let newMax = tempMax;
|
|
212
|
+
let newAvg = tempAvg;
|
|
213
|
+
|
|
214
|
+
if (tempMin === null || newValue < tempMin) {
|
|
215
|
+
newMin = newValue;
|
|
216
|
+
await adapter.setStateAsync(`${basePath}.temp_min_time`, { val: now, ack: true });
|
|
217
|
+
}
|
|
218
|
+
if (tempMax === null || newValue > tempMax) {
|
|
219
|
+
newMax = newValue;
|
|
220
|
+
await adapter.setStateAsync(`${basePath}.temp_max_time`, { val: now, ack: true });
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
const newCount = count + 1;
|
|
224
|
+
newAvg = tempAvg === null ? newValue : (tempAvg * count + newValue) / newCount;
|
|
225
|
+
|
|
226
|
+
newMin = Math.round(newMin * 10) / 10;
|
|
227
|
+
newMax = Math.round(newMax * 10) / 10;
|
|
228
|
+
newAvg = Math.round(newAvg * 10) / 10;
|
|
229
|
+
|
|
230
|
+
await adapter.setStateAsync(`${basePath}.temp_min`, { val: newMin, ack: true });
|
|
231
|
+
await adapter.setStateAsync(`${basePath}.temp_max`, { val: newMax, ack: true });
|
|
232
|
+
await adapter.setStateAsync(`${basePath}.temp_avg`, { val: Math.round(newAvg * 100) / 100, ack: true });
|
|
233
|
+
await adapter.setStateAsync(`${basePath}.data_points_count`, { val: newCount, ack: true });
|
|
234
|
+
await adapter.setStateAsync(`${basePath}.last_update`, { val: now, ack: true });
|
|
235
|
+
|
|
236
|
+
// Summary aktualisieren – erweitert um Datum, Zeitpunkte, Messanzahl, Name
|
|
237
|
+
const summary = {
|
|
238
|
+
name: 'Wochenstatistik',
|
|
239
|
+
week_range: this._getCurrentWeekRange(),
|
|
240
|
+
date: new Date().toISOString().slice(0, 10),
|
|
241
|
+
temp_min: newMin,
|
|
242
|
+
temp_min_time: (await adapter.getStateAsync(`${basePath}.temp_min_time`))?.val || '',
|
|
243
|
+
temp_max: newMax,
|
|
244
|
+
temp_max_time: (await adapter.getStateAsync(`${basePath}.temp_max_time`))?.val || '',
|
|
245
|
+
temp_avg: newAvg,
|
|
246
|
+
data_points_count: newCount,
|
|
247
|
+
updated: now,
|
|
248
|
+
};
|
|
249
|
+
await adapter.setStateAsync(`${basePath}.summary_json`, { val: JSON.stringify(summary), ack: true });
|
|
250
|
+
await adapter.setStateAsync(`${basePath}.summary_html`, {
|
|
251
|
+
val: `<div><b>Min:</b> ${newMin} °C / <b>Max:</b> ${newMax} °C / <b>Ø:</b> ${newAvg} °C</div>`,
|
|
252
|
+
ack: true,
|
|
253
|
+
});
|
|
254
|
+
|
|
255
|
+
await this._updateOverallSummary();
|
|
256
|
+
},
|
|
257
|
+
|
|
258
|
+
/**
|
|
259
|
+
* Gesamt-HTML/JSON-Ausgabe aktualisieren (erweiterte Version – Woche)
|
|
260
|
+
*/
|
|
261
|
+
async _updateOverallSummary() {
|
|
262
|
+
const adapter = this.adapter;
|
|
263
|
+
const allData = [];
|
|
264
|
+
|
|
265
|
+
try {
|
|
266
|
+
for (const sensor of this.sensors) {
|
|
267
|
+
const summaryState = await adapter.getStateAsync(
|
|
268
|
+
`analytics.statistics.temperature.week.${sensor.id}.summary_json`,
|
|
269
|
+
);
|
|
270
|
+
if (!summaryState || !summaryState.val) {
|
|
271
|
+
continue;
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
let parsed;
|
|
275
|
+
try {
|
|
276
|
+
parsed = JSON.parse(summaryState.val);
|
|
277
|
+
} catch {
|
|
278
|
+
continue;
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
const min = parsed.temp_min;
|
|
282
|
+
const max = parsed.temp_max;
|
|
283
|
+
const avg = parsed.temp_avg;
|
|
284
|
+
const date = parsed.date || '';
|
|
285
|
+
const minTime = parsed.temp_min_time || '';
|
|
286
|
+
const maxTime = parsed.temp_max_time || '';
|
|
287
|
+
const count = parsed.data_points_count || 0;
|
|
288
|
+
|
|
289
|
+
if (min == null && max == null && avg == null) {
|
|
290
|
+
continue;
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
const rMin = typeof min === 'number' ? Math.round(min * 10) / 10 : min;
|
|
294
|
+
const rMax = typeof max === 'number' ? Math.round(max * 10) / 10 : max;
|
|
295
|
+
const rAvg = typeof avg === 'number' ? Math.round(avg * 10) / 10 : avg;
|
|
296
|
+
|
|
297
|
+
allData.push({
|
|
298
|
+
week_range: this._getCurrentWeekRange(),
|
|
299
|
+
name: sensor.name,
|
|
300
|
+
date,
|
|
301
|
+
min: rMin,
|
|
302
|
+
min_time: minTime,
|
|
303
|
+
max: rMax,
|
|
304
|
+
max_time: maxTime,
|
|
305
|
+
avg: rAvg,
|
|
306
|
+
data_points_count: count,
|
|
307
|
+
});
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
if (allData.length === 0) {
|
|
311
|
+
await adapter.setStateChangedAsync('analytics.statistics.temperature.week.outputs.summary_all_json', {
|
|
312
|
+
val: '[]',
|
|
313
|
+
ack: true,
|
|
314
|
+
});
|
|
315
|
+
await adapter.setStateChangedAsync('analytics.statistics.temperature.week.outputs.summary_all_html', {
|
|
316
|
+
val: '',
|
|
317
|
+
ack: true,
|
|
318
|
+
});
|
|
319
|
+
return;
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
const jsonOutput = JSON.stringify(allData, null, 2);
|
|
323
|
+
|
|
324
|
+
let html = '<table style="width:100%;border-collapse:collapse;">';
|
|
325
|
+
html +=
|
|
326
|
+
'<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>';
|
|
327
|
+
for (const entry of allData) {
|
|
328
|
+
html += `<tr>
|
|
329
|
+
<td>${entry.name}</td>
|
|
330
|
+
<td>${entry.date || '-'}</td>
|
|
331
|
+
<td>${entry.min ?? '-'}</td>
|
|
332
|
+
<td>${entry.min_time || '-'}</td>
|
|
333
|
+
<td>${entry.max ?? '-'}</td>
|
|
334
|
+
<td>${entry.max_time || '-'}</td>
|
|
335
|
+
<td>${entry.avg ?? '-'}</td>
|
|
336
|
+
<td>${entry.data_points_count ?? '-'}</td>
|
|
337
|
+
</tr>`;
|
|
338
|
+
}
|
|
339
|
+
html += '</table>';
|
|
340
|
+
|
|
341
|
+
await adapter.setStateChangedAsync('analytics.statistics.temperature.week.outputs.summary_all_json', {
|
|
342
|
+
val: jsonOutput,
|
|
343
|
+
ack: true,
|
|
344
|
+
});
|
|
345
|
+
await adapter.setStateChangedAsync('analytics.statistics.temperature.week.outputs.summary_all_html', {
|
|
346
|
+
val: html,
|
|
347
|
+
ack: true,
|
|
348
|
+
});
|
|
349
|
+
} catch {
|
|
350
|
+
// bewusst kein Log hier – stiller Schutz
|
|
351
|
+
}
|
|
352
|
+
},
|
|
353
|
+
|
|
354
|
+
/**
|
|
355
|
+
* Wochen-Reset planen (Sonntag 00:05 Uhr)
|
|
356
|
+
*/
|
|
357
|
+
async _scheduleWeekReset() {
|
|
358
|
+
const adapter = this.adapter;
|
|
359
|
+
if (this.weekResetTimer) {
|
|
360
|
+
clearTimeout(this.weekResetTimer);
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
const now = new Date();
|
|
364
|
+
const nextReset = new Date(now);
|
|
365
|
+
// Sonntag 00:05
|
|
366
|
+
const daysUntilSunday = (7 - now.getDay()) % 7;
|
|
367
|
+
nextReset.setDate(now.getDate() + daysUntilSunday);
|
|
368
|
+
nextReset.setHours(0, 5, 0, 0);
|
|
369
|
+
|
|
370
|
+
const msUntilReset = nextReset.getTime() - now.getTime();
|
|
371
|
+
this.weekResetTimer = setTimeout(async () => {
|
|
372
|
+
await this._resetWeeklyTemperatureStats();
|
|
373
|
+
await this._scheduleWeekReset();
|
|
374
|
+
}, msUntilReset);
|
|
375
|
+
|
|
376
|
+
adapter.log.debug(`statisticsHelperWeek: Wochen-Reset geplant in ${Math.round(msUntilReset / 60000)} Minuten.`);
|
|
377
|
+
},
|
|
378
|
+
|
|
379
|
+
/**
|
|
380
|
+
* Wochenstatistik zurücksetzen
|
|
381
|
+
*/
|
|
382
|
+
async _resetWeeklyTemperatureStats() {
|
|
383
|
+
const adapter = this.adapter;
|
|
384
|
+
adapter.log.info('statisticsHelperWeek: Wochenstatistik wird zurückgesetzt.');
|
|
385
|
+
|
|
386
|
+
const resetDate = new Date().toISOString().slice(0, 19).replace('T', ' ');
|
|
387
|
+
|
|
388
|
+
for (const sensor of this.sensors) {
|
|
389
|
+
const basePath = `analytics.statistics.temperature.week.${sensor.id}`;
|
|
390
|
+
const activeState = `temperature.${sensor.id}.active`;
|
|
391
|
+
const isActive = (await adapter.getStateAsync(activeState))?.val === true;
|
|
392
|
+
|
|
393
|
+
const summaryJsonPath = `${basePath}.summary_json`;
|
|
394
|
+
const summaryHtmlPath = `${basePath}.summary_html`;
|
|
395
|
+
|
|
396
|
+
if (!isActive) {
|
|
397
|
+
await adapter.setStateAsync(summaryJsonPath, {
|
|
398
|
+
val: JSON.stringify({ status: 'kein Sensor aktiv' }),
|
|
399
|
+
ack: true,
|
|
400
|
+
});
|
|
401
|
+
await adapter.setStateAsync(summaryHtmlPath, {
|
|
402
|
+
val: '<div style="color:gray;">kein Sensor aktiv</div>',
|
|
403
|
+
ack: true,
|
|
404
|
+
});
|
|
405
|
+
continue;
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
const stateList = [
|
|
409
|
+
'temp_min',
|
|
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' }),
|
|
435
|
+
ack: true,
|
|
436
|
+
});
|
|
437
|
+
await adapter.setStateAsync(summaryHtmlPath, {
|
|
438
|
+
val: `<div style="color:gray;">Wochenwerte zurückgesetzt (${resetDate})</div>`,
|
|
439
|
+
ack: true,
|
|
440
|
+
});
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
await adapter.setStateAsync('analytics.statistics.temperature.week.outputs.summary_all_json', {
|
|
444
|
+
val: '{}',
|
|
445
|
+
ack: true,
|
|
446
|
+
});
|
|
447
|
+
await adapter.setStateAsync('analytics.statistics.temperature.week.outputs.summary_all_html', {
|
|
448
|
+
val: '',
|
|
449
|
+
ack: true,
|
|
450
|
+
});
|
|
451
|
+
|
|
452
|
+
adapter.log.debug('statisticsHelperWeek: Wochenstatistik zurückgesetzt.');
|
|
453
|
+
},
|
|
454
|
+
|
|
455
|
+
/**
|
|
456
|
+
* Stiller Überinstallationsschutz:
|
|
457
|
+
* Prüft und legt fehlende States erneut an, ohne bestehende Werte zu überschreiben.
|
|
458
|
+
*/
|
|
459
|
+
async _verifyStructure() {
|
|
460
|
+
try {
|
|
461
|
+
await this._createTemperatureStatistics();
|
|
462
|
+
} catch {
|
|
463
|
+
// bewusst keine Logs – stiller Selbstschutz
|
|
464
|
+
}
|
|
465
|
+
},
|
|
466
|
+
|
|
467
|
+
/**
|
|
468
|
+
* Berechnet den Zeitraum (Start/Ende) der aktuellen Woche.
|
|
469
|
+
* Rückgabe: z. B. "27.10.2025 – 02.11.2025"
|
|
470
|
+
*/
|
|
471
|
+
_getCurrentWeekRange() {
|
|
472
|
+
const today = new Date();
|
|
473
|
+
|
|
474
|
+
// Aktuellen Sonntag (nächster Wochenreset) finden
|
|
475
|
+
const nextSunday = new Date(today);
|
|
476
|
+
const day = today.getDay(); // Sonntag=0, Montag=1 …
|
|
477
|
+
const daysUntilSunday = (7 - day) % 7;
|
|
478
|
+
nextSunday.setDate(today.getDate() + daysUntilSunday);
|
|
479
|
+
|
|
480
|
+
// Wochenstart ist der Montag vor diesem Sonntag
|
|
481
|
+
const monday = new Date(nextSunday);
|
|
482
|
+
monday.setDate(nextSunday.getDate() - 6);
|
|
483
|
+
|
|
484
|
+
const fmt = d =>
|
|
485
|
+
d.toLocaleDateString('de-DE', {
|
|
486
|
+
day: '2-digit',
|
|
487
|
+
month: '2-digit',
|
|
488
|
+
year: 'numeric',
|
|
489
|
+
});
|
|
490
|
+
|
|
491
|
+
return `${fmt(monday)} – ${fmt(nextSunday)}`;
|
|
492
|
+
},
|
|
493
|
+
|
|
494
|
+
cleanup() {
|
|
495
|
+
if (this.weekResetTimer) {
|
|
496
|
+
clearTimeout(this.weekResetTimer);
|
|
497
|
+
}
|
|
498
|
+
},
|
|
499
|
+
};
|
|
500
|
+
|
|
501
|
+
module.exports = statisticsHelperWeek;
|
|
@@ -68,12 +68,19 @@ const timeHelper = {
|
|
|
68
68
|
});
|
|
69
69
|
}
|
|
70
70
|
|
|
71
|
-
//
|
|
72
|
-
await this.adapter.
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
71
|
+
// --- NEU: nur schalten, wenn sich der Zustand wirklich ändert ---
|
|
72
|
+
const currentState = (await this.adapter.getForeignStateAsync(pumpSwitchId))?.val;
|
|
73
|
+
if (currentState !== shouldRun) {
|
|
74
|
+
await this.adapter.setForeignStateAsync(pumpSwitchId, {
|
|
75
|
+
val: shouldRun,
|
|
76
|
+
ack: false,
|
|
77
|
+
});
|
|
78
|
+
this.adapter.log.debug(`[timeHelper] Pumpe ${shouldRun ? 'EIN' : 'AUS'} (${hhmm})`);
|
|
79
|
+
} else {
|
|
80
|
+
this.adapter.log.debug(
|
|
81
|
+
`[timeHelper] Keine Änderung (${hhmm}) – Zustand bleibt ${shouldRun ? 'EIN' : 'AUS'}.`,
|
|
82
|
+
);
|
|
83
|
+
}
|
|
77
84
|
} catch (err) {
|
|
78
85
|
this.adapter.log.warn(`[timeHelper] Fehler im Check: ${err.message}`);
|
|
79
86
|
}
|
|
@@ -3,8 +3,11 @@
|
|
|
3
3
|
/**
|
|
4
4
|
* statisticsStates.js
|
|
5
5
|
* -------------------
|
|
6
|
-
* Erstellt alle States für die
|
|
7
|
-
* Struktur:
|
|
6
|
+
* Erstellt alle States für die Temperaturstatistiken.
|
|
7
|
+
* Struktur:
|
|
8
|
+
* analytics.statistics.temperature.today.*
|
|
9
|
+
* analytics.statistics.temperature.week.*
|
|
10
|
+
* analytics.statistics.temperature.month.*
|
|
8
11
|
*
|
|
9
12
|
* - Sechs Sensorbereiche (outside, ground, surface, flow, return, collector)
|
|
10
13
|
* - Je Sensor: Min/Max/Avg + Zeitstempel + JSON/HTML-Ausgabe
|
|
@@ -17,7 +20,7 @@
|
|
|
17
20
|
* @param {ioBroker.Adapter} adapter - Instanz des ioBroker-Adapters
|
|
18
21
|
*/
|
|
19
22
|
async function createStatisticsStates(adapter) {
|
|
20
|
-
adapter.log.debug('statisticsStates: Initialisierung der
|
|
23
|
+
adapter.log.debug('statisticsStates: Initialisierung der Temperaturstatistiken gestartet.');
|
|
21
24
|
|
|
22
25
|
// Oberstruktur
|
|
23
26
|
await adapter.setObjectNotExistsAsync('analytics', {
|
|
@@ -38,9 +41,36 @@ async function createStatisticsStates(adapter) {
|
|
|
38
41
|
native: {},
|
|
39
42
|
});
|
|
40
43
|
|
|
41
|
-
|
|
44
|
+
// -------------------------------------------------------------
|
|
45
|
+
// 🔹 TAGESSTATISTIK
|
|
46
|
+
// -------------------------------------------------------------
|
|
47
|
+
await _createTemperatureStatsGroup(adapter, 'today', 'Tagesstatistik (Temperaturen)');
|
|
48
|
+
|
|
49
|
+
// -------------------------------------------------------------
|
|
50
|
+
// 🔹 WOCHENSTATISTIK
|
|
51
|
+
// -------------------------------------------------------------
|
|
52
|
+
await _createTemperatureStatsGroup(adapter, 'week', 'Wochenstatistik (Temperaturen)');
|
|
53
|
+
|
|
54
|
+
// -------------------------------------------------------------
|
|
55
|
+
// 🔹 MONATSSTATISTIK (NEU)
|
|
56
|
+
// -------------------------------------------------------------
|
|
57
|
+
await _createTemperatureStatsGroup(adapter, 'month', 'Monatsstatistik (Temperaturen)');
|
|
58
|
+
|
|
59
|
+
adapter.log.debug('statisticsStates: Tages-, Wochen- und Monatsstatistik (Temperatur) erfolgreich angelegt.');
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Erstellt eine Temperaturstatistik-Gruppe (z. B. "today", "week" oder "month").
|
|
64
|
+
*
|
|
65
|
+
* @param {ioBroker.Adapter} adapter - Aktive ioBroker-Adapterinstanz
|
|
66
|
+
* @param {string} periodId - z. B. "today", "week" oder "month"
|
|
67
|
+
* @param {string} displayName - Anzeigename im Objektbaum
|
|
68
|
+
*/
|
|
69
|
+
async function _createTemperatureStatsGroup(adapter, periodId, displayName) {
|
|
70
|
+
const basePathRoot = `analytics.statistics.temperature.${periodId}`;
|
|
71
|
+
await adapter.setObjectNotExistsAsync(basePathRoot, {
|
|
42
72
|
type: 'channel',
|
|
43
|
-
common: { name:
|
|
73
|
+
common: { name: displayName },
|
|
44
74
|
native: {},
|
|
45
75
|
});
|
|
46
76
|
|
|
@@ -55,11 +85,10 @@ async function createStatisticsStates(adapter) {
|
|
|
55
85
|
];
|
|
56
86
|
|
|
57
87
|
for (const sensor of sensors) {
|
|
58
|
-
const basePath =
|
|
59
|
-
|
|
88
|
+
const basePath = `${basePathRoot}.${sensor.id}`;
|
|
60
89
|
await adapter.setObjectNotExistsAsync(basePath, {
|
|
61
90
|
type: 'channel',
|
|
62
|
-
common: { name: `${sensor.name} (
|
|
91
|
+
common: { name: `${sensor.name} (${displayName})` },
|
|
63
92
|
native: {},
|
|
64
93
|
});
|
|
65
94
|
|
|
@@ -71,8 +100,18 @@ async function createStatisticsStates(adapter) {
|
|
|
71
100
|
{ id: 'temp_avg', name: 'Durchschnittstemperatur', type: 'number', role: 'value.temperature', unit: '°C' },
|
|
72
101
|
{ id: 'data_points_count', name: 'Anzahl Messwerte', type: 'number', role: 'value' },
|
|
73
102
|
{ id: 'last_update', name: 'Letzte Aktualisierung', type: 'string', role: 'value.time' },
|
|
74
|
-
{
|
|
75
|
-
|
|
103
|
+
{
|
|
104
|
+
id: 'summary_json',
|
|
105
|
+
name: `${displayName} (JSON)`,
|
|
106
|
+
type: 'string',
|
|
107
|
+
role: 'json',
|
|
108
|
+
},
|
|
109
|
+
{
|
|
110
|
+
id: 'summary_html',
|
|
111
|
+
name: `${displayName} (HTML)`,
|
|
112
|
+
type: 'string',
|
|
113
|
+
role: 'html',
|
|
114
|
+
},
|
|
76
115
|
];
|
|
77
116
|
|
|
78
117
|
for (const def of stateDefs) {
|
|
@@ -94,7 +133,7 @@ async function createStatisticsStates(adapter) {
|
|
|
94
133
|
}
|
|
95
134
|
|
|
96
135
|
// Gesamt-Ausgabe (Outputs)
|
|
97
|
-
const outputBase =
|
|
136
|
+
const outputBase = `${basePathRoot}.outputs`;
|
|
98
137
|
await adapter.setObjectNotExistsAsync(outputBase, {
|
|
99
138
|
type: 'channel',
|
|
100
139
|
common: { name: 'Gesamtausgaben (alle Sensoren)' },
|
|
@@ -104,12 +143,12 @@ async function createStatisticsStates(adapter) {
|
|
|
104
143
|
const outputs = [
|
|
105
144
|
{
|
|
106
145
|
id: 'summary_all_json',
|
|
107
|
-
name:
|
|
146
|
+
name: `Gesamtzusammenfassung aller Sensoren (${displayName}, JSON)`,
|
|
108
147
|
role: 'json',
|
|
109
148
|
},
|
|
110
149
|
{
|
|
111
150
|
id: 'summary_all_html',
|
|
112
|
-
name:
|
|
151
|
+
name: `Gesamtzusammenfassung aller Sensoren (${displayName}, HTML)`,
|
|
113
152
|
role: 'html',
|
|
114
153
|
},
|
|
115
154
|
];
|
|
@@ -129,8 +168,6 @@ async function createStatisticsStates(adapter) {
|
|
|
129
168
|
native: {},
|
|
130
169
|
});
|
|
131
170
|
}
|
|
132
|
-
|
|
133
|
-
adapter.log.debug('statisticsStates: Tagesstatistik (Temperatur) erfolgreich angelegt.');
|
|
134
171
|
}
|
|
135
172
|
|
|
136
173
|
module.exports = {
|
package/main.js
CHANGED
|
@@ -9,6 +9,8 @@ const temperatureHelper = require('./lib/helpers/temperatureHelper');
|
|
|
9
9
|
const timeHelper = require('./lib/helpers/timeHelper');
|
|
10
10
|
const runtimeHelper = require('./lib/helpers/runtimeHelper');
|
|
11
11
|
const statisticsHelper = require('./lib/helpers/statisticsHelper');
|
|
12
|
+
const statisticsHelperWeek = require('./lib/helpers/statisticsHelperWeek');
|
|
13
|
+
const statisticsHelperMonth = require('./lib/helpers/statisticsHelperMonth');
|
|
12
14
|
const pumpHelper = require('./lib/helpers/pumpHelper');
|
|
13
15
|
const pumpHelper2 = require('./lib/helpers/pumpHelper2');
|
|
14
16
|
const pumpHelper3 = require('./lib/helpers/pumpHelper3');
|
|
@@ -103,6 +105,8 @@ class Poolcontrol extends utils.Adapter {
|
|
|
103
105
|
timeHelper.init(this);
|
|
104
106
|
runtimeHelper.init(this);
|
|
105
107
|
statisticsHelper.init(this);
|
|
108
|
+
statisticsHelperWeek.init(this);
|
|
109
|
+
statisticsHelperMonth.init(this);
|
|
106
110
|
pumpHelper.init(this);
|
|
107
111
|
pumpHelper2.init(this);
|
|
108
112
|
pumpHelper3.init(this);
|