iobroker.poolcontrol 1.3.2 → 1.3.3

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.
@@ -0,0 +1,815 @@
1
+ 'use strict';
2
+
3
+ const { I18n } = require('@iobroker/adapter-core');
4
+
5
+ /**
6
+ * solarInsightsHelper
7
+ * - Analysiert verfügbare und verwendete Sensoren für analytics.insights.solar
8
+ * - Prüft, ob Solar heute überhaupt gelaufen ist
9
+ * - Schreibt Transparenz-, Referenz-, Leistungs- und Tagesertrags-States
10
+ * - Arbeitet eventbasiert mit debounce per adapter.setTimeout
11
+ */
12
+
13
+ const solarInsightsHelper = {
14
+ adapter: null,
15
+ checkTimer: null,
16
+ resetTimer: null,
17
+ lastCheckTimestamp: null,
18
+
19
+ init(adapter) {
20
+ this.adapter = adapter;
21
+
22
+ void this._subscribeStates();
23
+ this._scheduleDailyReset();
24
+ this._scheduleCheck(0);
25
+
26
+ this.adapter.log.debug('[solarInsightsHelper] Initialized (event-based precheck)');
27
+ },
28
+
29
+ onStateChange(id, state) {
30
+ if (!state || state.ack !== true) {
31
+ return;
32
+ }
33
+
34
+ if (!this._isRelevantState(id)) {
35
+ return;
36
+ }
37
+
38
+ // Tages-Latch sofort setzen, wenn Solar aktiv wird
39
+ if ((id === 'solar.active' || id === 'solar.extended.active') && state.val === true) {
40
+ void this.adapter.setStateChangedAsync('analytics.insights.solar.results.solar_ran_today', {
41
+ val: true,
42
+ ack: true,
43
+ });
44
+ }
45
+
46
+ this._scheduleCheck(250);
47
+ },
48
+
49
+ _scheduleCheck(delayMs = 0) {
50
+ if (this.checkTimer) {
51
+ this.adapter.clearTimeout(this.checkTimer);
52
+ this.checkTimer = null;
53
+ }
54
+
55
+ this.checkTimer = this.adapter.setTimeout(() => {
56
+ this.checkTimer = null;
57
+ void this._checkSolarInsights();
58
+ }, delayMs);
59
+ },
60
+
61
+ _scheduleDailyReset() {
62
+ if (this.resetTimer) {
63
+ this.adapter.clearTimeout(this.resetTimer);
64
+ this.resetTimer = null;
65
+ }
66
+
67
+ const now = new Date();
68
+ const next = new Date(now);
69
+ next.setDate(now.getDate() + 1);
70
+ next.setHours(0, 0, 5, 0);
71
+
72
+ const delay = Math.max(1000, next.getTime() - now.getTime());
73
+
74
+ this.resetTimer = this.adapter.setTimeout(async () => {
75
+ try {
76
+ await this.adapter.setStateChangedAsync('analytics.insights.solar.results.solar_ran_today', {
77
+ val: false,
78
+ ack: true,
79
+ });
80
+
81
+ await this.adapter.setStateChangedAsync('analytics.insights.solar.results.estimated_gain_today_wh', {
82
+ val: 0,
83
+ ack: true,
84
+ });
85
+
86
+ await this.adapter.setStateChangedAsync('analytics.insights.solar.results.estimated_gain_today_kwh', {
87
+ val: 0,
88
+ ack: true,
89
+ });
90
+
91
+ await this.adapter.setStateChangedAsync('analytics.insights.solar.results.active_minutes_today', {
92
+ val: 0,
93
+ ack: true,
94
+ });
95
+
96
+ await this.adapter.setStateChangedAsync('analytics.insights.solar.results.peak_power_today_w', {
97
+ val: 0,
98
+ ack: true,
99
+ });
100
+
101
+ this.lastCheckTimestamp = null;
102
+
103
+ await this.adapter.setStateChangedAsync('analytics.insights.solar.debug.last_update', {
104
+ val: new Date().toISOString(),
105
+ ack: true,
106
+ });
107
+
108
+ await this.adapter.setStateChangedAsync('analytics.insights.solar.debug.last_recalculation_reason', {
109
+ val: this._t('solar_insights_reason_daily_reset'),
110
+ ack: true,
111
+ });
112
+
113
+ await this.adapter.setStateChangedAsync('analytics.insights.solar.debug.debug_text', {
114
+ val: this._t('solar_insights_debug_daily_reset_executed'),
115
+ ack: true,
116
+ });
117
+
118
+ this.adapter.log.debug('[solarInsightsHelper] Daily reset executed');
119
+ } catch (err) {
120
+ this.adapter.log.warn(`[solarInsightsHelper] Daily reset failed: ${err.message}`);
121
+ }
122
+
123
+ this._scheduleDailyReset();
124
+ }, delay);
125
+ },
126
+
127
+ async _subscribeStates() {
128
+ const ids = [
129
+ 'temperature.collector_temp_active',
130
+ 'temperature.surface_temp_active',
131
+ 'temperature.ground_temp_active',
132
+ 'temperature.flow_temp_active',
133
+ 'temperature.return_temp_active',
134
+ 'temperature.outside_temp_active',
135
+
136
+ 'temperature.collector.current',
137
+ 'temperature.surface.current',
138
+ 'temperature.ground.current',
139
+ 'temperature.flow.current',
140
+ 'temperature.return.current',
141
+ 'temperature.outside.current',
142
+
143
+ 'pump.live.flow_current_lh',
144
+ 'pump.live.current_power_w',
145
+ 'temperature.delta.surface_ground',
146
+
147
+ 'solar.active',
148
+ 'solar.extended.active',
149
+ 'solar.control_mode',
150
+
151
+ 'ai.weather.outputs.daily_summary',
152
+ ];
153
+
154
+ for (const id of ids) {
155
+ await this.adapter.subscribeStatesAsync(id);
156
+ }
157
+
158
+ this.adapter.log.debug('[solarInsightsHelper] Relevant states subscribed');
159
+ },
160
+
161
+ _isRelevantState(id) {
162
+ const ids = [
163
+ 'temperature.collector_temp_active',
164
+ 'temperature.surface_temp_active',
165
+ 'temperature.ground_temp_active',
166
+ 'temperature.flow_temp_active',
167
+ 'temperature.return_temp_active',
168
+ 'temperature.outside_temp_active',
169
+
170
+ 'temperature.collector.current',
171
+ 'temperature.surface.current',
172
+ 'temperature.ground.current',
173
+ 'temperature.flow.current',
174
+ 'temperature.return.current',
175
+ 'temperature.outside.current',
176
+
177
+ 'pump.live.flow_current_lh',
178
+ 'pump.live.current_power_w',
179
+ 'temperature.delta.surface_ground',
180
+
181
+ 'solar.active',
182
+ 'solar.extended.active',
183
+ 'solar.control_mode',
184
+
185
+ 'ai.weather.outputs.daily_summary',
186
+ ];
187
+
188
+ return ids.includes(id);
189
+ },
190
+
191
+ async _checkSolarInsights() {
192
+ try {
193
+ const collectorAvailable = await this._isSensorAvailable(
194
+ 'temperature.collector_temp_active',
195
+ 'temperature.collector.current',
196
+ );
197
+ const surfaceAvailable = await this._isSensorAvailable(
198
+ 'temperature.surface_temp_active',
199
+ 'temperature.surface.current',
200
+ );
201
+ const groundAvailable = await this._isSensorAvailable(
202
+ 'temperature.ground_temp_active',
203
+ 'temperature.ground.current',
204
+ );
205
+ const flowTempAvailable = await this._isSensorAvailable(
206
+ 'temperature.flow_temp_active',
207
+ 'temperature.flow.current',
208
+ );
209
+ const returnAvailable = await this._isSensorAvailable(
210
+ 'temperature.return_temp_active',
211
+ 'temperature.return.current',
212
+ );
213
+ const outsideAvailable = await this._isSensorAvailable(
214
+ 'temperature.outside_temp_active',
215
+ 'temperature.outside.current',
216
+ );
217
+
218
+ const collectorTemp = await this._readNumber('temperature.collector.current');
219
+ const surfaceTemp = await this._readNumber('temperature.surface.current');
220
+ const groundTemp = await this._readNumber('temperature.ground.current');
221
+ const outsideTemp = await this._readNumber('temperature.outside.current');
222
+ const currentFlowLh = await this._readNumber('pump.live.flow_current_lh');
223
+ const pumpCurrentPowerW = await this._readNumber('pump.live.current_power_w');
224
+ const surfaceGroundDeltaState = await this._readNumber('temperature.delta.surface_ground');
225
+ const weatherText = await this._readString('ai.weather.outputs.daily_summary');
226
+
227
+ const flowAvailable = flowTempAvailable || Number.isFinite(currentFlowLh);
228
+ const weatherAvailable = weatherText !== '';
229
+
230
+ const collectorUsed = collectorAvailable;
231
+ const surfaceUsed = surfaceAvailable;
232
+ const groundUsed = groundAvailable;
233
+ const flowUsed = flowAvailable;
234
+ const returnUsed = returnAvailable;
235
+
236
+ // Block 7: Outside / weather now participate in evaluation logic
237
+ const outsideUsed = outsideAvailable;
238
+ const weatherUsed = weatherAvailable;
239
+
240
+ const usedSensors = [];
241
+ const availableSensors = [];
242
+
243
+ if (collectorAvailable) {
244
+ availableSensors.push('collector');
245
+ }
246
+ if (surfaceAvailable) {
247
+ availableSensors.push('surface');
248
+ }
249
+ if (groundAvailable) {
250
+ availableSensors.push('ground');
251
+ }
252
+ if (flowAvailable) {
253
+ availableSensors.push('calculated_flow');
254
+ }
255
+ if (returnAvailable) {
256
+ availableSensors.push('return');
257
+ }
258
+ if (outsideAvailable) {
259
+ availableSensors.push('outside');
260
+ }
261
+ if (weatherAvailable) {
262
+ availableSensors.push('weather');
263
+ }
264
+
265
+ if (collectorUsed) {
266
+ usedSensors.push('collector');
267
+ }
268
+ if (surfaceUsed) {
269
+ usedSensors.push('surface');
270
+ }
271
+ if (groundUsed) {
272
+ usedSensors.push('ground');
273
+ }
274
+ if (flowUsed) {
275
+ usedSensors.push('calculated_flow');
276
+ }
277
+ if (returnUsed) {
278
+ usedSensors.push('return');
279
+ }
280
+ if (outsideUsed) {
281
+ usedSensors.push('outside');
282
+ }
283
+ if (weatherUsed) {
284
+ usedSensors.push('weather');
285
+ }
286
+
287
+ const solarStandardActive = await this._readBoolean('solar.active');
288
+ const solarExtendedActive = await this._readBoolean('solar.extended.active');
289
+ const oldSolarRanToday = await this._readBoolean('analytics.insights.solar.results.solar_ran_today');
290
+
291
+ const solarLogicActive = solarStandardActive || solarExtendedActive;
292
+ const solarRanToday = oldSolarRanToday || solarLogicActive;
293
+
294
+ let poolReferenceSource = 'none';
295
+ let poolReferenceTemp = null;
296
+
297
+ if (surfaceAvailable && groundAvailable && Number.isFinite(surfaceTemp) && Number.isFinite(groundTemp)) {
298
+ poolReferenceSource = 'surface_ground_average';
299
+ poolReferenceTemp = Number(((surfaceTemp + groundTemp) / 2).toFixed(2));
300
+ } else if (surfaceAvailable && Number.isFinite(surfaceTemp)) {
301
+ poolReferenceSource = 'surface';
302
+ poolReferenceTemp = surfaceTemp;
303
+ } else if (groundAvailable && Number.isFinite(groundTemp)) {
304
+ poolReferenceSource = 'ground';
305
+ poolReferenceTemp = groundTemp;
306
+ }
307
+
308
+ let deltaTUsed = null;
309
+ if (Number.isFinite(collectorTemp) && Number.isFinite(poolReferenceTemp)) {
310
+ deltaTUsed = Number((collectorTemp - poolReferenceTemp).toFixed(2));
311
+ }
312
+
313
+ // --- Block 3: Thermische Leistung berechnen ---
314
+ let thermalPowerW = null;
315
+ let thermalPowerKW = null;
316
+
317
+ if (Number.isFinite(deltaTUsed) && Number.isFinite(currentFlowLh) && currentFlowLh > 0) {
318
+ thermalPowerW = Number((currentFlowLh * deltaTUsed * 1.16).toFixed(2));
319
+ thermalPowerKW = Number((thermalPowerW / 1000).toFixed(3));
320
+ }
321
+
322
+ const solarEffectiveNow =
323
+ solarLogicActive &&
324
+ ((Number.isFinite(currentFlowLh) && currentFlowLh > 0) ||
325
+ (Number.isFinite(pumpCurrentPowerW) && pumpCurrentPowerW > 0) ||
326
+ (Number.isFinite(thermalPowerW) && thermalPowerW > 0));
327
+
328
+ const flowSource = Number.isFinite(currentFlowLh) ? 'pump.live.flow_current_lh' : 'none';
329
+ const weatherCorrectionActive = outsideUsed || weatherUsed;
330
+
331
+ let estimatedEfficiencyRatio = null;
332
+
333
+ if (Number.isFinite(thermalPowerW) && Number.isFinite(pumpCurrentPowerW) && pumpCurrentPowerW > 0) {
334
+ estimatedEfficiencyRatio = Number((thermalPowerW / pumpCurrentPowerW).toFixed(2));
335
+ }
336
+
337
+ // --- Block 4: Tagesertrag / aktive Minuten / Peak berechnen ---
338
+ const nowTs = Date.now();
339
+
340
+ let estimatedGainTodayWh = await this._readNumber(
341
+ 'analytics.insights.solar.results.estimated_gain_today_wh',
342
+ );
343
+ let activeMinutesToday = await this._readNumber('analytics.insights.solar.results.active_minutes_today');
344
+ let peakPowerTodayW = await this._readNumber('analytics.insights.solar.results.peak_power_today_w');
345
+
346
+ if (!Number.isFinite(estimatedGainTodayWh)) {
347
+ estimatedGainTodayWh = 0;
348
+ }
349
+ if (!Number.isFinite(activeMinutesToday)) {
350
+ activeMinutesToday = 0;
351
+ }
352
+ if (!Number.isFinite(peakPowerTodayW)) {
353
+ peakPowerTodayW = 0;
354
+ }
355
+
356
+ if (this.lastCheckTimestamp && solarEffectiveNow && Number.isFinite(thermalPowerW) && thermalPowerW > 0) {
357
+ const deltaHours = (nowTs - this.lastCheckTimestamp) / 3600000;
358
+
359
+ // Schutz gegen unrealistisch große Sprünge
360
+ if (deltaHours > 0 && deltaHours <= 0.5) {
361
+ estimatedGainTodayWh = Number((estimatedGainTodayWh + thermalPowerW * deltaHours).toFixed(2));
362
+ activeMinutesToday = Number((activeMinutesToday + deltaHours * 60).toFixed(2));
363
+ }
364
+ }
365
+
366
+ if (solarEffectiveNow && Number.isFinite(thermalPowerW) && thermalPowerW > peakPowerTodayW) {
367
+ peakPowerTodayW = Number(thermalPowerW.toFixed(2));
368
+ }
369
+
370
+ const estimatedGainTodayKWh = Number((estimatedGainTodayWh / 1000).toFixed(3));
371
+
372
+ // --- Block 5: Confidence / Qualitätsbewertung ---
373
+ let confidencePercent = 25;
374
+
375
+ if (
376
+ Number.isFinite(collectorTemp) &&
377
+ Number.isFinite(poolReferenceTemp) &&
378
+ Number.isFinite(currentFlowLh)
379
+ ) {
380
+ confidencePercent = 50;
381
+ }
382
+
383
+ if (surfaceUsed && groundUsed) {
384
+ confidencePercent = 70;
385
+ }
386
+
387
+ if (returnUsed) {
388
+ confidencePercent = Math.max(confidencePercent, 80);
389
+ }
390
+
391
+ if (outsideUsed) {
392
+ confidencePercent = Math.max(confidencePercent, 85);
393
+ }
394
+
395
+ if (weatherUsed) {
396
+ confidencePercent = Math.max(confidencePercent, 90);
397
+ }
398
+
399
+ let surfaceGroundDelta = null;
400
+ if (Number.isFinite(surfaceGroundDeltaState)) {
401
+ surfaceGroundDelta = surfaceGroundDeltaState;
402
+ } else if (Number.isFinite(surfaceTemp) && Number.isFinite(groundTemp)) {
403
+ surfaceGroundDelta = Number((surfaceTemp - groundTemp).toFixed(2));
404
+ }
405
+
406
+ const summaryJson = {
407
+ block: 7,
408
+ mode: 'estimated_daily_gain',
409
+ solar_ran_today: solarRanToday,
410
+ solar_effective_now: solarEffectiveNow,
411
+ pool_reference_source: poolReferenceSource,
412
+ flow_source: flowSource,
413
+ weather_correction_active: weatherCorrectionActive,
414
+ confidence_percent: confidencePercent,
415
+ used_sensors: usedSensors,
416
+ values: {
417
+ collector_temp_used: Number.isFinite(collectorTemp) ? collectorTemp : null,
418
+ pool_reference_temp_used: Number.isFinite(poolReferenceTemp) ? poolReferenceTemp : null,
419
+ delta_t_used: Number.isFinite(deltaTUsed) ? deltaTUsed : null,
420
+ surface_ground_delta: Number.isFinite(surfaceGroundDelta) ? surfaceGroundDelta : null,
421
+ outside_temp_used: Number.isFinite(outsideTemp) ? outsideTemp : null,
422
+ flow_lh_used: Number.isFinite(currentFlowLh) ? currentFlowLh : null,
423
+ pump_power_w_used: Number.isFinite(pumpCurrentPowerW) ? pumpCurrentPowerW : null,
424
+ estimated_thermal_power_w: Number.isFinite(thermalPowerW) ? thermalPowerW : null,
425
+ estimated_thermal_power_kw: Number.isFinite(thermalPowerKW) ? thermalPowerKW : null,
426
+ estimated_efficiency_ratio: Number.isFinite(estimatedEfficiencyRatio)
427
+ ? estimatedEfficiencyRatio
428
+ : null,
429
+ estimated_gain_today_wh: Number.isFinite(estimatedGainTodayWh) ? estimatedGainTodayWh : null,
430
+ estimated_gain_today_kwh: Number.isFinite(estimatedGainTodayKWh) ? estimatedGainTodayKWh : null,
431
+ active_minutes_today: Number.isFinite(activeMinutesToday) ? activeMinutesToday : null,
432
+ peak_power_today_w: Number.isFinite(peakPowerTodayW) ? peakPowerTodayW : null,
433
+ },
434
+ note: this._t('solar_insights_summary_note_block_7'),
435
+ };
436
+
437
+ const summaryHtml = [
438
+ '<div>',
439
+ `<b>${this._t('solar_insights_label_mode')}:</b> ${this._t('solar_insights_mode_estimated_daily_gain')}<br>`,
440
+ `<b>${this._t('solar_insights_label_solar_ran_today')}:</b> ${solarRanToday}<br>`,
441
+ `<b>${this._t('solar_insights_label_solar_effective_now')}:</b> ${solarEffectiveNow}<br>`,
442
+ `<b>${this._t('solar_insights_label_pool_reference_source')}:</b> ${poolReferenceSource}<br>`,
443
+ `<b>${this._t('solar_insights_label_flow_source')}:</b> ${flowSource}<br>`,
444
+ `<b>${this._t('solar_insights_label_weather_correction_active')}:</b> ${weatherCorrectionActive}<br>`,
445
+ `<b>${this._t('solar_insights_label_confidence')}:</b> ${confidencePercent} %<br>`,
446
+ `<b>${this._t('solar_insights_label_used_sensors')}:</b> ${usedSensors.join(', ') || this._t('solar_insights_value_none')}<br>`,
447
+ `<b>${this._t('solar_insights_label_collector')}:</b> ${Number.isFinite(collectorTemp) ? `${collectorTemp} °C` : this._t('solar_insights_value_na')}<br>`,
448
+ `<b>${this._t('solar_insights_label_pool_reference')}:</b> ${Number.isFinite(poolReferenceTemp) ? `${poolReferenceTemp} °C` : this._t('solar_insights_value_na')}<br>`,
449
+ `<b>${this._t('solar_insights_label_delta_t')}:</b> ${Number.isFinite(deltaTUsed) ? `${deltaTUsed} K` : this._t('solar_insights_value_na')}<br>`,
450
+ `<b>${this._t('solar_insights_label_surface_ground_delta')}:</b> ${Number.isFinite(surfaceGroundDelta) ? `${surfaceGroundDelta} K` : this._t('solar_insights_value_na')}<br>`,
451
+ `<b>${this._t('solar_insights_label_outside')}:</b> ${Number.isFinite(outsideTemp) ? `${outsideTemp} °C` : this._t('solar_insights_value_na')}<br>`,
452
+ `<b>${this._t('solar_insights_label_flow')}:</b> ${Number.isFinite(currentFlowLh) ? `${currentFlowLh} l/h` : this._t('solar_insights_value_na')}<br>`,
453
+ `<b>${this._t('solar_insights_label_pump_power')}:</b> ${Number.isFinite(pumpCurrentPowerW) ? `${pumpCurrentPowerW} W` : this._t('solar_insights_value_na')}<br>`,
454
+ `<b>${this._t('solar_insights_label_thermal_power')}:</b> ${Number.isFinite(thermalPowerW) ? `${thermalPowerW} W (${thermalPowerKW} kW)` : this._t('solar_insights_value_na')}<br>`,
455
+ `<b>${this._t('solar_insights_label_estimated_efficiency_ratio')}:</b> ${Number.isFinite(estimatedEfficiencyRatio) ? estimatedEfficiencyRatio : this._t('solar_insights_value_na')}<br>`,
456
+ `<b>${this._t('solar_insights_label_gain_today')}:</b> ${Number.isFinite(estimatedGainTodayWh) ? `${estimatedGainTodayWh} Wh (${estimatedGainTodayKWh} kWh)` : this._t('solar_insights_value_na')}<br>`,
457
+ `<b>${this._t('solar_insights_label_active_minutes_today')}:</b> ${Number.isFinite(activeMinutesToday) ? `${activeMinutesToday} min` : this._t('solar_insights_value_na')}<br>`,
458
+ `<b>${this._t('solar_insights_label_peak_power_today')}:</b> ${Number.isFinite(peakPowerTodayW) ? `${peakPowerTodayW} W` : this._t('solar_insights_value_na')}<br>`,
459
+ `<b>${this._t('solar_insights_label_note')}:</b> ${this._t('solar_insights_html_note_block_7')}`,
460
+ '</div>',
461
+ ].join('');
462
+
463
+ // --- Inputs: available ---
464
+ await this.adapter.setStateChangedAsync('analytics.insights.solar.inputs.collector_available', {
465
+ val: collectorAvailable,
466
+ ack: true,
467
+ });
468
+ await this.adapter.setStateChangedAsync('analytics.insights.solar.inputs.surface_available', {
469
+ val: surfaceAvailable,
470
+ ack: true,
471
+ });
472
+ await this.adapter.setStateChangedAsync('analytics.insights.solar.inputs.ground_available', {
473
+ val: groundAvailable,
474
+ ack: true,
475
+ });
476
+ await this.adapter.setStateChangedAsync('analytics.insights.solar.inputs.flow_available', {
477
+ val: flowAvailable,
478
+ ack: true,
479
+ });
480
+ await this.adapter.setStateChangedAsync('analytics.insights.solar.inputs.return_available', {
481
+ val: returnAvailable,
482
+ ack: true,
483
+ });
484
+ await this.adapter.setStateChangedAsync('analytics.insights.solar.inputs.outside_available', {
485
+ val: outsideAvailable,
486
+ ack: true,
487
+ });
488
+ await this.adapter.setStateChangedAsync('analytics.insights.solar.inputs.weather_available', {
489
+ val: weatherAvailable,
490
+ ack: true,
491
+ });
492
+
493
+ // --- Inputs: used ---
494
+ await this.adapter.setStateChangedAsync('analytics.insights.solar.inputs.collector_used', {
495
+ val: collectorUsed,
496
+ ack: true,
497
+ });
498
+ await this.adapter.setStateChangedAsync('analytics.insights.solar.inputs.surface_used', {
499
+ val: surfaceUsed,
500
+ ack: true,
501
+ });
502
+ await this.adapter.setStateChangedAsync('analytics.insights.solar.inputs.ground_used', {
503
+ val: groundUsed,
504
+ ack: true,
505
+ });
506
+ await this.adapter.setStateChangedAsync('analytics.insights.solar.inputs.flow_used', {
507
+ val: flowUsed,
508
+ ack: true,
509
+ });
510
+ await this.adapter.setStateChangedAsync('analytics.insights.solar.inputs.return_used', {
511
+ val: returnUsed,
512
+ ack: true,
513
+ });
514
+ await this.adapter.setStateChangedAsync('analytics.insights.solar.inputs.outside_used', {
515
+ val: outsideUsed,
516
+ ack: true,
517
+ });
518
+ await this.adapter.setStateChangedAsync('analytics.insights.solar.inputs.weather_used', {
519
+ val: weatherUsed,
520
+ ack: true,
521
+ });
522
+
523
+ await this.adapter.setStateChangedAsync('analytics.insights.solar.inputs.used_sensors_text', {
524
+ val: usedSensors.join(', '),
525
+ ack: true,
526
+ });
527
+
528
+ await this.adapter.setStateChangedAsync('analytics.insights.solar.inputs.used_sensors_json', {
529
+ val: JSON.stringify({
530
+ available: availableSensors,
531
+ used: usedSensors,
532
+ mode: 'estimated_daily_gain',
533
+ block: 7,
534
+ }),
535
+ ack: true,
536
+ });
537
+
538
+ // --- Results: status ---
539
+ await this.adapter.setStateChangedAsync('analytics.insights.solar.results.solar_ran_today', {
540
+ val: solarRanToday,
541
+ ack: true,
542
+ });
543
+ await this.adapter.setStateChangedAsync('analytics.insights.solar.results.analysis_active', {
544
+ val: true,
545
+ ack: true,
546
+ });
547
+ await this.adapter.setStateChangedAsync('analytics.insights.solar.results.solar_effective_now', {
548
+ val: solarEffectiveNow,
549
+ ack: true,
550
+ });
551
+ await this.adapter.setStateChangedAsync('analytics.insights.solar.results.solar_gain_state', {
552
+ val: solarRanToday
553
+ ? this._t('solar_insights_status_estimated_daily_gain_ready')
554
+ : this._t('solar_insights_status_no_solar_runtime_today'),
555
+ ack: true,
556
+ });
557
+
558
+ // --- Results: direct values ---
559
+ await this.adapter.setStateChangedAsync('analytics.insights.solar.results.collector_temp_used', {
560
+ val: Number.isFinite(collectorTemp) ? collectorTemp : null,
561
+ ack: true,
562
+ });
563
+ await this.adapter.setStateChangedAsync('analytics.insights.solar.results.pool_reference_temp_used', {
564
+ val: Number.isFinite(poolReferenceTemp) ? poolReferenceTemp : null,
565
+ ack: true,
566
+ });
567
+ await this.adapter.setStateChangedAsync('analytics.insights.solar.results.delta_t_used', {
568
+ val: Number.isFinite(deltaTUsed) ? deltaTUsed : null,
569
+ ack: true,
570
+ });
571
+ await this.adapter.setStateChangedAsync('analytics.insights.solar.results.surface_ground_delta', {
572
+ val: Number.isFinite(surfaceGroundDelta) ? surfaceGroundDelta : null,
573
+ ack: true,
574
+ });
575
+ await this.adapter.setStateChangedAsync('analytics.insights.solar.results.outside_temp_used', {
576
+ val: Number.isFinite(outsideTemp) ? outsideTemp : null,
577
+ ack: true,
578
+ });
579
+ await this.adapter.setStateChangedAsync('analytics.insights.solar.results.flow_lh_used', {
580
+ val: Number.isFinite(currentFlowLh) ? currentFlowLh : null,
581
+ ack: true,
582
+ });
583
+ await this.adapter.setStateChangedAsync('analytics.insights.solar.results.pump_power_w_used', {
584
+ val: Number.isFinite(pumpCurrentPowerW) ? pumpCurrentPowerW : null,
585
+ ack: true,
586
+ });
587
+
588
+ // --- Results: thermal power ---
589
+ await this.adapter.setStateChangedAsync('analytics.insights.solar.results.estimated_thermal_power_w', {
590
+ val: Number.isFinite(thermalPowerW) ? thermalPowerW : null,
591
+ ack: true,
592
+ });
593
+ await this.adapter.setStateChangedAsync('analytics.insights.solar.results.estimated_thermal_power_kw', {
594
+ val: Number.isFinite(thermalPowerKW) ? thermalPowerKW : null,
595
+ ack: true,
596
+ });
597
+
598
+ await this.adapter.setStateChangedAsync('analytics.insights.solar.results.estimated_efficiency_ratio', {
599
+ val: Number.isFinite(estimatedEfficiencyRatio) ? estimatedEfficiencyRatio : null,
600
+ ack: true,
601
+ });
602
+
603
+ await this.adapter.setStateChangedAsync('analytics.insights.solar.results.estimated_gain_today_wh', {
604
+ val: Number.isFinite(estimatedGainTodayWh) ? estimatedGainTodayWh : 0,
605
+ ack: true,
606
+ });
607
+
608
+ await this.adapter.setStateChangedAsync('analytics.insights.solar.results.estimated_gain_today_kwh', {
609
+ val: Number.isFinite(estimatedGainTodayKWh) ? estimatedGainTodayKWh : 0,
610
+ ack: true,
611
+ });
612
+
613
+ await this.adapter.setStateChangedAsync('analytics.insights.solar.results.active_minutes_today', {
614
+ val: Number.isFinite(activeMinutesToday) ? activeMinutesToday : 0,
615
+ ack: true,
616
+ });
617
+
618
+ await this.adapter.setStateChangedAsync('analytics.insights.solar.results.peak_power_today_w', {
619
+ val: Number.isFinite(peakPowerTodayW) ? peakPowerTodayW : 0,
620
+ ack: true,
621
+ });
622
+
623
+ await this.adapter.setStateChangedAsync('analytics.insights.solar.results.summary_json', {
624
+ val: JSON.stringify(summaryJson),
625
+ ack: true,
626
+ });
627
+ await this.adapter.setStateChangedAsync('analytics.insights.solar.results.summary_html', {
628
+ val: summaryHtml,
629
+ ack: true,
630
+ });
631
+
632
+ // --- Calculation ---
633
+ await this.adapter.setStateChangedAsync('analytics.insights.solar.calculation.mode', {
634
+ val: this._t('solar_insights_mode_estimated_daily_gain'),
635
+ ack: true,
636
+ });
637
+ await this.adapter.setStateChangedAsync('analytics.insights.solar.calculation.quality_level', {
638
+ val:
639
+ confidencePercent >= 85
640
+ ? this._t('solar_insights_quality_advanced')
641
+ : confidencePercent >= 70
642
+ ? this._t('solar_insights_quality_enhanced')
643
+ : this._t('solar_insights_quality_basic'),
644
+ ack: true,
645
+ });
646
+ await this.adapter.setStateChangedAsync('analytics.insights.solar.calculation.confidence_percent', {
647
+ val: confidencePercent,
648
+ ack: true,
649
+ });
650
+ await this.adapter.setStateChangedAsync('analytics.insights.solar.calculation.pool_reference_source', {
651
+ val: poolReferenceSource,
652
+ ack: true,
653
+ });
654
+ await this.adapter.setStateChangedAsync('analytics.insights.solar.calculation.flow_source', {
655
+ val: flowSource,
656
+ ack: true,
657
+ });
658
+ await this.adapter.setStateChangedAsync('analytics.insights.solar.calculation.weather_correction_active', {
659
+ val: weatherCorrectionActive,
660
+ ack: true,
661
+ });
662
+ await this.adapter.setStateChangedAsync('analytics.insights.solar.calculation.note', {
663
+ val: this._t('solar_insights_calculation_note_block_7'),
664
+ ack: true,
665
+ });
666
+
667
+ // --- Debug ---
668
+ await this.adapter.setStateChangedAsync('analytics.insights.solar.debug.last_update', {
669
+ val: new Date().toISOString(),
670
+ ack: true,
671
+ });
672
+ await this.adapter.setStateChangedAsync('analytics.insights.solar.debug.last_recalculation_reason', {
673
+ val: this._t('solar_insights_reason_block_7_update'),
674
+ ack: true,
675
+ });
676
+ await this.adapter.setStateChangedAsync('analytics.insights.solar.debug.last_valid_mode', {
677
+ val: 'estimated_daily_gain',
678
+ ack: true,
679
+ });
680
+ await this.adapter.setStateChangedAsync('analytics.insights.solar.debug.last_invalid_reason', {
681
+ val: '',
682
+ ack: true,
683
+ });
684
+ await this.adapter.setStateChangedAsync('analytics.insights.solar.debug.debug_text', {
685
+ val: `${this._t('solar_insights_debug_block_7_updated')} ${usedSensors.join(', ') || this._t('solar_insights_value_none')}`,
686
+ ack: true,
687
+ });
688
+
689
+ this.lastCheckTimestamp = nowTs;
690
+ this.adapter.log.debug('[solarInsightsHelper] Block 7 updated successfully');
691
+ } catch (err) {
692
+ this.adapter.log.warn(`[solarInsightsHelper] Error in check: ${err.message}`);
693
+
694
+ await this.adapter.setStateChangedAsync('analytics.insights.solar.debug.last_update', {
695
+ val: new Date().toISOString(),
696
+ ack: true,
697
+ });
698
+ await this.adapter.setStateChangedAsync('analytics.insights.solar.debug.last_invalid_reason', {
699
+ val: err.message,
700
+ ack: true,
701
+ });
702
+ await this.adapter.setStateChangedAsync('analytics.insights.solar.debug.debug_text', {
703
+ val: `${this._t('solar_insights_debug_block_7_error')} ${err.message}`,
704
+ ack: true,
705
+ });
706
+ }
707
+ },
708
+
709
+ async _isSensorAvailable(activeId, valueId) {
710
+ const active = await this._readBoolean(activeId);
711
+ const value = await this._readNumber(valueId);
712
+
713
+ return active && Number.isFinite(value);
714
+ },
715
+
716
+ async _readState(id) {
717
+ try {
718
+ return await this.adapter.getStateAsync(id);
719
+ } catch {
720
+ return null;
721
+ }
722
+ },
723
+
724
+ async _readBoolean(id) {
725
+ const state = await this._readState(id);
726
+
727
+ if (!state) {
728
+ return false;
729
+ }
730
+
731
+ return state.val === true;
732
+ },
733
+
734
+ async _readNumber(id) {
735
+ const state = await this._readState(id);
736
+
737
+ if (!state || state.val === null || state.val === undefined || state.val === '') {
738
+ return null;
739
+ }
740
+
741
+ const value = Number(state.val);
742
+ return Number.isFinite(value) ? value : null;
743
+ },
744
+
745
+ async _readString(id) {
746
+ const state = await this._readState(id);
747
+
748
+ if (!state || state.val === null || state.val === undefined) {
749
+ return '';
750
+ }
751
+
752
+ return String(state.val);
753
+ },
754
+
755
+ _t(text) {
756
+ try {
757
+ return I18n.t(text);
758
+ } catch {
759
+ return text;
760
+ }
761
+ },
762
+
763
+ cleanup() {
764
+ if (this.checkTimer) {
765
+ this.adapter.clearTimeout(this.checkTimer);
766
+ this.checkTimer = null;
767
+ }
768
+
769
+ if (this.resetTimer) {
770
+ this.adapter.clearTimeout(this.resetTimer);
771
+ this.resetTimer = null;
772
+ }
773
+ },
774
+ };
775
+
776
+ module.exports = solarInsightsHelper;
777
+
778
+ // i18n keys required:
779
+ // solar_insights_summary_note_block_7
780
+ // solar_insights_label_mode
781
+ // solar_insights_label_solar_ran_today
782
+ // solar_insights_label_solar_effective_now
783
+ // solar_insights_label_pool_reference_source
784
+ // solar_insights_label_flow_source
785
+ // solar_insights_label_weather_correction_active
786
+ // solar_insights_label_confidence
787
+ // solar_insights_label_used_sensors
788
+ // solar_insights_label_collector
789
+ // solar_insights_label_pool_reference
790
+ // solar_insights_label_delta_t
791
+ // solar_insights_label_surface_ground_delta
792
+ // solar_insights_label_outside
793
+ // solar_insights_label_flow
794
+ // solar_insights_label_pump_power
795
+ // solar_insights_label_thermal_power
796
+ // solar_insights_label_estimated_efficiency_ratio
797
+ // solar_insights_label_gain_today
798
+ // solar_insights_label_active_minutes_today
799
+ // solar_insights_label_peak_power_today
800
+ // solar_insights_label_note
801
+ // solar_insights_value_none
802
+ // solar_insights_value_na
803
+ // solar_insights_reason_daily_reset
804
+ // solar_insights_debug_daily_reset_executed
805
+ // solar_insights_status_estimated_daily_gain_ready
806
+ // solar_insights_status_no_solar_runtime_today
807
+ // solar_insights_mode_estimated_daily_gain
808
+ // solar_insights_quality_advanced
809
+ // solar_insights_quality_enhanced
810
+ // solar_insights_quality_basic
811
+ // solar_insights_calculation_note_block_7
812
+ // solar_insights_reason_block_7_update
813
+ // solar_insights_debug_block_7_updated
814
+ // solar_insights_debug_block_7_error
815
+ // solar_insights_html_note_block_7