iobroker.pv-notifications 1.2.31 → 1.2.33
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 +32 -0
- package/io-package.json +27 -27
- package/main.js +363 -164
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -2,6 +2,16 @@
|
|
|
2
2
|
|
|
3
3
|

|
|
4
4
|
|
|
5
|
+

|
|
6
|
+
[](https://www.npmjs.com/package/iobroker.pv-notifications)
|
|
7
|
+
[](https://www.npmjs.com/package/iobroker.pv-notifications)
|
|
8
|
+
[](LICENSE)
|
|
9
|
+
|
|
10
|
+
[](https://nodei.co/npm/iobroker.pv-notifications/)
|
|
11
|
+
|
|
12
|
+
[](https://github.com/sadam6752-tech/ioBroker.pv-notifications/actions/workflows/test-and-release.yml)
|
|
13
|
+

|
|
14
|
+
|
|
5
15
|
Sends Telegram notifications for PV battery status (full, empty, intermediate levels).
|
|
6
16
|
|
|
7
17
|
## Features
|
|
@@ -314,6 +324,28 @@ sadam6752@gmail.com
|
|
|
314
324
|
### **WORK IN PROGRESS**
|
|
315
325
|
-->
|
|
316
326
|
|
|
327
|
+
### 1.2.33 (2026-10-08)
|
|
328
|
+
* (FIX) Telegram messages were sent to **all** users of the bot instead of only the configured users (`user` instead of the ignored `users` parameter)
|
|
329
|
+
* (FIX) Bold text was shown with literal asterisks - messages are now sent with HTML formatting (values from other adapters are escaped)
|
|
330
|
+
* (CHANGE) Full/empty thresholds and intermediate steps are detected when reached or crossed (e.g. SOC jumps from 39 to 41, decimal SOC values, battery stopping at 99 % with a 98 % threshold); a jump over several steps sends one message
|
|
331
|
+
* (FIX) Statistics were not saved when the adapter was stopped (unload handler was not registered)
|
|
332
|
+
* (FIX) Last week/month values were overwritten with 0 on the first adapter start of a day
|
|
333
|
+
* (FIX) Daily statistics reset now happens at midnight; before it only ran if the daily stats time was a multiple of 5 minutes
|
|
334
|
+
* (FIX) Min/max SOC of the day are written immediately
|
|
335
|
+
* (FIX) Weekly/monthly auto-save runs once in the window 23:55-23:59, so a delayed timer tick no longer skips it; monthly cycles roll over even when monthly statistics are disabled
|
|
336
|
+
* (FIX) Sunset object may also contain a timestamp or ISO date; missing time settings no longer crash the scheduler
|
|
337
|
+
* (FIX) 0 °C was not shown; numeric weather values no longer break the weather block
|
|
338
|
+
* (FIX) Errors while processing state changes no longer stop the adapter; test button cannot get stuck
|
|
339
|
+
|
|
340
|
+
### 1.2.32 (2026-10-08)
|
|
341
|
+
* (FIX) Daily, weekly and monthly statistics (cycles, min/max SOC) are no longer reset on every adapter restart
|
|
342
|
+
* (FIX) Monthly statistics reported the daily cycle counters; there are now real monthly counters (`statistics.fullCyclesMonth`, `statistics.emptyCyclesMonth`)
|
|
343
|
+
* (FIX) Weekly/monthly auto-save no longer fails with an unhandled error when the weekly/monthly data points are empty
|
|
344
|
+
* (FIX) SOC delivered as a string (e.g. "100") now triggers the full/empty/intermediate notifications
|
|
345
|
+
* (FIX) Test message: tomorrow's weather no longer breaks when only text or only temperature is configured
|
|
346
|
+
* (FIX) Weekly/monthly own consumption can no longer be negative
|
|
347
|
+
* (ADD) Unit tests (`npm run test:js`), README badges, `.gitattributes` (LF line endings)
|
|
348
|
+
|
|
317
349
|
### 1.2.31 (2026-09-21)
|
|
318
350
|
* (ADD) CI: Node.js 26 in the test matrix and a new deploy job using npm Trusted Publishing (OIDC) with provenance
|
|
319
351
|
* (FIX) Added the missing changelog entry for 1.2.30
|
package/io-package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"common": {
|
|
3
3
|
"name": "pv-notifications",
|
|
4
|
-
"version": "1.2.
|
|
4
|
+
"version": "1.2.33",
|
|
5
5
|
"titleLang": {
|
|
6
6
|
"en": "PV Notifications",
|
|
7
7
|
"de": "PV-Benachrichtigungen",
|
|
@@ -29,6 +29,32 @@
|
|
|
29
29
|
"zh-cn": "发送 Telegram 通知用于 PV 电池状态"
|
|
30
30
|
},
|
|
31
31
|
"news": {
|
|
32
|
+
"1.2.33": {
|
|
33
|
+
"en": "Fix: Telegram messages were sent to all users of the bot instead of only the configured users. Fix: bold text was shown with literal asterisks (messages now use HTML formatting). Thresholds and intermediate steps are now detected when reached or crossed (e.g. SOC jumps 39 -> 41 or decimal values). Fix: statistics were not saved on adapter stop; last week/month values were reset on the first start of a day; daily reset now happens at midnight; weekly/monthly saving is more robust. Fix: 0 °C was not shown.",
|
|
34
|
+
"de": "Fix: Telegram-Nachrichten gingen an alle Benutzer des Bots statt nur an die konfigurierten Benutzer. Fix: Fettschrift wurde mit sichtbaren Sternchen angezeigt (Nachrichten nutzen jetzt HTML-Formatierung). Schwellwerte und Zwischenstufen werden jetzt erkannt, wenn sie erreicht oder ueberschritten werden (z. B. SOC springt von 39 auf 41 oder Dezimalwerte). Fix: Statistik wurde beim Stoppen nicht gespeichert; Werte der letzten Woche/des letzten Monats wurden beim ersten Start eines Tages zurueckgesetzt; Tagesreset jetzt um Mitternacht; Wochen-/Monatsspeicherung robuster. Fix: 0 °C wurde nicht angezeigt.",
|
|
35
|
+
"ru": "Исправлено: сообщения Telegram отправлялись всем пользователям бота, а не только настроенным. Исправлено: жирный текст отображался с видимыми звёздочками (теперь используется HTML-форматирование). Пороги и промежуточные ступени теперь срабатывают при достижении или пересечении (например, скачок SOC с 39 на 41 или дробные значения). Исправлено: статистика не сохранялась при остановке адаптера; значения прошлой недели/месяца сбрасывались при первом запуске за день; дневной сброс теперь в полночь; сохранение недели/месяца надёжнее. Исправлено: 0 °C не отображался.",
|
|
36
|
+
"fr": "Correction : les messages Telegram etaient envoyes a tous les utilisateurs du bot au lieu des seuls utilisateurs configures. Correction : le texte en gras s'affichait avec des asterisques visibles (les messages utilisent maintenant le format HTML). Les seuils et paliers intermediaires sont detectes lorsqu'ils sont atteints ou franchis (p. ex. SOC passant de 39 a 41 ou valeurs decimales). Correction : statistiques non enregistrees a l'arret ; valeurs de la semaine/du mois precedent reinitialisees au premier demarrage du jour ; remise a zero quotidienne a minuit ; enregistrement hebdomadaire/mensuel plus robuste. Correction : 0 °C n'etait pas affiche.",
|
|
37
|
+
"it": "Correzione: i messaggi Telegram venivano inviati a tutti gli utenti del bot invece che solo agli utenti configurati. Correzione: il grassetto veniva mostrato con asterischi visibili (ora i messaggi usano la formattazione HTML). Soglie e livelli intermedi vengono ora rilevati quando raggiunti o superati (ad es. SOC che salta da 39 a 41 o valori decimali). Correzione: statistiche non salvate all'arresto; valori della settimana/mese precedente azzerati al primo avvio del giorno; azzeramento giornaliero ora a mezzanotte; salvataggio settimanale/mensile piu robusto. Correzione: 0 °C non veniva mostrato.",
|
|
38
|
+
"es": "Correccion: los mensajes de Telegram se enviaban a todos los usuarios del bot en lugar de solo a los usuarios configurados. Correccion: la negrita se mostraba con asteriscos visibles (los mensajes ahora usan formato HTML). Los umbrales y niveles intermedios ahora se detectan al alcanzarse o superarse (p. ej. SOC que salta de 39 a 41 o valores decimales). Correccion: las estadisticas no se guardaban al detener el adaptador; los valores de la semana/mes anterior se reiniciaban en el primer inicio del dia; reinicio diario ahora a medianoche; guardado semanal/mensual mas robusto. Correccion: 0 °C no se mostraba.",
|
|
39
|
+
"pl": "Poprawka: wiadomosci Telegram byly wysylane do wszystkich uzytkownikow bota zamiast tylko do skonfigurowanych. Poprawka: pogrubienie wyswietlalo sie z widocznymi gwiazdkami (wiadomosci uzywaja teraz formatowania HTML). Progi i stopnie posrednie sa teraz wykrywane po osiagnieciu lub przekroczeniu (np. skok SOC z 39 na 41 lub wartosci dziesietne). Poprawka: statystyki nie byly zapisywane przy zatrzymaniu adaptera; wartosci z poprzedniego tygodnia/miesiaca byly zerowane przy pierwszym uruchomieniu w danym dniu; dzienny reset teraz o polnocy; zapis tygodniowy/miesieczny bardziej niezawodny. Poprawka: 0 °C nie bylo wyswietlane.",
|
|
40
|
+
"pt": "Correcao: as mensagens do Telegram eram enviadas a todos os utilizadores do bot em vez de apenas aos utilizadores configurados. Correcao: o negrito aparecia com asteriscos visiveis (as mensagens usam agora formatacao HTML). Os limites e niveis intermedios sao agora detetados quando atingidos ou ultrapassados (p. ex. SOC a saltar de 39 para 41 ou valores decimais). Correcao: as estatisticas nao eram guardadas ao parar o adaptador; os valores da semana/mes anterior eram repostos no primeiro arranque do dia; reposicao diaria agora a meia-noite; gravacao semanal/mensal mais robusta. Correcao: 0 °C nao era mostrado.",
|
|
41
|
+
"nl": "Fix: Telegram-berichten werden naar alle gebruikers van de bot gestuurd in plaats van alleen naar de geconfigureerde gebruikers. Fix: vetgedrukte tekst werd met zichtbare sterretjes getoond (berichten gebruiken nu HTML-opmaak). Drempels en tussenstappen worden nu herkend wanneer ze bereikt of overschreden worden (bijv. SOC springt van 39 naar 41 of decimale waarden). Fix: statistieken werden niet opgeslagen bij het stoppen van de adapter; waarden van vorige week/maand werden bij de eerste start van een dag gereset; dagelijkse reset nu om middernacht; week-/maandopslag robuuster. Fix: 0 °C werd niet getoond.",
|
|
42
|
+
"uk": "Виправлено: повідомлення Telegram надсилалися всім користувачам бота, а не лише налаштованим. Виправлено: жирний текст показувався з видимими зірочками (тепер використовується HTML-форматування). Пороги та проміжні рівні тепер спрацьовують при досягненні або перетині (наприклад, стрибок SOC з 39 на 41 або дробові значення). Виправлено: статистика не зберігалася під час зупинки адаптера; значення минулого тижня/місяця скидалися під час першого запуску за день; щоденне скидання тепер опівночі; збереження тижня/місяця надійніше. Виправлено: 0 °C не відображався.",
|
|
43
|
+
"zh-cn": "修复:Telegram 消息会发送给机器人的所有用户,而不仅是已配置的用户。修复:粗体文字显示为可见的星号(消息现在使用 HTML 格式)。阈值和中间级别现在在达到或越过时触发(例如 SOC 从 39 跳到 41 或小数值)。修复:停止适配器时未保存统计数据;上周/上月数值在当天首次启动时被重置;每日重置改为午夜;周/月保存更可靠。修复:0 °C 未显示。"
|
|
44
|
+
},
|
|
45
|
+
"1.2.32": {
|
|
46
|
+
"en": "Fix: daily, weekly and monthly statistics are no longer lost on adapter restart. Fix: monthly statistics now report real monthly cycle counts. Fix: weekly/monthly saving no longer fails with empty data points. Fix: SOC delivered as text is accepted. Fix: test message with partially configured weather. Added unit tests and README badges.",
|
|
47
|
+
"de": "Fix: Tages-, Wochen- und Monatsstatistik gehen beim Neustart des Adapters nicht mehr verloren. Fix: Die Monatsstatistik zeigt jetzt die echten Monatszyklen. Fix: Wochen-/Monatsspeicherung schlaegt bei leeren Datenpunkten nicht mehr fehl. Fix: SOC als Text wird akzeptiert. Fix: Testnachricht bei teilweise konfiguriertem Wetter. Unit-Tests und README-Badges hinzugefuegt.",
|
|
48
|
+
"ru": "Исправлено: дневная, недельная и месячная статистика больше не теряется при перезапуске адаптера. Исправлено: месячная статистика показывает реальное число месячных циклов. Исправлено: сохранение недельных/месячных данных больше не падает при пустых точках данных. Исправлено: SOC в виде текста принимается. Исправлено: тестовое сообщение при частично настроенной погоде. Добавлены юнит-тесты и бейджи в README.",
|
|
49
|
+
"fr": "Correction : les statistiques journalieres, hebdomadaires et mensuelles ne sont plus perdues au redemarrage de l'adaptateur. Correction : les statistiques mensuelles indiquent les vrais cycles mensuels. Correction : l'enregistrement hebdomadaire/mensuel n'echoue plus avec des points de donnees vides. Correction : SOC fourni en texte accepte. Correction : message de test avec meteo partiellement configuree. Tests unitaires et badges README ajoutes.",
|
|
50
|
+
"it": "Correzione: le statistiche giornaliere, settimanali e mensili non vengono piu perse al riavvio dell'adattatore. Correzione: le statistiche mensili mostrano i veri cicli mensili. Correzione: il salvataggio settimanale/mensile non fallisce piu con punti dati vuoti. Correzione: SOC fornito come testo accettato. Correzione: messaggio di test con meteo configurato parzialmente. Aggiunti test unitari e badge README.",
|
|
51
|
+
"es": "Correccion: las estadisticas diarias, semanales y mensuales ya no se pierden al reiniciar el adaptador. Correccion: las estadisticas mensuales muestran los ciclos mensuales reales. Correccion: el guardado semanal/mensual ya no falla con puntos de datos vacios. Correccion: se acepta SOC entregado como texto. Correccion: mensaje de prueba con clima configurado parcialmente. Anadidas pruebas unitarias e insignias en README.",
|
|
52
|
+
"pl": "Poprawka: statystyki dzienne, tygodniowe i miesieczne nie sa juz tracone po restarcie adaptera. Poprawka: statystyki miesieczne pokazuja prawdziwe cykle miesieczne. Poprawka: zapis tygodniowy/miesieczny nie konczy sie juz bledem przy pustych punktach danych. Poprawka: SOC dostarczony jako tekst jest akceptowany. Poprawka: wiadomosc testowa przy czesciowo skonfigurowanej pogodzie. Dodano testy jednostkowe i odznaki w README.",
|
|
53
|
+
"pt": "Correcao: as estatisticas diarias, semanais e mensais ja nao se perdem ao reiniciar o adaptador. Correcao: as estatisticas mensais mostram os ciclos mensais reais. Correcao: a gravacao semanal/mensal ja nao falha com pontos de dados vazios. Correcao: SOC entregue como texto e aceite. Correcao: mensagem de teste com meteorologia parcialmente configurada. Adicionados testes unitarios e emblemas no README.",
|
|
54
|
+
"nl": "Fix: dag-, week- en maandstatistieken gaan niet meer verloren bij het herstarten van de adapter. Fix: maandstatistieken tonen de echte maandcycli. Fix: week-/maandopslag mislukt niet meer bij lege datapunten. Fix: SOC als tekst wordt geaccepteerd. Fix: testbericht bij gedeeltelijk geconfigureerd weer. Unit-tests en README-badges toegevoegd.",
|
|
55
|
+
"uk": "Виправлено: денна, тижнева та місячна статистика більше не втрачається під час перезапуску адаптера. Виправлено: місячна статистика показує реальну кількість місячних циклів. Виправлено: збереження тижневих/місячних даних більше не падає з порожніми точками даних. Виправлено: SOC у вигляді тексту приймається. Виправлено: тестове повідомлення при частково налаштованій погоді. Додано модульні тести та бейджі в README.",
|
|
56
|
+
"zh-cn": "修复:适配器重启后日、周、月统计数据不再丢失。修复:月统计现在显示真实的月循环次数。修复:数据点为空时每周/每月保存不再失败。修复:接受文本形式的SOC。修复:天气部分配置时的测试消息。新增单元测试和README徽章。"
|
|
57
|
+
},
|
|
32
58
|
"1.2.31": {
|
|
33
59
|
"en": "CI: Node.js 26 added to the test matrix and a new deploy job using npm Trusted Publishing (OIDC) with provenance. Added the missing changelog entry for 1.2.30 and completed the short 1.2.29 news translations. Dependencies: @iobroker/testing 6.2.x, @alcalzone/release-script-plugin-license 5.2.2.",
|
|
34
60
|
"de": "CI: Node.js 26 in die Test-Matrix aufgenommen und neuer Deploy-Job mit npm Trusted Publishing (OIDC) samt Provenance. Fehlender Changelog-Eintrag fuer 1.2.30 ergaenzt und zu kurze 1.2.29-News-Uebersetzungen vervollstaendigt. Abhaengigkeiten: @iobroker/testing 6.2.x, @alcalzone/release-script-plugin-license 5.2.2.",
|
|
@@ -93,32 +119,6 @@
|
|
|
93
119
|
"nl": "Fix CI: package-lock.json bijgewerkt om alle eslint peer dependencies op te nemen",
|
|
94
120
|
"uk": "Виправлено CI: оновлено package-lock.json для включення всіх peer-залежностей eslint",
|
|
95
121
|
"zh-cn": "修复CI:更新package-lock.json以包含所有eslint对等依赖项"
|
|
96
|
-
},
|
|
97
|
-
"1.2.26": {
|
|
98
|
-
"en": "Fix W5604: add missing i18n keys for sunset stats in 8 languages; move old changelog to CHANGELOG_OLD.md; fix README structure",
|
|
99
|
-
"de": "W5604: fehlende i18n-Schluessel fuer Sonnenuntergangs-Statistik in 8 Sprachen ergaenzt; alten Changelog in CHANGELOG_OLD.md verschoben; README-Struktur korrigiert",
|
|
100
|
-
"ru": "W5604: добавлены отсутствующие i18n ключи для статистики заката в 8 языках; старый changelog перенесён в CHANGELOG_OLD.md; исправлена структура README",
|
|
101
|
-
"fr": "W5604: clés i18n manquantes pour stats coucher du soleil ajoutées dans 8 langues; ancien changelog déplacé dans CHANGELOG_OLD.md; structure README corrigée",
|
|
102
|
-
"it": "W5604: chiavi i18n mancanti per stats tramonto aggiunte in 8 lingue; vecchio changelog spostato in CHANGELOG_OLD.md; struttura README corretta",
|
|
103
|
-
"es": "W5604: claves i18n faltantes para stats atardecer añadidas en 8 idiomas; changelog antiguo movido a CHANGELOG_OLD.md; estructura README corregida",
|
|
104
|
-
"pl": "W5604: brakujące klucze i18n dla statystyk zachodu słońca dodane w 8 językach; stary changelog przeniesiony do CHANGELOG_OLD.md; poprawiona struktura README",
|
|
105
|
-
"pt": "W5604: chaves i18n em falta para stats pôr do sol adicionadas em 8 idiomas; changelog antigo movido para CHANGELOG_OLD.md; estrutura README corrigida",
|
|
106
|
-
"nl": "W5604: ontbrekende i18n-sleutels voor zonsondergang-statistieken in 8 talen toegevoegd; oud changelog verplaatst naar CHANGELOG_OLD.md; README-structuur gecorrigeerd",
|
|
107
|
-
"uk": "W5604: додано відсутні i18n ключі для статистики заходу сонця у 8 мовах; старий changelog перенесено до CHANGELOG_OLD.md; виправлено структуру README",
|
|
108
|
-
"zh-cn": "修复W5604:在8种语言中添加日落统计缺失的i18n键;将旧changelog移至CHANGELOG_OLD.md;修复README结构"
|
|
109
|
-
},
|
|
110
|
-
"1.2.25": {
|
|
111
|
-
"en": "Fix W5604: add missing weather i18n keys in 8 languages; update engines.node to >=22; fix workflow node versions; update devDependencies; add tsconfig.json, prettier.config.mjs",
|
|
112
|
-
"de": "W5604 behoben: fehlende Wetter-i18n-Schlüssel in 8 Sprachen ergänzt; engines.node auf >=22 aktualisiert; Workflow-Node-Versionen korrigiert; devDependencies aktualisiert; tsconfig.json, prettier.config.mjs hinzugefügt",
|
|
113
|
-
"ru": "Исправление W5604: добавлены отсутствующие weather i18n ключи в 8 языках; engines.node обновлён до >=22; исправлены версии node в workflow; обновлены devDependencies; добавлены tsconfig.json, prettier.config.mjs",
|
|
114
|
-
"fr": "Fix W5604: clés i18n météo manquantes ajoutées dans 8 langues; engines.node mis à jour à >=22; versions node workflow corrigées; devDependencies mis à jour; tsconfig.json, prettier.config.mjs ajoutés",
|
|
115
|
-
"it": "Fix W5604: chiavi i18n meteo mancanti aggiunte in 8 lingue; engines.node aggiornato a >=22; versioni node workflow corrette; devDependencies aggiornati; tsconfig.json, prettier.config.mjs aggiunti",
|
|
116
|
-
"es": "Fix W5604: claves i18n clima faltantes añadidas en 8 idiomas; engines.node actualizado a >=22; versiones node workflow corregidas; devDependencies actualizados; tsconfig.json, prettier.config.mjs añadidos",
|
|
117
|
-
"pl": "Naprawa W5604: dodano brakujące klucze i18n pogody w 8 językach; engines.node zaktualizowano do >=22; poprawiono wersje node w workflow; zaktualizowano devDependencies; dodano tsconfig.json, prettier.config.mjs",
|
|
118
|
-
"pt": "Fix W5604: chaves i18n meteorologia em falta adicionadas em 8 idiomas; engines.node atualizado para >=22; versões node workflow corrigidas; devDependencies atualizados; tsconfig.json, prettier.config.mjs adicionados",
|
|
119
|
-
"nl": "Fix W5604: ontbrekende weer-i18n-sleutels in 8 talen toegevoegd; engines.node bijgewerkt naar >=22; node-versies workflow gecorrigeerd; devDependencies bijgewerkt; tsconfig.json, prettier.config.mjs toegevoegd",
|
|
120
|
-
"uk": "Виправлення W5604: додано відсутні weather i18n ключі у 8 мовах; engines.node оновлено до >=22; виправлено версії node у workflow; оновлено devDependencies; додано tsconfig.json, prettier.config.mjs",
|
|
121
|
-
"zh-cn": "修复W5604:在8种语言中添加缺失的weather i18n键;将engines.node更新为>=22;修复workflow中的node版本;更新devDependencies;添加tsconfig.json、prettier.config.mjs"
|
|
122
122
|
}
|
|
123
123
|
},
|
|
124
124
|
"authors": [
|
package/main.js
CHANGED
|
@@ -42,6 +42,8 @@ class PvNotifications extends utils.Adapter {
|
|
|
42
42
|
minSOC: 100,
|
|
43
43
|
weekFullCycles: 0,
|
|
44
44
|
weekEmptyCycles: 0,
|
|
45
|
+
monthFullCycles: 0,
|
|
46
|
+
monthEmptyCycles: 0,
|
|
45
47
|
lastStatsReset: new Date().getDate(),
|
|
46
48
|
lastWeekReset: new Date().getDay(),
|
|
47
49
|
lastMonthReset: 0,
|
|
@@ -73,6 +75,9 @@ class PvNotifications extends utils.Adapter {
|
|
|
73
75
|
|
|
74
76
|
// StateChange-Handler registrieren (für js-controller 7+)
|
|
75
77
|
this.on('stateChange', this.onStateChange);
|
|
78
|
+
|
|
79
|
+
// Unload handler (saves statistics, resets connection state)
|
|
80
|
+
this.on('unload', this.onUnload);
|
|
76
81
|
}
|
|
77
82
|
|
|
78
83
|
/**
|
|
@@ -155,6 +160,32 @@ class PvNotifications extends utils.Adapter {
|
|
|
155
160
|
type: 'state',
|
|
156
161
|
common: { name: 'Empty cycles this week', type: 'number', role: 'value', read: true, write: false, def: 0 },
|
|
157
162
|
});
|
|
163
|
+
await this.setObjectNotExists('statistics.fullCyclesMonth', {
|
|
164
|
+
type: 'state',
|
|
165
|
+
common: { name: 'Full cycles this month', type: 'number', role: 'value', read: true, write: false, def: 0 },
|
|
166
|
+
});
|
|
167
|
+
await this.setObjectNotExists('statistics.emptyCyclesMonth', {
|
|
168
|
+
type: 'state',
|
|
169
|
+
common: {
|
|
170
|
+
name: 'Empty cycles this month',
|
|
171
|
+
type: 'number',
|
|
172
|
+
role: 'value',
|
|
173
|
+
read: true,
|
|
174
|
+
write: false,
|
|
175
|
+
def: 0,
|
|
176
|
+
},
|
|
177
|
+
});
|
|
178
|
+
await this.setObjectNotExists('statistics.lastStatsReset', {
|
|
179
|
+
type: 'state',
|
|
180
|
+
common: {
|
|
181
|
+
name: 'Day of last daily reset',
|
|
182
|
+
type: 'number',
|
|
183
|
+
role: 'value',
|
|
184
|
+
read: true,
|
|
185
|
+
write: false,
|
|
186
|
+
def: 0,
|
|
187
|
+
},
|
|
188
|
+
});
|
|
158
189
|
await this.extendObject('statistics.currentSOC', {
|
|
159
190
|
type: 'state',
|
|
160
191
|
common: {
|
|
@@ -516,15 +547,35 @@ class PvNotifications extends utils.Adapter {
|
|
|
516
547
|
try {
|
|
517
548
|
const today = new Date().getDate();
|
|
518
549
|
const lastReset = await this.getStateAsync('statistics.lastStatsReset');
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
550
|
+
const num = async id => {
|
|
551
|
+
const st = await this.getStateAsync(id);
|
|
552
|
+
return st && typeof st.val === 'number' ? st.val : 0;
|
|
553
|
+
};
|
|
554
|
+
|
|
555
|
+
// Week/month counters survive restarts
|
|
556
|
+
this.stats.weekFullCycles = await num('statistics.fullCyclesWeek');
|
|
557
|
+
this.stats.weekEmptyCycles = await num('statistics.emptyCyclesWeek');
|
|
558
|
+
this.stats.monthFullCycles = await num('statistics.fullCyclesMonth');
|
|
559
|
+
this.stats.monthEmptyCycles = await num('statistics.emptyCyclesMonth');
|
|
560
|
+
|
|
561
|
+
if (lastReset && lastReset.val === today) {
|
|
562
|
+
// Same day - restore daily values (a restart must not wipe them)
|
|
563
|
+
this.stats.fullCycles = await num('statistics.fullCyclesToday');
|
|
564
|
+
this.stats.emptyCycles = await num('statistics.emptyCyclesToday');
|
|
565
|
+
const max = await this.getStateAsync('statistics.maxSOCToday');
|
|
566
|
+
const min = await this.getStateAsync('statistics.minSOCToday');
|
|
567
|
+
this.stats.maxSOC = max && typeof max.val === 'number' ? max.val : 0;
|
|
568
|
+
this.stats.minSOC = min && typeof min.val === 'number' ? min.val : 100;
|
|
569
|
+
this.stats.lastStatsReset = today;
|
|
570
|
+
}
|
|
571
|
+
const isNewDay = !(lastReset && lastReset.val === today);
|
|
572
|
+
if (isNewDay) {
|
|
573
|
+
// New day - reset statistics (saved below, after last week/month values are loaded)
|
|
522
574
|
this.stats.fullCycles = 0;
|
|
523
575
|
this.stats.emptyCycles = 0;
|
|
524
576
|
this.stats.maxSOC = 0;
|
|
525
577
|
this.stats.minSOC = 100;
|
|
526
578
|
this.stats.lastStatsReset = today;
|
|
527
|
-
await this.saveStatistics();
|
|
528
579
|
}
|
|
529
580
|
|
|
530
581
|
// Load saved last week data from states
|
|
@@ -567,6 +618,11 @@ class PvNotifications extends utils.Adapter {
|
|
|
567
618
|
this.stats.lastMonthEmptyCycles =
|
|
568
619
|
lastMonthEmptyCycles && lastMonthEmptyCycles.val !== null ? lastMonthEmptyCycles.val : 0;
|
|
569
620
|
|
|
621
|
+
if (isNewDay) {
|
|
622
|
+
// Must run after loading last week/month values, otherwise they are overwritten with 0
|
|
623
|
+
await this.saveStatistics();
|
|
624
|
+
}
|
|
625
|
+
|
|
570
626
|
this.log.debug('Statistics loaded from states');
|
|
571
627
|
} catch (e) {
|
|
572
628
|
this.log.error(`Error loading statistics: ${e.message}`);
|
|
@@ -586,7 +642,7 @@ class PvNotifications extends utils.Adapter {
|
|
|
586
642
|
const socState = await this.getForeignStateAsync(this.config.batterySOC);
|
|
587
643
|
if (socState && socState.val !== null) {
|
|
588
644
|
this.log.debug(`SOC read: ${socState.val}%`);
|
|
589
|
-
this.onBatterySOCChange(socState.val);
|
|
645
|
+
await this.onBatterySOCChange(socState.val);
|
|
590
646
|
} else {
|
|
591
647
|
this.log.warn('SOC state is null or undefined');
|
|
592
648
|
this.log.warn(`Please check: Does "${this.config.batterySOC}" exist in Objects?`);
|
|
@@ -617,6 +673,25 @@ class PvNotifications extends utils.Adapter {
|
|
|
617
673
|
}
|
|
618
674
|
}
|
|
619
675
|
|
|
676
|
+
/**
|
|
677
|
+
* Read a numeric foreign state; returns 0 for empty id, missing state or read error
|
|
678
|
+
*
|
|
679
|
+
* @param {string} id - Foreign state ID (may be empty)
|
|
680
|
+
*/
|
|
681
|
+
async readForeignNumber(id) {
|
|
682
|
+
if (!id) {
|
|
683
|
+
return 0;
|
|
684
|
+
}
|
|
685
|
+
try {
|
|
686
|
+
const state = await this.getForeignStateAsync(id);
|
|
687
|
+
const val = state ? Number(state.val) : NaN;
|
|
688
|
+
return state && state.val !== null && !isNaN(val) ? val : 0;
|
|
689
|
+
} catch (e) {
|
|
690
|
+
this.log.warn(`Could not read "${id}": ${e.message}`);
|
|
691
|
+
return 0;
|
|
692
|
+
}
|
|
693
|
+
}
|
|
694
|
+
|
|
620
695
|
/**
|
|
621
696
|
* Save statistics to states
|
|
622
697
|
*/
|
|
@@ -628,6 +703,9 @@ class PvNotifications extends utils.Adapter {
|
|
|
628
703
|
await this.setStateAsync('statistics.minSOCToday', this.stats.minSOC, true);
|
|
629
704
|
await this.setStateAsync('statistics.fullCyclesWeek', this.stats.weekFullCycles, true);
|
|
630
705
|
await this.setStateAsync('statistics.emptyCyclesWeek', this.stats.weekEmptyCycles, true);
|
|
706
|
+
await this.setStateAsync('statistics.fullCyclesMonth', this.stats.monthFullCycles, true);
|
|
707
|
+
await this.setStateAsync('statistics.emptyCyclesMonth', this.stats.monthEmptyCycles, true);
|
|
708
|
+
await this.setStateAsync('statistics.lastStatsReset', this.stats.lastStatsReset, true);
|
|
631
709
|
|
|
632
710
|
// Save last month/week data
|
|
633
711
|
await this.setStateAsync('statistics.lastMonthProduction', this.stats.lastMonthProduction, true);
|
|
@@ -654,6 +732,21 @@ class PvNotifications extends utils.Adapter {
|
|
|
654
732
|
* @param {ioBroker.State | null | undefined} state - State object
|
|
655
733
|
*/
|
|
656
734
|
async onStateChange(id, state) {
|
|
735
|
+
// Errors in an async event handler would become unhandled rejections and stop the adapter
|
|
736
|
+
try {
|
|
737
|
+
await this.handleStateChange(id, state);
|
|
738
|
+
} catch (e) {
|
|
739
|
+
this.log.error(`Error processing state change of ${id}: ${e.message}`);
|
|
740
|
+
}
|
|
741
|
+
}
|
|
742
|
+
|
|
743
|
+
/**
|
|
744
|
+
* Process a state change (called by onStateChange)
|
|
745
|
+
*
|
|
746
|
+
* @param {string} id - State ID
|
|
747
|
+
* @param {ioBroker.State | null | undefined} state - State object
|
|
748
|
+
*/
|
|
749
|
+
async handleStateChange(id, state) {
|
|
657
750
|
if (state) {
|
|
658
751
|
// Process test button (all states in own namespace)
|
|
659
752
|
if (id.startsWith(`${this.namespace}.testButton`)) {
|
|
@@ -662,10 +755,13 @@ class PvNotifications extends utils.Adapter {
|
|
|
662
755
|
this.status.testMessageRunning = true; // Set flag
|
|
663
756
|
this.log.debug(`Test button state received: ${id}, val=${state.val}`);
|
|
664
757
|
this.log.debug('Test button was pressed');
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
758
|
+
try {
|
|
759
|
+
await this.sendTestMessage();
|
|
760
|
+
} finally {
|
|
761
|
+
// Reset state and flag even if sending failed
|
|
762
|
+
this.status.testMessageRunning = false;
|
|
763
|
+
await this.setStateAsync('testButton', false, true);
|
|
764
|
+
}
|
|
669
765
|
}
|
|
670
766
|
return;
|
|
671
767
|
}
|
|
@@ -713,22 +809,26 @@ class PvNotifications extends utils.Adapter {
|
|
|
713
809
|
*/
|
|
714
810
|
async onBatterySOCChange(soc) {
|
|
715
811
|
// Check for undefined/null values
|
|
716
|
-
if (soc === null || soc === undefined || isNaN(soc)) {
|
|
812
|
+
if (soc === null || soc === undefined || soc === '' || isNaN(soc)) {
|
|
717
813
|
this.log.warn(`Invalid SOC value received: ${soc}`);
|
|
718
814
|
return;
|
|
719
815
|
}
|
|
816
|
+
// Some adapters deliver the SOC as string - strict comparisons below need a number
|
|
817
|
+
soc = Number(soc);
|
|
720
818
|
|
|
721
819
|
// Update current states
|
|
722
820
|
await this.setStateAsync('statistics.currentSOC', soc, true);
|
|
723
821
|
const currentKWh = this.round(((soc / 100) * this.config.batteryCapacityWh) / 1000, 1);
|
|
724
822
|
await this.setStateAsync('statistics.currentEnergyKWh', currentKWh, true);
|
|
725
823
|
|
|
726
|
-
// Update statistics
|
|
824
|
+
// Update statistics (persist immediately, otherwise the states stay stale until the next full/empty event)
|
|
727
825
|
if (soc > this.stats.maxSOC) {
|
|
728
826
|
this.stats.maxSOC = soc;
|
|
827
|
+
await this.setStateAsync('statistics.maxSOCToday', soc, true);
|
|
729
828
|
}
|
|
730
829
|
if (soc < this.stats.minSOC) {
|
|
731
830
|
this.stats.minSOC = soc;
|
|
831
|
+
await this.setStateAsync('statistics.minSOCToday', soc, true);
|
|
732
832
|
}
|
|
733
833
|
|
|
734
834
|
this.log.debug(`Battery SOC: ${soc}% | Status: full=${this.status.full}, empty=${this.status.empty}`);
|
|
@@ -742,8 +842,14 @@ class PvNotifications extends utils.Adapter {
|
|
|
742
842
|
: 'up';
|
|
743
843
|
|
|
744
844
|
// Store previous SOC for next update
|
|
845
|
+
const previousSOC = this.status.previousSOC;
|
|
745
846
|
this.status.previousSOC = soc;
|
|
746
847
|
|
|
848
|
+
// Thresholds are compared as "reached or crossed" - the SOC can jump (39 -> 41), be a decimal (40.3)
|
|
849
|
+
// or stop just below 100, so an exact comparison would miss notifications
|
|
850
|
+
const thresholdFull = Number(this.config.thresholdFull);
|
|
851
|
+
const thresholdEmpty = Number(this.config.thresholdEmpty);
|
|
852
|
+
|
|
747
853
|
// === NIGHT-TIME check with configurable time ===
|
|
748
854
|
const nightTime = this.isNightTime();
|
|
749
855
|
const nightModeActive = this.config.nightModeEnabled !== false;
|
|
@@ -754,7 +860,7 @@ class PvNotifications extends utils.Adapter {
|
|
|
754
860
|
const quietModeActive = this.config.quietModeEnabled !== false;
|
|
755
861
|
|
|
756
862
|
// === Batterie VOLL (100%) - Nicht nachts (wenn Nachtmodus aktiv) und nicht in Ruhezeit ===
|
|
757
|
-
if (soc
|
|
863
|
+
if (soc >= thresholdFull) {
|
|
758
864
|
// Prüfen ob Benachrichtigung erlaubt ist (nicht in Nachtzeit oder Ruhezeit)
|
|
759
865
|
const allowNotification = (!nightTime || !nightModeActive) && (!quietTime || !quietModeActive);
|
|
760
866
|
|
|
@@ -765,6 +871,7 @@ class PvNotifications extends utils.Adapter {
|
|
|
765
871
|
this.status.lastNotification.full = Date.now();
|
|
766
872
|
this.stats.fullCycles++;
|
|
767
873
|
this.stats.weekFullCycles++;
|
|
874
|
+
this.stats.monthFullCycles++;
|
|
768
875
|
this.saveStatistics();
|
|
769
876
|
this.log.info('Battery full - Telegram sent');
|
|
770
877
|
} else if (this.status.full && !this.canNotify('full')) {
|
|
@@ -780,7 +887,7 @@ class PvNotifications extends utils.Adapter {
|
|
|
780
887
|
}
|
|
781
888
|
|
|
782
889
|
// === Battery EMPTY (0%) - Always allow if nightModeIgnoreEmpty is active ===
|
|
783
|
-
if (soc
|
|
890
|
+
if (soc <= thresholdEmpty) {
|
|
784
891
|
if (!this.status.empty && this.canNotify('empty')) {
|
|
785
892
|
// Always notify at 0% if nightModeIgnoreEmpty is active
|
|
786
893
|
// But still respect quiet time (unless nightModeIgnoreEmpty is active)
|
|
@@ -794,6 +901,7 @@ class PvNotifications extends utils.Adapter {
|
|
|
794
901
|
this.status.lastNotification.empty = Date.now();
|
|
795
902
|
this.stats.emptyCycles++;
|
|
796
903
|
this.stats.weekEmptyCycles++;
|
|
904
|
+
this.stats.monthEmptyCycles++;
|
|
797
905
|
this.saveStatistics();
|
|
798
906
|
this.log.info('Battery empty - Telegram sent');
|
|
799
907
|
} else if (blockedByQuietTime) {
|
|
@@ -806,7 +914,7 @@ class PvNotifications extends utils.Adapter {
|
|
|
806
914
|
}
|
|
807
915
|
}
|
|
808
916
|
// === Intermediate-Stufen (nur wenn nicht voll/leer und nicht nachts und nicht in Ruhezeit) ===
|
|
809
|
-
if (soc
|
|
917
|
+
if (soc < thresholdFull && soc > thresholdEmpty) {
|
|
810
918
|
const intermediateSteps = (this.config.intermediateSteps || '20,40,60,80')
|
|
811
919
|
.split(',')
|
|
812
920
|
.map(s => parseInt(s.trim()))
|
|
@@ -816,22 +924,10 @@ class PvNotifications extends utils.Adapter {
|
|
|
816
924
|
const allowIntermediate = (!nightTime || !nightModeActive) && (!quietTime || !quietModeActive);
|
|
817
925
|
|
|
818
926
|
if (allowIntermediate) {
|
|
927
|
+
// Reset Intermediate-Flags wenn Stufe verlassen (±2% Toleranz) - vor der Prüfung, damit eine
|
|
928
|
+
// gerade überschrittene Stufe ihr Flag bis zum nächsten Wert behält (verhindert Flattern um die Stufe)
|
|
819
929
|
for (const step of intermediateSteps) {
|
|
820
|
-
if (soc
|
|
821
|
-
if (this.canNotify('intermediate')) {
|
|
822
|
-
const message = await this.buildIntermediateMessage(soc, direction);
|
|
823
|
-
this.sendTelegram(message);
|
|
824
|
-
this.status.intermediateNotified.push(step);
|
|
825
|
-
this.status.lastNotification.intermediate = Date.now();
|
|
826
|
-
this.log.info(`Intermediate ${step}% - Telegram sent`);
|
|
827
|
-
}
|
|
828
|
-
break;
|
|
829
|
-
}
|
|
830
|
-
}
|
|
831
|
-
|
|
832
|
-
// Reset Intermediate-Flags wenn Stufe verlassen (±2% Toleranz)
|
|
833
|
-
for (const step of intermediateSteps) {
|
|
834
|
-
if (soc !== step && Math.abs(soc - step) >= 2) {
|
|
930
|
+
if (Math.abs(soc - step) >= 2) {
|
|
835
931
|
const idx = this.status.intermediateNotified.indexOf(step);
|
|
836
932
|
if (idx > -1) {
|
|
837
933
|
this.status.intermediateNotified.splice(idx, 1);
|
|
@@ -839,8 +935,28 @@ class PvNotifications extends utils.Adapter {
|
|
|
839
935
|
}
|
|
840
936
|
}
|
|
841
937
|
}
|
|
842
|
-
|
|
843
|
-
|
|
938
|
+
|
|
939
|
+
// Steps reached or crossed since the last value; the step closest to the current SOC comes first.
|
|
940
|
+
// Without a previous value (adapter start) only an exact hit counts, to avoid messages on every restart.
|
|
941
|
+
const crossed = intermediateSteps
|
|
942
|
+
.filter(step => this.isStepCrossed(previousSOC, soc, step))
|
|
943
|
+
.filter(step => !this.status.intermediateNotified.includes(step))
|
|
944
|
+
.sort((a, b) => Math.abs(soc - a) - Math.abs(soc - b));
|
|
945
|
+
|
|
946
|
+
if (crossed.length > 0) {
|
|
947
|
+
if (this.canNotify('intermediate')) {
|
|
948
|
+
const message = await this.buildIntermediateMessage(soc, direction);
|
|
949
|
+
this.sendTelegram(message);
|
|
950
|
+
// A jump over several steps sends one message only
|
|
951
|
+
this.status.intermediateNotified.push(...crossed);
|
|
952
|
+
this.status.lastNotification.intermediate = Date.now();
|
|
953
|
+
this.log.info(`Intermediate ${crossed[0]}% (SOC ${soc}%) - Telegram sent`);
|
|
954
|
+
} else {
|
|
955
|
+
this.log.debug(`Intermediate ${crossed[0]}% reached, but interval not yet elapsed`);
|
|
956
|
+
}
|
|
957
|
+
}
|
|
958
|
+
} else {
|
|
959
|
+
this.log.debug('Night/quiet time - intermediate notifications suppressed');
|
|
844
960
|
}
|
|
845
961
|
}
|
|
846
962
|
|
|
@@ -857,6 +973,26 @@ class PvNotifications extends utils.Adapter {
|
|
|
857
973
|
}
|
|
858
974
|
}
|
|
859
975
|
|
|
976
|
+
/**
|
|
977
|
+
* Prüfe ob eine Stufe seit dem letzten Wert erreicht oder überschritten wurde (in beide Richtungen)
|
|
978
|
+
*
|
|
979
|
+
* @param {number | null} previousSOC - Previous SOC (null after adapter start)
|
|
980
|
+
* @param {number} soc - Current SOC
|
|
981
|
+
* @param {number} step - Intermediate step in percent
|
|
982
|
+
*/
|
|
983
|
+
isStepCrossed(previousSOC, soc, step) {
|
|
984
|
+
if (previousSOC === null || previousSOC === undefined) {
|
|
985
|
+
return soc === step;
|
|
986
|
+
}
|
|
987
|
+
if (soc > previousSOC) {
|
|
988
|
+
return previousSOC < step && step <= soc;
|
|
989
|
+
}
|
|
990
|
+
if (soc < previousSOC) {
|
|
991
|
+
return previousSOC > step && step >= soc;
|
|
992
|
+
}
|
|
993
|
+
return false;
|
|
994
|
+
}
|
|
995
|
+
|
|
860
996
|
/**
|
|
861
997
|
* Prüfe ob Mindestintervall eingehalten
|
|
862
998
|
*
|
|
@@ -920,6 +1056,22 @@ class PvNotifications extends utils.Adapter {
|
|
|
920
1056
|
return currentTime >= startTime && currentTime < endTime;
|
|
921
1057
|
}
|
|
922
1058
|
|
|
1059
|
+
/**
|
|
1060
|
+
* Nachricht für Telegram parse_mode HTML aufbereiten
|
|
1061
|
+
*
|
|
1062
|
+
* @param {string} message - Nachrichtentext mit *fett*-Markierungen
|
|
1063
|
+
* @returns {string} HTML-Text für Telegram
|
|
1064
|
+
*/
|
|
1065
|
+
formatTelegramHtml(message) {
|
|
1066
|
+
// Escape HTML first, so values from foreign states (weather texts etc.) can never break the message,
|
|
1067
|
+
// then turn the *bold* markers of the message templates into <b> tags (per line, always balanced)
|
|
1068
|
+
return String(message)
|
|
1069
|
+
.replace(/&/g, '&')
|
|
1070
|
+
.replace(/</g, '<')
|
|
1071
|
+
.replace(/>/g, '>')
|
|
1072
|
+
.replace(/\*([^*\n]+)\*/g, '<b>$1</b>');
|
|
1073
|
+
}
|
|
1074
|
+
|
|
923
1075
|
/**
|
|
924
1076
|
* Sende Telegram-Nachricht mit Zeitstempel
|
|
925
1077
|
*
|
|
@@ -942,8 +1094,12 @@ class PvNotifications extends utils.Adapter {
|
|
|
942
1094
|
this.config.telegramInstance,
|
|
943
1095
|
'send',
|
|
944
1096
|
{
|
|
945
|
-
text: fullMessage,
|
|
946
|
-
|
|
1097
|
+
text: this.formatTelegramHtml(fullMessage),
|
|
1098
|
+
// Without parse_mode Telegram shows the *bold* markers literally
|
|
1099
|
+
parse_mode: 'HTML',
|
|
1100
|
+
// The telegram adapter reads "user" (comma separated list) - without it the
|
|
1101
|
+
// message is broadcast to every user known to the bot
|
|
1102
|
+
user: usersList.join(','),
|
|
947
1103
|
},
|
|
948
1104
|
result => {
|
|
949
1105
|
if (result && result.error) {
|
|
@@ -1041,9 +1197,9 @@ ${consumptionPowerLine}
|
|
|
1041
1197
|
weatherTodayTextState && weatherTodayTextState.val !== null ? weatherTodayTextState.val : null;
|
|
1042
1198
|
const weatherTodayTemp =
|
|
1043
1199
|
weatherTodayTempState && weatherTodayTempState.val !== null ? weatherTodayTempState.val : null;
|
|
1044
|
-
const todayTempText = weatherTodayTemp ? ` ${this.round(weatherTodayTemp, 1)}°C` : '';
|
|
1200
|
+
const todayTempText = weatherTodayTemp !== null ? ` ${this.round(weatherTodayTemp, 1)}°C` : '';
|
|
1045
1201
|
|
|
1046
|
-
if (weatherTodayText || weatherTodayTemp) {
|
|
1202
|
+
if (weatherTodayText || weatherTodayTemp !== null) {
|
|
1047
1203
|
const weatherDesc = weatherTodayText ? this.getWeatherDescription(weatherTodayText) : '🌡️';
|
|
1048
1204
|
message += `\n🌤️ Heute: ${weatherDesc}${todayTempText}`;
|
|
1049
1205
|
}
|
|
@@ -1069,7 +1225,7 @@ ${consumptionPowerLine}
|
|
|
1069
1225
|
weatherTomorrowState && weatherTomorrowState.val !== null ? weatherTomorrowState.val : null;
|
|
1070
1226
|
const tempTomorrow =
|
|
1071
1227
|
tempTomorrowState && tempTomorrowState.val !== null ? tempTomorrowState.val : null;
|
|
1072
|
-
const tempText = tempTomorrow ? ` ${this.round(tempTomorrow, 1)}°C` : '';
|
|
1228
|
+
const tempText = tempTomorrow !== null ? ` ${this.round(tempTomorrow, 1)}°C` : '';
|
|
1073
1229
|
|
|
1074
1230
|
const weatherText = weatherTomorrowText || weatherTomorrow;
|
|
1075
1231
|
if (weatherText) {
|
|
@@ -1138,9 +1294,9 @@ ${consumptionPowerLine}${separator}`;
|
|
|
1138
1294
|
weatherTodayTextState && weatherTodayTextState.val !== null ? weatherTodayTextState.val : null;
|
|
1139
1295
|
const weatherTodayTemp =
|
|
1140
1296
|
weatherTodayTempState && weatherTodayTempState.val !== null ? weatherTodayTempState.val : null;
|
|
1141
|
-
const todayTempText = weatherTodayTemp ? ` ${this.round(weatherTodayTemp, 1)}°C` : '';
|
|
1297
|
+
const todayTempText = weatherTodayTemp !== null ? ` ${this.round(weatherTodayTemp, 1)}°C` : '';
|
|
1142
1298
|
|
|
1143
|
-
if (weatherTodayText || weatherTodayTemp) {
|
|
1299
|
+
if (weatherTodayText || weatherTodayTemp !== null) {
|
|
1144
1300
|
const weatherDesc = weatherTodayText ? this.getWeatherDescription(weatherTodayText) : '🌡️';
|
|
1145
1301
|
message += `\n🌤️ Heute: ${weatherDesc}${todayTempText}`;
|
|
1146
1302
|
}
|
|
@@ -1166,7 +1322,7 @@ ${consumptionPowerLine}${separator}`;
|
|
|
1166
1322
|
weatherTomorrowState && weatherTomorrowState.val !== null ? weatherTomorrowState.val : null;
|
|
1167
1323
|
const tempTomorrow =
|
|
1168
1324
|
tempTomorrowState && tempTomorrowState.val !== null ? tempTomorrowState.val : null;
|
|
1169
|
-
const tempText = tempTomorrow ? ` ${this.round(tempTomorrow, 1)}°C` : '';
|
|
1325
|
+
const tempText = tempTomorrow !== null ? ` ${this.round(tempTomorrow, 1)}°C` : '';
|
|
1170
1326
|
|
|
1171
1327
|
const weatherText = weatherTomorrowText || weatherTomorrow;
|
|
1172
1328
|
if (weatherText) {
|
|
@@ -1263,9 +1419,9 @@ ${statusText}
|
|
|
1263
1419
|
weatherTodayTextState && weatherTodayTextState.val !== null ? weatherTodayTextState.val : null;
|
|
1264
1420
|
const weatherTodayTemp =
|
|
1265
1421
|
weatherTodayTempState && weatherTodayTempState.val !== null ? weatherTodayTempState.val : null;
|
|
1266
|
-
const todayTempText = weatherTodayTemp ? ` ${this.round(weatherTodayTemp, 1)}°C` : '';
|
|
1422
|
+
const todayTempText = weatherTodayTemp !== null ? ` ${this.round(weatherTodayTemp, 1)}°C` : '';
|
|
1267
1423
|
|
|
1268
|
-
if (weatherTodayText || weatherTodayTemp) {
|
|
1424
|
+
if (weatherTodayText || weatherTodayTemp !== null) {
|
|
1269
1425
|
const weatherDesc = weatherTodayText ? this.getWeatherDescription(weatherTodayText) : '🌡️';
|
|
1270
1426
|
message += `\n🌤️ ${this.translate('Weather today')}: ${weatherDesc}${todayTempText}`;
|
|
1271
1427
|
this.log.debug(`Weather today added to intermediate message: ${weatherDesc}${todayTempText}`);
|
|
@@ -1292,7 +1448,7 @@ ${statusText}
|
|
|
1292
1448
|
weatherTomorrowState && weatherTomorrowState.val !== null ? weatherTomorrowState.val : null;
|
|
1293
1449
|
const tempTomorrow =
|
|
1294
1450
|
tempTomorrowState && tempTomorrowState.val !== null ? tempTomorrowState.val : null;
|
|
1295
|
-
const tempText = tempTomorrow ? ` ${this.round(tempTomorrow, 1)}°C` : '';
|
|
1451
|
+
const tempText = tempTomorrow !== null ? ` ${this.round(tempTomorrow, 1)}°C` : '';
|
|
1296
1452
|
|
|
1297
1453
|
const weatherText = weatherTomorrowText || weatherTomorrow;
|
|
1298
1454
|
if (weatherText) {
|
|
@@ -1392,11 +1548,12 @@ ${statusText}
|
|
|
1392
1548
|
const weatherTodayTemp =
|
|
1393
1549
|
weatherTodayTempState && weatherTodayTempState.val !== null ? weatherTodayTempState.val : null;
|
|
1394
1550
|
|
|
1395
|
-
if (weatherTodayText || weatherTodayTemp) {
|
|
1551
|
+
if (weatherTodayText || weatherTodayTemp !== null) {
|
|
1396
1552
|
const weatherDesc = weatherTodayText ? this.getWeatherDescription(weatherTodayText) : '🌡️';
|
|
1397
|
-
const todayTempSuffix =
|
|
1398
|
-
|
|
1399
|
-
|
|
1553
|
+
const todayTempSuffix =
|
|
1554
|
+
weatherTodayTemp !== null
|
|
1555
|
+
? ` (${this.translate('Currently')}: ${this.round(weatherTodayTemp, 1)}°C)`
|
|
1556
|
+
: '';
|
|
1400
1557
|
message += `\n━━━━━━━━━━━━━━━━━━━━━━\n🌤️ *${this.translate('Weather today')}:* ${weatherDesc}${todayTempSuffix}`;
|
|
1401
1558
|
weatherAdded = true;
|
|
1402
1559
|
}
|
|
@@ -1422,7 +1579,7 @@ ${statusText}
|
|
|
1422
1579
|
weatherTomorrowState && weatherTomorrowState.val !== null ? weatherTomorrowState.val : null;
|
|
1423
1580
|
const tempTomorrow =
|
|
1424
1581
|
tempTomorrowState && tempTomorrowState.val !== null ? tempTomorrowState.val : null;
|
|
1425
|
-
const tempText = tempTomorrow ? ` ${this.round(tempTomorrow, 1)}°C` : '';
|
|
1582
|
+
const tempText = tempTomorrow !== null ? ` ${this.round(tempTomorrow, 1)}°C` : '';
|
|
1426
1583
|
|
|
1427
1584
|
const weatherText = weatherTomorrowText || weatherTomorrow;
|
|
1428
1585
|
if (weatherText) {
|
|
@@ -1469,7 +1626,7 @@ ${statusText}
|
|
|
1469
1626
|
// const consumption = this.round(this.stats.lastWeekConsumption, 1); // ESLint: unused
|
|
1470
1627
|
const feedIn = this.round(Math.abs(this.stats.lastWeekFeedIn), 1);
|
|
1471
1628
|
const gridPower = this.round(this.stats.lastWeekGridPower, 1);
|
|
1472
|
-
const selfConsumption = this.round(totalProd - feedIn, 1);
|
|
1629
|
+
const selfConsumption = this.round(Math.max(0, totalProd - feedIn), 1);
|
|
1473
1630
|
const selfConsumptionRate = totalProd > 0 ? this.round((selfConsumption / totalProd) * 100, 1) : 0;
|
|
1474
1631
|
|
|
1475
1632
|
return `📊 *${this.translate('Weekly statistics PV system')}*
|
|
@@ -1494,7 +1651,7 @@ ${statusText}
|
|
|
1494
1651
|
// const consumption = this.round(this.stats.lastMonthConsumption, 1); // ESLint: unused
|
|
1495
1652
|
const feedIn = this.round(Math.abs(this.stats.lastMonthFeedIn), 1);
|
|
1496
1653
|
const gridPower = this.round(this.stats.lastMonthGridPower, 1);
|
|
1497
|
-
const selfConsumption = this.round(totalProd - feedIn, 1);
|
|
1654
|
+
const selfConsumption = this.round(Math.max(0, totalProd - feedIn), 1);
|
|
1498
1655
|
const selfConsumptionRate = totalProd > 0 ? this.round((selfConsumption / totalProd) * 100, 1) : 0;
|
|
1499
1656
|
|
|
1500
1657
|
return `📊 *${this.translate('Monthly statistics PV system')}*
|
|
@@ -1519,7 +1676,7 @@ ${statusText}
|
|
|
1519
1676
|
return '🌡️ unbekannt';
|
|
1520
1677
|
}
|
|
1521
1678
|
|
|
1522
|
-
const text = weatherText.toLowerCase();
|
|
1679
|
+
const text = String(weatherText).toLowerCase();
|
|
1523
1680
|
|
|
1524
1681
|
if (text.includes('sonnig') || text.includes('klar')) {
|
|
1525
1682
|
return '☀️ sonnig';
|
|
@@ -1562,7 +1719,7 @@ ${statusText}
|
|
|
1562
1719
|
if (!weatherText) {
|
|
1563
1720
|
return false;
|
|
1564
1721
|
}
|
|
1565
|
-
const text = weatherText.toLowerCase();
|
|
1722
|
+
const text = String(weatherText).toLowerCase();
|
|
1566
1723
|
return (
|
|
1567
1724
|
text.includes('sonnig') || text.includes('klar') || text.includes('clear') || text.includes('few clouds')
|
|
1568
1725
|
);
|
|
@@ -1577,7 +1734,7 @@ ${statusText}
|
|
|
1577
1734
|
if (!weatherText) {
|
|
1578
1735
|
return false;
|
|
1579
1736
|
}
|
|
1580
|
-
const text = weatherText.toLowerCase();
|
|
1737
|
+
const text = String(weatherText).toLowerCase();
|
|
1581
1738
|
return (
|
|
1582
1739
|
text.includes('regen') ||
|
|
1583
1740
|
text.includes('rain') ||
|
|
@@ -1603,65 +1760,60 @@ ${statusText}
|
|
|
1603
1760
|
return Math.round(value * Math.pow(10, decimals)) / Math.pow(10, decimals);
|
|
1604
1761
|
}
|
|
1605
1762
|
|
|
1763
|
+
/**
|
|
1764
|
+
* Parse a time string "HH:MM"
|
|
1765
|
+
*
|
|
1766
|
+
* @param {string} value - Time string
|
|
1767
|
+
* @returns {{ hours: number, minutes: number } | null} Parsed time or null if invalid
|
|
1768
|
+
*/
|
|
1769
|
+
parseTime(value) {
|
|
1770
|
+
const match = String(value || '')
|
|
1771
|
+
.trim()
|
|
1772
|
+
.match(/^(\d{1,2}):(\d{2})/);
|
|
1773
|
+
if (!match) {
|
|
1774
|
+
return null;
|
|
1775
|
+
}
|
|
1776
|
+
const hours = parseInt(match[1], 10);
|
|
1777
|
+
const minutes = parseInt(match[2], 10);
|
|
1778
|
+
return hours < 24 && minutes < 60 ? { hours, minutes } : null;
|
|
1779
|
+
}
|
|
1780
|
+
|
|
1781
|
+
/**
|
|
1782
|
+
* Parse the value of the sunset object ("HH:MM", ISO date string or timestamp) into local hours/minutes
|
|
1783
|
+
*
|
|
1784
|
+
* @param {ioBroker.StateValue | undefined} value - State value
|
|
1785
|
+
* @returns {{ hours: number, minutes: number } | null} Parsed time or null if invalid
|
|
1786
|
+
*/
|
|
1787
|
+
parseSunsetTime(value) {
|
|
1788
|
+
if (value === null || value === undefined || value === '') {
|
|
1789
|
+
return null;
|
|
1790
|
+
}
|
|
1791
|
+
const time = typeof value === 'string' ? this.parseTime(value) : null;
|
|
1792
|
+
if (time) {
|
|
1793
|
+
return time;
|
|
1794
|
+
}
|
|
1795
|
+
const date = new Date(typeof value === 'string' && /^\d+$/.test(value) ? Number(value) : value);
|
|
1796
|
+
return isNaN(date.getTime()) ? null : { hours: date.getHours(), minutes: date.getMinutes() };
|
|
1797
|
+
}
|
|
1798
|
+
|
|
1799
|
+
/**
|
|
1800
|
+
* Check whether the given time equals the configured time string
|
|
1801
|
+
*
|
|
1802
|
+
* @param {Date} now - Current time
|
|
1803
|
+
* @param {string} configTime - Configured time "HH:MM"
|
|
1804
|
+
*/
|
|
1805
|
+
isTime(now, configTime) {
|
|
1806
|
+
const time = this.parseTime(configTime);
|
|
1807
|
+
return !!time && now.getHours() === time.hours && now.getMinutes() === time.minutes;
|
|
1808
|
+
}
|
|
1809
|
+
|
|
1606
1810
|
/**
|
|
1607
1811
|
* Start scheduled tasks
|
|
1608
1812
|
*/
|
|
1609
1813
|
startScheduledTasks() {
|
|
1610
1814
|
// Check every minute
|
|
1611
1815
|
this.scheduledInterval = this.setInterval(() => {
|
|
1612
|
-
|
|
1613
|
-
const hours = now.getHours();
|
|
1614
|
-
const minutes = now.getMinutes();
|
|
1615
|
-
const jsDay = now.getDay(); // JavaScript: 0=So, 1=Mo, ..., 6=Sa
|
|
1616
|
-
// Umwandeln in ioBroker-Format: 0=Mo, 1=Di, ..., 6=So
|
|
1617
|
-
const day = jsDay === 0 ? 6 : jsDay - 1;
|
|
1618
|
-
const date = now.getDate();
|
|
1619
|
-
|
|
1620
|
-
// Alle 5 Minuten: Statistik prüfen (um :00, :05, :10, ...)
|
|
1621
|
-
if (minutes % 5 === 0) {
|
|
1622
|
-
this.resetDailyStats();
|
|
1623
|
-
this.resetWeeklyStats();
|
|
1624
|
-
this.resetMonthlyStats();
|
|
1625
|
-
}
|
|
1626
|
-
|
|
1627
|
-
// Tägliche Statistik — entweder zur konfigurierten Zeit oder zum Sonnenuntergang
|
|
1628
|
-
if (this.config.statsUseSunset && this.config.statsSunsetObject) {
|
|
1629
|
-
// Sonnenuntergangszeit aus dem konfigurierten Objekt lesen
|
|
1630
|
-
this.getForeignStateAsync(this.config.statsSunsetObject)
|
|
1631
|
-
.then(sunsetState => {
|
|
1632
|
-
if (sunsetState && sunsetState.val) {
|
|
1633
|
-
const sunsetTime = String(sunsetState.val).trim();
|
|
1634
|
-
const match = sunsetTime.match(/^(\d{1,2}):(\d{2})/);
|
|
1635
|
-
if (match) {
|
|
1636
|
-
const sunsetHours = parseInt(match[1], 10);
|
|
1637
|
-
const sunsetMinutes = parseInt(match[2], 10);
|
|
1638
|
-
if (hours === sunsetHours && minutes === sunsetMinutes) {
|
|
1639
|
-
this.sendDailyStatsMessage();
|
|
1640
|
-
}
|
|
1641
|
-
}
|
|
1642
|
-
}
|
|
1643
|
-
})
|
|
1644
|
-
.catch(e => this.log.warn(`Could not read sunset object: ${e.message}`));
|
|
1645
|
-
} else {
|
|
1646
|
-
const [dayHours, dayMinutes] = this.config.statsDayTime.split(':').map(Number);
|
|
1647
|
-
if (hours === dayHours && minutes === dayMinutes) {
|
|
1648
|
-
this.sendDailyStatsMessage();
|
|
1649
|
-
}
|
|
1650
|
-
}
|
|
1651
|
-
|
|
1652
|
-
// Wöchentliche Statistik am konfigurierten Tag und Zeit
|
|
1653
|
-
const [weekHours, weekMinutes] = this.config.statsWeekTime.split(':').map(Number);
|
|
1654
|
-
if (day === this.config.statsWeekDay && hours === weekHours && minutes === weekMinutes) {
|
|
1655
|
-
this.sendTelegram(this.buildWeeklyStatsMessage());
|
|
1656
|
-
}
|
|
1657
|
-
|
|
1658
|
-
// Monatsstatistik am konfigurierten Tag und Zeit
|
|
1659
|
-
if (this.config.monthlyStatsEnabled) {
|
|
1660
|
-
const [monthHours, monthMinutes] = this.config.monthlyStatsTime.split(':').map(Number);
|
|
1661
|
-
if (date === this.config.monthlyStatsDay && hours === monthHours && minutes === monthMinutes) {
|
|
1662
|
-
this.sendTelegram(this.buildMonthlyStatsMessage());
|
|
1663
|
-
}
|
|
1664
|
-
}
|
|
1816
|
+
this.runScheduledTasks(new Date()).catch(e => this.log.error(`Scheduled task failed: ${e.message}`));
|
|
1665
1817
|
}, 60000); // Jede Minute ausführen
|
|
1666
1818
|
|
|
1667
1819
|
this.log.info(
|
|
@@ -1670,19 +1822,61 @@ ${statusText}
|
|
|
1670
1822
|
}
|
|
1671
1823
|
|
|
1672
1824
|
/**
|
|
1673
|
-
*
|
|
1825
|
+
* Run the scheduled tasks for the given minute
|
|
1826
|
+
*
|
|
1827
|
+
* @param {Date} now - Current time
|
|
1674
1828
|
*/
|
|
1675
|
-
|
|
1676
|
-
const
|
|
1677
|
-
|
|
1678
|
-
const
|
|
1679
|
-
const
|
|
1829
|
+
async runScheduledTasks(now) {
|
|
1830
|
+
const jsDay = now.getDay(); // JavaScript: 0=So, 1=Mo, ..., 6=Sa
|
|
1831
|
+
// Umwandeln in ioBroker-Format: 0=Mo, 1=Di, ..., 6=So
|
|
1832
|
+
const day = jsDay === 0 ? 6 : jsDay - 1;
|
|
1833
|
+
const date = now.getDate();
|
|
1834
|
+
|
|
1835
|
+
// Statistik-Resets bei jedem Durchlauf prüfen (jeweils nur einmal pro Tag/Woche/Monat)
|
|
1836
|
+
this.resetDailyStats(now);
|
|
1837
|
+
await this.resetWeeklyStats(now);
|
|
1838
|
+
await this.resetMonthlyStats(now);
|
|
1839
|
+
|
|
1840
|
+
// Tägliche Statistik — entweder zur konfigurierten Zeit oder zum Sonnenuntergang
|
|
1841
|
+
if (this.config.statsUseSunset && this.config.statsSunsetObject) {
|
|
1842
|
+
// Sonnenuntergangszeit aus dem konfigurierten Objekt lesen
|
|
1843
|
+
try {
|
|
1844
|
+
const sunsetState = await this.getForeignStateAsync(this.config.statsSunsetObject);
|
|
1845
|
+
const sunset = this.parseSunsetTime(sunsetState ? sunsetState.val : null);
|
|
1846
|
+
if (sunset && now.getHours() === sunset.hours && now.getMinutes() === sunset.minutes) {
|
|
1847
|
+
await this.sendDailyStatsMessage();
|
|
1848
|
+
}
|
|
1849
|
+
} catch (e) {
|
|
1850
|
+
this.log.warn(`Could not read sunset object: ${e.message}`);
|
|
1851
|
+
}
|
|
1852
|
+
} else if (this.isTime(now, this.config.statsDayTime)) {
|
|
1853
|
+
await this.sendDailyStatsMessage();
|
|
1854
|
+
}
|
|
1855
|
+
|
|
1856
|
+
// Wöchentliche Statistik am konfigurierten Tag und Zeit
|
|
1857
|
+
if (day === Number(this.config.statsWeekDay) && this.isTime(now, this.config.statsWeekTime)) {
|
|
1858
|
+
this.sendTelegram(this.buildWeeklyStatsMessage());
|
|
1859
|
+
}
|
|
1680
1860
|
|
|
1681
|
-
//
|
|
1682
|
-
|
|
1861
|
+
// Monatsstatistik am konfigurierten Tag und Zeit
|
|
1862
|
+
if (
|
|
1863
|
+
this.config.monthlyStatsEnabled &&
|
|
1864
|
+
date === Number(this.config.monthlyStatsDay) &&
|
|
1865
|
+
this.isTime(now, this.config.monthlyStatsTime)
|
|
1866
|
+
) {
|
|
1867
|
+
this.sendTelegram(this.buildMonthlyStatsMessage());
|
|
1868
|
+
}
|
|
1869
|
+
}
|
|
1683
1870
|
|
|
1684
|
-
|
|
1685
|
-
|
|
1871
|
+
/**
|
|
1872
|
+
* Tägliche Statistik zurücksetzen (bei Tageswechsel, passend zu loadStatistics())
|
|
1873
|
+
*
|
|
1874
|
+
* @param {Date} [now] - Current time
|
|
1875
|
+
*/
|
|
1876
|
+
resetDailyStats(now = new Date()) {
|
|
1877
|
+
const today = now.getDate();
|
|
1878
|
+
|
|
1879
|
+
if (today !== this.stats.lastStatsReset) {
|
|
1686
1880
|
this.log.info('Resetting daily statistics');
|
|
1687
1881
|
this.stats.fullCycles = 0;
|
|
1688
1882
|
this.stats.emptyCycles = 0;
|
|
@@ -1694,32 +1888,36 @@ ${statusText}
|
|
|
1694
1888
|
}
|
|
1695
1889
|
|
|
1696
1890
|
/**
|
|
1697
|
-
*
|
|
1891
|
+
* Check whether the "before midnight" save window (23:55-23:59) is active
|
|
1892
|
+
*
|
|
1893
|
+
* @param {Date} now - Current time
|
|
1698
1894
|
*/
|
|
1699
|
-
|
|
1700
|
-
|
|
1895
|
+
isSaveWindow(now) {
|
|
1896
|
+
return now.getHours() === 23 && now.getMinutes() >= 55;
|
|
1897
|
+
}
|
|
1898
|
+
|
|
1899
|
+
/**
|
|
1900
|
+
* Reset weekly statistics - AUTOMATISCH am Sonntag ab 23:55 (vor sourceanalytix Reset)
|
|
1901
|
+
*
|
|
1902
|
+
* @param {Date} [now] - Current time
|
|
1903
|
+
*/
|
|
1904
|
+
async resetWeeklyStats(now = new Date()) {
|
|
1701
1905
|
const jsDay = now.getDay(); // JavaScript: 0=So, 1=Mo, ..., 6=Sa
|
|
1702
|
-
const
|
|
1703
|
-
const minutes = now.getMinutes();
|
|
1906
|
+
const dateKey = now.toDateString();
|
|
1704
1907
|
|
|
1705
|
-
// Automatische Speicherung: Jeden Sonntag
|
|
1706
|
-
//
|
|
1707
|
-
if (jsDay === 0 &&
|
|
1908
|
+
// Automatische Speicherung: Jeden Sonntag 23:55-23:59 (vor Mitternacht), einmal pro Woche.
|
|
1909
|
+
// Ein Fenster statt einer exakten Minute, damit ein verzögerter Timer-Tick den Abschluss nicht verpasst.
|
|
1910
|
+
if (jsDay === 0 && this.isSaveWindow(now) && this.status.lastWeekSave !== dateKey) {
|
|
1911
|
+
this.status.lastWeekSave = dateKey;
|
|
1708
1912
|
this.log.info('Auto-saving weekly statistics (Sunday 23:55, before sourceanalytix reset)');
|
|
1709
1913
|
|
|
1710
1914
|
// Aktuelle Daten aus externen States lesen (direkter Zugriff)
|
|
1711
1915
|
// WICHTIG: weeklyProduction/weeklyConsumption/etc. verwenden (sourceanalytix Wochenwerte)
|
|
1712
1916
|
// NICHT totalProduction/consumption/etc. (das sind Tageswerte!)
|
|
1713
|
-
|
|
1714
|
-
|
|
1715
|
-
|
|
1716
|
-
|
|
1717
|
-
|
|
1718
|
-
this.stats.lastWeekProduction = weeklyProd && weeklyProd.val !== null ? weeklyProd.val : 0;
|
|
1719
|
-
this.stats.lastWeekConsumption =
|
|
1720
|
-
weeklyConsumption && weeklyConsumption.val !== null ? weeklyConsumption.val : 0;
|
|
1721
|
-
this.stats.lastWeekFeedIn = weeklyFeedIn && weeklyFeedIn.val !== null ? weeklyFeedIn.val : 0;
|
|
1722
|
-
this.stats.lastWeekGridPower = weeklyGridPower && weeklyGridPower.val !== null ? weeklyGridPower.val : 0;
|
|
1917
|
+
this.stats.lastWeekProduction = await this.readForeignNumber(this.config.weeklyProduction);
|
|
1918
|
+
this.stats.lastWeekConsumption = await this.readForeignNumber(this.config.weeklyConsumption);
|
|
1919
|
+
this.stats.lastWeekFeedIn = await this.readForeignNumber(this.config.weeklyFeedIn);
|
|
1920
|
+
this.stats.lastWeekGridPower = await this.readForeignNumber(this.config.weeklyGridPower);
|
|
1723
1921
|
this.stats.lastWeekFullCycles = this.stats.weekFullCycles;
|
|
1724
1922
|
this.stats.lastWeekEmptyCycles = this.stats.weekEmptyCycles;
|
|
1725
1923
|
|
|
@@ -1727,52 +1925,49 @@ ${statusText}
|
|
|
1727
1925
|
this.stats.weekFullCycles = 0;
|
|
1728
1926
|
this.stats.weekEmptyCycles = 0;
|
|
1729
1927
|
|
|
1730
|
-
this.saveStatistics();
|
|
1928
|
+
await this.saveStatistics();
|
|
1731
1929
|
this.log.info(
|
|
1732
1930
|
`Weekly stats saved: Production=${this.stats.lastWeekProduction} kWh, FeedIn=${this.stats.lastWeekFeedIn} kWh`,
|
|
1733
1931
|
);
|
|
1734
|
-
// KEIN Senden hier - Senden erfolgt nur in
|
|
1932
|
+
// KEIN Senden hier - Senden erfolgt nur in runScheduledTasks() zur konfigurierten Zeit
|
|
1735
1933
|
}
|
|
1736
1934
|
}
|
|
1737
1935
|
|
|
1738
1936
|
/**
|
|
1739
|
-
* Monatsstatistik zurücksetzen - AUTOMATISCH am letzten Tag des Monats
|
|
1937
|
+
* Monatsstatistik zurücksetzen - AUTOMATISCH am letzten Tag des Monats ab 23:55 (vor sourceanalytix Reset).
|
|
1938
|
+
* Die Monatszyklen werden auch bei deaktivierter Monatsstatistik zurückgesetzt.
|
|
1939
|
+
*
|
|
1940
|
+
* @param {Date} [now] - Current time
|
|
1740
1941
|
*/
|
|
1741
|
-
async resetMonthlyStats() {
|
|
1742
|
-
if (!this.config.monthlyStatsEnabled) {
|
|
1743
|
-
return;
|
|
1744
|
-
}
|
|
1745
|
-
|
|
1746
|
-
const now = new Date();
|
|
1942
|
+
async resetMonthlyStats(now = new Date()) {
|
|
1747
1943
|
const today = now.getDate();
|
|
1748
|
-
const
|
|
1749
|
-
const minutes = now.getMinutes();
|
|
1944
|
+
const dateKey = now.toDateString();
|
|
1750
1945
|
|
|
1751
1946
|
// Letzten Tag des aktuellen Monats berechnen
|
|
1752
1947
|
// new Date(Jahr, Monat+1, 0) gibt den letzten Tag des aktuellen Monats zurück
|
|
1753
1948
|
const lastDayOfMonth = new Date(now.getFullYear(), now.getMonth() + 1, 0).getDate();
|
|
1754
1949
|
|
|
1755
|
-
// Automatische Speicherung: Letzter Tag des Monats
|
|
1756
|
-
if (today === lastDayOfMonth &&
|
|
1950
|
+
// Automatische Speicherung: Letzter Tag des Monats 23:55-23:59 (vor Mitternacht), einmal pro Monat
|
|
1951
|
+
if (today === lastDayOfMonth && this.isSaveWindow(now) && this.status.lastMonthSave !== dateKey) {
|
|
1952
|
+
this.status.lastMonthSave = dateKey;
|
|
1757
1953
|
this.log.info(
|
|
1758
1954
|
`Auto-saving monthly statistics (last day of month ${today}. ${now.getMonth() + 1}.${now.getFullYear()} 23:55, before sourceanalytix reset)`,
|
|
1759
1955
|
);
|
|
1760
1956
|
|
|
1761
1957
|
// Aktuelle Daten aus externen States lesen (direkter Zugriff)
|
|
1762
|
-
|
|
1763
|
-
|
|
1764
|
-
|
|
1765
|
-
|
|
1766
|
-
|
|
1767
|
-
|
|
1768
|
-
this.stats.
|
|
1769
|
-
this.stats.
|
|
1770
|
-
this.stats.
|
|
1771
|
-
this.stats.
|
|
1772
|
-
this.stats.lastMonthEmptyCycles = this.stats.emptyCycles;
|
|
1958
|
+
if (this.config.monthlyStatsEnabled) {
|
|
1959
|
+
this.stats.lastMonthProduction = await this.readForeignNumber(this.config.monthlyProduction);
|
|
1960
|
+
this.stats.lastMonthConsumption = await this.readForeignNumber(this.config.monthlyConsumption);
|
|
1961
|
+
this.stats.lastMonthFeedIn = await this.readForeignNumber(this.config.monthlyFeedIn);
|
|
1962
|
+
this.stats.lastMonthGridPower = await this.readForeignNumber(this.config.monthlyGridPower);
|
|
1963
|
+
}
|
|
1964
|
+
this.stats.lastMonthFullCycles = this.stats.monthFullCycles;
|
|
1965
|
+
this.stats.lastMonthEmptyCycles = this.stats.monthEmptyCycles;
|
|
1966
|
+
this.stats.monthFullCycles = 0;
|
|
1967
|
+
this.stats.monthEmptyCycles = 0;
|
|
1773
1968
|
|
|
1774
1969
|
this.stats.lastMonthReset = today;
|
|
1775
|
-
this.saveStatistics();
|
|
1970
|
+
await this.saveStatistics();
|
|
1776
1971
|
this.log.info(
|
|
1777
1972
|
`Monthly stats saved: Production=${this.stats.lastMonthProduction} kWh, FeedIn=${this.stats.lastMonthFeedIn} kWh`,
|
|
1778
1973
|
);
|
|
@@ -2055,9 +2250,9 @@ ${statusText}
|
|
|
2055
2250
|
weatherTodayTextState && weatherTodayTextState.val !== null ? weatherTodayTextState.val : null;
|
|
2056
2251
|
const weatherTodayTemp =
|
|
2057
2252
|
weatherTodayState && weatherTodayState.val !== null ? weatherTodayState.val : null;
|
|
2058
|
-
const tempText = weatherTodayTemp ? ` ${this.round(weatherTodayTemp, 1)}°C` : '';
|
|
2253
|
+
const tempText = weatherTodayTemp !== null ? ` ${this.round(weatherTodayTemp, 1)}°C` : '';
|
|
2059
2254
|
|
|
2060
|
-
if (weatherTodayText || weatherTodayTemp) {
|
|
2255
|
+
if (weatherTodayText || weatherTodayTemp !== null) {
|
|
2061
2256
|
const weatherDesc = weatherTodayText ? this.getWeatherDescription(weatherTodayText) : '🌡️';
|
|
2062
2257
|
message += `\n\n🌤️ *${this.translate('Weather today')}:* ${weatherDesc}${tempText}`;
|
|
2063
2258
|
}
|
|
@@ -2065,8 +2260,12 @@ ${statusText}
|
|
|
2065
2260
|
|
|
2066
2261
|
// Wetter morgen
|
|
2067
2262
|
if (this.config.weatherTomorrowText || this.config.weatherTomorrowTemp) {
|
|
2068
|
-
const weatherTomorrowTextState =
|
|
2069
|
-
|
|
2263
|
+
const weatherTomorrowTextState = this.config.weatherTomorrowText
|
|
2264
|
+
? await this.getForeignStateAsync(this.config.weatherTomorrowText)
|
|
2265
|
+
: null;
|
|
2266
|
+
const weatherTomorrowState = this.config.weatherTomorrowTemp
|
|
2267
|
+
? await this.getForeignStateAsync(this.config.weatherTomorrowTemp)
|
|
2268
|
+
: null;
|
|
2070
2269
|
|
|
2071
2270
|
const weatherTomorrowText =
|
|
2072
2271
|
weatherTomorrowTextState && weatherTomorrowTextState.val !== null
|
|
@@ -2074,9 +2273,9 @@ ${statusText}
|
|
|
2074
2273
|
: null;
|
|
2075
2274
|
const weatherTomorrowTemp =
|
|
2076
2275
|
weatherTomorrowState && weatherTomorrowState.val !== null ? weatherTomorrowState.val : null;
|
|
2077
|
-
const tempText = weatherTomorrowTemp ? ` ${this.round(weatherTomorrowTemp, 1)}°C` : '';
|
|
2276
|
+
const tempText = weatherTomorrowTemp !== null ? ` ${this.round(weatherTomorrowTemp, 1)}°C` : '';
|
|
2078
2277
|
|
|
2079
|
-
if (weatherTomorrowText || weatherTomorrowTemp) {
|
|
2278
|
+
if (weatherTomorrowText || weatherTomorrowTemp !== null) {
|
|
2080
2279
|
const weatherDesc = weatherTomorrowText
|
|
2081
2280
|
? this.getWeatherDescription(weatherTomorrowText)
|
|
2082
2281
|
: '🌡️';
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "iobroker.pv-notifications",
|
|
3
|
-
"version": "1.2.
|
|
3
|
+
"version": "1.2.33",
|
|
4
4
|
"description": "PV-Anlage Batterie-Status Telegram Benachrichtigungen",
|
|
5
5
|
"author": {
|
|
6
6
|
"name": "Alex1808 via LLM: Qwen",
|
|
@@ -58,7 +58,7 @@
|
|
|
58
58
|
"@iobroker/adapter-dev": "^1.5.0",
|
|
59
59
|
"@iobroker/dev-server": "^0.8.0",
|
|
60
60
|
"@iobroker/eslint-config": "^2.3.4",
|
|
61
|
-
"@iobroker/testing": "^6.
|
|
61
|
+
"@iobroker/testing": "^6.3.0",
|
|
62
62
|
"@tsconfig/node22": "^22.0.5",
|
|
63
63
|
"@types/node": "^22.19.21",
|
|
64
64
|
"typescript": "~5.9.3"
|