iobroker.poolcontrol 0.2.2 → 0.3.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.
@@ -12,6 +12,7 @@ const statusHelper = {
12
12
  adapter: null,
13
13
  midnightTimer: null,
14
14
  pumpOn: null, // interner Merker für Pumpenstatus
15
+ _lastSummaryUpdate: 0, // FIX: Zeitstempel für Throttle-Schutz
15
16
 
16
17
  init(adapter) {
17
18
  this.adapter = adapter;
@@ -95,8 +96,23 @@ const statusHelper = {
95
96
  await this.updateSummary();
96
97
  },
97
98
 
99
+ // FIX: Hilfsfunktion zur sicheren Formatierung
100
+ safeValue(v, digits = 1) {
101
+ if (v == null || isNaN(v)) {
102
+ return '–';
103
+ }
104
+ return Number(v).toFixed(digits);
105
+ },
106
+
98
107
  async updateSummary() {
99
108
  try {
109
+ // FIX: Throttle - Mehrfachupdates innerhalb 1 Sekunde vermeiden
110
+ if (Date.now() - this._lastSummaryUpdate < 1000) {
111
+ this.adapter.log.debug('[statusHelper] UpdateSummary übersprungen (Throttle)');
112
+ return;
113
+ }
114
+ this._lastSummaryUpdate = Date.now();
115
+
100
116
  // Werte laden
101
117
  const pumpStatus = (await this.adapter.getStateAsync('pump.status'))?.val || 'unbekannt';
102
118
  const pumpMode = (await this.adapter.getStateAsync('pump.mode'))?.val || 'unknown';
@@ -112,32 +128,41 @@ const statusHelper = {
112
128
  // Laufzeit formatieren
113
129
  const h = Math.floor(runtimeToday / 3600);
114
130
  const m = Math.floor((runtimeToday % 3600) / 60);
115
- const runtimeFormatted = `${h}h ${m}m`;
131
+ const runtimeFormatted = isNaN(h) || isNaN(m) ? '0h 00m' : `${h}h ${m}m`; // FIX: Schutz gegen NaN
116
132
 
117
133
  // Umwälzungsquote
118
134
  let circulationPct = 0;
119
135
  if (dailyRequired > 0) {
120
136
  circulationPct = Math.round((dailyTotal / dailyRequired) * 100);
121
137
  }
138
+ if (isNaN(circulationPct)) {
139
+ circulationPct = 0;
140
+ } // FIX: NaN-Absicherung
122
141
 
123
142
  // Text bauen
124
143
  let text = `Pumpe: ${pumpStatus}`;
125
144
  if (pumpMode && pumpMode !== 'unknown') {
126
145
  text += ` (Modus: ${pumpMode})`;
127
146
  }
147
+
148
+ const safe = this.safeValue.bind(this); // FIX: Kurzreferenz
128
149
  if (poolTemp != null) {
129
- text += `. Pool: ${poolTemp.toFixed(1)} °C`;
150
+ text += `. Pool: ${safe(poolTemp)} °C`;
130
151
  }
131
152
  if (collectorTemp != null) {
132
- text += `, Kollektor: ${collectorTemp.toFixed(1)} °C`;
153
+ text += `, Kollektor: ${safe(collectorTemp)} °C`;
133
154
  }
134
155
  if (outsideTemp != null) {
135
- text += `, Außentemperatur: ${outsideTemp.toFixed(1)} °C`;
156
+ text += `, Außentemperatur: ${safe(outsideTemp)} °C`;
136
157
  }
137
158
  text += `. Tageslaufzeit: ${runtimeFormatted} (${circulationPct}% der Soll-Umwälzung).`;
138
159
 
139
- // In States schreiben
140
- await this.adapter.setStateAsync('status.summary', { val: text, ack: true });
160
+ // In States schreiben (nur bei Änderung)
161
+ const current = (await this.adapter.getStateAsync('status.summary'))?.val;
162
+ if (current !== text) {
163
+ await this.adapter.setStateAsync('status.summary', { val: text, ack: true });
164
+ }
165
+
141
166
  await this.adapter.setStateAsync('status.last_summary_update', {
142
167
  val: new Date().toISOString(),
143
168
  ack: true,
@@ -147,10 +172,10 @@ const statusHelper = {
147
172
  const json = {
148
173
  pump: pumpStatus,
149
174
  mode: pumpMode,
150
- pool: poolTemp,
151
- collector: collectorTemp,
152
- outside: outsideTemp,
153
- runtime_today: runtimeToday,
175
+ pool: poolTemp ?? null,
176
+ collector: collectorTemp ?? null,
177
+ outside: outsideTemp ?? null,
178
+ runtime_today: runtimeToday ?? 0,
154
179
  runtime_formatted: runtimeFormatted,
155
180
  circulation_pct: circulationPct,
156
181
  };
@@ -36,7 +36,7 @@ async function createControlStates(adapter) {
36
36
  native: {},
37
37
  });
38
38
 
39
- // State: control.season.active
39
+ // State: control.season.active (mit Persist-Schutz)
40
40
  await adapter.setObjectNotExistsAsync('control.season.active', {
41
41
  type: 'state',
42
42
  common: {
@@ -47,15 +47,21 @@ async function createControlStates(adapter) {
47
47
  read: true,
48
48
  write: true,
49
49
  def: false,
50
+ persist: true, // dauerhaft speichern
50
51
  },
51
52
  native: {},
52
53
  });
53
54
 
54
- // Initialwert aus der Instanzkonfiguration übernehmen
55
- const cfgValue = !!adapter.config.season_active;
56
- await adapter.setStateAsync('control.season.active', { val: cfgValue, ack: true });
57
-
58
- adapter.log.debug(`[controlStates] State control.season.active initialisiert mit Wert: ${cfgValue}`);
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
+ }
59
65
 
60
66
  // ---------------------------------------------------------------------
61
67
  // Channel: control.pump
@@ -100,7 +106,7 @@ async function createControlStates(adapter) {
100
106
  });
101
107
  await adapter.setStateAsync('control.pump.backwash_active', { val: false, ack: true });
102
108
 
103
- // Rückspülungsdauer
109
+ // Rückspülungsdauer (mit Persist-Schutz)
104
110
  await adapter.setObjectNotExistsAsync('control.pump.backwash_duration', {
105
111
  type: 'state',
106
112
  common: {
@@ -113,12 +119,20 @@ async function createControlStates(adapter) {
113
119
  def: 1,
114
120
  min: 1,
115
121
  max: 60,
122
+ persist: true, // dauerhaft speichern
116
123
  },
117
124
  native: {},
118
125
  });
119
- await adapter.setStateAsync('control.pump.backwash_duration', { val: 1, ack: true });
126
+ const existingBackwashDuration = await adapter.getStateAsync('control.pump.backwash_duration');
127
+ if (
128
+ existingBackwashDuration === null ||
129
+ existingBackwashDuration.val === null ||
130
+ existingBackwashDuration.val === undefined
131
+ ) {
132
+ await adapter.setStateAsync('control.pump.backwash_duration', { val: 1, ack: true });
133
+ }
120
134
 
121
- // >>> NEU: Rückspülerinnerung
135
+ // Rückspülerinnerung aktiv (mit Persist-Schutz)
122
136
  await adapter.setObjectNotExistsAsync('control.pump.backwash_reminder_active', {
123
137
  type: 'state',
124
138
  common: {
@@ -129,11 +143,16 @@ async function createControlStates(adapter) {
129
143
  read: true,
130
144
  write: true,
131
145
  def: false,
146
+ persist: true, // dauerhaft speichern
132
147
  },
133
148
  native: {},
134
149
  });
135
- await adapter.setStateAsync('control.pump.backwash_reminder_active', { val: false, ack: true });
150
+ const existingBackwashRem = await adapter.getStateAsync('control.pump.backwash_reminder_active');
151
+ if (existingBackwashRem === null || existingBackwashRem.val === null || existingBackwashRem.val === undefined) {
152
+ await adapter.setStateAsync('control.pump.backwash_reminder_active', { val: false, ack: true });
153
+ }
136
154
 
155
+ // Rückspülintervall (mit Persist-Schutz)
137
156
  await adapter.setObjectNotExistsAsync('control.pump.backwash_interval_days', {
138
157
  type: 'state',
139
158
  common: {
@@ -147,10 +166,18 @@ async function createControlStates(adapter) {
147
166
  def: 7,
148
167
  min: 1,
149
168
  max: 60,
169
+ persist: true, // dauerhaft speichern
150
170
  },
151
171
  native: {},
152
172
  });
153
- await adapter.setStateAsync('control.pump.backwash_interval_days', { val: 7, ack: true });
173
+ const existingBackwashInterval = await adapter.getStateAsync('control.pump.backwash_interval_days');
174
+ if (
175
+ existingBackwashInterval === null ||
176
+ existingBackwashInterval.val === null ||
177
+ existingBackwashInterval.val === undefined
178
+ ) {
179
+ await adapter.setStateAsync('control.pump.backwash_interval_days', { val: 7, ack: true });
180
+ }
154
181
 
155
182
  await adapter.setObjectNotExistsAsync('control.pump.backwash_last_date', {
156
183
  type: 'state',
@@ -181,7 +208,6 @@ async function createControlStates(adapter) {
181
208
  });
182
209
  await adapter.setStateAsync('control.pump.backwash_required', { val: false, ack: true });
183
210
 
184
- // ---------------------------------------------------------------------
185
211
  // Wartungsmodus aktiv
186
212
  await adapter.setObjectNotExistsAsync('control.pump.maintenance_active', {
187
213
  type: 'state',
@@ -198,7 +224,7 @@ async function createControlStates(adapter) {
198
224
  });
199
225
  await adapter.setStateAsync('control.pump.maintenance_active', { val: false, ack: true });
200
226
 
201
- // Benachrichtigungen aktivieren
227
+ // Benachrichtigungen aktivieren (mit Persist-Schutz)
202
228
  await adapter.setObjectNotExistsAsync('control.pump.notifications_enabled', {
203
229
  type: 'state',
204
230
  common: {
@@ -209,10 +235,14 @@ async function createControlStates(adapter) {
209
235
  read: true,
210
236
  write: true,
211
237
  def: true,
238
+ persist: true, // dauerhaft speichern
212
239
  },
213
240
  native: {},
214
241
  });
215
- await adapter.setStateAsync('control.pump.notifications_enabled', { val: true, ack: true });
242
+ const existingNoti = await adapter.getStateAsync('control.pump.notifications_enabled');
243
+ if (existingNoti === null || existingNoti.val === null || existingNoti.val === undefined) {
244
+ await adapter.setStateAsync('control.pump.notifications_enabled', { val: true, ack: true });
245
+ }
216
246
 
217
247
  // ---------------------------------------------------------------------
218
248
  // Channel: control.energy
@@ -252,7 +282,7 @@ async function createControlStates(adapter) {
252
282
  native: {},
253
283
  });
254
284
 
255
- // State: Modus (auto/manual/notify/off)
285
+ // Modus der Umwälzungsprüfung
256
286
  await adapter.setObjectNotExistsAsync('control.circulation.mode', {
257
287
  type: 'state',
258
288
  common: {
@@ -263,6 +293,7 @@ async function createControlStates(adapter) {
263
293
  read: true,
264
294
  write: true,
265
295
  def: 'notify',
296
+ persist: true,
266
297
  states: {
267
298
  auto: 'Automatik',
268
299
  manual: 'Manuell',
@@ -272,9 +303,14 @@ async function createControlStates(adapter) {
272
303
  },
273
304
  native: {},
274
305
  });
306
+ try {
307
+ await adapter.extendObjectAsync('control.circulation.mode', { common: { persist: true } });
308
+ } catch (err) {
309
+ adapter.log.warn(`[controlStates] persist-Flag für control.circulation.mode nicht gesetzt: ${err.message}`);
310
+ }
275
311
  await adapter.setStateAsync('control.circulation.mode', { val: 'notify', ack: true });
276
312
 
277
- // State: Prüfzeitpunkt
313
+ // Prüfzeitpunkt (mit Persist-Schutz)
278
314
  await adapter.setObjectNotExistsAsync('control.circulation.check_time', {
279
315
  type: 'state',
280
316
  common: {
@@ -285,12 +321,16 @@ async function createControlStates(adapter) {
285
321
  read: true,
286
322
  write: true,
287
323
  def: '18:00',
324
+ persist: true, // dauerhaft speichern
288
325
  },
289
326
  native: {},
290
327
  });
291
- await adapter.setStateAsync('control.circulation.check_time', { val: '18:00', ack: true });
328
+ const existingCheckTime = await adapter.getStateAsync('control.circulation.check_time');
329
+ if (existingCheckTime === null || existingCheckTime.val === null || existingCheckTime.val === undefined) {
330
+ await adapter.setStateAsync('control.circulation.check_time', { val: '18:00', ack: true });
331
+ }
292
332
 
293
- // State: letzter Bericht
333
+ // letzter Bericht
294
334
  await adapter.setObjectNotExistsAsync('control.circulation.last_report', {
295
335
  type: 'state',
296
336
  common: {
@@ -98,7 +98,7 @@ async function createPumpStates(adapter) {
98
98
  });
99
99
  await adapter.setStateAsync('pump.pump_switch', { val: false, ack: true });
100
100
 
101
- // Pumpenmodus
101
+ // Pumpenmodus (mit Persist-Schutz)
102
102
  await adapter.setObjectNotExistsAsync('pump.mode', {
103
103
  type: 'state',
104
104
  common: {
@@ -107,6 +107,7 @@ async function createPumpStates(adapter) {
107
107
  role: 'state',
108
108
  read: true,
109
109
  write: true,
110
+ persist: true, // NEU: dauerhaft speichern
110
111
  states: {
111
112
  auto: 'Automatik',
112
113
  manual: 'Manuell',
@@ -116,9 +117,14 @@ async function createPumpStates(adapter) {
116
117
  },
117
118
  native: {},
118
119
  });
119
- await adapter.setStateAsync('pump.mode', { val: 'auto', ack: true });
120
120
 
121
- // Sicherheitslogik im manuellen Modus
121
+ // Prüfen, ob bereits ein Wert vorhanden ist
122
+ const existingPumpMode = await adapter.getStateAsync('pump.mode');
123
+ if (existingPumpMode === null || existingPumpMode.val === null || existingPumpMode.val === undefined) {
124
+ await adapter.setStateAsync('pump.mode', { val: 'auto', ack: true });
125
+ }
126
+
127
+ // Sicherheitslogik im manuellen Modus (mit Persist-Schutz)
122
128
  await adapter.setObjectNotExistsAsync('pump.manual_safety_enabled', {
123
129
  type: 'state',
124
130
  common: {
@@ -127,13 +133,19 @@ async function createPumpStates(adapter) {
127
133
  role: 'switch',
128
134
  read: true,
129
135
  write: true,
136
+ persist: true, // NEU: dauerhaft speichern
130
137
  },
131
138
  native: {},
132
139
  });
133
- await adapter.setStateAsync('pump.manual_safety_enabled', {
134
- val: adapter.config.manual_safety_enabled ?? true,
135
- ack: true,
136
- });
140
+
141
+ // Prüfen, ob bereits ein persistierter Wert vorhanden ist
142
+ const existingSafety = await adapter.getStateAsync('pump.manual_safety_enabled');
143
+ if (existingSafety === null || existingSafety.val === null || existingSafety.val === undefined) {
144
+ await adapter.setStateAsync('pump.manual_safety_enabled', {
145
+ val: adapter.config.manual_safety_enabled ?? true,
146
+ ack: true,
147
+ });
148
+ }
137
149
 
138
150
  // Pumpenstatus (Text)
139
151
  await adapter.setObjectNotExistsAsync('pump.status', {
@@ -196,6 +208,20 @@ async function createPumpStates(adapter) {
196
208
  native: {},
197
209
  });
198
210
  await adapter.setStateAsync('pump.current_power', { val: 0, ack: true });
211
+
212
+ await adapter.setObjectNotExistsAsync('pump.reason', {
213
+ type: 'state',
214
+ common: {
215
+ name: 'Grund für aktuellen Pumpenstatus',
216
+ desc: 'Wird intern verwendet, um den Grund des aktuellen Pumpenstatus zu speichern (z. B. Rückspülen, Wartung, Nachpumpen)',
217
+ type: 'string',
218
+ role: 'text',
219
+ read: true,
220
+ write: true,
221
+ },
222
+ native: {},
223
+ });
224
+ await adapter.setStateAsync('pump.reason', { val: '', ack: true });
199
225
  }
200
226
 
201
227
  module.exports = {
@@ -0,0 +1,125 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * pumpStates2.js
5
+ * ----------------------------------------------------------
6
+ * Legt die erweiterten States für Pumpen-Livewerte an.
7
+ *
8
+ * Diese Datei ergänzt die bestehenden pumpStates.js um
9
+ * reelle Durchflussberechnung und Leistungsdaten.
10
+ *
11
+ * Ordnerstruktur:
12
+ * pump.live.*
13
+ * ├── current_power_w (W) - aktuelle Leistung
14
+ * ├── flow_current_lh (L/h) - reell berechneter Durchfluss
15
+ * ├── flow_percent (%) - aktuelle Auslastung
16
+ * ├── last_flow_lh (L/h) - letzter bekannter Durchfluss
17
+ *
18
+ * Alle States sind persistent und bleiben über Updates erhalten.
19
+ * ----------------------------------------------------------
20
+ * Version: 1.0.0
21
+ */
22
+
23
+ /**
24
+ * Erstellt die erweiterten Pumpen-Live-States.
25
+ *
26
+ * @param {import("iobroker").Adapter} adapter - Aktive ioBroker Adapterinstanz
27
+ * @returns {Promise<void>}
28
+ */
29
+ async function createPumpStates2(adapter) {
30
+ // ------------------------------------------------------
31
+ // Root-Kanal: pump.live
32
+ // ------------------------------------------------------
33
+ await adapter.setObjectNotExistsAsync('pump.live', {
34
+ type: 'channel',
35
+ common: {
36
+ name: 'Pumpen-Livewerte (aktuelle Betriebsdaten)',
37
+ },
38
+ native: {},
39
+ });
40
+
41
+ // ------------------------------------------------------
42
+ // Aktuelle Leistung (W)
43
+ // ------------------------------------------------------
44
+ await adapter.setObjectNotExistsAsync('pump.live.current_power_w', {
45
+ type: 'state',
46
+ common: {
47
+ name: 'Aktuelle elektrische Leistung',
48
+ desc: 'Momentane Leistungsaufnahme der Pumpe (Watt)',
49
+ type: 'number',
50
+ role: 'value.power',
51
+ unit: 'W',
52
+ read: true,
53
+ write: false,
54
+ persist: true,
55
+ },
56
+ native: {},
57
+ });
58
+ await adapter.setStateAsync('pump.live.current_power_w', { val: 0, ack: true });
59
+
60
+ // ------------------------------------------------------
61
+ // Reell berechneter Durchfluss (L/h)
62
+ // ------------------------------------------------------
63
+ await adapter.setObjectNotExistsAsync('pump.live.flow_current_lh', {
64
+ type: 'state',
65
+ common: {
66
+ name: 'Aktueller reeller Durchfluss',
67
+ desc: 'Berechnete Umwälzleistung basierend auf aktueller Leistung',
68
+ type: 'number',
69
+ role: 'value.flow',
70
+ unit: 'l/h',
71
+ read: true,
72
+ write: false,
73
+ persist: true,
74
+ },
75
+ native: {},
76
+ });
77
+ await adapter.setStateAsync('pump.live.flow_current_lh', { val: 0, ack: true });
78
+
79
+ // ------------------------------------------------------
80
+ // Prozentuale Auslastung (%)
81
+ // ------------------------------------------------------
82
+ await adapter.setObjectNotExistsAsync('pump.live.flow_percent', {
83
+ type: 'state',
84
+ common: {
85
+ name: 'Aktuelle Pumpenauslastung (%)',
86
+ desc: 'Aktuelle Pumpenleistung in Prozent der maximalen Leistung',
87
+ type: 'number',
88
+ role: 'value.percent',
89
+ unit: '%',
90
+ read: true,
91
+ write: false,
92
+ persist: true,
93
+ },
94
+ native: {},
95
+ });
96
+ await adapter.setStateAsync('pump.live.flow_percent', { val: 0, ack: true });
97
+
98
+ // ------------------------------------------------------
99
+ // Letzter bekannter Durchfluss (L/h)
100
+ // ------------------------------------------------------
101
+ await adapter.setObjectNotExistsAsync('pump.live.last_flow_lh', {
102
+ type: 'state',
103
+ common: {
104
+ name: 'Letzter bekannter Durchfluss',
105
+ desc: 'Speichert den letzten berechneten Durchflusswert vor Pumpenstopp',
106
+ type: 'number',
107
+ role: 'value.flow',
108
+ unit: 'l/h',
109
+ read: true,
110
+ write: false,
111
+ persist: true,
112
+ },
113
+ native: {},
114
+ });
115
+ await adapter.setStateAsync('pump.live.last_flow_lh', { val: 0, ack: true });
116
+
117
+ // ------------------------------------------------------
118
+ // Abschluss-Logeintrag
119
+ // ------------------------------------------------------
120
+ adapter.log.debug('[pumpStates2] Live-State-Definitionen erstellt oder geprüft.');
121
+ }
122
+
123
+ module.exports = {
124
+ createPumpStates2,
125
+ };