iobroker.pv-notifications 1.2.32 → 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 +13 -0
- package/io-package.json +14 -14
- package/main.js +269 -139
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -324,6 +324,19 @@ sadam6752@gmail.com
|
|
|
324
324
|
### **WORK IN PROGRESS**
|
|
325
325
|
-->
|
|
326
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
|
+
|
|
327
340
|
### 1.2.32 (2026-10-08)
|
|
328
341
|
* (FIX) Daily, weekly and monthly statistics (cycles, min/max SOC) are no longer reset on every adapter restart
|
|
329
342
|
* (FIX) Monthly statistics reported the daily cycle counters; there are now real monthly counters (`statistics.fullCyclesMonth`, `statistics.emptyCyclesMonth`)
|
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,19 @@
|
|
|
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
|
+
},
|
|
32
45
|
"1.2.32": {
|
|
33
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.",
|
|
34
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.",
|
|
@@ -106,19 +119,6 @@
|
|
|
106
119
|
"nl": "Fix CI: package-lock.json bijgewerkt om alle eslint peer dependencies op te nemen",
|
|
107
120
|
"uk": "Виправлено CI: оновлено package-lock.json для включення всіх peer-залежностей eslint",
|
|
108
121
|
"zh-cn": "修复CI:更新package-lock.json以包含所有eslint对等依赖项"
|
|
109
|
-
},
|
|
110
|
-
"1.2.26": {
|
|
111
|
-
"en": "Fix W5604: add missing i18n keys for sunset stats in 8 languages; move old changelog to CHANGELOG_OLD.md; fix README structure",
|
|
112
|
-
"de": "W5604: fehlende i18n-Schluessel fuer Sonnenuntergangs-Statistik in 8 Sprachen ergaenzt; alten Changelog in CHANGELOG_OLD.md verschoben; README-Struktur korrigiert",
|
|
113
|
-
"ru": "W5604: добавлены отсутствующие i18n ключи для статистики заката в 8 языках; старый changelog перенесён в CHANGELOG_OLD.md; исправлена структура README",
|
|
114
|
-
"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",
|
|
115
|
-
"it": "W5604: chiavi i18n mancanti per stats tramonto aggiunte in 8 lingue; vecchio changelog spostato in CHANGELOG_OLD.md; struttura README corretta",
|
|
116
|
-
"es": "W5604: claves i18n faltantes para stats atardecer añadidas en 8 idiomas; changelog antiguo movido a CHANGELOG_OLD.md; estructura README corregida",
|
|
117
|
-
"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",
|
|
118
|
-
"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",
|
|
119
|
-
"nl": "W5604: ontbrekende i18n-sleutels voor zonsondergang-statistieken in 8 talen toegevoegd; oud changelog verplaatst naar CHANGELOG_OLD.md; README-structuur gecorrigeerd",
|
|
120
|
-
"uk": "W5604: додано відсутні i18n ключі для статистики заходу сонця у 8 мовах; старий changelog перенесено до CHANGELOG_OLD.md; виправлено структуру README",
|
|
121
|
-
"zh-cn": "修复W5604:在8种语言中添加日落统计缺失的i18n键;将旧changelog移至CHANGELOG_OLD.md;修复README结构"
|
|
122
122
|
}
|
|
123
123
|
},
|
|
124
124
|
"authors": [
|
package/main.js
CHANGED
|
@@ -75,6 +75,9 @@ class PvNotifications extends utils.Adapter {
|
|
|
75
75
|
|
|
76
76
|
// StateChange-Handler registrieren (für js-controller 7+)
|
|
77
77
|
this.on('stateChange', this.onStateChange);
|
|
78
|
+
|
|
79
|
+
// Unload handler (saves statistics, resets connection state)
|
|
80
|
+
this.on('unload', this.onUnload);
|
|
78
81
|
}
|
|
79
82
|
|
|
80
83
|
/**
|
|
@@ -564,14 +567,15 @@ class PvNotifications extends utils.Adapter {
|
|
|
564
567
|
this.stats.maxSOC = max && typeof max.val === 'number' ? max.val : 0;
|
|
565
568
|
this.stats.minSOC = min && typeof min.val === 'number' ? min.val : 100;
|
|
566
569
|
this.stats.lastStatsReset = today;
|
|
567
|
-
}
|
|
568
|
-
|
|
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)
|
|
569
574
|
this.stats.fullCycles = 0;
|
|
570
575
|
this.stats.emptyCycles = 0;
|
|
571
576
|
this.stats.maxSOC = 0;
|
|
572
577
|
this.stats.minSOC = 100;
|
|
573
578
|
this.stats.lastStatsReset = today;
|
|
574
|
-
await this.saveStatistics();
|
|
575
579
|
}
|
|
576
580
|
|
|
577
581
|
// Load saved last week data from states
|
|
@@ -614,6 +618,11 @@ class PvNotifications extends utils.Adapter {
|
|
|
614
618
|
this.stats.lastMonthEmptyCycles =
|
|
615
619
|
lastMonthEmptyCycles && lastMonthEmptyCycles.val !== null ? lastMonthEmptyCycles.val : 0;
|
|
616
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
|
+
|
|
617
626
|
this.log.debug('Statistics loaded from states');
|
|
618
627
|
} catch (e) {
|
|
619
628
|
this.log.error(`Error loading statistics: ${e.message}`);
|
|
@@ -723,6 +732,21 @@ class PvNotifications extends utils.Adapter {
|
|
|
723
732
|
* @param {ioBroker.State | null | undefined} state - State object
|
|
724
733
|
*/
|
|
725
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) {
|
|
726
750
|
if (state) {
|
|
727
751
|
// Process test button (all states in own namespace)
|
|
728
752
|
if (id.startsWith(`${this.namespace}.testButton`)) {
|
|
@@ -731,10 +755,13 @@ class PvNotifications extends utils.Adapter {
|
|
|
731
755
|
this.status.testMessageRunning = true; // Set flag
|
|
732
756
|
this.log.debug(`Test button state received: ${id}, val=${state.val}`);
|
|
733
757
|
this.log.debug('Test button was pressed');
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
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
|
+
}
|
|
738
765
|
}
|
|
739
766
|
return;
|
|
740
767
|
}
|
|
@@ -794,12 +821,14 @@ class PvNotifications extends utils.Adapter {
|
|
|
794
821
|
const currentKWh = this.round(((soc / 100) * this.config.batteryCapacityWh) / 1000, 1);
|
|
795
822
|
await this.setStateAsync('statistics.currentEnergyKWh', currentKWh, true);
|
|
796
823
|
|
|
797
|
-
// Update statistics
|
|
824
|
+
// Update statistics (persist immediately, otherwise the states stay stale until the next full/empty event)
|
|
798
825
|
if (soc > this.stats.maxSOC) {
|
|
799
826
|
this.stats.maxSOC = soc;
|
|
827
|
+
await this.setStateAsync('statistics.maxSOCToday', soc, true);
|
|
800
828
|
}
|
|
801
829
|
if (soc < this.stats.minSOC) {
|
|
802
830
|
this.stats.minSOC = soc;
|
|
831
|
+
await this.setStateAsync('statistics.minSOCToday', soc, true);
|
|
803
832
|
}
|
|
804
833
|
|
|
805
834
|
this.log.debug(`Battery SOC: ${soc}% | Status: full=${this.status.full}, empty=${this.status.empty}`);
|
|
@@ -813,8 +842,14 @@ class PvNotifications extends utils.Adapter {
|
|
|
813
842
|
: 'up';
|
|
814
843
|
|
|
815
844
|
// Store previous SOC for next update
|
|
845
|
+
const previousSOC = this.status.previousSOC;
|
|
816
846
|
this.status.previousSOC = soc;
|
|
817
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
|
+
|
|
818
853
|
// === NIGHT-TIME check with configurable time ===
|
|
819
854
|
const nightTime = this.isNightTime();
|
|
820
855
|
const nightModeActive = this.config.nightModeEnabled !== false;
|
|
@@ -825,7 +860,7 @@ class PvNotifications extends utils.Adapter {
|
|
|
825
860
|
const quietModeActive = this.config.quietModeEnabled !== false;
|
|
826
861
|
|
|
827
862
|
// === Batterie VOLL (100%) - Nicht nachts (wenn Nachtmodus aktiv) und nicht in Ruhezeit ===
|
|
828
|
-
if (soc
|
|
863
|
+
if (soc >= thresholdFull) {
|
|
829
864
|
// Prüfen ob Benachrichtigung erlaubt ist (nicht in Nachtzeit oder Ruhezeit)
|
|
830
865
|
const allowNotification = (!nightTime || !nightModeActive) && (!quietTime || !quietModeActive);
|
|
831
866
|
|
|
@@ -852,7 +887,7 @@ class PvNotifications extends utils.Adapter {
|
|
|
852
887
|
}
|
|
853
888
|
|
|
854
889
|
// === Battery EMPTY (0%) - Always allow if nightModeIgnoreEmpty is active ===
|
|
855
|
-
if (soc
|
|
890
|
+
if (soc <= thresholdEmpty) {
|
|
856
891
|
if (!this.status.empty && this.canNotify('empty')) {
|
|
857
892
|
// Always notify at 0% if nightModeIgnoreEmpty is active
|
|
858
893
|
// But still respect quiet time (unless nightModeIgnoreEmpty is active)
|
|
@@ -879,7 +914,7 @@ class PvNotifications extends utils.Adapter {
|
|
|
879
914
|
}
|
|
880
915
|
}
|
|
881
916
|
// === Intermediate-Stufen (nur wenn nicht voll/leer und nicht nachts und nicht in Ruhezeit) ===
|
|
882
|
-
if (soc
|
|
917
|
+
if (soc < thresholdFull && soc > thresholdEmpty) {
|
|
883
918
|
const intermediateSteps = (this.config.intermediateSteps || '20,40,60,80')
|
|
884
919
|
.split(',')
|
|
885
920
|
.map(s => parseInt(s.trim()))
|
|
@@ -889,22 +924,10 @@ class PvNotifications extends utils.Adapter {
|
|
|
889
924
|
const allowIntermediate = (!nightTime || !nightModeActive) && (!quietTime || !quietModeActive);
|
|
890
925
|
|
|
891
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)
|
|
892
929
|
for (const step of intermediateSteps) {
|
|
893
|
-
if (soc
|
|
894
|
-
if (this.canNotify('intermediate')) {
|
|
895
|
-
const message = await this.buildIntermediateMessage(soc, direction);
|
|
896
|
-
this.sendTelegram(message);
|
|
897
|
-
this.status.intermediateNotified.push(step);
|
|
898
|
-
this.status.lastNotification.intermediate = Date.now();
|
|
899
|
-
this.log.info(`Intermediate ${step}% - Telegram sent`);
|
|
900
|
-
}
|
|
901
|
-
break;
|
|
902
|
-
}
|
|
903
|
-
}
|
|
904
|
-
|
|
905
|
-
// Reset Intermediate-Flags wenn Stufe verlassen (±2% Toleranz)
|
|
906
|
-
for (const step of intermediateSteps) {
|
|
907
|
-
if (soc !== step && Math.abs(soc - step) >= 2) {
|
|
930
|
+
if (Math.abs(soc - step) >= 2) {
|
|
908
931
|
const idx = this.status.intermediateNotified.indexOf(step);
|
|
909
932
|
if (idx > -1) {
|
|
910
933
|
this.status.intermediateNotified.splice(idx, 1);
|
|
@@ -912,6 +935,26 @@ class PvNotifications extends utils.Adapter {
|
|
|
912
935
|
}
|
|
913
936
|
}
|
|
914
937
|
}
|
|
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
|
+
}
|
|
915
958
|
} else {
|
|
916
959
|
this.log.debug('Night/quiet time - intermediate notifications suppressed');
|
|
917
960
|
}
|
|
@@ -930,6 +973,26 @@ class PvNotifications extends utils.Adapter {
|
|
|
930
973
|
}
|
|
931
974
|
}
|
|
932
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
|
+
|
|
933
996
|
/**
|
|
934
997
|
* Prüfe ob Mindestintervall eingehalten
|
|
935
998
|
*
|
|
@@ -993,6 +1056,22 @@ class PvNotifications extends utils.Adapter {
|
|
|
993
1056
|
return currentTime >= startTime && currentTime < endTime;
|
|
994
1057
|
}
|
|
995
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
|
+
|
|
996
1075
|
/**
|
|
997
1076
|
* Sende Telegram-Nachricht mit Zeitstempel
|
|
998
1077
|
*
|
|
@@ -1015,8 +1094,12 @@ class PvNotifications extends utils.Adapter {
|
|
|
1015
1094
|
this.config.telegramInstance,
|
|
1016
1095
|
'send',
|
|
1017
1096
|
{
|
|
1018
|
-
text: fullMessage,
|
|
1019
|
-
|
|
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(','),
|
|
1020
1103
|
},
|
|
1021
1104
|
result => {
|
|
1022
1105
|
if (result && result.error) {
|
|
@@ -1114,9 +1197,9 @@ ${consumptionPowerLine}
|
|
|
1114
1197
|
weatherTodayTextState && weatherTodayTextState.val !== null ? weatherTodayTextState.val : null;
|
|
1115
1198
|
const weatherTodayTemp =
|
|
1116
1199
|
weatherTodayTempState && weatherTodayTempState.val !== null ? weatherTodayTempState.val : null;
|
|
1117
|
-
const todayTempText = weatherTodayTemp ? ` ${this.round(weatherTodayTemp, 1)}°C` : '';
|
|
1200
|
+
const todayTempText = weatherTodayTemp !== null ? ` ${this.round(weatherTodayTemp, 1)}°C` : '';
|
|
1118
1201
|
|
|
1119
|
-
if (weatherTodayText || weatherTodayTemp) {
|
|
1202
|
+
if (weatherTodayText || weatherTodayTemp !== null) {
|
|
1120
1203
|
const weatherDesc = weatherTodayText ? this.getWeatherDescription(weatherTodayText) : '🌡️';
|
|
1121
1204
|
message += `\n🌤️ Heute: ${weatherDesc}${todayTempText}`;
|
|
1122
1205
|
}
|
|
@@ -1142,7 +1225,7 @@ ${consumptionPowerLine}
|
|
|
1142
1225
|
weatherTomorrowState && weatherTomorrowState.val !== null ? weatherTomorrowState.val : null;
|
|
1143
1226
|
const tempTomorrow =
|
|
1144
1227
|
tempTomorrowState && tempTomorrowState.val !== null ? tempTomorrowState.val : null;
|
|
1145
|
-
const tempText = tempTomorrow ? ` ${this.round(tempTomorrow, 1)}°C` : '';
|
|
1228
|
+
const tempText = tempTomorrow !== null ? ` ${this.round(tempTomorrow, 1)}°C` : '';
|
|
1146
1229
|
|
|
1147
1230
|
const weatherText = weatherTomorrowText || weatherTomorrow;
|
|
1148
1231
|
if (weatherText) {
|
|
@@ -1211,9 +1294,9 @@ ${consumptionPowerLine}${separator}`;
|
|
|
1211
1294
|
weatherTodayTextState && weatherTodayTextState.val !== null ? weatherTodayTextState.val : null;
|
|
1212
1295
|
const weatherTodayTemp =
|
|
1213
1296
|
weatherTodayTempState && weatherTodayTempState.val !== null ? weatherTodayTempState.val : null;
|
|
1214
|
-
const todayTempText = weatherTodayTemp ? ` ${this.round(weatherTodayTemp, 1)}°C` : '';
|
|
1297
|
+
const todayTempText = weatherTodayTemp !== null ? ` ${this.round(weatherTodayTemp, 1)}°C` : '';
|
|
1215
1298
|
|
|
1216
|
-
if (weatherTodayText || weatherTodayTemp) {
|
|
1299
|
+
if (weatherTodayText || weatherTodayTemp !== null) {
|
|
1217
1300
|
const weatherDesc = weatherTodayText ? this.getWeatherDescription(weatherTodayText) : '🌡️';
|
|
1218
1301
|
message += `\n🌤️ Heute: ${weatherDesc}${todayTempText}`;
|
|
1219
1302
|
}
|
|
@@ -1239,7 +1322,7 @@ ${consumptionPowerLine}${separator}`;
|
|
|
1239
1322
|
weatherTomorrowState && weatherTomorrowState.val !== null ? weatherTomorrowState.val : null;
|
|
1240
1323
|
const tempTomorrow =
|
|
1241
1324
|
tempTomorrowState && tempTomorrowState.val !== null ? tempTomorrowState.val : null;
|
|
1242
|
-
const tempText = tempTomorrow ? ` ${this.round(tempTomorrow, 1)}°C` : '';
|
|
1325
|
+
const tempText = tempTomorrow !== null ? ` ${this.round(tempTomorrow, 1)}°C` : '';
|
|
1243
1326
|
|
|
1244
1327
|
const weatherText = weatherTomorrowText || weatherTomorrow;
|
|
1245
1328
|
if (weatherText) {
|
|
@@ -1336,9 +1419,9 @@ ${statusText}
|
|
|
1336
1419
|
weatherTodayTextState && weatherTodayTextState.val !== null ? weatherTodayTextState.val : null;
|
|
1337
1420
|
const weatherTodayTemp =
|
|
1338
1421
|
weatherTodayTempState && weatherTodayTempState.val !== null ? weatherTodayTempState.val : null;
|
|
1339
|
-
const todayTempText = weatherTodayTemp ? ` ${this.round(weatherTodayTemp, 1)}°C` : '';
|
|
1422
|
+
const todayTempText = weatherTodayTemp !== null ? ` ${this.round(weatherTodayTemp, 1)}°C` : '';
|
|
1340
1423
|
|
|
1341
|
-
if (weatherTodayText || weatherTodayTemp) {
|
|
1424
|
+
if (weatherTodayText || weatherTodayTemp !== null) {
|
|
1342
1425
|
const weatherDesc = weatherTodayText ? this.getWeatherDescription(weatherTodayText) : '🌡️';
|
|
1343
1426
|
message += `\n🌤️ ${this.translate('Weather today')}: ${weatherDesc}${todayTempText}`;
|
|
1344
1427
|
this.log.debug(`Weather today added to intermediate message: ${weatherDesc}${todayTempText}`);
|
|
@@ -1365,7 +1448,7 @@ ${statusText}
|
|
|
1365
1448
|
weatherTomorrowState && weatherTomorrowState.val !== null ? weatherTomorrowState.val : null;
|
|
1366
1449
|
const tempTomorrow =
|
|
1367
1450
|
tempTomorrowState && tempTomorrowState.val !== null ? tempTomorrowState.val : null;
|
|
1368
|
-
const tempText = tempTomorrow ? ` ${this.round(tempTomorrow, 1)}°C` : '';
|
|
1451
|
+
const tempText = tempTomorrow !== null ? ` ${this.round(tempTomorrow, 1)}°C` : '';
|
|
1369
1452
|
|
|
1370
1453
|
const weatherText = weatherTomorrowText || weatherTomorrow;
|
|
1371
1454
|
if (weatherText) {
|
|
@@ -1465,11 +1548,12 @@ ${statusText}
|
|
|
1465
1548
|
const weatherTodayTemp =
|
|
1466
1549
|
weatherTodayTempState && weatherTodayTempState.val !== null ? weatherTodayTempState.val : null;
|
|
1467
1550
|
|
|
1468
|
-
if (weatherTodayText || weatherTodayTemp) {
|
|
1551
|
+
if (weatherTodayText || weatherTodayTemp !== null) {
|
|
1469
1552
|
const weatherDesc = weatherTodayText ? this.getWeatherDescription(weatherTodayText) : '🌡️';
|
|
1470
|
-
const todayTempSuffix =
|
|
1471
|
-
|
|
1472
|
-
|
|
1553
|
+
const todayTempSuffix =
|
|
1554
|
+
weatherTodayTemp !== null
|
|
1555
|
+
? ` (${this.translate('Currently')}: ${this.round(weatherTodayTemp, 1)}°C)`
|
|
1556
|
+
: '';
|
|
1473
1557
|
message += `\n━━━━━━━━━━━━━━━━━━━━━━\n🌤️ *${this.translate('Weather today')}:* ${weatherDesc}${todayTempSuffix}`;
|
|
1474
1558
|
weatherAdded = true;
|
|
1475
1559
|
}
|
|
@@ -1495,7 +1579,7 @@ ${statusText}
|
|
|
1495
1579
|
weatherTomorrowState && weatherTomorrowState.val !== null ? weatherTomorrowState.val : null;
|
|
1496
1580
|
const tempTomorrow =
|
|
1497
1581
|
tempTomorrowState && tempTomorrowState.val !== null ? tempTomorrowState.val : null;
|
|
1498
|
-
const tempText = tempTomorrow ? ` ${this.round(tempTomorrow, 1)}°C` : '';
|
|
1582
|
+
const tempText = tempTomorrow !== null ? ` ${this.round(tempTomorrow, 1)}°C` : '';
|
|
1499
1583
|
|
|
1500
1584
|
const weatherText = weatherTomorrowText || weatherTomorrow;
|
|
1501
1585
|
if (weatherText) {
|
|
@@ -1592,7 +1676,7 @@ ${statusText}
|
|
|
1592
1676
|
return '🌡️ unbekannt';
|
|
1593
1677
|
}
|
|
1594
1678
|
|
|
1595
|
-
const text = weatherText.toLowerCase();
|
|
1679
|
+
const text = String(weatherText).toLowerCase();
|
|
1596
1680
|
|
|
1597
1681
|
if (text.includes('sonnig') || text.includes('klar')) {
|
|
1598
1682
|
return '☀️ sonnig';
|
|
@@ -1635,7 +1719,7 @@ ${statusText}
|
|
|
1635
1719
|
if (!weatherText) {
|
|
1636
1720
|
return false;
|
|
1637
1721
|
}
|
|
1638
|
-
const text = weatherText.toLowerCase();
|
|
1722
|
+
const text = String(weatherText).toLowerCase();
|
|
1639
1723
|
return (
|
|
1640
1724
|
text.includes('sonnig') || text.includes('klar') || text.includes('clear') || text.includes('few clouds')
|
|
1641
1725
|
);
|
|
@@ -1650,7 +1734,7 @@ ${statusText}
|
|
|
1650
1734
|
if (!weatherText) {
|
|
1651
1735
|
return false;
|
|
1652
1736
|
}
|
|
1653
|
-
const text = weatherText.toLowerCase();
|
|
1737
|
+
const text = String(weatherText).toLowerCase();
|
|
1654
1738
|
return (
|
|
1655
1739
|
text.includes('regen') ||
|
|
1656
1740
|
text.includes('rain') ||
|
|
@@ -1676,65 +1760,60 @@ ${statusText}
|
|
|
1676
1760
|
return Math.round(value * Math.pow(10, decimals)) / Math.pow(10, decimals);
|
|
1677
1761
|
}
|
|
1678
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
|
+
|
|
1679
1810
|
/**
|
|
1680
1811
|
* Start scheduled tasks
|
|
1681
1812
|
*/
|
|
1682
1813
|
startScheduledTasks() {
|
|
1683
1814
|
// Check every minute
|
|
1684
1815
|
this.scheduledInterval = this.setInterval(() => {
|
|
1685
|
-
|
|
1686
|
-
const hours = now.getHours();
|
|
1687
|
-
const minutes = now.getMinutes();
|
|
1688
|
-
const jsDay = now.getDay(); // JavaScript: 0=So, 1=Mo, ..., 6=Sa
|
|
1689
|
-
// Umwandeln in ioBroker-Format: 0=Mo, 1=Di, ..., 6=So
|
|
1690
|
-
const day = jsDay === 0 ? 6 : jsDay - 1;
|
|
1691
|
-
const date = now.getDate();
|
|
1692
|
-
|
|
1693
|
-
// Alle 5 Minuten: Statistik prüfen (um :00, :05, :10, ...)
|
|
1694
|
-
if (minutes % 5 === 0) {
|
|
1695
|
-
this.resetDailyStats();
|
|
1696
|
-
this.resetWeeklyStats().catch(e => this.log.error(`Weekly reset failed: ${e.message}`));
|
|
1697
|
-
this.resetMonthlyStats().catch(e => this.log.error(`Monthly reset failed: ${e.message}`));
|
|
1698
|
-
}
|
|
1699
|
-
|
|
1700
|
-
// Tägliche Statistik — entweder zur konfigurierten Zeit oder zum Sonnenuntergang
|
|
1701
|
-
if (this.config.statsUseSunset && this.config.statsSunsetObject) {
|
|
1702
|
-
// Sonnenuntergangszeit aus dem konfigurierten Objekt lesen
|
|
1703
|
-
this.getForeignStateAsync(this.config.statsSunsetObject)
|
|
1704
|
-
.then(sunsetState => {
|
|
1705
|
-
if (sunsetState && sunsetState.val) {
|
|
1706
|
-
const sunsetTime = String(sunsetState.val).trim();
|
|
1707
|
-
const match = sunsetTime.match(/^(\d{1,2}):(\d{2})/);
|
|
1708
|
-
if (match) {
|
|
1709
|
-
const sunsetHours = parseInt(match[1], 10);
|
|
1710
|
-
const sunsetMinutes = parseInt(match[2], 10);
|
|
1711
|
-
if (hours === sunsetHours && minutes === sunsetMinutes) {
|
|
1712
|
-
this.sendDailyStatsMessage();
|
|
1713
|
-
}
|
|
1714
|
-
}
|
|
1715
|
-
}
|
|
1716
|
-
})
|
|
1717
|
-
.catch(e => this.log.warn(`Could not read sunset object: ${e.message}`));
|
|
1718
|
-
} else {
|
|
1719
|
-
const [dayHours, dayMinutes] = this.config.statsDayTime.split(':').map(Number);
|
|
1720
|
-
if (hours === dayHours && minutes === dayMinutes) {
|
|
1721
|
-
this.sendDailyStatsMessage();
|
|
1722
|
-
}
|
|
1723
|
-
}
|
|
1724
|
-
|
|
1725
|
-
// Wöchentliche Statistik am konfigurierten Tag und Zeit
|
|
1726
|
-
const [weekHours, weekMinutes] = this.config.statsWeekTime.split(':').map(Number);
|
|
1727
|
-
if (day === this.config.statsWeekDay && hours === weekHours && minutes === weekMinutes) {
|
|
1728
|
-
this.sendTelegram(this.buildWeeklyStatsMessage());
|
|
1729
|
-
}
|
|
1730
|
-
|
|
1731
|
-
// Monatsstatistik am konfigurierten Tag und Zeit
|
|
1732
|
-
if (this.config.monthlyStatsEnabled) {
|
|
1733
|
-
const [monthHours, monthMinutes] = this.config.monthlyStatsTime.split(':').map(Number);
|
|
1734
|
-
if (date === this.config.monthlyStatsDay && hours === monthHours && minutes === monthMinutes) {
|
|
1735
|
-
this.sendTelegram(this.buildMonthlyStatsMessage());
|
|
1736
|
-
}
|
|
1737
|
-
}
|
|
1816
|
+
this.runScheduledTasks(new Date()).catch(e => this.log.error(`Scheduled task failed: ${e.message}`));
|
|
1738
1817
|
}, 60000); // Jede Minute ausführen
|
|
1739
1818
|
|
|
1740
1819
|
this.log.info(
|
|
@@ -1743,19 +1822,61 @@ ${statusText}
|
|
|
1743
1822
|
}
|
|
1744
1823
|
|
|
1745
1824
|
/**
|
|
1746
|
-
*
|
|
1825
|
+
* Run the scheduled tasks for the given minute
|
|
1826
|
+
*
|
|
1827
|
+
* @param {Date} now - Current time
|
|
1747
1828
|
*/
|
|
1748
|
-
|
|
1749
|
-
const
|
|
1750
|
-
|
|
1751
|
-
const
|
|
1752
|
-
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
|
+
}
|
|
1753
1855
|
|
|
1754
|
-
//
|
|
1755
|
-
|
|
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
|
+
}
|
|
1756
1860
|
|
|
1757
|
-
//
|
|
1758
|
-
if (
|
|
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
|
+
}
|
|
1870
|
+
|
|
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) {
|
|
1759
1880
|
this.log.info('Resetting daily statistics');
|
|
1760
1881
|
this.stats.fullCycles = 0;
|
|
1761
1882
|
this.stats.emptyCycles = 0;
|
|
@@ -1767,17 +1888,27 @@ ${statusText}
|
|
|
1767
1888
|
}
|
|
1768
1889
|
|
|
1769
1890
|
/**
|
|
1770
|
-
*
|
|
1891
|
+
* Check whether the "before midnight" save window (23:55-23:59) is active
|
|
1892
|
+
*
|
|
1893
|
+
* @param {Date} now - Current time
|
|
1771
1894
|
*/
|
|
1772
|
-
|
|
1773
|
-
|
|
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()) {
|
|
1774
1905
|
const jsDay = now.getDay(); // JavaScript: 0=So, 1=Mo, ..., 6=Sa
|
|
1775
|
-
const
|
|
1776
|
-
const minutes = now.getMinutes();
|
|
1906
|
+
const dateKey = now.toDateString();
|
|
1777
1907
|
|
|
1778
|
-
// Automatische Speicherung: Jeden Sonntag
|
|
1779
|
-
//
|
|
1780
|
-
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;
|
|
1781
1912
|
this.log.info('Auto-saving weekly statistics (Sunday 23:55, before sourceanalytix reset)');
|
|
1782
1913
|
|
|
1783
1914
|
// Aktuelle Daten aus externen States lesen (direkter Zugriff)
|
|
@@ -1794,50 +1925,49 @@ ${statusText}
|
|
|
1794
1925
|
this.stats.weekFullCycles = 0;
|
|
1795
1926
|
this.stats.weekEmptyCycles = 0;
|
|
1796
1927
|
|
|
1797
|
-
this.saveStatistics();
|
|
1928
|
+
await this.saveStatistics();
|
|
1798
1929
|
this.log.info(
|
|
1799
1930
|
`Weekly stats saved: Production=${this.stats.lastWeekProduction} kWh, FeedIn=${this.stats.lastWeekFeedIn} kWh`,
|
|
1800
1931
|
);
|
|
1801
|
-
// KEIN Senden hier - Senden erfolgt nur in
|
|
1932
|
+
// KEIN Senden hier - Senden erfolgt nur in runScheduledTasks() zur konfigurierten Zeit
|
|
1802
1933
|
}
|
|
1803
1934
|
}
|
|
1804
1935
|
|
|
1805
1936
|
/**
|
|
1806
|
-
* 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
|
|
1807
1941
|
*/
|
|
1808
|
-
async resetMonthlyStats() {
|
|
1809
|
-
if (!this.config.monthlyStatsEnabled) {
|
|
1810
|
-
return;
|
|
1811
|
-
}
|
|
1812
|
-
|
|
1813
|
-
const now = new Date();
|
|
1942
|
+
async resetMonthlyStats(now = new Date()) {
|
|
1814
1943
|
const today = now.getDate();
|
|
1815
|
-
const
|
|
1816
|
-
const minutes = now.getMinutes();
|
|
1944
|
+
const dateKey = now.toDateString();
|
|
1817
1945
|
|
|
1818
1946
|
// Letzten Tag des aktuellen Monats berechnen
|
|
1819
1947
|
// new Date(Jahr, Monat+1, 0) gibt den letzten Tag des aktuellen Monats zurück
|
|
1820
1948
|
const lastDayOfMonth = new Date(now.getFullYear(), now.getMonth() + 1, 0).getDate();
|
|
1821
1949
|
|
|
1822
|
-
// Automatische Speicherung: Letzter Tag des Monats
|
|
1823
|
-
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;
|
|
1824
1953
|
this.log.info(
|
|
1825
1954
|
`Auto-saving monthly statistics (last day of month ${today}. ${now.getMonth() + 1}.${now.getFullYear()} 23:55, before sourceanalytix reset)`,
|
|
1826
1955
|
);
|
|
1827
1956
|
|
|
1828
1957
|
// Aktuelle Daten aus externen States lesen (direkter Zugriff)
|
|
1829
|
-
|
|
1830
|
-
|
|
1831
|
-
|
|
1832
|
-
|
|
1833
|
-
|
|
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
|
+
}
|
|
1834
1964
|
this.stats.lastMonthFullCycles = this.stats.monthFullCycles;
|
|
1835
1965
|
this.stats.lastMonthEmptyCycles = this.stats.monthEmptyCycles;
|
|
1836
1966
|
this.stats.monthFullCycles = 0;
|
|
1837
1967
|
this.stats.monthEmptyCycles = 0;
|
|
1838
1968
|
|
|
1839
1969
|
this.stats.lastMonthReset = today;
|
|
1840
|
-
this.saveStatistics();
|
|
1970
|
+
await this.saveStatistics();
|
|
1841
1971
|
this.log.info(
|
|
1842
1972
|
`Monthly stats saved: Production=${this.stats.lastMonthProduction} kWh, FeedIn=${this.stats.lastMonthFeedIn} kWh`,
|
|
1843
1973
|
);
|
|
@@ -2120,9 +2250,9 @@ ${statusText}
|
|
|
2120
2250
|
weatherTodayTextState && weatherTodayTextState.val !== null ? weatherTodayTextState.val : null;
|
|
2121
2251
|
const weatherTodayTemp =
|
|
2122
2252
|
weatherTodayState && weatherTodayState.val !== null ? weatherTodayState.val : null;
|
|
2123
|
-
const tempText = weatherTodayTemp ? ` ${this.round(weatherTodayTemp, 1)}°C` : '';
|
|
2253
|
+
const tempText = weatherTodayTemp !== null ? ` ${this.round(weatherTodayTemp, 1)}°C` : '';
|
|
2124
2254
|
|
|
2125
|
-
if (weatherTodayText || weatherTodayTemp) {
|
|
2255
|
+
if (weatherTodayText || weatherTodayTemp !== null) {
|
|
2126
2256
|
const weatherDesc = weatherTodayText ? this.getWeatherDescription(weatherTodayText) : '🌡️';
|
|
2127
2257
|
message += `\n\n🌤️ *${this.translate('Weather today')}:* ${weatherDesc}${tempText}`;
|
|
2128
2258
|
}
|
|
@@ -2143,9 +2273,9 @@ ${statusText}
|
|
|
2143
2273
|
: null;
|
|
2144
2274
|
const weatherTomorrowTemp =
|
|
2145
2275
|
weatherTomorrowState && weatherTomorrowState.val !== null ? weatherTomorrowState.val : null;
|
|
2146
|
-
const tempText = weatherTomorrowTemp ? ` ${this.round(weatherTomorrowTemp, 1)}°C` : '';
|
|
2276
|
+
const tempText = weatherTomorrowTemp !== null ? ` ${this.round(weatherTomorrowTemp, 1)}°C` : '';
|
|
2147
2277
|
|
|
2148
|
-
if (weatherTomorrowText || weatherTomorrowTemp) {
|
|
2278
|
+
if (weatherTomorrowText || weatherTomorrowTemp !== null) {
|
|
2149
2279
|
const weatherDesc = weatherTomorrowText
|
|
2150
2280
|
? this.getWeatherDescription(weatherTomorrowText)
|
|
2151
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"
|