iobroker.poolcontrol 0.3.1 → 0.4.0
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 +24 -18
- package/io-package.json +14 -14
- package/lib/helpers/consumptionHelper.js +27 -32
- package/lib/helpers/frostHelper.js +3 -8
- package/lib/helpers/pumpHelper.js +22 -12
- package/lib/helpers/pumpHelper3.js +51 -16
- package/lib/helpers/runtimeHelper.js +73 -32
- package/lib/helpers/statisticsHelper.js +392 -0
- package/lib/stateDefinitions/controlStates.js +0 -61
- package/lib/stateDefinitions/pumpStates3.js +22 -0
- package/lib/stateDefinitions/runtimeStates.js +20 -5
- package/lib/stateDefinitions/statisticsStates.js +138 -0
- package/main.js +9 -4
- package/package.json +1 -1
- package/lib/stateDefinitions/hardwareStates.js +0 -184
|
@@ -0,0 +1,392 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* statisticsHelper.js
|
|
5
|
+
* -------------------
|
|
6
|
+
* Vollständige Steuerung der Tagesstatistik (Temperatur)
|
|
7
|
+
* im Bereich analytics.statistics.temperature.today.*
|
|
8
|
+
*
|
|
9
|
+
* - Erstellt alle States (Überinstallationsschutz + Persistenz)
|
|
10
|
+
* - Erkennt aktive Sensoren anhand temperature.<sensor>.active
|
|
11
|
+
* - Reagiert eventbasiert auf Änderungen der Temperaturwerte
|
|
12
|
+
* - Berechnet laufend Min/Max/Durchschnitt
|
|
13
|
+
* - Aktualisiert JSON- und HTML-Ausgaben (pro Sensor & gesamt)
|
|
14
|
+
* - Führt automatisch täglichen Reset um Mitternacht durch
|
|
15
|
+
*
|
|
16
|
+
* @param {ioBroker.Adapter} adapter - Die aktuelle Adapterinstanz (this),
|
|
17
|
+
* über die alle ioBroker-Funktionen wie setStateAsync, getStateAsync usw. aufgerufen werden.
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
const statisticsHelper = {
|
|
21
|
+
adapter: null,
|
|
22
|
+
midnightTimer: null,
|
|
23
|
+
sensors: [
|
|
24
|
+
{ id: 'outside', name: 'Außentemperatur' },
|
|
25
|
+
{ id: 'ground', name: 'Bodentemperatur' },
|
|
26
|
+
{ id: 'surface', name: 'Pooloberfläche' },
|
|
27
|
+
{ id: 'flow', name: 'Vorlauf' },
|
|
28
|
+
{ id: 'return', name: 'Rücklauf' },
|
|
29
|
+
{ id: 'collector', name: 'Kollektor (Solar)' },
|
|
30
|
+
],
|
|
31
|
+
|
|
32
|
+
async init(adapter) {
|
|
33
|
+
this.adapter = adapter;
|
|
34
|
+
adapter.log.debug('statisticsHelper: Initialisierung gestartet.');
|
|
35
|
+
|
|
36
|
+
try {
|
|
37
|
+
await this._createTemperatureStatistics();
|
|
38
|
+
await this._subscribeActiveSensors();
|
|
39
|
+
await this._scheduleMidnightReset();
|
|
40
|
+
adapter.log.debug('statisticsHelper: Initialisierung abgeschlossen (Sensorüberwachung aktiv).');
|
|
41
|
+
} catch (err) {
|
|
42
|
+
adapter.log.warn(`statisticsHelper: Fehler bei Initialisierung: ${err.message}`);
|
|
43
|
+
}
|
|
44
|
+
},
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Erstellt States, falls sie fehlen (Überinstallationsschutz)
|
|
48
|
+
*/
|
|
49
|
+
async _createTemperatureStatistics() {
|
|
50
|
+
const adapter = this.adapter;
|
|
51
|
+
|
|
52
|
+
for (const sensor of this.sensors) {
|
|
53
|
+
const basePath = `analytics.statistics.temperature.today.${sensor.id}`;
|
|
54
|
+
await adapter.setObjectNotExistsAsync(basePath, {
|
|
55
|
+
type: 'channel',
|
|
56
|
+
common: { name: `${sensor.name} (Tagesstatistik)` },
|
|
57
|
+
native: {},
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
const activeState = `temperature.${sensor.id}.active`;
|
|
61
|
+
const isActive = (await adapter.getStateAsync(activeState))?.val === true;
|
|
62
|
+
|
|
63
|
+
const summaryJsonPath = `${basePath}.summary_json`;
|
|
64
|
+
const summaryHtmlPath = `${basePath}.summary_html`;
|
|
65
|
+
|
|
66
|
+
if (!isActive) {
|
|
67
|
+
await adapter.setStateAsync(summaryJsonPath, {
|
|
68
|
+
val: JSON.stringify({ status: 'kein Sensor aktiv' }),
|
|
69
|
+
ack: true,
|
|
70
|
+
});
|
|
71
|
+
await adapter.setStateAsync(summaryHtmlPath, {
|
|
72
|
+
val: '<div style="color:gray;">kein Sensor aktiv</div>',
|
|
73
|
+
ack: true,
|
|
74
|
+
});
|
|
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.today.outputs';
|
|
120
|
+
await adapter.setObjectNotExistsAsync(outputBase, {
|
|
121
|
+
type: 'channel',
|
|
122
|
+
common: { name: 'Gesamtausgaben (alle Sensoren)' },
|
|
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 betroffenen Sensors (z. B. "outside" oder "flow").
|
|
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.today.${sensorId}`;
|
|
193
|
+
const now = new Date().toLocaleTimeString('de-DE', { hour: '2-digit', minute: '2-digit' });
|
|
194
|
+
|
|
195
|
+
if (typeof newValue !== 'number') {
|
|
196
|
+
return;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
const tempMin = (await adapter.getStateAsync(`${basePath}.temp_min`))?.val;
|
|
200
|
+
const tempMax = (await adapter.getStateAsync(`${basePath}.temp_max`))?.val;
|
|
201
|
+
const tempAvg = (await adapter.getStateAsync(`${basePath}.temp_avg`))?.val;
|
|
202
|
+
const count = (await adapter.getStateAsync(`${basePath}.data_points_count`))?.val || 0;
|
|
203
|
+
|
|
204
|
+
let newMin = tempMin;
|
|
205
|
+
let newMax = tempMax;
|
|
206
|
+
let newAvg = tempAvg;
|
|
207
|
+
|
|
208
|
+
if (tempMin === null || newValue < tempMin) {
|
|
209
|
+
newMin = newValue;
|
|
210
|
+
await adapter.setStateAsync(`${basePath}.temp_min_time`, { val: now, ack: true });
|
|
211
|
+
}
|
|
212
|
+
if (tempMax === null || newValue > tempMax) {
|
|
213
|
+
newMax = newValue;
|
|
214
|
+
await adapter.setStateAsync(`${basePath}.temp_max_time`, { val: now, ack: true });
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
// Durchschnitt (gleitend)
|
|
218
|
+
const newCount = count + 1;
|
|
219
|
+
newAvg = tempAvg === null ? newValue : (tempAvg * count + newValue) / newCount;
|
|
220
|
+
|
|
221
|
+
await adapter.setStateAsync(`${basePath}.temp_min`, { val: newMin, ack: true });
|
|
222
|
+
await adapter.setStateAsync(`${basePath}.temp_max`, { val: newMax, ack: true });
|
|
223
|
+
await adapter.setStateAsync(`${basePath}.temp_avg`, { val: Math.round(newAvg * 100) / 100, ack: true });
|
|
224
|
+
await adapter.setStateAsync(`${basePath}.data_points_count`, { val: newCount, ack: true });
|
|
225
|
+
await adapter.setStateAsync(`${basePath}.last_update`, { val: now, ack: true });
|
|
226
|
+
|
|
227
|
+
// Summary aktualisieren
|
|
228
|
+
const summary = {
|
|
229
|
+
temp_min: newMin,
|
|
230
|
+
temp_max: newMax,
|
|
231
|
+
temp_avg: Math.round(newAvg * 100) / 100,
|
|
232
|
+
updated: now,
|
|
233
|
+
};
|
|
234
|
+
await adapter.setStateAsync(`${basePath}.summary_json`, { val: JSON.stringify(summary), ack: true });
|
|
235
|
+
await adapter.setStateAsync(`${basePath}.summary_html`, {
|
|
236
|
+
val: `<div><b>Min:</b> ${newMin} °C / <b>Max:</b> ${newMax} °C / <b>Ø:</b> ${Math.round(newAvg * 100) / 100} °C</div>`,
|
|
237
|
+
ack: true,
|
|
238
|
+
});
|
|
239
|
+
|
|
240
|
+
await this._updateOverallSummary();
|
|
241
|
+
},
|
|
242
|
+
|
|
243
|
+
/**
|
|
244
|
+
* Gesamt-HTML/JSON-Ausgabe aktualisieren
|
|
245
|
+
*/
|
|
246
|
+
async _updateOverallSummary() {
|
|
247
|
+
const adapter = this.adapter;
|
|
248
|
+
const allData = [];
|
|
249
|
+
|
|
250
|
+
for (const sensor of this.sensors) {
|
|
251
|
+
const active = (await adapter.getStateAsync(`temperature.${sensor.id}.active`))?.val === true;
|
|
252
|
+
if (!active) {
|
|
253
|
+
allData.push({ name: sensor.name, status: 'kein Sensor aktiv' });
|
|
254
|
+
continue;
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
const min = (await adapter.getStateAsync(`analytics.statistics.temperature.today.${sensor.id}.temp_min`))
|
|
258
|
+
?.val;
|
|
259
|
+
const max = (await adapter.getStateAsync(`analytics.statistics.temperature.today.${sensor.id}.temp_max`))
|
|
260
|
+
?.val;
|
|
261
|
+
const avg = (await adapter.getStateAsync(`analytics.statistics.temperature.today.${sensor.id}.temp_avg`))
|
|
262
|
+
?.val;
|
|
263
|
+
|
|
264
|
+
allData.push({ name: sensor.name, min, max, avg });
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
await adapter.setStateAsync('analytics.statistics.temperature.today.outputs.summary_all_json', {
|
|
268
|
+
val: JSON.stringify(allData),
|
|
269
|
+
ack: true,
|
|
270
|
+
});
|
|
271
|
+
|
|
272
|
+
let html = '<table style="width:100%;border-collapse:collapse;">';
|
|
273
|
+
html += '<tr><th style="text-align:left;">Sensor</th><th>Min</th><th>Max</th><th>Ø</th></tr>';
|
|
274
|
+
for (const entry of allData) {
|
|
275
|
+
html += `<tr><td>${entry.name}</td><td>${entry.min ?? '-'}</td><td>${entry.max ?? '-'}</td><td>${entry.avg ?? '-'}</td></tr>`;
|
|
276
|
+
}
|
|
277
|
+
html += '</table>';
|
|
278
|
+
|
|
279
|
+
await adapter.setStateAsync('analytics.statistics.temperature.today.outputs.summary_all_html', {
|
|
280
|
+
val: html,
|
|
281
|
+
ack: true,
|
|
282
|
+
});
|
|
283
|
+
},
|
|
284
|
+
|
|
285
|
+
/**
|
|
286
|
+
* Mitternacht-Reset planen
|
|
287
|
+
*/
|
|
288
|
+
async _scheduleMidnightReset() {
|
|
289
|
+
const adapter = this.adapter;
|
|
290
|
+
if (this.midnightTimer) {
|
|
291
|
+
clearTimeout(this.midnightTimer);
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
const now = new Date();
|
|
295
|
+
const nextMidnight = new Date(now);
|
|
296
|
+
nextMidnight.setHours(24, 0, 5, 0);
|
|
297
|
+
const msUntilMidnight = nextMidnight.getTime() - now.getTime();
|
|
298
|
+
|
|
299
|
+
this.midnightTimer = setTimeout(async () => {
|
|
300
|
+
await this._resetDailyTemperatureStats();
|
|
301
|
+
await this._scheduleMidnightReset();
|
|
302
|
+
}, msUntilMidnight);
|
|
303
|
+
|
|
304
|
+
adapter.log.debug(
|
|
305
|
+
`statisticsHelper: Mitternacht-Reset geplant in ${Math.round(msUntilMidnight / 60000)} Minuten.`,
|
|
306
|
+
);
|
|
307
|
+
},
|
|
308
|
+
|
|
309
|
+
/**
|
|
310
|
+
* Tagesstatistik zurücksetzen
|
|
311
|
+
*/
|
|
312
|
+
async _resetDailyTemperatureStats() {
|
|
313
|
+
const adapter = this.adapter;
|
|
314
|
+
adapter.log.info('statisticsHelper: Tagesstatistik wird zurückgesetzt.');
|
|
315
|
+
|
|
316
|
+
const resetDate = new Date().toISOString().slice(0, 19).replace('T', ' ');
|
|
317
|
+
|
|
318
|
+
for (const sensor of this.sensors) {
|
|
319
|
+
const basePath = `analytics.statistics.temperature.today.${sensor.id}`;
|
|
320
|
+
const activeState = `temperature.${sensor.id}.active`;
|
|
321
|
+
const isActive = (await adapter.getStateAsync(activeState))?.val === true;
|
|
322
|
+
|
|
323
|
+
const summaryJsonPath = `${basePath}.summary_json`;
|
|
324
|
+
const summaryHtmlPath = `${basePath}.summary_html`;
|
|
325
|
+
|
|
326
|
+
if (!isActive) {
|
|
327
|
+
await adapter.setStateAsync(summaryJsonPath, {
|
|
328
|
+
val: JSON.stringify({ status: 'kein Sensor aktiv' }),
|
|
329
|
+
ack: true,
|
|
330
|
+
});
|
|
331
|
+
await adapter.setStateAsync(summaryHtmlPath, {
|
|
332
|
+
val: '<div style="color:gray;">kein Sensor aktiv</div>',
|
|
333
|
+
ack: true,
|
|
334
|
+
});
|
|
335
|
+
continue;
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
const stateList = [
|
|
339
|
+
'temp_min',
|
|
340
|
+
'temp_max',
|
|
341
|
+
'temp_min_time',
|
|
342
|
+
'temp_max_time',
|
|
343
|
+
'temp_avg',
|
|
344
|
+
'data_points_count',
|
|
345
|
+
'last_update',
|
|
346
|
+
];
|
|
347
|
+
|
|
348
|
+
for (const state of stateList) {
|
|
349
|
+
const fullPath = `${basePath}.${state}`;
|
|
350
|
+
let defValue = null;
|
|
351
|
+
if (state.includes('time')) {
|
|
352
|
+
defValue = '';
|
|
353
|
+
}
|
|
354
|
+
if (state === 'data_points_count') {
|
|
355
|
+
defValue = 0;
|
|
356
|
+
}
|
|
357
|
+
if (state === 'last_update') {
|
|
358
|
+
defValue = resetDate;
|
|
359
|
+
}
|
|
360
|
+
await adapter.setStateAsync(fullPath, { val: defValue, ack: true });
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
await adapter.setStateAsync(summaryJsonPath, {
|
|
364
|
+
val: JSON.stringify({ date_reset: resetDate, status: 'Tageswerte zurückgesetzt' }),
|
|
365
|
+
ack: true,
|
|
366
|
+
});
|
|
367
|
+
await adapter.setStateAsync(summaryHtmlPath, {
|
|
368
|
+
val: `<div style="color:gray;">Tageswerte zurückgesetzt (${resetDate})</div>`,
|
|
369
|
+
ack: true,
|
|
370
|
+
});
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
await adapter.setStateAsync('analytics.statistics.temperature.today.outputs.summary_all_json', {
|
|
374
|
+
val: '{}',
|
|
375
|
+
ack: true,
|
|
376
|
+
});
|
|
377
|
+
await adapter.setStateAsync('analytics.statistics.temperature.today.outputs.summary_all_html', {
|
|
378
|
+
val: '',
|
|
379
|
+
ack: true,
|
|
380
|
+
});
|
|
381
|
+
|
|
382
|
+
adapter.log.debug('statisticsHelper: Tagesstatistik zurückgesetzt.');
|
|
383
|
+
},
|
|
384
|
+
|
|
385
|
+
cleanup() {
|
|
386
|
+
if (this.midnightTimer) {
|
|
387
|
+
clearTimeout(this.midnightTimer);
|
|
388
|
+
}
|
|
389
|
+
},
|
|
390
|
+
};
|
|
391
|
+
|
|
392
|
+
module.exports = statisticsHelper;
|
|
@@ -26,43 +26,6 @@ async function createControlStates(adapter) {
|
|
|
26
26
|
native: {},
|
|
27
27
|
});
|
|
28
28
|
|
|
29
|
-
// Channel: control.season
|
|
30
|
-
await adapter.setObjectNotExistsAsync('control.season', {
|
|
31
|
-
type: 'channel',
|
|
32
|
-
common: {
|
|
33
|
-
name: 'Saisonsteuerung',
|
|
34
|
-
desc: 'Steuerung der aktiven Poolsaison und saisonabhängiger Funktionen',
|
|
35
|
-
},
|
|
36
|
-
native: {},
|
|
37
|
-
});
|
|
38
|
-
|
|
39
|
-
// State: control.season.active (mit Persist-Schutz)
|
|
40
|
-
await adapter.setObjectNotExistsAsync('control.season.active', {
|
|
41
|
-
type: 'state',
|
|
42
|
-
common: {
|
|
43
|
-
name: 'Poolsaison aktiv',
|
|
44
|
-
desc: 'Zeigt an, ob die Poolsaison aktiv ist (steuerbar über VIS oder Blockly)',
|
|
45
|
-
type: 'boolean',
|
|
46
|
-
role: 'switch',
|
|
47
|
-
read: true,
|
|
48
|
-
write: true,
|
|
49
|
-
def: false,
|
|
50
|
-
persist: true, // dauerhaft speichern
|
|
51
|
-
},
|
|
52
|
-
native: {},
|
|
53
|
-
});
|
|
54
|
-
|
|
55
|
-
const existingSeasonActive = await adapter.getStateAsync('control.season.active');
|
|
56
|
-
if (
|
|
57
|
-
existingSeasonActive === null ||
|
|
58
|
-
existingSeasonActive.val === null ||
|
|
59
|
-
existingSeasonActive.val === undefined
|
|
60
|
-
) {
|
|
61
|
-
const cfgValue = !!adapter.config.season_active;
|
|
62
|
-
await adapter.setStateAsync('control.season.active', { val: cfgValue, ack: true });
|
|
63
|
-
adapter.log.debug(`[controlStates] State control.season.active initialisiert mit Wert: ${cfgValue}`);
|
|
64
|
-
}
|
|
65
|
-
|
|
66
29
|
// ---------------------------------------------------------------------
|
|
67
30
|
// Channel: control.pump
|
|
68
31
|
await adapter.setObjectNotExistsAsync('control.pump', {
|
|
@@ -347,30 +310,6 @@ async function createControlStates(adapter) {
|
|
|
347
310
|
} catch (err) {
|
|
348
311
|
adapter.log.error(`[controlStates] Fehler beim Erstellen der Control-States: ${err.message}`);
|
|
349
312
|
}
|
|
350
|
-
// ------------------------------------------------------
|
|
351
|
-
// Control – Hardware
|
|
352
|
-
// ------------------------------------------------------
|
|
353
|
-
await adapter.setObjectNotExistsAsync('control.hardware', {
|
|
354
|
-
type: 'channel',
|
|
355
|
-
common: { name: 'Hardware-Steuerung (externe Boxen)' },
|
|
356
|
-
native: {},
|
|
357
|
-
});
|
|
358
|
-
|
|
359
|
-
// TempBox aktivieren/deaktivieren
|
|
360
|
-
await adapter.setObjectNotExistsAsync('control.hardware.use_tempbox', {
|
|
361
|
-
type: 'state',
|
|
362
|
-
common: {
|
|
363
|
-
name: 'Externe Temperaturbox verwenden',
|
|
364
|
-
desc: 'Wenn aktiviert, nutzt PoolControl die Werte der externen TempBox anstelle der manuellen Temperatursensoren.',
|
|
365
|
-
type: 'boolean',
|
|
366
|
-
role: 'switch',
|
|
367
|
-
def: false,
|
|
368
|
-
read: true,
|
|
369
|
-
write: true,
|
|
370
|
-
persist: true,
|
|
371
|
-
},
|
|
372
|
-
native: {},
|
|
373
|
-
});
|
|
374
313
|
}
|
|
375
314
|
|
|
376
315
|
module.exports = { createControlStates };
|
|
@@ -199,6 +199,28 @@ async function createPumpStates3(adapter) {
|
|
|
199
199
|
await adapter.setStateAsync('pump.learning.deviation_flow_percent', { val: 0, ack: true });
|
|
200
200
|
}
|
|
201
201
|
|
|
202
|
+
// ------------------------------------------------------
|
|
203
|
+
// Toleranzbereich für Normalbereichserkennung (%)
|
|
204
|
+
// ------------------------------------------------------
|
|
205
|
+
await adapter.setObjectNotExistsAsync('pump.learning.tolerance_percent', {
|
|
206
|
+
type: 'state',
|
|
207
|
+
common: {
|
|
208
|
+
name: 'Toleranzbereich für Normalbereichserkennung (%)',
|
|
209
|
+
desc: 'Abweichung, die noch als normal gilt, bevor eine Warnung ausgelöst wird',
|
|
210
|
+
type: 'number',
|
|
211
|
+
role: 'value.percent',
|
|
212
|
+
unit: '%',
|
|
213
|
+
read: true,
|
|
214
|
+
write: true,
|
|
215
|
+
persist: true,
|
|
216
|
+
},
|
|
217
|
+
native: {},
|
|
218
|
+
});
|
|
219
|
+
const existingTolerance = await adapter.getStateAsync('pump.learning.tolerance_percent');
|
|
220
|
+
if (existingTolerance === null || existingTolerance.val === null || existingTolerance.val === undefined) {
|
|
221
|
+
await adapter.setStateAsync('pump.learning.tolerance_percent', { val: 20, ack: true });
|
|
222
|
+
}
|
|
223
|
+
|
|
202
224
|
// ------------------------------------------------------
|
|
203
225
|
// Textbewertung (Status)
|
|
204
226
|
// ------------------------------------------------------
|
|
@@ -36,7 +36,10 @@ async function createRuntimeStates(adapter) {
|
|
|
36
36
|
},
|
|
37
37
|
native: {},
|
|
38
38
|
});
|
|
39
|
-
await adapter.
|
|
39
|
+
const existingTotal = await adapter.getStateAsync('runtime.total');
|
|
40
|
+
if (!existingTotal || existingTotal.val === null || existingTotal.val === undefined) {
|
|
41
|
+
await adapter.setStateAsync('runtime.total', { val: '0h 0m 0s', ack: true }); // FIX: Nur setzen, wenn leer
|
|
42
|
+
}
|
|
40
43
|
|
|
41
44
|
// Tageslaufzeit (formatiert)
|
|
42
45
|
await adapter.setObjectNotExistsAsync('runtime.today', {
|
|
@@ -51,7 +54,10 @@ async function createRuntimeStates(adapter) {
|
|
|
51
54
|
},
|
|
52
55
|
native: {},
|
|
53
56
|
});
|
|
54
|
-
await adapter.
|
|
57
|
+
const existingToday = await adapter.getStateAsync('runtime.today');
|
|
58
|
+
if (!existingToday || existingToday.val === null || existingToday.val === undefined) {
|
|
59
|
+
await adapter.setStateAsync('runtime.today', { val: '0h 0m 0s', ack: true }); // FIX
|
|
60
|
+
}
|
|
55
61
|
|
|
56
62
|
// -------------------------------------------------------------------------
|
|
57
63
|
// NEU: Pumpenstarts heute
|
|
@@ -67,7 +73,10 @@ async function createRuntimeStates(adapter) {
|
|
|
67
73
|
},
|
|
68
74
|
native: {},
|
|
69
75
|
});
|
|
70
|
-
await adapter.
|
|
76
|
+
const existingStartCount = await adapter.getStateAsync('runtime.start_count_today');
|
|
77
|
+
if (!existingStartCount || existingStartCount.val === null || existingStartCount.val === undefined) {
|
|
78
|
+
await adapter.setStateAsync('runtime.start_count_today', { val: 0, ack: true }); // FIX
|
|
79
|
+
}
|
|
71
80
|
|
|
72
81
|
// NEU: Aktuelle Laufzeit (seit Einschalten)
|
|
73
82
|
await adapter.setObjectNotExistsAsync('runtime.current_session', {
|
|
@@ -82,7 +91,10 @@ async function createRuntimeStates(adapter) {
|
|
|
82
91
|
},
|
|
83
92
|
native: {},
|
|
84
93
|
});
|
|
85
|
-
await adapter.
|
|
94
|
+
const existingCurrent = await adapter.getStateAsync('runtime.current_session');
|
|
95
|
+
if (!existingCurrent || existingCurrent.val === null || existingCurrent.val === undefined) {
|
|
96
|
+
await adapter.setStateAsync('runtime.current_session', { val: '0h 0m 0s', ack: true }); // FIX
|
|
97
|
+
}
|
|
86
98
|
|
|
87
99
|
// NEU: Gesamtlaufzeit der aktuellen Saison (formatiert)
|
|
88
100
|
await adapter.setObjectNotExistsAsync('runtime.season_total', {
|
|
@@ -97,7 +109,10 @@ async function createRuntimeStates(adapter) {
|
|
|
97
109
|
},
|
|
98
110
|
native: {},
|
|
99
111
|
});
|
|
100
|
-
await adapter.
|
|
112
|
+
const existingSeason = await adapter.getStateAsync('runtime.season_total');
|
|
113
|
+
if (!existingSeason || existingSeason.val === null || existingSeason.val === undefined) {
|
|
114
|
+
await adapter.setStateAsync('runtime.season_total', { val: '0h 0m 0s', ack: true }); // FIX
|
|
115
|
+
}
|
|
101
116
|
|
|
102
117
|
// -------------------------------------------------------------------------
|
|
103
118
|
// --- Kanal circulation ---
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* statisticsStates.js
|
|
5
|
+
* -------------------
|
|
6
|
+
* Erstellt alle States für die Tagesstatistik der Temperatursensoren.
|
|
7
|
+
* Struktur: analytics.statistics.temperature.today.*
|
|
8
|
+
*
|
|
9
|
+
* - Sechs Sensorbereiche (outside, ground, surface, flow, return, collector)
|
|
10
|
+
* - Je Sensor: Min/Max/Avg + Zeitstempel + JSON/HTML-Ausgabe
|
|
11
|
+
* - Zusätzlich: Gesamt-Ausgabe (summary_all_json / summary_all_html)
|
|
12
|
+
*
|
|
13
|
+
* Alle States sind persistiert, schreibgeschützt und überinstallationssicher.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* @param {ioBroker.Adapter} adapter - Instanz des ioBroker-Adapters
|
|
18
|
+
*/
|
|
19
|
+
async function createStatisticsStates(adapter) {
|
|
20
|
+
adapter.log.debug('statisticsStates: Initialisierung der Tagesstatistik (Temperatur) gestartet.');
|
|
21
|
+
|
|
22
|
+
// Oberstruktur
|
|
23
|
+
await adapter.setObjectNotExistsAsync('analytics', {
|
|
24
|
+
type: 'channel',
|
|
25
|
+
common: { name: 'Analysen & Auswertungen (Statistik, Historie, Berichte)' },
|
|
26
|
+
native: {},
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
await adapter.setObjectNotExistsAsync('analytics.statistics', {
|
|
30
|
+
type: 'channel',
|
|
31
|
+
common: { name: 'Statistische Auswertungen' },
|
|
32
|
+
native: {},
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
await adapter.setObjectNotExistsAsync('analytics.statistics.temperature', {
|
|
36
|
+
type: 'channel',
|
|
37
|
+
common: { name: 'Temperaturstatistik' },
|
|
38
|
+
native: {},
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
await adapter.setObjectNotExistsAsync('analytics.statistics.temperature.today', {
|
|
42
|
+
type: 'channel',
|
|
43
|
+
common: { name: 'Tagesstatistik (Temperaturen)' },
|
|
44
|
+
native: {},
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
// Definierte Sensoren
|
|
48
|
+
const sensors = [
|
|
49
|
+
{ id: 'outside', name: 'Außentemperatur' },
|
|
50
|
+
{ id: 'ground', name: 'Bodentemperatur' },
|
|
51
|
+
{ id: 'surface', name: 'Pooloberfläche' },
|
|
52
|
+
{ id: 'flow', name: 'Vorlauf' },
|
|
53
|
+
{ id: 'return', name: 'Rücklauf' },
|
|
54
|
+
{ id: 'collector', name: 'Kollektor (Solar)' },
|
|
55
|
+
];
|
|
56
|
+
|
|
57
|
+
for (const sensor of sensors) {
|
|
58
|
+
const basePath = `analytics.statistics.temperature.today.${sensor.id}`;
|
|
59
|
+
|
|
60
|
+
await adapter.setObjectNotExistsAsync(basePath, {
|
|
61
|
+
type: 'channel',
|
|
62
|
+
common: { name: `${sensor.name} (Tagesstatistik)` },
|
|
63
|
+
native: {},
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
const stateDefs = [
|
|
67
|
+
{ id: 'temp_min', name: 'Niedrigste Temperatur', type: 'number', role: 'value.temperature', unit: '°C' },
|
|
68
|
+
{ id: 'temp_max', name: 'Höchste Temperatur', type: 'number', role: 'value.temperature', unit: '°C' },
|
|
69
|
+
{ id: 'temp_min_time', name: 'Zeitpunkt Minimum', type: 'string', role: 'value.time' },
|
|
70
|
+
{ id: 'temp_max_time', name: 'Zeitpunkt Maximum', type: 'string', role: 'value.time' },
|
|
71
|
+
{ id: 'temp_avg', name: 'Durchschnittstemperatur', type: 'number', role: 'value.temperature', unit: '°C' },
|
|
72
|
+
{ id: 'data_points_count', name: 'Anzahl Messwerte', type: 'number', role: 'value' },
|
|
73
|
+
{ id: 'last_update', name: 'Letzte Aktualisierung', type: 'string', role: 'value.time' },
|
|
74
|
+
{ id: 'summary_json', name: 'Tageszusammenfassung (JSON)', type: 'string', role: 'json' },
|
|
75
|
+
{ id: 'summary_html', name: 'Tageszusammenfassung (HTML)', type: 'string', role: 'html' },
|
|
76
|
+
];
|
|
77
|
+
|
|
78
|
+
for (const def of stateDefs) {
|
|
79
|
+
await adapter.setObjectNotExistsAsync(`${basePath}.${def.id}`, {
|
|
80
|
+
type: 'state',
|
|
81
|
+
common: {
|
|
82
|
+
name: def.name,
|
|
83
|
+
type: def.type,
|
|
84
|
+
role: def.role,
|
|
85
|
+
unit: def.unit || undefined,
|
|
86
|
+
read: true,
|
|
87
|
+
write: false,
|
|
88
|
+
def: def.type === 'number' ? null : '',
|
|
89
|
+
persist: true,
|
|
90
|
+
},
|
|
91
|
+
native: {},
|
|
92
|
+
});
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// Gesamt-Ausgabe (Outputs)
|
|
97
|
+
const outputBase = 'analytics.statistics.temperature.today.outputs';
|
|
98
|
+
await adapter.setObjectNotExistsAsync(outputBase, {
|
|
99
|
+
type: 'channel',
|
|
100
|
+
common: { name: 'Gesamtausgaben (alle Sensoren)' },
|
|
101
|
+
native: {},
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
const outputs = [
|
|
105
|
+
{
|
|
106
|
+
id: 'summary_all_json',
|
|
107
|
+
name: 'Gesamtzusammenfassung aller Sensoren (JSON)',
|
|
108
|
+
role: 'json',
|
|
109
|
+
},
|
|
110
|
+
{
|
|
111
|
+
id: 'summary_all_html',
|
|
112
|
+
name: 'Gesamtzusammenfassung aller Sensoren (HTML)',
|
|
113
|
+
role: 'html',
|
|
114
|
+
},
|
|
115
|
+
];
|
|
116
|
+
|
|
117
|
+
for (const out of outputs) {
|
|
118
|
+
await adapter.setObjectNotExistsAsync(`${outputBase}.${out.id}`, {
|
|
119
|
+
type: 'state',
|
|
120
|
+
common: {
|
|
121
|
+
name: out.name,
|
|
122
|
+
type: 'string',
|
|
123
|
+
role: out.role,
|
|
124
|
+
read: true,
|
|
125
|
+
write: false,
|
|
126
|
+
def: '',
|
|
127
|
+
persist: true,
|
|
128
|
+
},
|
|
129
|
+
native: {},
|
|
130
|
+
});
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
adapter.log.debug('statisticsStates: Tagesstatistik (Temperatur) erfolgreich angelegt.');
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
module.exports = {
|
|
137
|
+
createStatisticsStates,
|
|
138
|
+
};
|