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.
Files changed (4) hide show
  1. package/README.md +13 -0
  2. package/io-package.json +14 -14
  3. package/main.js +269 -139
  4. 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.32",
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
- } else {
568
- // New day - reset statistics
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
- await this.sendTestMessage();
735
- // Reset state
736
- await this.setStateAsync('testButton', false, true);
737
- this.status.testMessageRunning = false; // Reset flag
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 === this.config.thresholdFull) {
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 === this.config.thresholdEmpty) {
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 !== this.config.thresholdFull && soc !== this.config.thresholdEmpty) {
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 === step && !this.status.intermediateNotified.includes(step)) {
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, '&amp;')
1070
+ .replace(/</g, '&lt;')
1071
+ .replace(/>/g, '&gt;')
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
- users: usersList.join(', '),
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 = weatherTodayTemp
1471
- ? ` (${this.translate('Currently')}: ${this.round(weatherTodayTemp, 1)}°C)`
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
- const now = new Date();
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
- * Tägliche Statistik zurücksetzen (zur konfigurierten Zeit)
1825
+ * Run the scheduled tasks for the given minute
1826
+ *
1827
+ * @param {Date} now - Current time
1747
1828
  */
1748
- resetDailyStats() {
1749
- const today = new Date().getDate();
1750
- const now = new Date();
1751
- const hours = now.getHours();
1752
- const minutes = now.getMinutes();
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
- // Konfigurierte Zeit parsen
1755
- const [resetHours, resetMinutes] = this.config.statsDayTime.split(':').map(Number);
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
- // Reset zur konfigurierten Zeit
1758
- if (today !== this.stats.lastStatsReset && hours === resetHours && minutes === resetMinutes) {
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
- * Reset weekly statistics - AUTOMATISCH am Sonntag um 23:55 (vor sourceanalytix Reset)
1891
+ * Check whether the "before midnight" save window (23:55-23:59) is active
1892
+ *
1893
+ * @param {Date} now - Current time
1771
1894
  */
1772
- async resetWeeklyStats() {
1773
- const now = new Date();
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 hours = now.getHours();
1776
- const minutes = now.getMinutes();
1906
+ const dateKey = now.toDateString();
1777
1907
 
1778
- // Automatische Speicherung: Jeden Sonntag um 23:55 (vor Mitternacht)
1779
- // JavaScript: 0 = Sonntag → genau das wollen wir!
1780
- if (jsDay === 0 && hours === 23 && minutes === 55) {
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 startScheduledTasks() zur konfigurierten Zeit
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 um 23:55 (vor sourceanalytix Reset)
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 hours = now.getHours();
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 um 23:55 (vor Mitternacht)
1823
- if (today === lastDayOfMonth && hours === 23 && minutes === 55) {
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
- this.stats.lastMonthProduction = await this.readForeignNumber(this.config.monthlyProduction);
1830
- this.stats.lastMonthConsumption = await this.readForeignNumber(this.config.monthlyConsumption);
1831
- this.stats.lastMonthFeedIn = await this.readForeignNumber(this.config.monthlyFeedIn);
1832
- this.stats.lastMonthGridPower = await this.readForeignNumber(this.config.monthlyGridPower);
1833
- // Monthly cycle counters (previously the daily counters were stored by mistake)
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.32",
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.2.1",
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"