iobroker.sun2000 2.4.3 → 2.4.5
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 +12 -9
- package/io-package.json +27 -27
- package/lib/json_helper.js +148 -0
- package/lib/modbus/modbus_server.js +8 -4
- package/lib/statistics.js +177 -237
- package/lib/tools.js +13 -9
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -67,9 +67,21 @@ The sun2000 adapter calculates how much of your self-generated solar energy is a
|
|
|
67
67
|
## Changelog
|
|
68
68
|
<!--
|
|
69
69
|
Placeholder for the next version (at the beginning of the line):
|
|
70
|
+
### **WORK IN PROGRESS**
|
|
70
71
|
-->
|
|
72
|
+
### 2.4.5 (2026-05-14)
|
|
73
|
+
* statistics fix: return weekly range up to current Monday
|
|
74
|
+
* statistics: added support for generating statistics templates directly from built-in charts
|
|
75
|
+
* statistics: improved tooltip formatter - tooltip units are now provided explicitly via tooltip.valueFormatter
|
|
76
|
+
|
|
77
|
+
### 2.4.4 (2026-05-04)
|
|
78
|
+
* statistics fix: add error handling for waitForValue function
|
|
79
|
+
|
|
71
80
|
### 2.4.3 (2026-04-19)
|
|
72
81
|
* statistics: new state `statistics.jsonToday` — live summary of today's energy values
|
|
82
|
+
* statistics: default chart shows energy flows above/below zero line, SOC (hourly only), self-sufficiency and self-consumption on second Y-axis
|
|
83
|
+
* statistics: computed values `selfSufficiency` and `selfConsumption` calculated automatically in all time-series states
|
|
84
|
+
* statistics: data placeholders (`%%solarYield%%`, `%%selfSufficiency%%` etc.) and negated variants (`%%gridExportNeg%%` etc.) for mirrored chart layouts
|
|
73
85
|
|
|
74
86
|
### 2.4.2 (2026-04-04)
|
|
75
87
|
* fix test-and-release: deploy with 24.x
|
|
@@ -84,15 +96,6 @@ The sun2000 adapter calculates how much of your self-generated solar energy is a
|
|
|
84
96
|
* new state `inverter.x.emma.activeAlarmSN` and `inverter.x.emma.HistoricalAlarmSN` : emma alarms [#226](https://github.com/bolliy/ioBroker.sun2000/issues/226)
|
|
85
97
|
* statistics: Aggregates historical collected datapoints into time-based summaries (e.g. hourly, daily, monthly, yearly). The data is stored in the path `statistics` as JSON.
|
|
86
98
|
|
|
87
|
-
### 2.3.7 (2026-02-01)
|
|
88
|
-
* deleted deprecated state `collected.usableSurplusPower`
|
|
89
|
-
|
|
90
|
-
### 2.3.6 (2026-01-29)
|
|
91
|
-
* dependency and configuration updates
|
|
92
|
-
* new state `inverter.x.derived.alarmsJSON` : json array with intverter alarms (id, name, level) [#226](https://github.com/bolliy/ioBroker.sun2000/issues/226)
|
|
93
|
-
* add ChargeDischargePower for Battery units [#234](https://github.com/bolliy/ioBroker.sun2000/issues/234)
|
|
94
|
-
* add minimum and maximum temperature for battery packs [#236](https://github.com/bolliy/ioBroker.sun2000/issues/236)
|
|
95
|
-
|
|
96
99
|
## License
|
|
97
100
|
MIT License
|
|
98
101
|
|
package/io-package.json
CHANGED
|
@@ -1,8 +1,34 @@
|
|
|
1
1
|
{
|
|
2
2
|
"common": {
|
|
3
3
|
"name": "sun2000",
|
|
4
|
-
"version": "2.4.
|
|
4
|
+
"version": "2.4.5",
|
|
5
5
|
"news": {
|
|
6
|
+
"2.4.5": {
|
|
7
|
+
"en": "statistics fix: return weekly range up to current Monday\nstatistics: added support for generating statistics templates directly from built-in charts\nstatistics: improved tooltip formatter - tooltip units are now provided explicitly via tooltip.valueFormatter",
|
|
8
|
+
"de": "statistik fix: wöchentliche Rückgabe bis zum aktuellen Montag\nstatistik: unterstützung für die erstellung von statistiken vorlagen direkt aus integrierten diagrammen\nstatistiken: verbesserter Tooltip-Formater - Tooltip-Einheiten werden nun explizit über tooltip.valueFormatter bereitgestellt",
|
|
9
|
+
"ru": "исправление статистики: еженедельный диапазон возврата до текущего понедельника\nстатистика: добавлена поддержка для создания шаблонов статистики непосредственно из встроенных диаграмм\nстатистика: улучшенный инструментальный формататор - инструментальные блоки теперь предоставляются явно через tooltip.value",
|
|
10
|
+
"pt": "estatística correção: retorno semanal intervalo até a segunda-feira atual\nestatística: suporte adicionado para gerar modelos estatísticos diretamente de gráficos embutidos\nestatísticas: a dica de formatação melhorada - as unidades de dica são agora fornecidas explicitamente via tooltip.valueFormatter",
|
|
11
|
+
"nl": "statistieken fix: retour wekelijkse bereik tot de huidige maandag\nstatistieken: extra ondersteuning voor het genereren van statistieken templates rechtstreeks uit ingebouwde grafieken\nstatistieken: verbeterde tooltip formatter - tooltip-eenheden worden nu expliciet geleverd via tooltip.valueFormatter",
|
|
12
|
+
"fr": "correction statistique: retour hebdomadaire jusqu'à lundi actuel\nstatistiques: appui supplémentaire à la production de modèles de statistiques directement à partir de graphiques intégrés\nstatistiques: amélioration de l'outil pour la matière - les unités d'outil sont désormais fournies explicitement via tooltip.valueFormatter",
|
|
13
|
+
"it": "statistiche fix: rientro settimanale fino al lunedì corrente\nstatistiche: aggiunto il supporto per la generazione di modelli di statistiche direttamente da grafici incorporati\nstatistiche: migliorato tooltip formatter - le unità tooltip sono ora fornite esplicitamente tramite tooltip.valueFormatter",
|
|
14
|
+
"es": "solucion de estadísticas: retorno semanal hasta el lunes actual\nestadística: apoyo añadido para generar plantillas de estadísticas directamente desde gráficos incorporados\nestadística: mejorada herramienta formatter - unidades de punta de herramientas se proporcionan ahora explícitamente a través de tooltip.valueFormatter",
|
|
15
|
+
"pl": "statystyki naprawić: wrócić tygodniowy zakres do bieżącego poniedziałku\nstatystyki: dodano wsparcie dla tworzenia szablonów statystycznych bezpośrednio z wykresów built- in\nstatystyki: ulepszone formatowanie podpowiedzi narzędziowej - jednostki podpowiedzi są teraz wyraźnie dostarczane za pośrednictwem tooltip.valueFormatter",
|
|
16
|
+
"uk": "статистика виправити: повернути щотижневий діапазон до поточного понеділка\nстатистика: додана підтримка створення шаблонів статистики безпосередньо з вбудованих діаграм\nстатистика: поліпшений форматувальник інструментів - блоки інструментів тепер забезпечують явно через tooltip.valueFormatter",
|
|
17
|
+
"zh-cn": "统计修补:截至本星期一的每周返回范围\n统计:为直接从内建图表生成统计模板提供更多支持\n统计:改进后的工具提示用于物质-工具提示单位现在通过工具提示明确提供. valueFormatter"
|
|
18
|
+
},
|
|
19
|
+
"2.4.4": {
|
|
20
|
+
"en": "statistics fix: add error handling for waitForValue function",
|
|
21
|
+
"de": "statistik fix: Fehlerbehandlung für waitForValue-Funktion hinzufügen",
|
|
22
|
+
"ru": "исправление статистики: добавьте обработку ошибок в функцию WaitForValue",
|
|
23
|
+
"pt": "estatística corrigir: adicionar o tratamento de erro para a função waitForValue",
|
|
24
|
+
"nl": "statistieken fix: voeg fout behandeling voor waitForValue functie",
|
|
25
|
+
"fr": "correction statistique : ajouter la gestion des erreurs pour la fonction waitForValue",
|
|
26
|
+
"it": "statistiche fix: aggiungere la gestione degli errori per la funzione WaitForValue",
|
|
27
|
+
"es": "estadística fijado: añadir error de manejo para waitForValue función",
|
|
28
|
+
"pl": "statystyki naprawić: dodać obsługę błędów dla funkcji waitForValue",
|
|
29
|
+
"uk": "виправлення статистичних даних: Додавання обробки помилок для очікування",
|
|
30
|
+
"zh-cn": "统计修补: 添加waitForValue函数的错误处理"
|
|
31
|
+
},
|
|
6
32
|
"2.4.3": {
|
|
7
33
|
"en": "statistics: new state `statistics.jsonToday` — live summary of today's energy values",
|
|
8
34
|
"de": "statistik: new state `statistics.jsonToday` — Live-Zusammenfassung der heutigen Energiewerte",
|
|
@@ -29,19 +55,6 @@
|
|
|
29
55
|
"uk": "виправити тест-і-випуск: розгортання з 24.x",
|
|
30
56
|
"zh-cn": "固定测试和释放:部署24x"
|
|
31
57
|
},
|
|
32
|
-
"2.4.1": {
|
|
33
|
-
"en": "statistics: flexcharts integration — built-in Apache ECharts configuration with bar and line chart support\nstatistics: day-break visualization with alternating shaded areas for hourly charts\nstatistics: per chart-type templates (`statistics.flexCharts.template.hourly` etc.) for full ECharts customization including functions\nstatistics: data placeholders (`%%solarYield%%`, `%%gridExport%%` etc.) allow complete chart layout control via template states\nstatistics: chart output states (`statistics.flexCharts.jsonOutput.hourly` etc.) updated automatically each hour",
|
|
34
|
-
"de": "statistik: flexcharts integration — integrierte Apache ECharts-Konfiguration mit Bar- und Liniendiagrammunterstützung\nstatistik: tages-früh-visualisierung mit alternierenden schattierten bereichen für stundendiagramme\nstatistiken: pro Diagrammvorlagen (`statistics.flexCharts.template.hourly` etc.) für die vollständige ECharts Anpassung einschließlich Funktionen\nstatistik: Datenplatzhalter (`%%solar%%%%`, `%%gridExport%% ` etc.) ermöglichen vollständige Chart-Layout-Kontrolle über Template-Staaten\nstatistiken: Diagrammausgabezustände (`statistics.flexCharts.jsonOutput.hourly` etc.) automatisch jede Stunde aktualisiert",
|
|
35
|
-
"ru": "статистика: интеграция flexcharts — встроенная конфигурация Apache ECharts с поддержкой штрих- и линейных графиков\nстатистика: визуализация на рассвете с чередующимися затененными областями для часовых графиков\nстатистика: на шаблоны типа диаграммы («statistics.flexCharts.template.hourly» и т. д.) для полной настройки EChart, включая функции\nстатистика: держатели данных («%%solarYield%%», «%%gridExport%%» и т.д.) позволяют полностью контролировать макет диаграммы через шаблонные состояния\nстатистика: состояния выхода диаграммы (statistics.flexCharts.jsonOutput.hourly и т. д.) обновляются автоматически каждый час",
|
|
36
|
-
"pt": "estatísticas: integração de flexcharts — configuração integrada do Apache ECharts com suporte a barras e gráficos de linha\nestatísticas: visualização diurna com áreas de sombra alternadas para gráficos horários\nestatísticas: por modelos de tipo gráfico (`statistics.flexCharts.template.hourly` etc.) para personalização completa de ECharts, incluindo funções\nestatísticas: placeholders de dados (` %s olarYield%% `, `% gridExport%% ` etc.) permitem o controle completo do layout do gráfico através de estados de modelo\nestatísticas: estados de saída do gráfico (`statistics.flexCharts.jsonOutput.hourly` etc.) atualizados automaticamente a cada hora",
|
|
37
|
-
"nl": "statistics: flexcharts integration Ingebouwde Apache ECharts configuratie met ondersteuning voor bar en lijndiagram\nstatistieken: dagvakvisualisatie met afwisselende schaduwgebieden voor uurkaarten\nstatistieken: per grafiek-type sjablonen (\nstatistieken: data placeholders (\nstatistieken: grafiek output states (",
|
|
38
|
-
"fr": "statistiques: intégration des flexcharts — configuration Apache ECharts intégrée avec support de la barre et de la ligne\nstatistiques: visualisation de la pause-jour avec zones ombrées alternées pour les graphiques horaires\nstatistiques: par modèle type de graphique (`statistics.flexCharts.template.hourly` etc.) pour la personnalisation complète d'ECharts, y compris les fonctions\nstatistiques : les détenteurs de place de données (`%%solarYield%%`, `%gridExport%%%` etc.) permettent un contrôle complet de la disposition des graphiques via les états de gabarit\nstatistiques: états de sortie du graphique (`statistics.flexCharts.jsonOutput.hourly` etc.) mis à jour automatiquement chaque heure",
|
|
39
|
-
"it": "statistiche: integrazione di flexcharts — configurazione integrata di Apache ECharts con supporto grafico a barre e linea\nstatistiche: visualizzazione day-break con aree ombreggiate alternate per grafici orari\nstatistiche: per modelli di tipo grafico (`statistics.flexCharts.template.hourly` ecc.) per la personalizzazione completa di ECharts comprese le funzioni\nstatistiche: i segnaposto dei dati (%solarYield%%, `%gridExport%%` ecc.) permettono il controllo completo del layout del grafico tramite gli stati del modello\nstatistiche: stati di output grafico (`statistics.flexCharts.jsonOutput.hourly` ecc.) aggiornati automaticamente ogni ora",
|
|
40
|
-
"es": "estadística: integración de flexcharts — configuración integrada de Apache ECharts con soporte de barras y gráficos\nestadística: visualización del día con áreas alternadas sombreadas para gráficos por hora\nestadística: por plantillas tipo gráfico (`statistics.flexCharts.template.hourly` etc.) para la personalización completa de ECharts incluyendo funciones\nestadística: marcadores de datos ( \"%solarYield%%% \" , \"%gridExport%%% \" , etc.) permiten un control completo de la distribución de gráficos a través de estados de plantilla\nestadística: estados de salida del gráfico (`statistics.flexCharts.jsonOutput.hourly` etc.) actualizados automáticamente cada hora",
|
|
41
|
-
"pl": "statystyki: integracja flexcharts - built- w konfiguracji Apache ECharts z obsługą wykresu paska i linii\nstatystyki: wizualizacja z naprzemiennymi zacienionymi obszarami dla wykresów godzinowych\nstatystyki: na szablony typu chart- ('statistics.flexCharts.template.hourly' itp.) dla pełnej personalizacji ECharts, w tym funkcji\nstatystyki: Posiadacze danych ('% %s olarYeld%%', '%% gridExport%%' itd.) pozwalają na pełną kontrolę układu wykresu za pomocą szablonów stanów\nstatystyki: stany wyjściowe wykresu ('statistics.flexCharts.jsonOutput.hourly' itp.) aktualizowane automatycznie co godzinę",
|
|
42
|
-
"uk": "статистика: інтеграція флекса — вбудована конфігурація Apache ECharts з підтримкою діаграми бару та лінії\nстатистика: візуалізація денного розриву з чергуванням затінених зон на часових графіках\nстатистика: за шаблони діаграм типу (`statistics.flexCharts.template.hrly` і т.д.) для повної настройки ECharts, включаючи функції\nстатистика: Держателі (`%solarYield%%`, `%gridExport%%` і т.д.) дозволяють повністю контролювати макети за допомогою шаблонних станів\nстатистика: діаграми вихідних станів (`statistics.flexCharts.jsonOutput.hrly` і т.д.) оновлено автоматично за кожну годину",
|
|
43
|
-
"zh-cn": "统计:弹性图集成——内置的Apache ECharts配置,并有栏和行图支持\n统计:日间可视化,小时图表可交替显示阴影区域\n统计:每个图表类型的模板(`Statistics.flex Charts.template.hourly'等),用于包括功能在内的全部ECharts定制\n统计:数据占位符(`%%solarYield%%`,`%%gridExport%%`等)允许通过模板状态进行完整的图表布局控制\n统计:图表输出状态(`STATistics.flexCHarts.jsonOutput.hourly'等)"
|
|
44
|
-
},
|
|
45
58
|
"2.4.0": {
|
|
46
59
|
"en": "fix: the order of bit assignment corrected of alarmsJSON\nnew state `inverter.x.emma.activeAlarmSN` and `inverter.x.emma.HistoricalAlarmSN` : emma alarms [#226](https://github.com/bolliy/ioBroker.sun2000/issues/226)\nstatistics: Aggregates historical collected datapoints into time-based summaries (e.g. hourly, daily, monthly, yearly). The data is stored in the path `statistics` as JSON.",
|
|
47
60
|
"de": "fix: die Reihenfolge der Bitzuweisung korrigiert von Alarmen JSON\nneuer Zustand `inverter.x.emma.activeAlarmSN` und `inverter.x.emma.HistoricalAlarmSN`: Emma alarms [#226](https://github.com/bolliy/ioBroker.sun2000/issues/226)\nstatistik: Aggregate historische gesammelte Datenpunkte in zeitbasierte Zusammenfassungen (z.B. stündlich, täglich, monatlich, jährlich). Die Daten werden im Pfad `statistics` als JSON gespeichert.",
|
|
@@ -80,19 +93,6 @@
|
|
|
80
93
|
"pl": "aktualizacje zależności i konfiguracji\nnowość \"inverter.x.derived.alarmsJSON\": tablica json z alarmami intverter (id, name, level) [# 226] (https: / / github.com / bolliy / ioBroker.sun2000 / issues / 226)\ndata umieszczenia w wykazie\ndodać minimalną i maksymalną temperaturę dla baterii [# 236] (https: / / github.com / bolliy / ioBroker.sun2000 / issues / 236)",
|
|
81
94
|
"uk": "оновлення залежності та конфігурації\nновий стан `inverter.x.derived.alarmsJSON` : json array with intverter тривоги (id, назва, рівень) [#226](https://github.com/bolliy/ioBroker.sun2000/products/226)\nadd ChargeDischargePower для батарей [#234](https://github.com/bolliy/ioBroker.sun2000/products/234)\nдодайте мінімальну і максимальну температуру для акумуляторних пакетів [#236](https://github.com/bolliy/ioBroker.sun2000/products/236)",
|
|
82
95
|
"zh-cn": "依赖和配置更新\nnew state `inverter.x. inducted.alarmsJSON ' : json 数组带内置式警报器(id, name, level) [# 226] (https://github.com/bolliy/ioBroker.sun2000/issues/226) (中文(简体) )\n添加电池单位充电放电器[#234](https://github.com/bolliy/ioBroker.sun2000/issues/234)\n增加电池包的最低和最高温度[第236号](https://github.com/bolliy/ioBroker.sun2000/issues/236)"
|
|
83
|
-
},
|
|
84
|
-
"2.3.5": {
|
|
85
|
-
"en": "dependency and configuration updates\nBattery status check was suspended in inverter control [#220](https://github.com/bolliy/ioBroker.sun2000/issues/220)\nEmma: dynamic detection of sun2000 inverters and integration of devices such as sun2000\nallow Modbus ID 0 when using the sDongle [#218](https://github.com/bolliy/ioBroker.sun2000/issues/118)",
|
|
86
|
-
"de": "abhängigkeits- und konfigurationsupdates\nDie Batteriestatusprüfung wurde in der Wechselrichtersteuerung [#220](https://github.com/bolliy/ioBroker.sun2000/issues/220) ausgesetzt\nEmma: dynamische Erkennung von Wechselrichtern von Sun2000 und Integration von Geräten wie sun2000\nerlauben Sie Modbus ID 0 bei Verwendung des sDongle [#218](https://github.com/bolliy/ioBroker.sun2000/issues/118)",
|
|
87
|
-
"ru": "обновления зависимостей и конфигурации\nПроверка состояния батареи была приостановлена в инверторном управлении [#220] (https://github.com/bolliy/ioBroker.sun2000/issues/220)\nЭмма: динамическое обнаружение инверторов Sun2000 и интеграция таких устройств, как Sun2000\nразрешить Modbus ID 0 при использовании sDongle [#218] (https://github.com/bolliy/ioBroker.sun2000/issues/118)",
|
|
88
|
-
"pt": "atualizações de dependência e configuração\nA verificação do estado da bateria foi suspensa no controle do inversor [#220](https://github.com/bolliy/ioBroker.sun2000/issues/220)\nEmma: detecção dinâmica de inversores sun2000 e integração de dispositivos como sun2000\npermitir Modbus ID 0 ao usar o sDongle [#218](https://github.com/bolliy/ioBroker.sun2000/issues/118)",
|
|
89
|
-
"nl": "afhankelijkheid en configuratie-updates\nDe controle van de status van de batterij werd opgeschort bij controle van de inverter [#220](https://github.com/bolliy/ioBroker.sun2000/issues/220)\nEmma: dynamische detectie van zonne2000 omvormers en integratie van apparaten zoals sun2000\nmodbus ID 0 toestaan bij gebruik van sDongle [#218](https://github.com/bolliy/ioBroker.sun2000/issues/118)",
|
|
90
|
-
"fr": "mises à jour de la dépendance et de la configuration\nLe contrôle de l'état de la batterie a été suspendu dans le contrôle de l'onduleur [#220](https://github.com/bolliy/ioBroker.sun2000/issues/220)\nEmma : détection dynamique des onduleurs Sun2000 et intégration de dispositifs tels que Sun2000\npermettre Modbus ID 0 lors de l'utilisation de sDongle [#218](https://github.com/bolliy/ioBroker.sun2000/issues/118)",
|
|
91
|
-
"it": "aggiornamenti di dipendenza e configurazione\nIl controllo dello stato della batteria è stato sospeso nel controllo dell'inverter [#220](https://github.com/bolliy/ioBroker.sun2000/issues/220)\nEmma: rilevazione dinamica degli inverter Sun2000 e integrazione di dispositivi come sun2000\nconsentire Modbus ID 0 quando si utilizza lo sDongle [#218](https://github.com/bolliy/ioBroker.sun2000/issues/118)",
|
|
92
|
-
"es": "actualizaciones de dependencia y configuración\nSe suspendió el control del estado de la batería [#220](https://github.com/bolliy/ioBroker.sun2000/issues/220)\nEmma: detección dinámica de inversores de sol2000 e integración de dispositivos como el sol2000\npermitir Modbus ID 0 al utilizar el sDongle [#218](https://github.com/bolliy/ioBroker.sun2000/issues/118)",
|
|
93
|
-
"pl": "aktualizacje zależności i konfiguracji\nKontrola stanu baterii została zawieszona w kontrolach inwertera [# 220] (https: / / github.com / bolliy / ioBroker.sun2000 / issues / 220)\nEmma: dynamiczna detekcja inwerterów sun2000 i integracja urządzeń takich jak sun2000\nw przypadku stosowania sDongle [# 218] (https: / / github.com / bolliy / ioBroker.sun2000 / issues / 118)",
|
|
94
|
-
"uk": "оновлення залежності та конфігурації\nПеревірка стану акумулятора була припинена в інверторному режимі [#220] (https://github.com/bolliy/ioBroker.sun2000/issues/220)\nЕмма: динамічне виявлення інверторів сонця2000 та інтеграції пристроїв, таких як сонце2000\nдозволити Modbus ID 0 при використанні sDongle [#218](https://github.com/bolliy/ioBroker.sun2000/issues/118)",
|
|
95
|
-
"zh-cn": "依赖和配置更新\n在反转控制中暂停了电池状态检查[#220](https://github.com/bolliy/ioBroker.sun2000/issues/220)\n艾玛:对太阳2000反转器的动态检测和太阳2000等设备的集成\n允许在使用 sDongle [# 218] 时使用 Modbus ID (https:// github.com/ bolliy/ioBroker.sun2000/issues/118) "
|
|
96
96
|
}
|
|
97
97
|
},
|
|
98
98
|
"titleLang": {
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Serializes an object to JSON, converting functions to their string representation.
|
|
5
|
+
*
|
|
6
|
+
* @param {any} obj - The object to serialize.
|
|
7
|
+
* @returns {string} JSON string where functions are represented as strings.
|
|
8
|
+
*/
|
|
9
|
+
function stringifyWithFunctions(obj) {
|
|
10
|
+
return JSON.stringify(
|
|
11
|
+
obj,
|
|
12
|
+
(key, value) => {
|
|
13
|
+
if (typeof value === 'function') {
|
|
14
|
+
return value.toString();
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
if (value === undefined) {
|
|
18
|
+
return null;
|
|
19
|
+
}
|
|
20
|
+
return value;
|
|
21
|
+
},
|
|
22
|
+
2,
|
|
23
|
+
);
|
|
24
|
+
}
|
|
25
|
+
/*
|
|
26
|
+
const FUNCTION_KEYS = ['formatter', 'render', 'parser'];
|
|
27
|
+
|
|
28
|
+
function reviveFunctions(obj, key = '') {
|
|
29
|
+
if (typeof obj === 'string' && FUNCTION_KEYS.includes(key)) {
|
|
30
|
+
const str = obj.trim();
|
|
31
|
+
|
|
32
|
+
try {
|
|
33
|
+
return new Function(`return (${str})`)();
|
|
34
|
+
} catch {
|
|
35
|
+
return obj;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
if (typeof obj === 'object' && obj !== null) {
|
|
40
|
+
for (const k in obj) {
|
|
41
|
+
obj[k] = reviveFunctions(obj[k], k);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
return obj;
|
|
46
|
+
}
|
|
47
|
+
*/
|
|
48
|
+
const FUNCTION_KEYS = ['formatter', 'parser', 'render', 'labelFormatter', 'valueFormatter', 'position'];
|
|
49
|
+
|
|
50
|
+
function reviveFunctions(obj, key = '') {
|
|
51
|
+
if (typeof obj === 'string' && FUNCTION_KEYS.includes(key)) {
|
|
52
|
+
const str = obj.trim();
|
|
53
|
+
|
|
54
|
+
// Arrow Function erkennen
|
|
55
|
+
if (str.includes('=>')) {
|
|
56
|
+
try {
|
|
57
|
+
return new Function(`return (${str})`)();
|
|
58
|
+
} catch {
|
|
59
|
+
return obj;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// klassische function() {} erkennen
|
|
64
|
+
if (str.startsWith('function')) {
|
|
65
|
+
try {
|
|
66
|
+
return new Function(`return (${str})`)();
|
|
67
|
+
} catch {
|
|
68
|
+
return obj;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
if (typeof obj === 'object' && obj !== null) {
|
|
74
|
+
for (const key in obj) {
|
|
75
|
+
obj[key] = reviveFunctions(obj[key], key);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
return obj;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Deeply merges source object properties into the target object.
|
|
84
|
+
*
|
|
85
|
+
* @param {object} target - The target object to merge into.
|
|
86
|
+
* @param {object} source - The source object whose properties are merged.
|
|
87
|
+
* @returns {object} The merged target object.
|
|
88
|
+
*/
|
|
89
|
+
function deepMerge(target, source) {
|
|
90
|
+
for (const key of Object.keys(source)) {
|
|
91
|
+
if (
|
|
92
|
+
source[key] !== null &&
|
|
93
|
+
typeof source[key] === 'object' &&
|
|
94
|
+
!Array.isArray(source[key]) &&
|
|
95
|
+
target[key] !== null &&
|
|
96
|
+
typeof target[key] === 'object' &&
|
|
97
|
+
!Array.isArray(target[key])
|
|
98
|
+
) {
|
|
99
|
+
deepMerge(target[key], source[key]);
|
|
100
|
+
} else {
|
|
101
|
+
target[key] = source[key];
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
return target;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Sets a value on an object using a dot‑separated path, creating intermediate objects as needed.
|
|
109
|
+
*
|
|
110
|
+
* @param {object} obj - The object to modify.
|
|
111
|
+
* @param {string} path - Dot‑separated path (e.g., "a.b.c").
|
|
112
|
+
* @param {any} value - The value to set at the specified path.
|
|
113
|
+
*/
|
|
114
|
+
function setByPath(obj, path, value) {
|
|
115
|
+
const keys = path.split('.');
|
|
116
|
+
let current = obj;
|
|
117
|
+
|
|
118
|
+
while (keys.length > 1) {
|
|
119
|
+
const key = keys.shift();
|
|
120
|
+
|
|
121
|
+
if (!(key in current)) {
|
|
122
|
+
current[key] = {};
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
current = current[key];
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
current[keys[0]] = value;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* Applies a set of overrides to a target object based on dot‑separated paths.
|
|
133
|
+
*
|
|
134
|
+
* @param {object} target - The target object to which overrides are applied.
|
|
135
|
+
* @param {object} overrides - An object whose keys are dot‑separated paths and values are the overrides.
|
|
136
|
+
*/
|
|
137
|
+
function applyOverrides(target, overrides) {
|
|
138
|
+
for (const path in overrides) {
|
|
139
|
+
setByPath(target, path, overrides[path]);
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
module.exports = {
|
|
144
|
+
stringifyWithFunctions,
|
|
145
|
+
reviveFunctions,
|
|
146
|
+
deepMerge,
|
|
147
|
+
applyOverrides,
|
|
148
|
+
};
|
|
@@ -147,10 +147,14 @@ class ModbusServer {
|
|
|
147
147
|
return new Promise(resolve => {
|
|
148
148
|
this._isConnected = false;
|
|
149
149
|
if (this.serverTCP) {
|
|
150
|
-
|
|
151
|
-
this.
|
|
152
|
-
|
|
153
|
-
|
|
150
|
+
try {
|
|
151
|
+
this.serverTCP.close(() => {
|
|
152
|
+
this.log.info('Modbus-proxy closed');
|
|
153
|
+
resolve({});
|
|
154
|
+
});
|
|
155
|
+
} catch (err) {
|
|
156
|
+
this.log.error(`Modbus-proxy couldnt close ${err?.message}`);
|
|
157
|
+
}
|
|
154
158
|
} else {
|
|
155
159
|
resolve({});
|
|
156
160
|
}
|
package/lib/statistics.js
CHANGED
|
@@ -18,6 +18,7 @@ in ioBroker VIS using the ioBroker.flexcharts adapter.
|
|
|
18
18
|
const stringify = require('javascript-stringify').stringify;
|
|
19
19
|
const { dataRefreshRate, statisticsType } = require(`${__dirname}/types.js`);
|
|
20
20
|
const tools = require(`${__dirname}/tools.js`);
|
|
21
|
+
const { stringifyWithFunctions, reviveFunctions } = require(`${__dirname}/json_helper.js`);
|
|
21
22
|
|
|
22
23
|
class statistics {
|
|
23
24
|
constructor(adapterInstance, stateCache) {
|
|
@@ -511,109 +512,6 @@ class statistics {
|
|
|
511
512
|
this.adapter.logger.warn(`Error during ${periodType} aggregation: ${err.message}`);
|
|
512
513
|
}
|
|
513
514
|
}
|
|
514
|
-
// eslint-disable-next-line jsdoc/require-returns-check
|
|
515
|
-
/**
|
|
516
|
-
* Calculates and aggregates consumption statistics based on the given parameters.
|
|
517
|
-
*
|
|
518
|
-
* @param {string} sourceStateId
|
|
519
|
-
* @param {string} targetStateId
|
|
520
|
-
* @param {Function} getWindow
|
|
521
|
-
* @param {string} periodType
|
|
522
|
-
* @returns {boolean} true if a new entry was appended, false otherwise.
|
|
523
|
-
*/
|
|
524
|
-
_calculateAggregation_old(sourceStateId, targetStateId, getWindow, periodType) {
|
|
525
|
-
try {
|
|
526
|
-
const now = new Date();
|
|
527
|
-
const window = getWindow(now);
|
|
528
|
-
const fromDate = window.from;
|
|
529
|
-
const toDate = window.to;
|
|
530
|
-
if (now < toDate) {
|
|
531
|
-
this.adapter.logger.debug(`statistics.js: Skipping ${periodType} aggregation because current time is before end of aggregation window`);
|
|
532
|
-
return false;
|
|
533
|
-
}
|
|
534
|
-
const toStr = this._localIsoWithOffset(toDate);
|
|
535
|
-
//const fromStr = this._localIsoWithOffset(fromDate);
|
|
536
|
-
|
|
537
|
-
let jsonTarget = this.stateCache.get(targetStateId)?.value ?? '[]';
|
|
538
|
-
let targetArray = [];
|
|
539
|
-
try {
|
|
540
|
-
targetArray = JSON.parse(jsonTarget);
|
|
541
|
-
if (!Array.isArray(targetArray)) targetArray = [];
|
|
542
|
-
} catch {
|
|
543
|
-
targetArray = [];
|
|
544
|
-
}
|
|
545
|
-
|
|
546
|
-
const last = targetArray.length > 0 ? targetArray[targetArray.length - 1] : {};
|
|
547
|
-
|
|
548
|
-
if (last.to === toStr) return false;
|
|
549
|
-
|
|
550
|
-
const target = {
|
|
551
|
-
from: this._localIsoWithOffset(fromDate),
|
|
552
|
-
to: toStr,
|
|
553
|
-
};
|
|
554
|
-
|
|
555
|
-
let jsonStr = this.stateCache.get(sourceStateId)?.value ?? '[]';
|
|
556
|
-
let sourceEntries = [];
|
|
557
|
-
try {
|
|
558
|
-
sourceEntries = JSON.parse(jsonStr);
|
|
559
|
-
if (!Array.isArray(sourceEntries)) sourceEntries = [];
|
|
560
|
-
} catch {
|
|
561
|
-
sourceEntries = [];
|
|
562
|
-
}
|
|
563
|
-
|
|
564
|
-
sourceEntries = sourceEntries.filter(item => {
|
|
565
|
-
const ts = Date.parse(item.from);
|
|
566
|
-
return !Number.isNaN(ts) && ts >= fromDate.getTime() && ts < toDate.getTime();
|
|
567
|
-
});
|
|
568
|
-
|
|
569
|
-
if (sourceEntries.length > 0) {
|
|
570
|
-
this.adapter.logger.debug(
|
|
571
|
-
`statistics.js: Found ${sourceEntries.length} source entries for ${periodType} aggregation between ${fromDate.toISOString()} and ${toDate.toISOString()}`,
|
|
572
|
-
);
|
|
573
|
-
|
|
574
|
-
// First pass: sum delta/deltaReset stats
|
|
575
|
-
for (const stat of this.stats) {
|
|
576
|
-
if (stat.type === statisticsType.level) continue;
|
|
577
|
-
if (stat.type === statisticsType.computed) continue;
|
|
578
|
-
|
|
579
|
-
let sum = 0;
|
|
580
|
-
try {
|
|
581
|
-
sourceEntries.forEach(entry => {
|
|
582
|
-
sum += Number(entry[stat.targetPath]?.['value'] ?? 0);
|
|
583
|
-
});
|
|
584
|
-
} catch (e) {
|
|
585
|
-
this.adapter.logger.warn(`statistics.js: Error during ${periodType} statistic aggregation: ${e.message}`);
|
|
586
|
-
}
|
|
587
|
-
sum = Math.round((Number(sum) + Number.EPSILON) * 1000) / 1000;
|
|
588
|
-
target[stat.targetPath] = { value: Number(sum.toFixed(3)), unit: stat.unit || 'kWh' };
|
|
589
|
-
}
|
|
590
|
-
|
|
591
|
-
// Second pass: compute derived stats from aggregated values
|
|
592
|
-
for (const stat of this.stats) {
|
|
593
|
-
if (stat.type !== statisticsType.computed) continue;
|
|
594
|
-
try {
|
|
595
|
-
const value = stat.compute(target);
|
|
596
|
-
target[stat.targetPath] = {
|
|
597
|
-
value: Number(Number(value).toFixed(3)),
|
|
598
|
-
unit: stat.unit || '%',
|
|
599
|
-
};
|
|
600
|
-
} catch (e) {
|
|
601
|
-
this.adapter.logger.warn(`statistics: error computing aggregated ${stat.targetPath}: ${e.message}`);
|
|
602
|
-
target[stat.targetPath] = { value: 0, unit: stat.unit || '%' };
|
|
603
|
-
}
|
|
604
|
-
}
|
|
605
|
-
|
|
606
|
-
targetArray.push(target);
|
|
607
|
-
}
|
|
608
|
-
|
|
609
|
-
targetArray.sort((a, b) => Date.parse(a.to) - Date.parse(b.to));
|
|
610
|
-
this.stateCache.set(targetStateId, JSON.stringify(targetArray), { type: 'string' });
|
|
611
|
-
this.adapter.logger.debug(`Appended ${periodType} statistic ${toStr}`);
|
|
612
|
-
return targetArray.length > 0;
|
|
613
|
-
} catch (err) {
|
|
614
|
-
this.adapter.logger.warn(`Error during ${periodType} aggregation: ${err.message}`);
|
|
615
|
-
}
|
|
616
|
-
}
|
|
617
515
|
|
|
618
516
|
/**
|
|
619
517
|
* Updates the statistics.jsonToday state with the current live day values.
|
|
@@ -741,10 +639,12 @@ class statistics {
|
|
|
741
639
|
monday.setDate(now.getDate() - (now.getDay() || 7) + 1);
|
|
742
640
|
const prevMonday = new Date(monday);
|
|
743
641
|
prevMonday.setDate(monday.getDate() - 7);
|
|
642
|
+
/*
|
|
744
643
|
const nextMonday = new Date(monday);
|
|
745
644
|
nextMonday.setDate(monday.getDate() + 7);
|
|
645
|
+
*/
|
|
746
646
|
// Window: previous Monday → next Monday (covers both last and current week)
|
|
747
|
-
return { from: prevMonday, to:
|
|
647
|
+
return { from: prevMonday, to: monday };
|
|
748
648
|
},
|
|
749
649
|
'weekly',
|
|
750
650
|
);
|
|
@@ -895,8 +795,13 @@ class statistics {
|
|
|
895
795
|
|
|
896
796
|
state = await this.adapter.getState('statistics.jsonToday');
|
|
897
797
|
this.stateCache.set('statistics.jsonToday', state?.val ?? '{}', { type: 'string', stored: true });
|
|
898
|
-
|
|
899
|
-
|
|
798
|
+
try {
|
|
799
|
+
await tools.waitForValue(() => this.stateCache.get('collected.accumulatedEnergyYield')?.value, 5 * 60000);
|
|
800
|
+
} catch {
|
|
801
|
+
this.adapter.logger.warn(
|
|
802
|
+
"statistics: waited 5 minutes for state 'collected.accumulatedEnergyYield' to be available but it didn't, computed statistics will not work until this state is present",
|
|
803
|
+
);
|
|
804
|
+
}
|
|
900
805
|
|
|
901
806
|
// Load templates — one per chart type
|
|
902
807
|
for (const chartType of ['hourly', 'daily', 'weekly', 'monthly', 'annual']) {
|
|
@@ -1025,12 +930,23 @@ class statistics {
|
|
|
1025
930
|
seriesData[`${stat.targetPath}Neg`] = negate(values);
|
|
1026
931
|
}
|
|
1027
932
|
|
|
933
|
+
// --- X-Axis label formatter (hourly only) ---
|
|
934
|
+
const xAxisFormatter = value => {
|
|
935
|
+
if (value.includes('|')) return value;
|
|
936
|
+
return value.split(' ')[1] ?? value;
|
|
937
|
+
};
|
|
938
|
+
|
|
1028
939
|
// --- Tooltip formatter ---
|
|
1029
940
|
const tooltipFormatter = params => {
|
|
1030
941
|
if (!Array.isArray(params)) params = [params];
|
|
1031
942
|
return params
|
|
1032
943
|
.filter(p => p.seriesName !== 'DayBreak')
|
|
1033
944
|
.map(p => {
|
|
945
|
+
if (typeof p.value === 'object') {
|
|
946
|
+
const val = p.value.value;
|
|
947
|
+
const unit = p.value.unit || 'kWh';
|
|
948
|
+
return `${p.marker}${p.seriesName}: <b>${val}${unit}</b>`;
|
|
949
|
+
}
|
|
1034
950
|
const negatedSeries = ['Grid Export', 'Charge'];
|
|
1035
951
|
const val = negatedSeries.includes(p.seriesName) ? Math.abs(p.value) : p.value;
|
|
1036
952
|
const unit = ['SOC', 'Self-sufficiency', 'Self-consumption'].includes(p.seriesName) ? ' %' : ' kWh';
|
|
@@ -1046,19 +962,93 @@ class statistics {
|
|
|
1046
962
|
const outputStateId = `statistics.flexCharts.jsonOutput.${myChart}`;
|
|
1047
963
|
|
|
1048
964
|
const templateStr = this.stateCache.get(templateStateId)?.value ?? '{}';
|
|
1049
|
-
let
|
|
965
|
+
let template = {}; //empty object as fallback if parsing fails or template is empty
|
|
966
|
+
let command = '';
|
|
1050
967
|
|
|
1051
968
|
try {
|
|
1052
969
|
const templ = JSON.parse(templateStr);
|
|
1053
|
-
|
|
1054
|
-
|
|
970
|
+
command = templ.command || '';
|
|
971
|
+
if (Object.keys(templ).length === 0 || command === 'createTemplateFromBuiltin') {
|
|
972
|
+
template = this._buildDefaultTemplate(myChart, chartStyle);
|
|
1055
973
|
} else {
|
|
1056
|
-
|
|
974
|
+
delete templ._meta;
|
|
975
|
+
template = reviveFunctions(templ); // Functions in templates are stored as strings and need to be revived before usage
|
|
1057
976
|
}
|
|
1058
977
|
} catch (e) {
|
|
1059
978
|
this.adapter.logger.warn(`statistics: invalid template for ${myChart}: ${e.message}`);
|
|
1060
|
-
|
|
979
|
+
template = this._buildDefaultTemplate(myChart, chartStyle);
|
|
1061
980
|
}
|
|
981
|
+
// No-data hint — chart-type specific
|
|
982
|
+
const noDataHints = {
|
|
983
|
+
hourly: 'No data yet — first entry available after the next full hour.',
|
|
984
|
+
daily: 'No data yet — first entry available tomorrow after midnight.',
|
|
985
|
+
weekly: 'No data yet — first entry available after the current week ends.',
|
|
986
|
+
monthly: 'No data yet — first entry available after the current month ends.',
|
|
987
|
+
annual: 'No data yet — first entry available after the current year ends.',
|
|
988
|
+
};
|
|
989
|
+
|
|
990
|
+
const chart = {
|
|
991
|
+
...template,
|
|
992
|
+
series: [
|
|
993
|
+
...(template.series ?? []),
|
|
994
|
+
// Day-break areas (hourly only)
|
|
995
|
+
...(dayAreas.length > 0
|
|
996
|
+
? [
|
|
997
|
+
{
|
|
998
|
+
name: 'DayBreak',
|
|
999
|
+
type: 'bar',
|
|
1000
|
+
barWidth: 0,
|
|
1001
|
+
data: [],
|
|
1002
|
+
legendHoverLink: false,
|
|
1003
|
+
silent: true,
|
|
1004
|
+
markArea: {
|
|
1005
|
+
silent: true,
|
|
1006
|
+
data: dayAreas,
|
|
1007
|
+
},
|
|
1008
|
+
},
|
|
1009
|
+
]
|
|
1010
|
+
: []),
|
|
1011
|
+
],
|
|
1012
|
+
// No-data graphic — shown only when data is empty
|
|
1013
|
+
graphic:
|
|
1014
|
+
xAxisData.length === 0
|
|
1015
|
+
? [
|
|
1016
|
+
{
|
|
1017
|
+
type: 'text',
|
|
1018
|
+
left: 'center',
|
|
1019
|
+
top: 'middle',
|
|
1020
|
+
style: {
|
|
1021
|
+
text: noDataHints[myChart] || 'No data available yet.',
|
|
1022
|
+
fontSize: 14,
|
|
1023
|
+
fill: '#999',
|
|
1024
|
+
},
|
|
1025
|
+
},
|
|
1026
|
+
]
|
|
1027
|
+
: [],
|
|
1028
|
+
};
|
|
1029
|
+
|
|
1030
|
+
// --- Slider start position — show recent entries by default ---
|
|
1031
|
+
// For hourly: show last 25 entries (~1 day)
|
|
1032
|
+
// For daily: show last 7 entries (~1 week)
|
|
1033
|
+
// For weekly: show last 8 entries (~2 months)
|
|
1034
|
+
// For monthly: show last 13 entries (~1 year)
|
|
1035
|
+
// For annual: show full range
|
|
1036
|
+
const sliderDefaults = {
|
|
1037
|
+
hourly: { start: Math.max(0, Math.round((1 - 25 / Math.max(xAxisData.length, 1)) * 100)), end: 100 },
|
|
1038
|
+
daily: { start: Math.max(0, Math.round((1 - 7 / Math.max(xAxisData.length, 1)) * 100)), end: 100 },
|
|
1039
|
+
weekly: { start: Math.max(0, Math.round((1 - 8 / Math.max(xAxisData.length, 1)) * 100)), end: 100 },
|
|
1040
|
+
monthly: { start: Math.max(0, Math.round((1 - 13 / Math.max(xAxisData.length, 1)) * 100)), end: 100 },
|
|
1041
|
+
annual: { start: 0, end: 100 },
|
|
1042
|
+
};
|
|
1043
|
+
const slider = sliderDefaults[myChart] ?? { start: 0, end: 100 };
|
|
1044
|
+
|
|
1045
|
+
// If template has a dataZoom component without start/end, apply defaults
|
|
1046
|
+
if (chart?.dataZoom?.[0] && !chart.dataZoom[0].start && !chart.dataZoom[0].end) {
|
|
1047
|
+
chart.dataZoom[0].start = slider.start;
|
|
1048
|
+
chart.dataZoom[0].end = slider.end;
|
|
1049
|
+
}
|
|
1050
|
+
|
|
1051
|
+
let chartStr = stringify(chart);
|
|
1062
1052
|
|
|
1063
1053
|
// --- Replace placeholders ---
|
|
1064
1054
|
chartStr = chartStr
|
|
@@ -1067,6 +1057,7 @@ class statistics {
|
|
|
1067
1057
|
.replace("'%%xAxisMax%%'", String(xAxisData.length - 1))
|
|
1068
1058
|
.replace("'%%chartTitle%%'", JSON.stringify(`PV Statistics — ${myChart}`))
|
|
1069
1059
|
.replace("'%%dayAreas%%'", JSON.stringify(dayAreas))
|
|
1060
|
+
.replace("'%%xAxisFormatter%%'", stringify(xAxisFormatter))
|
|
1070
1061
|
.replace("'%%tooltipFormatter%%'", stringify(tooltipFormatter));
|
|
1071
1062
|
|
|
1072
1063
|
// All this.stats entries dynamically — both positive and negated
|
|
@@ -1077,7 +1068,17 @@ class statistics {
|
|
|
1077
1068
|
|
|
1078
1069
|
this.stateCache.set(outputStateId, chartStr, { type: 'string' });
|
|
1079
1070
|
this.adapter.logger.debug(`statistics: flexCharts built for ${myChart}/${chartStyle}`);
|
|
1080
|
-
|
|
1071
|
+
if (command === 'createTemplateFromBuiltin') {
|
|
1072
|
+
this.adapter.logger.debug(`statistics: created new template for ${myChart} based on built-in`);
|
|
1073
|
+
template = {
|
|
1074
|
+
_meta: {
|
|
1075
|
+
generatedFrom: 'builtin',
|
|
1076
|
+
generatedAt: new Date().toISOString(),
|
|
1077
|
+
},
|
|
1078
|
+
...template,
|
|
1079
|
+
};
|
|
1080
|
+
this.stateCache.set(templateStateId, stringifyWithFunctions(template), { type: 'string' }); // Store the final chart configuration back in the template state for persistence and debugging
|
|
1081
|
+
}
|
|
1081
1082
|
return chartStr;
|
|
1082
1083
|
}
|
|
1083
1084
|
|
|
@@ -1086,69 +1087,20 @@ class statistics {
|
|
|
1086
1087
|
* Used when no template is provided.
|
|
1087
1088
|
* @param myChart
|
|
1088
1089
|
* @param chartStyle
|
|
1089
|
-
* @param xAxisData
|
|
1090
|
-
* @param xAxisDataShort
|
|
1091
|
-
* @param dayAreas
|
|
1092
|
-
* @param seriesData
|
|
1093
1090
|
*/
|
|
1094
|
-
|
|
1095
|
-
const xAxisFormatterHourly = value => {
|
|
1096
|
-
if (value.includes('|')) return value;
|
|
1097
|
-
return value.split(' ')[1] ?? value;
|
|
1098
|
-
};
|
|
1099
|
-
|
|
1091
|
+
_buildDefaultTemplate(myChart, chartStyle) {
|
|
1100
1092
|
const seriesType = chartStyle === 'line' ? 'line' : 'bar';
|
|
1101
1093
|
const lineOptions =
|
|
1102
1094
|
chartStyle === 'line' ? { smooth: true, symbol: 'circle', symbolSize: 4, lineStyle: { width: 2 }, areaStyle: { opacity: 0.15 } } : {};
|
|
1103
1095
|
|
|
1104
|
-
const negate = arr => arr.map(v => Number((-v).toFixed(3)));
|
|
1105
1096
|
const showSOC = myChart === 'hourly';
|
|
1106
1097
|
|
|
1107
|
-
|
|
1108
|
-
const noDataHints = {
|
|
1109
|
-
hourly: 'No data yet — first entry available after the next full hour.',
|
|
1110
|
-
daily: 'No data yet — first entry available tomorrow after midnight.',
|
|
1111
|
-
weekly: 'No data yet — first entry available after the current week ends.',
|
|
1112
|
-
monthly: 'No data yet — first entry available after the current month ends.',
|
|
1113
|
-
annual: 'No data yet — first entry available after the current year ends.',
|
|
1114
|
-
};
|
|
1115
|
-
|
|
1116
|
-
const tooltipFormatter = params => {
|
|
1117
|
-
if (!Array.isArray(params)) params = [params];
|
|
1118
|
-
return params
|
|
1119
|
-
.filter(p => p.seriesName !== 'DayBreak')
|
|
1120
|
-
.map(p => {
|
|
1121
|
-
const negatedSeries = ['Grid Export', 'Charge'];
|
|
1122
|
-
const val = negatedSeries.includes(p.seriesName) ? Math.abs(p.value) : p.value;
|
|
1123
|
-
const unit = ['SOC', 'Self-sufficiency', 'Self-consumption'].includes(p.seriesName) ? ' %' : ' kWh';
|
|
1124
|
-
const seriesName =
|
|
1125
|
-
myChart === 'hourly' && ['Self-sufficiency', 'Self-consumption'].includes(p.seriesName) ? `${p.seriesName} today` : p.seriesName;
|
|
1126
|
-
return `${p.marker}${seriesName}: <b>${val}${unit}</b>`;
|
|
1127
|
-
})
|
|
1128
|
-
.join('<br/>');
|
|
1129
|
-
};
|
|
1130
|
-
|
|
1131
|
-
// --- Slider start position — show recent entries by default ---
|
|
1132
|
-
// For hourly: show last 25 entries (~1 day)
|
|
1133
|
-
// For daily: show last 7 entries (~1 week)
|
|
1134
|
-
// For weekly: show last 8 entries (~2 months)
|
|
1135
|
-
// For monthly: show last 13 entries (~1 year)
|
|
1136
|
-
// For annual: show full range
|
|
1137
|
-
const sliderDefaults = {
|
|
1138
|
-
hourly: { start: Math.max(0, Math.round((1 - 25 / Math.max(xAxisData.length, 1)) * 100)), end: 100 },
|
|
1139
|
-
daily: { start: Math.max(0, Math.round((1 - 7 / Math.max(xAxisData.length, 1)) * 100)), end: 100 },
|
|
1140
|
-
weekly: { start: Math.max(0, Math.round((1 - 8 / Math.max(xAxisData.length, 1)) * 100)), end: 100 },
|
|
1141
|
-
monthly: { start: Math.max(0, Math.round((1 - 13 / Math.max(xAxisData.length, 1)) * 100)), end: 100 },
|
|
1142
|
-
annual: { start: 0, end: 100 },
|
|
1143
|
-
};
|
|
1144
|
-
const slider = sliderDefaults[myChart] ?? { start: 0, end: 100 };
|
|
1145
|
-
|
|
1146
|
-
const chart = {
|
|
1098
|
+
const template = {
|
|
1147
1099
|
backgroundColor: '#fff',
|
|
1148
1100
|
animation: false,
|
|
1149
1101
|
title: {
|
|
1150
1102
|
left: 'center',
|
|
1151
|
-
text:
|
|
1103
|
+
text: '%%chartTitle%%',
|
|
1152
1104
|
},
|
|
1153
1105
|
legend: {
|
|
1154
1106
|
top: 35,
|
|
@@ -1192,22 +1144,6 @@ class statistics {
|
|
|
1192
1144
|
saveAsImage: { show: true },
|
|
1193
1145
|
},
|
|
1194
1146
|
},
|
|
1195
|
-
// No-data graphic — shown only when data is empty
|
|
1196
|
-
graphic:
|
|
1197
|
-
xAxisData.length === 0
|
|
1198
|
-
? [
|
|
1199
|
-
{
|
|
1200
|
-
type: 'text',
|
|
1201
|
-
left: 'center',
|
|
1202
|
-
top: 'middle',
|
|
1203
|
-
style: {
|
|
1204
|
-
text: noDataHints[myChart] || 'No data available yet.',
|
|
1205
|
-
fontSize: 14,
|
|
1206
|
-
fill: '#999',
|
|
1207
|
-
},
|
|
1208
|
-
},
|
|
1209
|
-
]
|
|
1210
|
-
: [],
|
|
1211
1147
|
grid: [
|
|
1212
1148
|
{ left: '8%', right: showSOC ? '8%' : '4%', top: 80, height: '45%' },
|
|
1213
1149
|
{ left: '8%', right: showSOC ? '8%' : '4%', top: '72%', height: '15%' },
|
|
@@ -1215,25 +1151,36 @@ class statistics {
|
|
|
1215
1151
|
xAxis: [
|
|
1216
1152
|
{
|
|
1217
1153
|
type: 'category',
|
|
1218
|
-
data: xAxisDataShort,
|
|
1154
|
+
data: '%%xAxisDataShort%%',
|
|
1219
1155
|
scale: true,
|
|
1220
1156
|
boundaryGap: chartStyle !== 'line',
|
|
1221
1157
|
axisLine: { onZero: false },
|
|
1222
1158
|
splitLine: { show: false },
|
|
1223
1159
|
axisPointer: { z: 100 },
|
|
1224
|
-
min: 0,
|
|
1225
|
-
max: xAxisDataShort.length - 1,
|
|
1226
1160
|
axisLabel: {
|
|
1227
1161
|
interval: 0,
|
|
1228
1162
|
lineHeight: 16,
|
|
1229
1163
|
fontSize: 11,
|
|
1230
1164
|
formatter: '%%xAxisFormatter%%',
|
|
1231
1165
|
},
|
|
1166
|
+
min: 0,
|
|
1167
|
+
max: '%%xAxisMax%%',
|
|
1168
|
+
/*
|
|
1169
|
+
axisTick: { show: false },
|
|
1170
|
+
gridIndex: 0,
|
|
1171
|
+
splitNumber: 5,
|
|
1172
|
+
name: myChart === 'hourly' ? 'Time' : 'Date',
|
|
1173
|
+
nameLocation: 'middle',
|
|
1174
|
+
nameGap: 30,
|
|
1175
|
+
nameTextStyle: { fontSize: 12, fontWeight: 'bold' },
|
|
1176
|
+
splitLine: { show: false },
|
|
1177
|
+
axisLine: { show: true },
|
|
1178
|
+
*/
|
|
1232
1179
|
},
|
|
1233
1180
|
{
|
|
1234
1181
|
type: 'category',
|
|
1235
1182
|
gridIndex: 1,
|
|
1236
|
-
data: xAxisData,
|
|
1183
|
+
data: '%%xAxisData%%',
|
|
1237
1184
|
scale: true,
|
|
1238
1185
|
boundaryGap: chartStyle !== 'line',
|
|
1239
1186
|
axisLine: { onZero: false },
|
|
@@ -1241,7 +1188,7 @@ class statistics {
|
|
|
1241
1188
|
splitLine: { show: false },
|
|
1242
1189
|
axisLabel: { show: false },
|
|
1243
1190
|
min: 0,
|
|
1244
|
-
max:
|
|
1191
|
+
max: '%%xAxisMax%%',
|
|
1245
1192
|
},
|
|
1246
1193
|
],
|
|
1247
1194
|
yAxis: [
|
|
@@ -1286,16 +1233,12 @@ class statistics {
|
|
|
1286
1233
|
{
|
|
1287
1234
|
type: 'inside',
|
|
1288
1235
|
xAxisIndex: [0, 1],
|
|
1289
|
-
start: slider.start,
|
|
1290
|
-
end: slider.end,
|
|
1291
1236
|
},
|
|
1292
1237
|
{
|
|
1293
1238
|
show: true,
|
|
1294
1239
|
xAxisIndex: [0, 1],
|
|
1295
1240
|
type: 'slider',
|
|
1296
1241
|
bottom: 5,
|
|
1297
|
-
start: slider.start,
|
|
1298
|
-
end: slider.end,
|
|
1299
1242
|
},
|
|
1300
1243
|
],
|
|
1301
1244
|
series: [
|
|
@@ -1303,42 +1246,57 @@ class statistics {
|
|
|
1303
1246
|
{
|
|
1304
1247
|
name: 'Solar Yield',
|
|
1305
1248
|
type: seriesType,
|
|
1306
|
-
data:
|
|
1249
|
+
data: '%%solarYield%%',
|
|
1307
1250
|
itemStyle: { color: '#f6c94e' },
|
|
1308
1251
|
emphasis: { focus: 'series' },
|
|
1252
|
+
tooltip: {
|
|
1253
|
+
valueFormatter: value => ({ value: value, unit: 'kWh' }),
|
|
1254
|
+
},
|
|
1309
1255
|
...lineOptions,
|
|
1310
1256
|
},
|
|
1311
1257
|
// Negative values (below zero line)
|
|
1312
1258
|
{
|
|
1313
1259
|
name: 'Grid Export',
|
|
1314
1260
|
type: seriesType,
|
|
1315
|
-
data:
|
|
1261
|
+
data: '%%gridExportNeg%%',
|
|
1316
1262
|
itemStyle: { color: '#5cb85c' },
|
|
1317
1263
|
emphasis: { focus: 'series' },
|
|
1264
|
+
tooltip: {
|
|
1265
|
+
valueFormatter: value => ({ value: -value, unit: 'kWh' }),
|
|
1266
|
+
},
|
|
1318
1267
|
...lineOptions,
|
|
1319
1268
|
},
|
|
1320
1269
|
{
|
|
1321
1270
|
name: 'Grid Import',
|
|
1322
1271
|
type: seriesType,
|
|
1323
|
-
data:
|
|
1272
|
+
data: '%%gridImport%%',
|
|
1324
1273
|
itemStyle: { color: '#ec0000' },
|
|
1325
1274
|
emphasis: { focus: 'series' },
|
|
1275
|
+
tooltip: {
|
|
1276
|
+
valueFormatter: value => ({ value: value, unit: 'kWh' }),
|
|
1277
|
+
},
|
|
1326
1278
|
...lineOptions,
|
|
1327
1279
|
},
|
|
1328
1280
|
{
|
|
1329
1281
|
name: 'Charge',
|
|
1330
1282
|
type: seriesType,
|
|
1331
|
-
data:
|
|
1283
|
+
data: '%%chargeCapacityNeg%%',
|
|
1332
1284
|
itemStyle: { color: '#5bc0de' },
|
|
1333
1285
|
emphasis: { focus: 'series' },
|
|
1286
|
+
tooltip: {
|
|
1287
|
+
valueFormatter: value => ({ value: -value, unit: 'kWh' }),
|
|
1288
|
+
},
|
|
1334
1289
|
...lineOptions,
|
|
1335
1290
|
},
|
|
1336
1291
|
{
|
|
1337
1292
|
name: 'Discharge',
|
|
1338
1293
|
type: seriesType,
|
|
1339
|
-
data:
|
|
1294
|
+
data: '%%dischargeCapacity%%',
|
|
1340
1295
|
itemStyle: { color: '#ed50e0' },
|
|
1341
1296
|
emphasis: { focus: 'series' },
|
|
1297
|
+
tooltip: {
|
|
1298
|
+
valueFormatter: value => ({ value: value, unit: 'kWh' }),
|
|
1299
|
+
},
|
|
1342
1300
|
...lineOptions,
|
|
1343
1301
|
},
|
|
1344
1302
|
// SOC — hourly only, on right axis
|
|
@@ -1348,11 +1306,14 @@ class statistics {
|
|
|
1348
1306
|
name: 'SOC',
|
|
1349
1307
|
type: 'line',
|
|
1350
1308
|
yAxisIndex: 1,
|
|
1351
|
-
data:
|
|
1309
|
+
data: '%%SOC%%',
|
|
1352
1310
|
itemStyle: { color: '#985e24' },
|
|
1353
1311
|
lineStyle: { width: 2, type: 'dashed' },
|
|
1354
1312
|
symbol: 'none',
|
|
1355
1313
|
smooth: true,
|
|
1314
|
+
tooltip: {
|
|
1315
|
+
valueFormatter: value => ({ value: value, unit: '%' }),
|
|
1316
|
+
},
|
|
1356
1317
|
},
|
|
1357
1318
|
]
|
|
1358
1319
|
: []),
|
|
@@ -1361,76 +1322,53 @@ class statistics {
|
|
|
1361
1322
|
name: 'Self-sufficiency',
|
|
1362
1323
|
type: 'line',
|
|
1363
1324
|
yAxisIndex: 1,
|
|
1364
|
-
data:
|
|
1325
|
+
data: '%%selfSufficiency%%',
|
|
1365
1326
|
itemStyle: { color: '#9c27b0' },
|
|
1366
1327
|
lineStyle: { width: 2, type: 'dashed' },
|
|
1367
1328
|
symbol: 'circle',
|
|
1368
1329
|
symbolSize: 4,
|
|
1369
1330
|
smooth: true,
|
|
1331
|
+
tooltip: {
|
|
1332
|
+
valueFormatter: value => ({ value: value, unit: '%' }),
|
|
1333
|
+
},
|
|
1370
1334
|
},
|
|
1371
1335
|
// Self-consumption — right axis
|
|
1372
1336
|
{
|
|
1373
1337
|
name: 'Self-consumption',
|
|
1374
1338
|
type: 'line',
|
|
1375
1339
|
yAxisIndex: 1,
|
|
1376
|
-
data:
|
|
1340
|
+
data: '%%selfConsumption%%',
|
|
1377
1341
|
itemStyle: { color: '#ff9800' },
|
|
1378
1342
|
lineStyle: { width: 2, type: 'dashed' },
|
|
1379
1343
|
symbol: 'circle',
|
|
1380
1344
|
symbolSize: 4,
|
|
1381
1345
|
smooth: true,
|
|
1346
|
+
tooltip: {
|
|
1347
|
+
valueFormatter: value => ({ value: value, unit: '%' }),
|
|
1348
|
+
},
|
|
1382
1349
|
},
|
|
1383
1350
|
// Consumption in lower grid — always yAxisIndex 2
|
|
1384
1351
|
{
|
|
1385
1352
|
name: 'Consumption',
|
|
1386
1353
|
type: seriesType,
|
|
1387
|
-
data:
|
|
1354
|
+
data: '%%consumption%%',
|
|
1388
1355
|
itemStyle: { color: '#337ab7' },
|
|
1389
1356
|
xAxisIndex: 1,
|
|
1390
1357
|
yAxisIndex: 2,
|
|
1358
|
+
/*
|
|
1359
|
+
emphasis: {
|
|
1360
|
+
focus: 'series',
|
|
1361
|
+
},
|
|
1362
|
+
*/
|
|
1363
|
+
tooltip: {
|
|
1364
|
+
valueFormatter: value => ({ value: value, unit: 'kWh' }),
|
|
1365
|
+
},
|
|
1391
1366
|
...lineOptions,
|
|
1392
1367
|
},
|
|
1393
|
-
// Day-break areas (hourly only)
|
|
1394
|
-
...(dayAreas.length > 0
|
|
1395
|
-
? [
|
|
1396
|
-
{
|
|
1397
|
-
name: 'DayBreak',
|
|
1398
|
-
type: 'bar',
|
|
1399
|
-
barWidth: 0,
|
|
1400
|
-
data: [],
|
|
1401
|
-
legendHoverLink: false,
|
|
1402
|
-
silent: true,
|
|
1403
|
-
markArea: { silent: true, data: dayAreas },
|
|
1404
|
-
},
|
|
1405
|
-
]
|
|
1406
|
-
: []),
|
|
1407
1368
|
],
|
|
1408
1369
|
};
|
|
1409
1370
|
|
|
1410
|
-
return
|
|
1411
|
-
}
|
|
1412
|
-
|
|
1413
|
-
/**
|
|
1414
|
-
* Merges two objects deeply.
|
|
1415
|
-
* @param target
|
|
1416
|
-
* @param source
|
|
1417
|
-
*/
|
|
1418
|
-
_deepMerge(target, source) {
|
|
1419
|
-
for (const key of Object.keys(source)) {
|
|
1420
|
-
if (
|
|
1421
|
-
source[key] !== null &&
|
|
1422
|
-
typeof source[key] === 'object' &&
|
|
1423
|
-
!Array.isArray(source[key]) &&
|
|
1424
|
-
target[key] !== null &&
|
|
1425
|
-
typeof target[key] === 'object' &&
|
|
1426
|
-
!Array.isArray(target[key])
|
|
1427
|
-
) {
|
|
1428
|
-
this._deepMerge(target[key], source[key]);
|
|
1429
|
-
} else {
|
|
1430
|
-
target[key] = source[key];
|
|
1431
|
-
}
|
|
1432
|
-
}
|
|
1433
|
-
return target;
|
|
1371
|
+
return template; //as object, not stringified, since we use reviveFunctions to restore the functions after stringification in the state
|
|
1434
1372
|
}
|
|
1435
1373
|
|
|
1436
1374
|
/**
|
|
@@ -1448,8 +1386,10 @@ class statistics {
|
|
|
1448
1386
|
if (state?.val != null) {
|
|
1449
1387
|
this.adapter.logger.debug(`statistics: Event - state: ${chartType} changed: ${state.val} ack: ${state.ack}`);
|
|
1450
1388
|
this.stateCache.set(templateStateId, state.val, { type: 'string', stored: true });
|
|
1451
|
-
await this.adapter.setState(templateStateId, { val: state.val, ack: true });
|
|
1389
|
+
//await this.adapter.setState(templateStateId, { val: state.val, ack: true });
|
|
1452
1390
|
this._buildFlexchart(chartType);
|
|
1391
|
+
await this.adapter.setState(templateStateId, { val: this.stateCache.get(templateStateId)?.value, ack: true });
|
|
1392
|
+
//this.stateCache.set(templateStateId, this.stateCache.get(templateStateId), { type: 'string', renew: true });
|
|
1453
1393
|
}
|
|
1454
1394
|
}
|
|
1455
1395
|
|
package/lib/tools.js
CHANGED
|
@@ -260,15 +260,19 @@ const createAsyncLock = () => {
|
|
|
260
260
|
const waitForValue = (func, timeout = 5000) => {
|
|
261
261
|
return new Promise((resolve, reject) => {
|
|
262
262
|
const timer = setInterval(() => {
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
263
|
+
try {
|
|
264
|
+
const variable = func();
|
|
265
|
+
if (variable !== undefined && variable !== null) {
|
|
266
|
+
clearInterval(timer);
|
|
267
|
+
resolve(variable);
|
|
268
|
+
}
|
|
269
|
+
timeout -= 200;
|
|
270
|
+
if (timeout <= 0) {
|
|
271
|
+
clearInterval(timer);
|
|
272
|
+
reject('waitForValue: Timeout waiting for value');
|
|
273
|
+
}
|
|
274
|
+
} catch (err) {
|
|
275
|
+
this.adapter.logger.warn(`waitForValue ${err?.message}`);
|
|
272
276
|
}
|
|
273
277
|
}, 200); // alle 200ms prüfen
|
|
274
278
|
});
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "iobroker.sun2000",
|
|
3
|
-
"version": "2.4.
|
|
3
|
+
"version": "2.4.5",
|
|
4
4
|
"description": "sun2000",
|
|
5
5
|
"author": {
|
|
6
6
|
"name": "bolliy",
|
|
@@ -42,7 +42,7 @@
|
|
|
42
42
|
"@iobroker/eslint-config": "^2.2.0",
|
|
43
43
|
"@iobroker/testing": "^5.2.2",
|
|
44
44
|
"@tsconfig/node22": "^22.0.5",
|
|
45
|
-
"@types/node": "^25.
|
|
45
|
+
"@types/node": "^25.6.0",
|
|
46
46
|
"globals": "^16.5.0",
|
|
47
47
|
"typescript": "~5.9.3"
|
|
48
48
|
},
|