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,466 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* statisticsHelperMonth.js
|
|
5
|
+
* ------------------------
|
|
6
|
+
* Vollständige Steuerung der Monatsstatistik (Temperatur)
|
|
7
|
+
* im Bereich analytics.statistics.temperature.month.*
|
|
8
|
+
*
|
|
9
|
+
* - Erkennt aktive Sensoren anhand temperature.<sensor>.active
|
|
10
|
+
* - Reagiert eventbasiert auf Änderungen der Temperaturwerte
|
|
11
|
+
* - Berechnet laufend Min/Max/Durchschnitt über 30 Tage
|
|
12
|
+
* - Aktualisiert JSON- und HTML-Ausgaben (pro Sensor & gesamt)
|
|
13
|
+
* - Führt automatischen Monats-Reset (1. Tag des Monats 00:05 Uhr) durch
|
|
14
|
+
*
|
|
15
|
+
* @param {ioBroker.Adapter} adapter - Aktive ioBroker-Adapterinstanz
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
const statisticsHelperMonth = {
|
|
19
|
+
adapter: null,
|
|
20
|
+
monthResetTimer: 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('statisticsHelperMonth: Initialisierung gestartet.');
|
|
33
|
+
|
|
34
|
+
// --- Überinstallationsschutz ---
|
|
35
|
+
try {
|
|
36
|
+
await this._verifyStructure();
|
|
37
|
+
} catch {
|
|
38
|
+
// keine Log-Ausgabe – stiller Schutz
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
try {
|
|
42
|
+
await this._createTemperatureStatistics();
|
|
43
|
+
await this._subscribeActiveSensors();
|
|
44
|
+
await this._scheduleMonthReset();
|
|
45
|
+
adapter.log.debug('statisticsHelperMonth: Initialisierung abgeschlossen (Sensorüberwachung aktiv).');
|
|
46
|
+
} catch (err) {
|
|
47
|
+
adapter.log.warn(`statisticsHelperMonth: Fehler bei Initialisierung: ${err.message}`);
|
|
48
|
+
}
|
|
49
|
+
},
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Erstellt States, falls sie fehlen (Überinstallationsschutz)
|
|
53
|
+
*/
|
|
54
|
+
async _createTemperatureStatistics() {
|
|
55
|
+
const adapter = this.adapter;
|
|
56
|
+
|
|
57
|
+
for (const sensor of this.sensors) {
|
|
58
|
+
const basePath = `analytics.statistics.temperature.month.${sensor.id}`;
|
|
59
|
+
await adapter.setObjectNotExistsAsync(basePath, {
|
|
60
|
+
type: 'channel',
|
|
61
|
+
common: { name: `${sensor.name} (Monatsstatistik)` },
|
|
62
|
+
native: {},
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
const activeState = `temperature.${sensor.id}.active`;
|
|
66
|
+
const isActive = (await adapter.getStateAsync(activeState))?.val === true;
|
|
67
|
+
|
|
68
|
+
const summaryJsonPath = `${basePath}.summary_json`;
|
|
69
|
+
const summaryHtmlPath = `${basePath}.summary_html`;
|
|
70
|
+
|
|
71
|
+
if (!isActive) {
|
|
72
|
+
await adapter.setStateAsync(summaryJsonPath, { val: '[]', ack: true });
|
|
73
|
+
await adapter.setStateAsync(summaryHtmlPath, { val: '', ack: true });
|
|
74
|
+
continue;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
const stateDefs = [
|
|
78
|
+
{ id: 'temp_min', def: null },
|
|
79
|
+
{ id: 'temp_max', def: null },
|
|
80
|
+
{ id: 'temp_min_time', def: '' },
|
|
81
|
+
{ id: 'temp_max_time', def: '' },
|
|
82
|
+
{ id: 'temp_avg', def: null },
|
|
83
|
+
{ id: 'data_points_count', def: 0 },
|
|
84
|
+
{ id: 'last_update', def: '' },
|
|
85
|
+
{ id: 'summary_json', def: '' },
|
|
86
|
+
{ id: 'summary_html', def: '' },
|
|
87
|
+
];
|
|
88
|
+
|
|
89
|
+
for (const def of stateDefs) {
|
|
90
|
+
const fullPath = `${basePath}.${def.id}`;
|
|
91
|
+
const obj = await adapter.getObjectAsync(fullPath);
|
|
92
|
+
if (!obj) {
|
|
93
|
+
await adapter.setObjectNotExistsAsync(fullPath, {
|
|
94
|
+
type: 'state',
|
|
95
|
+
common: {
|
|
96
|
+
name: def.id,
|
|
97
|
+
type: typeof def.def === 'number' ? 'number' : 'string',
|
|
98
|
+
role: def.id.includes('time')
|
|
99
|
+
? 'value.time'
|
|
100
|
+
: def.id.includes('temp')
|
|
101
|
+
? 'value.temperature'
|
|
102
|
+
: 'value',
|
|
103
|
+
read: true,
|
|
104
|
+
write: false,
|
|
105
|
+
persist: true,
|
|
106
|
+
},
|
|
107
|
+
native: {},
|
|
108
|
+
});
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
const state = await adapter.getStateAsync(fullPath);
|
|
112
|
+
if (!state || state.val === null || state.val === undefined) {
|
|
113
|
+
await adapter.setStateAsync(fullPath, { val: def.def, ack: true });
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
const outputBase = 'analytics.statistics.temperature.month.outputs';
|
|
119
|
+
await adapter.setObjectNotExistsAsync(outputBase, {
|
|
120
|
+
type: 'channel',
|
|
121
|
+
common: { name: 'Gesamtausgaben (alle Sensoren – Monat)' },
|
|
122
|
+
native: {},
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
const outputStates = [
|
|
126
|
+
{ id: 'summary_all_json', def: '' },
|
|
127
|
+
{ id: 'summary_all_html', def: '' },
|
|
128
|
+
];
|
|
129
|
+
|
|
130
|
+
for (const out of outputStates) {
|
|
131
|
+
const fullPath = `${outputBase}.${out.id}`;
|
|
132
|
+
const obj = await adapter.getObjectAsync(fullPath);
|
|
133
|
+
if (!obj) {
|
|
134
|
+
await adapter.setObjectNotExistsAsync(fullPath, {
|
|
135
|
+
type: 'state',
|
|
136
|
+
common: {
|
|
137
|
+
name: out.id,
|
|
138
|
+
type: 'string',
|
|
139
|
+
role: out.id.endsWith('json') ? 'json' : 'html',
|
|
140
|
+
read: true,
|
|
141
|
+
write: false,
|
|
142
|
+
persist: true,
|
|
143
|
+
},
|
|
144
|
+
native: {},
|
|
145
|
+
});
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
const state = await adapter.getStateAsync(fullPath);
|
|
149
|
+
if (!state || state.val === null || state.val === undefined) {
|
|
150
|
+
await adapter.setStateAsync(fullPath, { val: out.id.endsWith('json') ? '{}' : '', ack: true });
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
},
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* Abonniert alle aktiven Temperatursensoren
|
|
157
|
+
*/
|
|
158
|
+
async _subscribeActiveSensors() {
|
|
159
|
+
const adapter = this.adapter;
|
|
160
|
+
for (const sensor of this.sensors) {
|
|
161
|
+
const activeState = `temperature.${sensor.id}.active`;
|
|
162
|
+
const isActive = (await adapter.getStateAsync(activeState))?.val === true;
|
|
163
|
+
if (isActive) {
|
|
164
|
+
const stateId = `temperature.${sensor.id}.current`;
|
|
165
|
+
adapter.subscribeStates(stateId);
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
adapter.on('stateChange', async (id, state) => {
|
|
170
|
+
if (!state || state.ack === false) {
|
|
171
|
+
return;
|
|
172
|
+
}
|
|
173
|
+
for (const sensor of this.sensors) {
|
|
174
|
+
if (id.endsWith(`temperature.${sensor.id}.current`)) {
|
|
175
|
+
await this._processTemperatureChange(sensor.id, state.val);
|
|
176
|
+
break;
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
});
|
|
180
|
+
},
|
|
181
|
+
|
|
182
|
+
/**
|
|
183
|
+
* Verarbeitung einer Temperaturänderung für einen Sensor.
|
|
184
|
+
*
|
|
185
|
+
* @param {string} sensorId - ID des Sensors
|
|
186
|
+
* @param {number} newValue - Neuer Messwert in °C
|
|
187
|
+
*/
|
|
188
|
+
async _processTemperatureChange(sensorId, newValue) {
|
|
189
|
+
const adapter = this.adapter;
|
|
190
|
+
const basePath = `analytics.statistics.temperature.month.${sensorId}`;
|
|
191
|
+
const now = `${new Date().toLocaleDateString('de-DE', {
|
|
192
|
+
day: '2-digit',
|
|
193
|
+
month: '2-digit',
|
|
194
|
+
})} ${new Date().toLocaleTimeString('de-DE', { hour: '2-digit', minute: '2-digit' })}`;
|
|
195
|
+
|
|
196
|
+
if (typeof newValue !== 'number') {
|
|
197
|
+
return;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
newValue = Math.round(newValue * 10) / 10;
|
|
201
|
+
|
|
202
|
+
const tempMin = (await adapter.getStateAsync(`${basePath}.temp_min`))?.val;
|
|
203
|
+
const tempMax = (await adapter.getStateAsync(`${basePath}.temp_max`))?.val;
|
|
204
|
+
const tempAvg = (await adapter.getStateAsync(`${basePath}.temp_avg`))?.val;
|
|
205
|
+
const count = (await adapter.getStateAsync(`${basePath}.data_points_count`))?.val || 0;
|
|
206
|
+
|
|
207
|
+
let newMin = tempMin;
|
|
208
|
+
let newMax = tempMax;
|
|
209
|
+
let newAvg = tempAvg;
|
|
210
|
+
|
|
211
|
+
if (tempMin === null || newValue < tempMin) {
|
|
212
|
+
newMin = newValue;
|
|
213
|
+
await adapter.setStateAsync(`${basePath}.temp_min_time`, { val: now, ack: true });
|
|
214
|
+
}
|
|
215
|
+
if (tempMax === null || newValue > tempMax) {
|
|
216
|
+
newMax = newValue;
|
|
217
|
+
await adapter.setStateAsync(`${basePath}.temp_max_time`, { val: now, ack: true });
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
const newCount = count + 1;
|
|
221
|
+
newAvg = tempAvg === null ? newValue : (tempAvg * count + newValue) / newCount;
|
|
222
|
+
|
|
223
|
+
newMin = Math.round(newMin * 10) / 10;
|
|
224
|
+
newMax = Math.round(newMax * 10) / 10;
|
|
225
|
+
newAvg = Math.round(newAvg * 10) / 10;
|
|
226
|
+
|
|
227
|
+
await adapter.setStateAsync(`${basePath}.temp_min`, { val: newMin, ack: true });
|
|
228
|
+
await adapter.setStateAsync(`${basePath}.temp_max`, { val: newMax, ack: true });
|
|
229
|
+
await adapter.setStateAsync(`${basePath}.temp_avg`, { val: Math.round(newAvg * 100) / 100, ack: true });
|
|
230
|
+
await adapter.setStateAsync(`${basePath}.data_points_count`, { val: newCount, ack: true });
|
|
231
|
+
await adapter.setStateAsync(`${basePath}.last_update`, { val: now, ack: true });
|
|
232
|
+
|
|
233
|
+
const summary = {
|
|
234
|
+
name: 'Monatsstatistik',
|
|
235
|
+
month_label: this._getCurrentMonthLabel(),
|
|
236
|
+
date: new Date().toISOString().slice(0, 10),
|
|
237
|
+
temp_min: newMin,
|
|
238
|
+
temp_min_time: (await adapter.getStateAsync(`${basePath}.temp_min_time`))?.val || '',
|
|
239
|
+
temp_max: newMax,
|
|
240
|
+
temp_max_time: (await adapter.getStateAsync(`${basePath}.temp_max_time`))?.val || '',
|
|
241
|
+
temp_avg: newAvg,
|
|
242
|
+
data_points_count: newCount,
|
|
243
|
+
updated: now,
|
|
244
|
+
};
|
|
245
|
+
await adapter.setStateAsync(`${basePath}.summary_json`, { val: JSON.stringify(summary), ack: true });
|
|
246
|
+
await adapter.setStateAsync(`${basePath}.summary_html`, {
|
|
247
|
+
val: `<div><b>Min:</b> ${newMin} °C / <b>Max:</b> ${newMax} °C / <b>Ø:</b> ${newAvg} °C</div>`,
|
|
248
|
+
ack: true,
|
|
249
|
+
});
|
|
250
|
+
|
|
251
|
+
await this._updateOverallSummary();
|
|
252
|
+
},
|
|
253
|
+
|
|
254
|
+
/**
|
|
255
|
+
* Gesamtzusammenfassung (JSON + HTML)
|
|
256
|
+
*/
|
|
257
|
+
async _updateOverallSummary() {
|
|
258
|
+
const adapter = this.adapter;
|
|
259
|
+
const allData = [];
|
|
260
|
+
|
|
261
|
+
try {
|
|
262
|
+
for (const sensor of this.sensors) {
|
|
263
|
+
const summaryState = await adapter.getStateAsync(
|
|
264
|
+
`analytics.statistics.temperature.month.${sensor.id}.summary_json`,
|
|
265
|
+
);
|
|
266
|
+
if (!summaryState || !summaryState.val) {
|
|
267
|
+
continue;
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
let parsed;
|
|
271
|
+
try {
|
|
272
|
+
parsed = JSON.parse(summaryState.val);
|
|
273
|
+
} catch {
|
|
274
|
+
continue;
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
const { temp_min: min, temp_max: max, temp_avg: avg } = parsed;
|
|
278
|
+
if (min == null && max == null && avg == null) {
|
|
279
|
+
continue;
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
const rMin = typeof min === 'number' ? Math.round(min * 10) / 10 : min;
|
|
283
|
+
const rMax = typeof max === 'number' ? Math.round(max * 10) / 10 : max;
|
|
284
|
+
const rAvg = typeof avg === 'number' ? Math.round(avg * 10) / 10 : avg;
|
|
285
|
+
const date = parsed.date || '';
|
|
286
|
+
const minTime = parsed.temp_min_time || '';
|
|
287
|
+
const maxTime = parsed.temp_max_time || '';
|
|
288
|
+
const count = parsed.data_points_count || 0;
|
|
289
|
+
|
|
290
|
+
allData.push({
|
|
291
|
+
month_label: this._getCurrentMonthLabel(),
|
|
292
|
+
name: sensor.name,
|
|
293
|
+
date,
|
|
294
|
+
min: rMin,
|
|
295
|
+
min_time: minTime,
|
|
296
|
+
max: rMax,
|
|
297
|
+
max_time: maxTime,
|
|
298
|
+
avg: rAvg,
|
|
299
|
+
data_points_count: count,
|
|
300
|
+
});
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
if (allData.length === 0) {
|
|
304
|
+
await adapter.setStateChangedAsync('analytics.statistics.temperature.month.outputs.summary_all_json', {
|
|
305
|
+
val: '[]',
|
|
306
|
+
ack: true,
|
|
307
|
+
});
|
|
308
|
+
await adapter.setStateChangedAsync('analytics.statistics.temperature.month.outputs.summary_all_html', {
|
|
309
|
+
val: '',
|
|
310
|
+
ack: true,
|
|
311
|
+
});
|
|
312
|
+
return;
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
const jsonOutput = JSON.stringify(allData);
|
|
316
|
+
let html = '<table style="width:100%;border-collapse:collapse;">';
|
|
317
|
+
html += '<tr><th style="text-align:left;">Sensor</th><th>Min</th><th>Max</th><th>Ø</th></tr>';
|
|
318
|
+
for (const entry of allData) {
|
|
319
|
+
html += `<tr><td>${entry.name}</td><td>${entry.min ?? '-'}</td><td>${entry.max ?? '-'}</td><td>${entry.avg ?? '-'}</td></tr>`;
|
|
320
|
+
}
|
|
321
|
+
html += '</table>';
|
|
322
|
+
|
|
323
|
+
await adapter.setStateChangedAsync('analytics.statistics.temperature.month.outputs.summary_all_json', {
|
|
324
|
+
val: jsonOutput,
|
|
325
|
+
ack: true,
|
|
326
|
+
});
|
|
327
|
+
await adapter.setStateChangedAsync('analytics.statistics.temperature.month.outputs.summary_all_html', {
|
|
328
|
+
val: html,
|
|
329
|
+
ack: true,
|
|
330
|
+
});
|
|
331
|
+
|
|
332
|
+
adapter.log.debug('statisticsHelperMonth: Gesamtzusammenfassung erfolgreich aktualisiert.');
|
|
333
|
+
} catch (err) {
|
|
334
|
+
adapter.log.warn(`statisticsHelperMonth: Fehler bei Gesamtzusammenfassung: ${err.message}`);
|
|
335
|
+
}
|
|
336
|
+
},
|
|
337
|
+
|
|
338
|
+
/**
|
|
339
|
+
* Monats-Reset planen (1. Tag des Monats 00:05 Uhr)
|
|
340
|
+
*/
|
|
341
|
+
async _scheduleMonthReset() {
|
|
342
|
+
const adapter = this.adapter;
|
|
343
|
+
if (this.monthResetTimer) {
|
|
344
|
+
clearTimeout(this.monthResetTimer);
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
const now = new Date();
|
|
348
|
+
const nextReset = new Date(now.getFullYear(), now.getMonth() + 1, 1, 0, 5, 0, 0);
|
|
349
|
+
const msUntilReset = nextReset.getTime() - now.getTime();
|
|
350
|
+
|
|
351
|
+
this.monthResetTimer = setTimeout(async () => {
|
|
352
|
+
await this._resetMonthlyTemperatureStats();
|
|
353
|
+
await this._scheduleMonthReset();
|
|
354
|
+
}, msUntilReset);
|
|
355
|
+
|
|
356
|
+
adapter.log.debug(
|
|
357
|
+
`statisticsHelperMonth: Monats-Reset geplant in ${Math.round(msUntilReset / 60000)} Minuten.`,
|
|
358
|
+
);
|
|
359
|
+
},
|
|
360
|
+
|
|
361
|
+
/**
|
|
362
|
+
* Monatsstatistik zurücksetzen
|
|
363
|
+
*/
|
|
364
|
+
async _resetMonthlyTemperatureStats() {
|
|
365
|
+
const adapter = this.adapter;
|
|
366
|
+
adapter.log.info('statisticsHelperMonth: Monatsstatistik wird zurückgesetzt.');
|
|
367
|
+
|
|
368
|
+
const resetDate = new Date().toISOString().slice(0, 19).replace('T', ' ');
|
|
369
|
+
|
|
370
|
+
for (const sensor of this.sensors) {
|
|
371
|
+
const basePath = `analytics.statistics.temperature.month.${sensor.id}`;
|
|
372
|
+
const activeState = `temperature.${sensor.id}.active`;
|
|
373
|
+
const isActive = (await adapter.getStateAsync(activeState))?.val === true;
|
|
374
|
+
|
|
375
|
+
const summaryJsonPath = `${basePath}.summary_json`;
|
|
376
|
+
const summaryHtmlPath = `${basePath}.summary_html`;
|
|
377
|
+
|
|
378
|
+
if (!isActive) {
|
|
379
|
+
await adapter.setStateAsync(summaryJsonPath, {
|
|
380
|
+
val: JSON.stringify({ status: 'kein Sensor aktiv' }),
|
|
381
|
+
ack: true,
|
|
382
|
+
});
|
|
383
|
+
await adapter.setStateAsync(summaryHtmlPath, {
|
|
384
|
+
val: '<div style="color:gray;">kein Sensor aktiv</div>',
|
|
385
|
+
ack: true,
|
|
386
|
+
});
|
|
387
|
+
continue;
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
const stateList = [
|
|
391
|
+
'temp_min',
|
|
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' }),
|
|
417
|
+
ack: true,
|
|
418
|
+
});
|
|
419
|
+
await adapter.setStateAsync(summaryHtmlPath, {
|
|
420
|
+
val: `<div style="color:gray;">Monatswerte zurückgesetzt (${resetDate})</div>`,
|
|
421
|
+
ack: true,
|
|
422
|
+
});
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
await adapter.setStateAsync('analytics.statistics.temperature.month.outputs.summary_all_json', {
|
|
426
|
+
val: '{}',
|
|
427
|
+
ack: true,
|
|
428
|
+
});
|
|
429
|
+
await adapter.setStateAsync('analytics.statistics.temperature.month.outputs.summary_all_html', {
|
|
430
|
+
val: '',
|
|
431
|
+
ack: true,
|
|
432
|
+
});
|
|
433
|
+
|
|
434
|
+
adapter.log.debug('statisticsHelperMonth: Monatsstatistik zurückgesetzt.');
|
|
435
|
+
},
|
|
436
|
+
|
|
437
|
+
/**
|
|
438
|
+
* Stiller Überinstallationsschutz:
|
|
439
|
+
* Prüft und legt fehlende States erneut an, ohne bestehende Werte zu überschreiben.
|
|
440
|
+
*/
|
|
441
|
+
async _verifyStructure() {
|
|
442
|
+
try {
|
|
443
|
+
await this._createTemperatureStatistics();
|
|
444
|
+
} catch {
|
|
445
|
+
// bewusst keine Logs – stiller Selbstschutz
|
|
446
|
+
}
|
|
447
|
+
},
|
|
448
|
+
|
|
449
|
+
/**
|
|
450
|
+
* Gibt den aktuellen Monat als lesbares Label zurück, z. B. "Oktober 2025".
|
|
451
|
+
*/
|
|
452
|
+
_getCurrentMonthLabel() {
|
|
453
|
+
const d = new Date();
|
|
454
|
+
const month = d.toLocaleString('de-DE', { month: 'long' });
|
|
455
|
+
const year = d.getFullYear();
|
|
456
|
+
return `${month.charAt(0).toUpperCase() + month.slice(1)} ${year}`;
|
|
457
|
+
},
|
|
458
|
+
|
|
459
|
+
cleanup() {
|
|
460
|
+
if (this.monthResetTimer) {
|
|
461
|
+
clearTimeout(this.monthResetTimer);
|
|
462
|
+
}
|
|
463
|
+
},
|
|
464
|
+
};
|
|
465
|
+
|
|
466
|
+
module.exports = statisticsHelperMonth;
|