iobroker.poolcontrol 1.3.28 → 1.3.29
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 +16 -6
- package/admin/i18n/de/translations.json +4 -0
- package/admin/i18n/en/translations.json +8 -2
- package/admin/i18n/es/translations.json +4 -0
- package/admin/i18n/fr/translations.json +4 -0
- package/admin/i18n/it/translations.json +4 -0
- package/admin/i18n/nl/translations.json +4 -0
- package/admin/i18n/pl/translations.json +4 -0
- package/admin/i18n/pt/translations.json +4 -0
- package/admin/i18n/ru/translations.json +4 -0
- package/admin/i18n/uk/translations.json +4 -0
- package/admin/i18n/zh-cn/translations.json +4 -0
- package/admin/jsonConfig.json +31 -1
- package/io-package.json +14 -14
- package/lib/helpers/aiHelper.js +5 -5
- package/lib/helpers/consumptionHelper.js +47 -5
- package/lib/helpers/heatHelper.js +12 -9
- package/lib/helpers/infoHelper.js +11 -11
- package/lib/helpers/poolInsightsHelper.js +620 -0
- package/lib/helpers/pumpHelper.js +1 -4
- package/lib/helpers/statisticsHelper.js +7 -3
- package/lib/helpers/statisticsHelperMonth.js +19 -15
- package/lib/helpers/statisticsHelperWeek.js +11 -5
- package/lib/i18n/de.json +31 -1
- package/lib/i18n/en.json +34 -4
- package/lib/i18n/es.json +31 -1
- package/lib/i18n/fr.json +31 -1
- package/lib/i18n/it.json +31 -1
- package/lib/i18n/nl.json +31 -1
- package/lib/i18n/pl.json +31 -1
- package/lib/i18n/pt.json +31 -1
- package/lib/i18n/ru.json +31 -1
- package/lib/i18n/uk.json +31 -1
- package/lib/i18n/zh-cn.json +31 -1
- package/lib/stateDefinitions/poolInsightsStates.js +289 -0
- package/lib/stateDefinitions/solarStates.js +12 -3
- package/lib/stateDefinitions/statusStates.js +1 -1
- package/main.js +20 -0
- package/package.json +1 -1
|
@@ -0,0 +1,620 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const { I18n } = require('@iobroker/adapter-core');
|
|
4
|
+
|
|
5
|
+
const POOL_INSIGHTS_PREFIX = 'analytics.insights.pool';
|
|
6
|
+
const DAILY_ANALYSIS_HOUR = 20;
|
|
7
|
+
const SPEECH_COOLDOWN_MS = 6 * 60 * 60 * 1000;
|
|
8
|
+
|
|
9
|
+
const poolInsightsHelper = {
|
|
10
|
+
adapter: null,
|
|
11
|
+
dailyTimer: null,
|
|
12
|
+
running: false,
|
|
13
|
+
|
|
14
|
+
init(adapter) {
|
|
15
|
+
this.adapter = adapter;
|
|
16
|
+
|
|
17
|
+
this.adapter.subscribeStates(`${POOL_INSIGHTS_PREFIX}.enabled`);
|
|
18
|
+
this.adapter.subscribeStates(`${POOL_INSIGHTS_PREFIX}.manual_trigger`);
|
|
19
|
+
this.adapter.subscribeStates(`${POOL_INSIGHTS_PREFIX}.send_to_speech_queue`);
|
|
20
|
+
|
|
21
|
+
void this._refreshSchedule();
|
|
22
|
+
this.adapter.log.debug('[poolInsightsHelper] Initialized');
|
|
23
|
+
},
|
|
24
|
+
|
|
25
|
+
handleStateChange(id, state) {
|
|
26
|
+
if (!state || state.ack === true) {
|
|
27
|
+
return;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
if (id.endsWith(`${POOL_INSIGHTS_PREFIX}.manual_trigger`) && state.val === true) {
|
|
31
|
+
void this._handleManualTrigger();
|
|
32
|
+
return;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
if (id.endsWith(`${POOL_INSIGHTS_PREFIX}.enabled`)) {
|
|
36
|
+
void this._refreshSchedule();
|
|
37
|
+
}
|
|
38
|
+
},
|
|
39
|
+
|
|
40
|
+
async _handleManualTrigger() {
|
|
41
|
+
try {
|
|
42
|
+
await this._runAnalysis('manual', true);
|
|
43
|
+
} finally {
|
|
44
|
+
await this._setState(`${POOL_INSIGHTS_PREFIX}.manual_trigger`, false);
|
|
45
|
+
}
|
|
46
|
+
},
|
|
47
|
+
|
|
48
|
+
async _refreshSchedule() {
|
|
49
|
+
if (this.dailyTimer) {
|
|
50
|
+
this.adapter.clearTimeout(this.dailyTimer);
|
|
51
|
+
this.dailyTimer = null;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const enabled = await this._readBoolean(`${POOL_INSIGHTS_PREFIX}.enabled`);
|
|
55
|
+
if (!enabled) {
|
|
56
|
+
await this._writeDisabled('disabled');
|
|
57
|
+
return;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
await this._scheduleDailyAnalysis();
|
|
61
|
+
},
|
|
62
|
+
|
|
63
|
+
async _scheduleDailyAnalysis() {
|
|
64
|
+
if (this.dailyTimer) {
|
|
65
|
+
this.adapter.clearTimeout(this.dailyTimer);
|
|
66
|
+
this.dailyTimer = null;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
const now = new Date();
|
|
70
|
+
const next = new Date(now);
|
|
71
|
+
next.setHours(DAILY_ANALYSIS_HOUR, 0, 0, 0);
|
|
72
|
+
if (next <= now) {
|
|
73
|
+
next.setDate(next.getDate() + 1);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
const delay = Math.max(1000, next.getTime() - now.getTime());
|
|
77
|
+
this.dailyTimer = this.adapter.setTimeout(async () => {
|
|
78
|
+
this.dailyTimer = null;
|
|
79
|
+
await this._runAnalysis('daily', true);
|
|
80
|
+
await this._refreshSchedule();
|
|
81
|
+
}, delay);
|
|
82
|
+
|
|
83
|
+
await this._setState(`${POOL_INSIGHTS_PREFIX}.status`, 'scheduled');
|
|
84
|
+
this.adapter.log.debug(`[poolInsightsHelper] Daily analysis scheduled for ${next.toISOString()}`);
|
|
85
|
+
},
|
|
86
|
+
|
|
87
|
+
async _runAnalysis(reason, allowSpeech) {
|
|
88
|
+
if (this.running) {
|
|
89
|
+
this.adapter.log.debug('[poolInsightsHelper] Analysis already running - skipped');
|
|
90
|
+
return;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
this.running = true;
|
|
94
|
+
try {
|
|
95
|
+
await this._setState(`${POOL_INSIGHTS_PREFIX}.status`, 'running');
|
|
96
|
+
|
|
97
|
+
const snapshot = await this._readSnapshot();
|
|
98
|
+
const result = this._buildResult(snapshot, reason);
|
|
99
|
+
|
|
100
|
+
await this._writeResult(result);
|
|
101
|
+
|
|
102
|
+
if (allowSpeech) {
|
|
103
|
+
await this._sendSpeechIfAllowed(result, reason);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
this.adapter.log.debug(`[poolInsightsHelper] Analysis completed (${reason})`);
|
|
107
|
+
} catch (err) {
|
|
108
|
+
await this._writeError(reason, err);
|
|
109
|
+
this.adapter.log.warn(`[poolInsightsHelper] Analysis failed: ${err.message}`);
|
|
110
|
+
} finally {
|
|
111
|
+
this.running = false;
|
|
112
|
+
}
|
|
113
|
+
},
|
|
114
|
+
|
|
115
|
+
async _readSnapshot() {
|
|
116
|
+
return {
|
|
117
|
+
temperature: {
|
|
118
|
+
surfaceCurrent: await this._readNumber('temperature.surface.current'),
|
|
119
|
+
surfaceMinToday: await this._readNumber('temperature.surface.min_today'),
|
|
120
|
+
surfaceMaxToday: await this._readNumber('temperature.surface.max_today'),
|
|
121
|
+
surfaceSummaryJson: await this._readString(
|
|
122
|
+
'analytics.statistics.temperature.today.surface.summary_json',
|
|
123
|
+
),
|
|
124
|
+
},
|
|
125
|
+
pump: {
|
|
126
|
+
runtimeTodaySeconds: await this._readNumber('runtime.today_seconds'),
|
|
127
|
+
startCountToday: await this._readNumber('runtime.start_count_today'),
|
|
128
|
+
circulationRequired: await this._readNumber('circulation.daily_required'),
|
|
129
|
+
circulationRemaining: await this._readNumber('circulation.daily_remaining'),
|
|
130
|
+
mode: await this._readString('pump.mode'),
|
|
131
|
+
status: await this._readString('pump.status'),
|
|
132
|
+
error: await this._readBoolean('pump.error'),
|
|
133
|
+
activeHelper: await this._readString('pump.active_helper'),
|
|
134
|
+
},
|
|
135
|
+
solar: {
|
|
136
|
+
ranToday: await this._readBoolean('analytics.insights.solar.results.solar_ran_today'),
|
|
137
|
+
estimatedGainTodayKwh: await this._readNumber(
|
|
138
|
+
'analytics.insights.solar.results.estimated_gain_today_kwh',
|
|
139
|
+
),
|
|
140
|
+
evaluationAvailable:
|
|
141
|
+
(await this._readString('analytics.insights.solar.results.summary_json')) !== '' ||
|
|
142
|
+
(await this._readString('analytics.insights.solar.results.summary_html')) !== '',
|
|
143
|
+
},
|
|
144
|
+
photovoltaic: {
|
|
145
|
+
activeToday: await this._readBoolean('analytics.insights.photovoltaic.results.active_today'),
|
|
146
|
+
startsToday: await this._readNumber('analytics.insights.photovoltaic.results.starts_today'),
|
|
147
|
+
runtimeTodayMin: await this._readNumber('analytics.insights.photovoltaic.results.runtime_today_min'),
|
|
148
|
+
evaluationAvailable:
|
|
149
|
+
(await this._readString('analytics.insights.photovoltaic.results.summary_json')) !== '' ||
|
|
150
|
+
(await this._readString('analytics.insights.photovoltaic.results.summary_text')) !== '',
|
|
151
|
+
},
|
|
152
|
+
consumption: {
|
|
153
|
+
dayKwh: await this._readNumber('consumption.day_kwh'),
|
|
154
|
+
dayEur: await this._readNumber('costs.day_eur'),
|
|
155
|
+
},
|
|
156
|
+
chemistry: {
|
|
157
|
+
phAvailable: await this._hasValue('chemistry.ph.outputs.summary_text'),
|
|
158
|
+
tdsAvailable: await this._hasValue('chemistry.tds.outputs.summary_text'),
|
|
159
|
+
orpAvailable: await this._hasValue('chemistry.orp.outputs.summary_text'),
|
|
160
|
+
},
|
|
161
|
+
};
|
|
162
|
+
},
|
|
163
|
+
|
|
164
|
+
_buildResult(snapshot, reason) {
|
|
165
|
+
const observations = [];
|
|
166
|
+
const recommendations = [];
|
|
167
|
+
let level = 'ok';
|
|
168
|
+
|
|
169
|
+
const tempDelta = this._getTemperatureDelta(snapshot.temperature);
|
|
170
|
+
if (tempDelta !== null) {
|
|
171
|
+
const rounded = this._round(tempDelta, 1);
|
|
172
|
+
observations.push({
|
|
173
|
+
area: 'temperature',
|
|
174
|
+
level: tempDelta >= 1 ? 'ok' : 'info',
|
|
175
|
+
text: this._translate('pool_insights_observation_temperature_delta', { delta: rounded }),
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
if (tempDelta <= 0.2) {
|
|
179
|
+
level = this._raiseLevel(level, 'info');
|
|
180
|
+
recommendations.push({
|
|
181
|
+
area: 'temperature',
|
|
182
|
+
level: 'info',
|
|
183
|
+
text: this._translate('pool_insights_recommendation_temperature_low_change'),
|
|
184
|
+
});
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
if (snapshot.pump.runtimeTodaySeconds !== null) {
|
|
189
|
+
const runtimeLevel = snapshot.pump.runtimeTodaySeconds > 0 ? 'ok' : 'info';
|
|
190
|
+
level = this._raiseLevel(level, runtimeLevel);
|
|
191
|
+
observations.push({
|
|
192
|
+
area: 'pump',
|
|
193
|
+
level: runtimeLevel,
|
|
194
|
+
text: this._translate('pool_insights_observation_pump_runtime', {
|
|
195
|
+
runtime: this._formatRuntime(snapshot.pump.runtimeTodaySeconds),
|
|
196
|
+
}),
|
|
197
|
+
});
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
if (snapshot.pump.startCountToday !== null) {
|
|
201
|
+
observations.push({
|
|
202
|
+
area: 'pump',
|
|
203
|
+
level: snapshot.pump.startCountToday > 12 ? 'info' : 'ok',
|
|
204
|
+
text: this._translate('pool_insights_observation_pump_starts', {
|
|
205
|
+
count: snapshot.pump.startCountToday,
|
|
206
|
+
}),
|
|
207
|
+
});
|
|
208
|
+
|
|
209
|
+
if (snapshot.pump.startCountToday > 12) {
|
|
210
|
+
level = this._raiseLevel(level, 'info');
|
|
211
|
+
recommendations.push({
|
|
212
|
+
area: 'pump',
|
|
213
|
+
level: 'info',
|
|
214
|
+
text: this._translate('pool_insights_recommendation_many_pump_starts'),
|
|
215
|
+
});
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
if (snapshot.pump.error === true) {
|
|
220
|
+
level = 'warning';
|
|
221
|
+
observations.push({
|
|
222
|
+
area: 'pump',
|
|
223
|
+
level: 'warning',
|
|
224
|
+
text: this._translate('pool_insights_observation_pump_error'),
|
|
225
|
+
});
|
|
226
|
+
recommendations.push({
|
|
227
|
+
area: 'pump',
|
|
228
|
+
level: 'warning',
|
|
229
|
+
text: this._translate('pool_insights_recommendation_pump_error'),
|
|
230
|
+
});
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
this._appendSolarObservations(snapshot.solar, observations);
|
|
234
|
+
this._appendPhotovoltaicObservations(snapshot.photovoltaic, observations);
|
|
235
|
+
|
|
236
|
+
if (snapshot.photovoltaic.startsToday !== null && snapshot.photovoltaic.startsToday > 10) {
|
|
237
|
+
level = this._raiseLevel(level, 'info');
|
|
238
|
+
recommendations.push({
|
|
239
|
+
area: 'photovoltaic',
|
|
240
|
+
level: 'info',
|
|
241
|
+
text: this._translate('pool_insights_recommendation_many_pv_starts'),
|
|
242
|
+
});
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
this._appendConsumptionObservations(snapshot.consumption, observations);
|
|
246
|
+
this._appendChemistryObservations(snapshot.chemistry, observations);
|
|
247
|
+
|
|
248
|
+
if (this._hasLimitedCoreData(snapshot)) {
|
|
249
|
+
level = this._raiseLevel(level, 'info');
|
|
250
|
+
observations.push({
|
|
251
|
+
area: 'pool',
|
|
252
|
+
level: 'info',
|
|
253
|
+
text: this._translate('pool_insights_observation_not_enough_data'),
|
|
254
|
+
});
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
if (observations.length === 0) {
|
|
258
|
+
observations.push({
|
|
259
|
+
area: 'pool',
|
|
260
|
+
level: 'ok',
|
|
261
|
+
text: this._translate('pool_insights_observation_not_enough_data'),
|
|
262
|
+
});
|
|
263
|
+
level = 'info';
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
level = observations.reduce((current, entry) => this._raiseLevel(current, entry.level), level);
|
|
267
|
+
|
|
268
|
+
const summaryText = this._buildSummaryText(level, observations, recommendations);
|
|
269
|
+
const now = new Date().toISOString();
|
|
270
|
+
|
|
271
|
+
return {
|
|
272
|
+
status: 'completed',
|
|
273
|
+
level,
|
|
274
|
+
summaryText,
|
|
275
|
+
summaryHtml: this._buildSummaryHtml(level, observations, recommendations),
|
|
276
|
+
summaryJson: {
|
|
277
|
+
status: 'completed',
|
|
278
|
+
level,
|
|
279
|
+
reason,
|
|
280
|
+
last_update: now,
|
|
281
|
+
inputs: snapshot,
|
|
282
|
+
observations,
|
|
283
|
+
recommendations,
|
|
284
|
+
},
|
|
285
|
+
observations,
|
|
286
|
+
recommendations,
|
|
287
|
+
lastUpdate: now,
|
|
288
|
+
reason,
|
|
289
|
+
};
|
|
290
|
+
},
|
|
291
|
+
|
|
292
|
+
_getTemperatureDelta(temperature) {
|
|
293
|
+
if (temperature.surfaceMaxToday === null || temperature.surfaceMinToday === null) {
|
|
294
|
+
return null;
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
return Math.max(0, temperature.surfaceMaxToday - temperature.surfaceMinToday);
|
|
298
|
+
},
|
|
299
|
+
|
|
300
|
+
_appendSolarObservations(solar, observations) {
|
|
301
|
+
if (solar.ranToday === true) {
|
|
302
|
+
observations.push({
|
|
303
|
+
area: 'solar',
|
|
304
|
+
level: 'ok',
|
|
305
|
+
text: this._translate('pool_insights_observation_solar_ran_today'),
|
|
306
|
+
});
|
|
307
|
+
} else if (solar.evaluationAvailable) {
|
|
308
|
+
observations.push({
|
|
309
|
+
area: 'solar',
|
|
310
|
+
level: 'info',
|
|
311
|
+
text: this._translate('pool_insights_observation_solar_not_ran_today'),
|
|
312
|
+
});
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
if (solar.estimatedGainTodayKwh !== null && solar.estimatedGainTodayKwh > 0) {
|
|
316
|
+
observations.push({
|
|
317
|
+
area: 'solar',
|
|
318
|
+
level: 'ok',
|
|
319
|
+
text: this._translate('pool_insights_observation_solar_gain_today', {
|
|
320
|
+
kwh: this._round(solar.estimatedGainTodayKwh, 2),
|
|
321
|
+
}),
|
|
322
|
+
});
|
|
323
|
+
} else if (solar.evaluationAvailable && solar.ranToday !== true) {
|
|
324
|
+
observations.push({
|
|
325
|
+
area: 'solar',
|
|
326
|
+
level: 'info',
|
|
327
|
+
text: this._translate('pool_insights_observation_solar_evaluation_available'),
|
|
328
|
+
});
|
|
329
|
+
}
|
|
330
|
+
},
|
|
331
|
+
|
|
332
|
+
_appendPhotovoltaicObservations(photovoltaic, observations) {
|
|
333
|
+
if (photovoltaic.activeToday === true) {
|
|
334
|
+
observations.push({
|
|
335
|
+
area: 'photovoltaic',
|
|
336
|
+
level: 'ok',
|
|
337
|
+
text: this._translate('pool_insights_observation_pv_used_today'),
|
|
338
|
+
});
|
|
339
|
+
} else if (photovoltaic.evaluationAvailable) {
|
|
340
|
+
observations.push({
|
|
341
|
+
area: 'photovoltaic',
|
|
342
|
+
level: 'info',
|
|
343
|
+
text: this._translate('pool_insights_observation_pv_evaluation_available'),
|
|
344
|
+
});
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
if (photovoltaic.runtimeTodayMin !== null && photovoltaic.runtimeTodayMin > 0) {
|
|
348
|
+
observations.push({
|
|
349
|
+
area: 'photovoltaic',
|
|
350
|
+
level: 'ok',
|
|
351
|
+
text: this._translate('pool_insights_observation_pv_runtime_today', {
|
|
352
|
+
minutes: this._round(photovoltaic.runtimeTodayMin, 0),
|
|
353
|
+
}),
|
|
354
|
+
});
|
|
355
|
+
}
|
|
356
|
+
},
|
|
357
|
+
|
|
358
|
+
_appendConsumptionObservations(consumption, observations) {
|
|
359
|
+
if (consumption.dayKwh !== null) {
|
|
360
|
+
observations.push({
|
|
361
|
+
area: 'energy',
|
|
362
|
+
level: 'ok',
|
|
363
|
+
text: this._translate('pool_insights_observation_consumption_today', {
|
|
364
|
+
kwh: this._round(consumption.dayKwh, 2),
|
|
365
|
+
}),
|
|
366
|
+
});
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
if (consumption.dayEur !== null) {
|
|
370
|
+
observations.push({
|
|
371
|
+
area: 'energy',
|
|
372
|
+
level: 'ok',
|
|
373
|
+
text: this._translate('pool_insights_observation_costs_today', {
|
|
374
|
+
eur: this._round(consumption.dayEur, 2),
|
|
375
|
+
}),
|
|
376
|
+
});
|
|
377
|
+
}
|
|
378
|
+
},
|
|
379
|
+
|
|
380
|
+
_appendChemistryObservations(chemistry, observations) {
|
|
381
|
+
const chemistryEvaluations = [
|
|
382
|
+
{
|
|
383
|
+
area: 'chemistry_ph',
|
|
384
|
+
available: chemistry.phAvailable,
|
|
385
|
+
key: 'pool_insights_observation_ph_evaluation_available',
|
|
386
|
+
},
|
|
387
|
+
{
|
|
388
|
+
area: 'chemistry_tds',
|
|
389
|
+
available: chemistry.tdsAvailable,
|
|
390
|
+
key: 'pool_insights_observation_tds_evaluation_available',
|
|
391
|
+
},
|
|
392
|
+
{
|
|
393
|
+
area: 'chemistry_orp',
|
|
394
|
+
available: chemistry.orpAvailable,
|
|
395
|
+
key: 'pool_insights_observation_orp_evaluation_available',
|
|
396
|
+
},
|
|
397
|
+
];
|
|
398
|
+
|
|
399
|
+
for (const entry of chemistryEvaluations) {
|
|
400
|
+
if (!entry.available) {
|
|
401
|
+
continue;
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
observations.push({
|
|
405
|
+
area: entry.area,
|
|
406
|
+
level: 'info',
|
|
407
|
+
text: this._translate(entry.key),
|
|
408
|
+
});
|
|
409
|
+
}
|
|
410
|
+
},
|
|
411
|
+
|
|
412
|
+
_hasLimitedCoreData(snapshot) {
|
|
413
|
+
return (
|
|
414
|
+
this._getTemperatureDelta(snapshot.temperature) === null ||
|
|
415
|
+
snapshot.pump.runtimeTodaySeconds === null ||
|
|
416
|
+
snapshot.pump.startCountToday === null ||
|
|
417
|
+
snapshot.consumption.dayKwh === null
|
|
418
|
+
);
|
|
419
|
+
},
|
|
420
|
+
|
|
421
|
+
_buildSummaryText(level, observations, recommendations) {
|
|
422
|
+
const lead = {
|
|
423
|
+
ok: this._translate('pool_insights_summary_ok'),
|
|
424
|
+
info: this._translate('pool_insights_summary_info'),
|
|
425
|
+
warning: this._translate('pool_insights_summary_warning'),
|
|
426
|
+
}[level];
|
|
427
|
+
|
|
428
|
+
const parts = [lead || this._translate('pool_insights_summary_completed')];
|
|
429
|
+
for (const entry of observations.slice(0, 4)) {
|
|
430
|
+
parts.push(entry.text);
|
|
431
|
+
}
|
|
432
|
+
for (const entry of recommendations.slice(0, 2)) {
|
|
433
|
+
parts.push(entry.text);
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
return parts.join(' ');
|
|
437
|
+
},
|
|
438
|
+
|
|
439
|
+
_buildSummaryHtml(level, observations, recommendations) {
|
|
440
|
+
const items = observations.map(entry => `<li>${this._escapeHtml(entry.text)}</li>`).join('');
|
|
441
|
+
const recommendationItems = recommendations.map(entry => `<li>${this._escapeHtml(entry.text)}</li>`).join('');
|
|
442
|
+
|
|
443
|
+
return [
|
|
444
|
+
`<div class="pool-insights pool-insights-${this._escapeHtml(level)}">`,
|
|
445
|
+
`<strong>${this._escapeHtml(level)}</strong>`,
|
|
446
|
+
items ? `<ul>${items}</ul>` : '',
|
|
447
|
+
recommendationItems
|
|
448
|
+
? `<p>${this._translate('pool_insights_label_recommendations')}:</p><ul>${recommendationItems}</ul>`
|
|
449
|
+
: '',
|
|
450
|
+
'</div>',
|
|
451
|
+
].join('');
|
|
452
|
+
},
|
|
453
|
+
|
|
454
|
+
async _writeResult(result) {
|
|
455
|
+
await this._setState(`${POOL_INSIGHTS_PREFIX}.status`, result.status);
|
|
456
|
+
await this._setState(`${POOL_INSIGHTS_PREFIX}.level`, result.level);
|
|
457
|
+
await this._setState(`${POOL_INSIGHTS_PREFIX}.summary_text`, result.summaryText);
|
|
458
|
+
await this._setState(`${POOL_INSIGHTS_PREFIX}.summary_html`, result.summaryHtml);
|
|
459
|
+
await this._setState(`${POOL_INSIGHTS_PREFIX}.summary_json`, JSON.stringify(result.summaryJson));
|
|
460
|
+
await this._setState(`${POOL_INSIGHTS_PREFIX}.observations_json`, JSON.stringify(result.observations));
|
|
461
|
+
await this._setState(`${POOL_INSIGHTS_PREFIX}.recommendations_json`, JSON.stringify(result.recommendations));
|
|
462
|
+
await this._setState(`${POOL_INSIGHTS_PREFIX}.last_update`, result.lastUpdate);
|
|
463
|
+
await this._setState(`${POOL_INSIGHTS_PREFIX}.debug.last_reason`, result.reason);
|
|
464
|
+
},
|
|
465
|
+
|
|
466
|
+
async _writeDisabled(reason) {
|
|
467
|
+
await this._setState(`${POOL_INSIGHTS_PREFIX}.status`, 'disabled');
|
|
468
|
+
await this._setState(`${POOL_INSIGHTS_PREFIX}.level`, 'none');
|
|
469
|
+
await this._setState(`${POOL_INSIGHTS_PREFIX}.summary_text`, '');
|
|
470
|
+
await this._setState(`${POOL_INSIGHTS_PREFIX}.summary_html`, '');
|
|
471
|
+
await this._setState(`${POOL_INSIGHTS_PREFIX}.summary_json`, '{}');
|
|
472
|
+
await this._setState(`${POOL_INSIGHTS_PREFIX}.observations_json`, '[]');
|
|
473
|
+
await this._setState(`${POOL_INSIGHTS_PREFIX}.recommendations_json`, '[]');
|
|
474
|
+
await this._setState(`${POOL_INSIGHTS_PREFIX}.last_update`, new Date().toISOString());
|
|
475
|
+
await this._setState(`${POOL_INSIGHTS_PREFIX}.debug.last_reason`, reason);
|
|
476
|
+
},
|
|
477
|
+
|
|
478
|
+
async _writeError(reason, err) {
|
|
479
|
+
const now = new Date().toISOString();
|
|
480
|
+
const text = this._translate('pool_insights_error_analysis_failed');
|
|
481
|
+
|
|
482
|
+
await this._setState(`${POOL_INSIGHTS_PREFIX}.status`, 'error');
|
|
483
|
+
await this._setState(`${POOL_INSIGHTS_PREFIX}.level`, 'warning');
|
|
484
|
+
await this._setState(`${POOL_INSIGHTS_PREFIX}.summary_text`, text);
|
|
485
|
+
await this._setState(
|
|
486
|
+
`${POOL_INSIGHTS_PREFIX}.summary_html`,
|
|
487
|
+
`<div class="pool-insights-error">${this._escapeHtml(text)}</div>`,
|
|
488
|
+
);
|
|
489
|
+
await this._setState(
|
|
490
|
+
`${POOL_INSIGHTS_PREFIX}.summary_json`,
|
|
491
|
+
JSON.stringify({
|
|
492
|
+
status: 'error',
|
|
493
|
+
level: 'warning',
|
|
494
|
+
reason,
|
|
495
|
+
last_update: now,
|
|
496
|
+
error: err?.message || 'unknown',
|
|
497
|
+
}),
|
|
498
|
+
);
|
|
499
|
+
await this._setState(`${POOL_INSIGHTS_PREFIX}.observations_json`, '[]');
|
|
500
|
+
await this._setState(
|
|
501
|
+
`${POOL_INSIGHTS_PREFIX}.recommendations_json`,
|
|
502
|
+
JSON.stringify([
|
|
503
|
+
{
|
|
504
|
+
area: 'pool',
|
|
505
|
+
level: 'warning',
|
|
506
|
+
text: this._translate('pool_insights_error_check_log'),
|
|
507
|
+
},
|
|
508
|
+
]),
|
|
509
|
+
);
|
|
510
|
+
await this._setState(`${POOL_INSIGHTS_PREFIX}.last_update`, now);
|
|
511
|
+
await this._setState(`${POOL_INSIGHTS_PREFIX}.debug.last_reason`, reason);
|
|
512
|
+
},
|
|
513
|
+
|
|
514
|
+
async _sendSpeechIfAllowed(result, reason) {
|
|
515
|
+
const sendToSpeech = await this._readBoolean(`${POOL_INSIGHTS_PREFIX}.send_to_speech_queue`);
|
|
516
|
+
if (!sendToSpeech || !result.summaryText) {
|
|
517
|
+
return;
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
if (reason === 'daily') {
|
|
521
|
+
const lastSpeechAt = await this._readString(`${POOL_INSIGHTS_PREFIX}.last_speech_at`);
|
|
522
|
+
const lastSpeechTime = Date.parse(lastSpeechAt || '');
|
|
523
|
+
if (Number.isFinite(lastSpeechTime) && Date.now() - lastSpeechTime < SPEECH_COOLDOWN_MS) {
|
|
524
|
+
this.adapter.log.debug('[poolInsightsHelper] Automatic speech skipped due to cooldown');
|
|
525
|
+
return;
|
|
526
|
+
}
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
// Use the relative state id intentionally so multiple adapter instances write to their own speech.queue.
|
|
530
|
+
await this._setState('speech.queue', result.summaryText, false);
|
|
531
|
+
await this._setState(`${POOL_INSIGHTS_PREFIX}.last_speech_at`, new Date().toISOString());
|
|
532
|
+
},
|
|
533
|
+
|
|
534
|
+
async _readState(id) {
|
|
535
|
+
try {
|
|
536
|
+
return await this.adapter.getStateAsync(id);
|
|
537
|
+
} catch {
|
|
538
|
+
return null;
|
|
539
|
+
}
|
|
540
|
+
},
|
|
541
|
+
|
|
542
|
+
async _readString(id) {
|
|
543
|
+
const state = await this._readState(id);
|
|
544
|
+
if (state === null || state.val === null || state.val === undefined || state.val === '') {
|
|
545
|
+
return '';
|
|
546
|
+
}
|
|
547
|
+
return String(state.val);
|
|
548
|
+
},
|
|
549
|
+
|
|
550
|
+
async _readNumber(id) {
|
|
551
|
+
const state = await this._readState(id);
|
|
552
|
+
if (state === null || state.val === null || state.val === undefined || state.val === '') {
|
|
553
|
+
return null;
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
const value = Number(state.val);
|
|
557
|
+
return Number.isFinite(value) ? value : null;
|
|
558
|
+
},
|
|
559
|
+
|
|
560
|
+
async _readBoolean(id) {
|
|
561
|
+
const state = await this._readState(id);
|
|
562
|
+
return state?.val === true;
|
|
563
|
+
},
|
|
564
|
+
|
|
565
|
+
async _hasValue(id) {
|
|
566
|
+
return (await this._readString(id)) !== '';
|
|
567
|
+
},
|
|
568
|
+
|
|
569
|
+
async _setState(id, val, ack = true) {
|
|
570
|
+
await this.adapter.setStateChangedAsync(id, { val, ack });
|
|
571
|
+
},
|
|
572
|
+
|
|
573
|
+
_formatRuntime(seconds) {
|
|
574
|
+
const totalMinutes = Math.max(0, Math.round(seconds / 60));
|
|
575
|
+
const hours = Math.floor(totalMinutes / 60);
|
|
576
|
+
const minutes = totalMinutes % 60;
|
|
577
|
+
if (hours > 0) {
|
|
578
|
+
return `${hours} h ${minutes} min`;
|
|
579
|
+
}
|
|
580
|
+
return `${minutes} min`;
|
|
581
|
+
},
|
|
582
|
+
|
|
583
|
+
_raiseLevel(current, candidate) {
|
|
584
|
+
const order = { ok: 0, info: 1, warning: 2 };
|
|
585
|
+
return order[candidate] > order[current] ? candidate : current;
|
|
586
|
+
},
|
|
587
|
+
|
|
588
|
+
_round(value, digits) {
|
|
589
|
+
const factor = 10 ** digits;
|
|
590
|
+
return Math.round(value * factor) / factor;
|
|
591
|
+
},
|
|
592
|
+
|
|
593
|
+
_translate(key, replacements = {}) {
|
|
594
|
+
let text = I18n.translate(key);
|
|
595
|
+
for (const [name, value] of Object.entries(replacements)) {
|
|
596
|
+
text = text.replace(`{${name}}`, String(value));
|
|
597
|
+
}
|
|
598
|
+
return text;
|
|
599
|
+
},
|
|
600
|
+
|
|
601
|
+
_escapeHtml(text) {
|
|
602
|
+
return String(text)
|
|
603
|
+
.replace(/&/g, '&')
|
|
604
|
+
.replace(/</g, '<')
|
|
605
|
+
.replace(/>/g, '>')
|
|
606
|
+
.replace(/"/g, '"')
|
|
607
|
+
.replace(/'/g, ''');
|
|
608
|
+
},
|
|
609
|
+
|
|
610
|
+
cleanup() {
|
|
611
|
+
if (this.dailyTimer) {
|
|
612
|
+
this.adapter.clearTimeout(this.dailyTimer);
|
|
613
|
+
this.dailyTimer = null;
|
|
614
|
+
}
|
|
615
|
+
this.running = false;
|
|
616
|
+
this.adapter && this.adapter.log.debug('[poolInsightsHelper] Cleanup completed');
|
|
617
|
+
},
|
|
618
|
+
};
|
|
619
|
+
|
|
620
|
+
module.exports = poolInsightsHelper;
|
|
@@ -416,7 +416,8 @@ const statisticsHelper = {
|
|
|
416
416
|
async _scheduleMidnightReset() {
|
|
417
417
|
const adapter = this.adapter;
|
|
418
418
|
if (this.midnightTimer) {
|
|
419
|
-
clearTimeout(this.midnightTimer);
|
|
419
|
+
this.adapter.clearTimeout(this.midnightTimer);
|
|
420
|
+
this.midnightTimer = null;
|
|
420
421
|
}
|
|
421
422
|
|
|
422
423
|
const now = new Date();
|
|
@@ -424,7 +425,9 @@ const statisticsHelper = {
|
|
|
424
425
|
nextMidnight.setHours(24, 0, 5, 0);
|
|
425
426
|
const msUntilMidnight = nextMidnight.getTime() - now.getTime();
|
|
426
427
|
|
|
427
|
-
this.midnightTimer = setTimeout(async () => {
|
|
428
|
+
this.midnightTimer = this.adapter.setTimeout(async () => {
|
|
429
|
+
this.midnightTimer = null;
|
|
430
|
+
|
|
428
431
|
await this._resetDailyTemperatureStats();
|
|
429
432
|
await this._scheduleMidnightReset();
|
|
430
433
|
}, msUntilMidnight);
|
|
@@ -523,7 +526,8 @@ const statisticsHelper = {
|
|
|
523
526
|
|
|
524
527
|
cleanup() {
|
|
525
528
|
if (this.midnightTimer) {
|
|
526
|
-
clearTimeout(this.midnightTimer);
|
|
529
|
+
this.adapter.clearTimeout(this.midnightTimer);
|
|
530
|
+
this.midnightTimer = null;
|
|
527
531
|
}
|
|
528
532
|
},
|
|
529
533
|
};
|