iobroker.sun2000 2.4.0 → 2.4.2
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 +14 -4
- package/io-package.json +28 -27
- package/lib/register.js +1 -1
- package/lib/statistics.js +599 -217
- package/main.js +8 -0
- package/package.json +5 -4
package/README.md
CHANGED
|
@@ -17,7 +17,6 @@
|
|
|
17
17
|
|
|
18
18
|
**This adapter uses Sentry libraries to automatically report exceptions and code errors to the developers.**\
|
|
19
19
|
For more details and for information how to disable the error reporting see [Sentry-Plugin Documentation](https://github.com/ioBroker/plugin-sentry#plugin-sentry)!\
|
|
20
|
-
Sentry reporting is used starting with js-controller 3.0.
|
|
21
20
|
|
|
22
21
|
## sun2000 adapter for ioBroker
|
|
23
22
|
|
|
@@ -59,9 +58,10 @@ browse in the [wiki](https://github.com/bolliy/ioBroker.sun2000/wiki)
|
|
|
59
58
|
* Huawei [`SmartLogger`](https://github.com/bolliy/ioBroker.sun2000/wiki/SmartLogger) integration: Monitors and manages the PV power system. The adapter saves the collected data in the same way as it does when read out the inverter directly.
|
|
60
59
|
* Huawei [`Emma`](https://github.com/bolliy/ioBroker.sun2000/wiki/Emma) integration: The Modbus access, network connectivity (WiFi and Ethernet) and the DDSU/DTSU-666H smart meter functions are integrated in one unit - the use of the Sdongle becomes redundant. In addition Huawei EV chargers and load shedding/control (via selected Shelly devices) are supported and "intelligent" controlled.
|
|
61
60
|
* Huawei [`Charger`](https://github.com/bolliy/ioBroker.sun2000/issues/171) via Emma integration: The chargers are automatically recognized and the data is saved in their own path.
|
|
62
|
-
* Statistics: Aggregates historical collected datapoints into time-based summaries (e.g. hourly, daily, monthly, yearly).
|
|
63
|
-
|
|
64
|
-
|
|
61
|
+
* [`Statistics`](https://github.com/bolliy/ioBroker.sun2000/wiki/Statistk-(statistics)): Aggregates historical collected datapoints into time-based summaries (e.g. hourly, daily, monthly, yearly).
|
|
62
|
+
These statistics should be able to be visualized in ioBroker VIS using the flexcharts adapter to create interactive diagrams for inverter performance and energy production.
|
|
63
|
+
* [`Surplus Power Control`](https://github.com/bolliy/ioBroker.sun2000/wiki/%C3%9Cberschuss-(surplus))
|
|
64
|
+
The sun2000 adapter calculates how much of your self-generated solar energy is available to power devices in your home — instead of sending it to the grid.
|
|
65
65
|
|
|
66
66
|
|
|
67
67
|
## Changelog
|
|
@@ -69,6 +69,16 @@ In the medium term, these statistics should be able to be visualized in ioBroker
|
|
|
69
69
|
Placeholder for the next version (at the beginning of the line):
|
|
70
70
|
### **WORK IN PROGRESS**
|
|
71
71
|
-->
|
|
72
|
+
### 2.4.2 (2026-04-04)
|
|
73
|
+
* fix test-and-release: deploy with 24.x
|
|
74
|
+
|
|
75
|
+
### 2.4.1 (2026-04-04)
|
|
76
|
+
* statistics: flexcharts integration — built-in Apache ECharts configuration with bar and line chart support
|
|
77
|
+
* statistics: day-break visualization with alternating shaded areas for hourly charts
|
|
78
|
+
* statistics: per chart-type templates (`statistics.flexCharts.template.hourly` etc.) for full ECharts customization including functions
|
|
79
|
+
* statistics: data placeholders (`%%solarYield%%`, `%%gridExport%%` etc.) allow complete chart layout control via template states
|
|
80
|
+
* statistics: chart output states (`statistics.flexCharts.jsonOutput.hourly` etc.) updated automatically each hour
|
|
81
|
+
|
|
72
82
|
### 2.4.0 (2026-03-14)
|
|
73
83
|
* fix: the order of bit assignment corrected of alarmsJSON
|
|
74
84
|
* new state `inverter.x.emma.activeAlarmSN` and `inverter.x.emma.HistoricalAlarmSN` : emma alarms [#226](https://github.com/bolliy/ioBroker.sun2000/issues/226)
|
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.2",
|
|
5
5
|
"news": {
|
|
6
|
+
"2.4.2": {
|
|
7
|
+
"en": "fix test-and-release: deploy with 24.x",
|
|
8
|
+
"de": "fix test-and-release: einsatz mit 24.x",
|
|
9
|
+
"ru": "исправление тест-и-релиз: развертывание с 24.x",
|
|
10
|
+
"pt": "corrigir teste e liberação: implantar com 24.x",
|
|
11
|
+
"nl": "fix test-and-release: inzet met 24.x",
|
|
12
|
+
"fr": "correction test-and-release: déployer avec 24.x",
|
|
13
|
+
"it": "fix test-and-release: implementare con 24.x",
|
|
14
|
+
"es": "fijar prueba y liberación: desplegar con 24.x",
|
|
15
|
+
"pl": "fix test- and- release: rozmieszczanie z 24.x",
|
|
16
|
+
"uk": "виправити тест-і-випуск: розгортання з 24.x",
|
|
17
|
+
"zh-cn": "固定测试和释放:部署24x"
|
|
18
|
+
},
|
|
19
|
+
"2.4.1": {
|
|
20
|
+
"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",
|
|
21
|
+
"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",
|
|
22
|
+
"ru": "статистика: интеграция flexcharts — встроенная конфигурация Apache ECharts с поддержкой штрих- и линейных графиков\nстатистика: визуализация на рассвете с чередующимися затененными областями для часовых графиков\nстатистика: на шаблоны типа диаграммы («statistics.flexCharts.template.hourly» и т. д.) для полной настройки EChart, включая функции\nстатистика: держатели данных («%%solarYield%%», «%%gridExport%%» и т.д.) позволяют полностью контролировать макет диаграммы через шаблонные состояния\nстатистика: состояния выхода диаграммы (statistics.flexCharts.jsonOutput.hourly и т. д.) обновляются автоматически каждый час",
|
|
23
|
+
"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",
|
|
24
|
+
"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 (",
|
|
25
|
+
"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",
|
|
26
|
+
"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",
|
|
27
|
+
"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",
|
|
28
|
+
"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ę",
|
|
29
|
+
"uk": "статистика: інтеграція флекса — вбудована конфігурація Apache ECharts з підтримкою діаграми бару та лінії\nстатистика: візуалізація денного розриву з чергуванням затінених зон на часових графіках\nстатистика: за шаблони діаграм типу (`statistics.flexCharts.template.hrly` і т.д.) для повної настройки ECharts, включаючи функції\nстатистика: Держателі (`%solarYield%%`, `%gridExport%%` і т.д.) дозволяють повністю контролювати макети за допомогою шаблонних станів\nстатистика: діаграми вихідних станів (`statistics.flexCharts.jsonOutput.hrly` і т.д.) оновлено автоматично за кожну годину",
|
|
30
|
+
"zh-cn": "统计:弹性图集成——内置的Apache ECharts配置,并有栏和行图支持\n统计:日间可视化,小时图表可交替显示阴影区域\n统计:每个图表类型的模板(`Statistics.flex Charts.template.hourly'等),用于包括功能在内的全部ECharts定制\n统计:数据占位符(`%%solarYield%%`,`%%gridExport%%`等)允许通过模板状态进行完整的图表布局控制\n统计:图表输出状态(`STATistics.flexCHarts.jsonOutput.hourly'等)"
|
|
31
|
+
},
|
|
6
32
|
"2.4.0": {
|
|
7
33
|
"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.",
|
|
8
34
|
"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.",
|
|
@@ -67,32 +93,6 @@
|
|
|
67
93
|
"pl": "aktualizacje zależności i konfiguracji\nnowy stan \"collected.dailyExternalYield\" Riemann sum of 'collected.externalPower'",
|
|
68
94
|
"uk": "оновлення залежності та конфігурації\nновий стан `collected.dailyExternalYield` Сума Riemann `collected.externalPower й",
|
|
69
95
|
"zh-cn": "依赖和配置更新\n新国家`收集.每日外部耶尔德' 收集的里曼总和。 `"
|
|
70
|
-
},
|
|
71
|
-
"2.3.3": {
|
|
72
|
-
"en": "Improved verification of the adapter configuration\nnew state `inverter.x.derived.dailyActiveEnergy` Inverter daily active energy, which is determined via the Riemann sum of `inverter.x.activePower`\nstate `collected.dailyInputYield` was redisigned based on inverter.[0..n-1].derived.dailyActiveEnergy",
|
|
73
|
-
"de": "Verbesserte Überprüfung der Adapterkonfiguration\n`inverter.x.derived.dailyActiveEnergy` Inverter tägliche aktive Energie, die über die Riemann Summe von `inverter.x.activePower bestimmt wird `\nstate `collected.dailyInputYield` wurde basierend auf Inverter zurückgeteilt. [0.n-1].derived.dailyActiveEnergy",
|
|
74
|
-
"ru": "Улучшенная проверка конфигурации адаптера\nновое состояние «inverter.x.derived.dailyActiveEnergy» Инверторная ежедневная активная энергия, которая определяется через сумму Римана 'inverter.x.activePower пункт\nstate 'collected.dailyInputYield' была переопределена на основе инвертора. [0..n-1].derived.dailyActiveEnergy",
|
|
75
|
-
"pt": "Verificação melhorada da configuração do adaptador\nnovo estado «inverter.x.derived.dailyActiveEnergy» Energia ativa diária do inversor, que é determinada através da soma Riemann de `inverter.x.activePower `\nestado `colleted.dailyInputYield` foi reassinado com base no inversor. [0..n-1].derivado.diárioAtividadeEnergia",
|
|
76
|
-
"nl": "Verbeterde verificatie van de configuratie van de adapter\nnieuwe staat Inverter dagelijkse actieve energie, die wordt bepaald via de Riemann som van Wat\nstaat [0..n-1].afgeleid.dailyActiveEnergy",
|
|
77
|
-
"fr": "Amélioration de la vérification de la configuration de l'adaptateur\nnouvel état `inverter.x.derived.dailyActiveEnergy` Inverter l'énergie active quotidienne, qui est déterminée par la somme Riemann de `inverter.x.activePower \"\nl'état `collected.dailyInputYield` a été résigné sur la base de l'onduleur. Énergie active",
|
|
78
|
-
"it": "Verifica migliorata della configurazione dell'adattatore\nnuovo stato `inverter.x.derived.dailyActiveEnergy` Inverter energia attiva quotidiana, che è determinata tramite la somma Riemann di `inverter.x.activePower #\nstato `collect.dailyInputYield` è stato ridisegnato sulla base di inverter. [0.n-1]",
|
|
79
|
-
"es": "Mejor verificación de la configuración del adaptador\nnuevo estado `inverter.x.derived.dailyActiveEnergy` Inverter energía activa diaria, que se determina a través de la suma Riemann de `inverter.x.activePower `\nestado `colected.dailyInputYield` fue redisigned basado en inverter. [0.n-1].derived.dailyActiveEnergy",
|
|
80
|
-
"pl": "Ulepszona weryfikacja konfiguracji adaptera\nnowy stan 'inverter.x.derived.dailyActiveEnergy' Inwerter dzienna aktywna energia, która jest określana za pomocą sumy Riemann 'inverter.x.activePower'\nstan 'collected.dailyInputYield' został wycofany na podstawie inwertera. [0.. n-1] .derived.dailyActiveEnergy",
|
|
81
|
-
"uk": "Покращена перевірка конфігурації адаптера\nновий стан `inverter.x.derived.dailyActiveEnergy` Інверторна щоденна активна енергія, яка визначається за рахунок `inverter.x.activePower й\n`collected.dailyInputYield` був перерахований на основі інвертора. [0..n-1].derived.dailyActiveEnergy",
|
|
82
|
-
"zh-cn": "改进适配器配置的核查\n新州`反转器.x.衍生的每日能源 ' 每日反转活性能量,通过`反转器.x.activePower的Riemann总和确定 `\ndaily InputYield)根据反差重新签名。 [0.n-1]. 衍生的每日能源"
|
|
83
|
-
},
|
|
84
|
-
"2.3.2": {
|
|
85
|
-
"en": "allows again `control.battery.chargeFromGridFunction` when using the Emma",
|
|
86
|
-
"de": "erlaubt wieder `control.battery.chargeFromGridFunction` bei Verwendung der Emma",
|
|
87
|
-
"ru": "позволяет снова «control.battery.chargeFromGridFunction» при использовании Emma",
|
|
88
|
-
"pt": "permite novamente `control.battery.chargeFromGridFunction` ao usar a Emma",
|
|
89
|
-
"nl": "maakt het mogelijk opnieuw te controleren.battery.chargeFromGridFunction",
|
|
90
|
-
"fr": "permet de nouveau `control.battery.chargeFromGridFunction` lors de l'utilisation de l'Emma",
|
|
91
|
-
"it": "permette di nuovo `control.battery.chargeFromGridFunction` quando si utilizza Emma",
|
|
92
|
-
"es": "permite de nuevo 'control.battery.chargeDesdeGridFunction' al utilizar Emma",
|
|
93
|
-
"pl": "pozwala ponownie 'control.battery.chargeFromGridFunction' podczas korzystania z Emmy",
|
|
94
|
-
"uk": "дозволяє знову `control.battery.chargeЗ альбомуGridFunction` при використанні Емма",
|
|
95
|
-
"zh-cn": "允许在使用 Emma 时再次使用 \" control.battery. charge from GridFunction \" "
|
|
96
96
|
}
|
|
97
97
|
},
|
|
98
98
|
"titleLang": {
|
|
@@ -129,6 +129,7 @@
|
|
|
129
129
|
"modbus",
|
|
130
130
|
"sun2000",
|
|
131
131
|
"luna2000",
|
|
132
|
+
"emma",
|
|
132
133
|
"inverter",
|
|
133
134
|
"smartcharger",
|
|
134
135
|
"sdongle"
|
package/lib/register.js
CHANGED
package/lib/statistics.js
CHANGED
|
@@ -15,6 +15,7 @@ in ioBroker VIS using the ioBroker.flexcharts adapter.
|
|
|
15
15
|
|
|
16
16
|
'use strict';
|
|
17
17
|
|
|
18
|
+
const stringify = require('javascript-stringify').stringify;
|
|
18
19
|
const { dataRefreshRate, statisticsType } = require(`${__dirname}/types.js`);
|
|
19
20
|
const tools = require(`${__dirname}/tools.js`);
|
|
20
21
|
|
|
@@ -23,16 +24,8 @@ class statistics {
|
|
|
23
24
|
this.adapter = adapterInstance;
|
|
24
25
|
this.stateCache = stateCache;
|
|
25
26
|
this.taskTimer = null;
|
|
27
|
+
this._path = 'statistics';
|
|
26
28
|
this.testing = false; // set to true for testing purposes
|
|
27
|
-
// initialize to current time to avoid immediate backfill on startup
|
|
28
|
-
//const nowInit = new Date();
|
|
29
|
-
this.lastExecution = {
|
|
30
|
-
hourly: undefined,
|
|
31
|
-
daily: undefined,
|
|
32
|
-
weekly: undefined,
|
|
33
|
-
monthly: undefined,
|
|
34
|
-
annual: undefined,
|
|
35
|
-
};
|
|
36
29
|
|
|
37
30
|
this.stats = [
|
|
38
31
|
{
|
|
@@ -41,14 +34,6 @@ class statistics {
|
|
|
41
34
|
unit: 'kWh',
|
|
42
35
|
type: statisticsType.deltaReset, // value is a total that resets at the start of the period, so we need to calculate the delta to get the actual consumption for the period
|
|
43
36
|
},
|
|
44
|
-
/*
|
|
45
|
-
{
|
|
46
|
-
sourceId: 'collected.consumptionSum',
|
|
47
|
-
targetPath: 'consumptionSum',
|
|
48
|
-
unit: 'kWh',
|
|
49
|
-
type: statisticsType.delta,
|
|
50
|
-
},
|
|
51
|
-
*/
|
|
52
37
|
{
|
|
53
38
|
sourceId: 'collected.dailySolarYield',
|
|
54
39
|
targetPath: 'solarYield',
|
|
@@ -120,16 +105,93 @@ class statistics {
|
|
|
120
105
|
initVal: '[]',
|
|
121
106
|
},
|
|
122
107
|
// a state where users may store a Flexcharts/eCharts options template
|
|
123
|
-
|
|
108
|
+
// --- Templates: eines pro Chart-Typ ---
|
|
124
109
|
{
|
|
125
|
-
id: 'statistics.
|
|
126
|
-
name: 'Flexcharts template',
|
|
110
|
+
id: 'statistics.flexCharts.template.hourly',
|
|
111
|
+
name: 'Flexcharts template hourly',
|
|
127
112
|
type: 'string',
|
|
128
113
|
role: 'json',
|
|
129
|
-
desc: 'Optional eCharts
|
|
114
|
+
desc: 'Optional eCharts template for hourly chart. Leave empty {} for built-in layout.',
|
|
115
|
+
write: true,
|
|
116
|
+
initVal: '{}',
|
|
117
|
+
},
|
|
118
|
+
{
|
|
119
|
+
id: 'statistics.flexCharts.template.daily',
|
|
120
|
+
name: 'Flexcharts template daily',
|
|
121
|
+
type: 'string',
|
|
122
|
+
role: 'json',
|
|
123
|
+
desc: 'Optional eCharts template for daily chart. Leave empty {} for built-in layout.',
|
|
124
|
+
write: true,
|
|
125
|
+
initVal: '{}',
|
|
126
|
+
},
|
|
127
|
+
{
|
|
128
|
+
id: 'statistics.flexCharts.template.weekly',
|
|
129
|
+
name: 'Flexcharts template weekly',
|
|
130
|
+
type: 'string',
|
|
131
|
+
role: 'json',
|
|
132
|
+
desc: 'Optional eCharts template for weekly chart. Leave empty {} for built-in layout.',
|
|
133
|
+
write: true,
|
|
134
|
+
initVal: '{}',
|
|
135
|
+
},
|
|
136
|
+
{
|
|
137
|
+
id: 'statistics.flexCharts.template.monthly',
|
|
138
|
+
name: 'Flexcharts template monthly',
|
|
139
|
+
type: 'string',
|
|
140
|
+
role: 'json',
|
|
141
|
+
desc: 'Optional eCharts template for monthly chart. Leave empty {} for built-in layout.',
|
|
142
|
+
write: true,
|
|
143
|
+
initVal: '{}',
|
|
144
|
+
},
|
|
145
|
+
{
|
|
146
|
+
id: 'statistics.flexCharts.template.annual',
|
|
147
|
+
name: 'Flexcharts template annual',
|
|
148
|
+
type: 'string',
|
|
149
|
+
role: 'json',
|
|
150
|
+
desc: 'Optional eCharts template for annual chart. Leave empty {} for built-in layout.',
|
|
151
|
+
write: true,
|
|
152
|
+
initVal: '{}',
|
|
153
|
+
},
|
|
154
|
+
// --- Output: eines pro Chart-Typ ---
|
|
155
|
+
{
|
|
156
|
+
id: 'statistics.flexCharts.jsonOutput.hourly',
|
|
157
|
+
name: 'Flexcharts output hourly',
|
|
158
|
+
type: 'string',
|
|
159
|
+
role: 'json',
|
|
160
|
+
desc: 'ECharts configuration for hourly chart',
|
|
161
|
+
initVal: '{}',
|
|
162
|
+
},
|
|
163
|
+
{
|
|
164
|
+
id: 'statistics.flexCharts.jsonOutput.daily',
|
|
165
|
+
name: 'Flexcharts output daily',
|
|
166
|
+
type: 'string',
|
|
167
|
+
role: 'json',
|
|
168
|
+
desc: 'ECharts configuration for daily chart',
|
|
169
|
+
initVal: '{}',
|
|
170
|
+
},
|
|
171
|
+
{
|
|
172
|
+
id: 'statistics.flexCharts.jsonOutput.weekly',
|
|
173
|
+
name: 'Flexcharts output weekly',
|
|
174
|
+
type: 'string',
|
|
175
|
+
role: 'json',
|
|
176
|
+
desc: 'ECharts configuration for weekly chart',
|
|
177
|
+
initVal: '{}',
|
|
178
|
+
},
|
|
179
|
+
{
|
|
180
|
+
id: 'statistics.flexCharts.jsonOutput.monthly',
|
|
181
|
+
name: 'Flexcharts output monthly',
|
|
182
|
+
type: 'string',
|
|
183
|
+
role: 'json',
|
|
184
|
+
desc: 'ECharts configuration for monthly chart',
|
|
185
|
+
initVal: '{}',
|
|
186
|
+
},
|
|
187
|
+
{
|
|
188
|
+
id: 'statistics.flexCharts.jsonOutput.annual',
|
|
189
|
+
name: 'Flexcharts output annual',
|
|
190
|
+
type: 'string',
|
|
191
|
+
role: 'json',
|
|
192
|
+
desc: 'ECharts configuration for annual chart',
|
|
130
193
|
initVal: '{}',
|
|
131
194
|
},
|
|
132
|
-
*/
|
|
133
195
|
],
|
|
134
196
|
},
|
|
135
197
|
];
|
|
@@ -172,7 +234,7 @@ class statistics {
|
|
|
172
234
|
* @returns {Promise<void>}
|
|
173
235
|
*/
|
|
174
236
|
|
|
175
|
-
|
|
237
|
+
_calculateGeneric(stateId, periodStart, periodEnde) {
|
|
176
238
|
const toStr = this._localIsoWithOffset(periodEnde);
|
|
177
239
|
let jsonStr = this.stateCache.get(stateId)?.value ?? '[]';
|
|
178
240
|
let arr = [];
|
|
@@ -243,31 +305,10 @@ class statistics {
|
|
|
243
305
|
|
|
244
306
|
this.stateCache.set(stateId, JSON.stringify(arr), { type: 'string' });
|
|
245
307
|
this.adapter.logger.debug(`Appended ${stateId} statistic ${toStr}`);
|
|
308
|
+
return arr.length > 0;
|
|
246
309
|
}
|
|
247
310
|
|
|
248
|
-
|
|
249
|
-
* Calculates and updates hourly consumption statistics.
|
|
250
|
-
*
|
|
251
|
-
* This function calculates the hourly consumption statistics based on the current day's data.
|
|
252
|
-
* It retrieves the consumption data and updates the hourly consumption JSON accordingly.
|
|
253
|
-
*
|
|
254
|
-
* @returns {void}
|
|
255
|
-
*/
|
|
256
|
-
async _calculateHourly() {
|
|
257
|
-
const now = new Date();
|
|
258
|
-
if (this.testing) {
|
|
259
|
-
const state = await this.adapter.getState('statistics.jsonHourly');
|
|
260
|
-
this.stateCache.set('statistics.jsonHourly', state?.val ?? '[]', { type: 'string', stored: true });
|
|
261
|
-
now.setDate(now.getDate() + 1); // set to start of day for testing to have consistent results
|
|
262
|
-
now.setHours(1, 0, 0, 1); // set to 1ms after midnight to trigger hourly calculation for the new day
|
|
263
|
-
}
|
|
264
|
-
const startOfDay = new Date(now.getFullYear(), now.getMonth(), now.getDate(), 0, 0, 0, 0);
|
|
265
|
-
const lastHour = new Date(now.getFullYear(), now.getMonth(), now.getDate(), now.getHours(), 0, 0, 0);
|
|
266
|
-
this.adapter.log.debug('### Hourly execution triggered ###');
|
|
267
|
-
this._calculateGeneric('statistics.jsonHourly', startOfDay, lastHour) && (this.lastExecution.hourly = now); // only update last execution time if calculation was performed to avoid backfilling multiple hours at startup
|
|
268
|
-
}
|
|
269
|
-
|
|
270
|
-
async _clearGeneric(stateId, periodStart) {
|
|
311
|
+
_clearGeneric(stateId, periodStart) {
|
|
271
312
|
let jsonStr = this.stateCache.get(stateId)?.value ?? '[]';
|
|
272
313
|
let arr = [];
|
|
273
314
|
try {
|
|
@@ -298,7 +339,7 @@ class statistics {
|
|
|
298
339
|
* @param {string} periodType - The type of period for which the aggregation is performed.
|
|
299
340
|
* @returns {void}
|
|
300
341
|
*/
|
|
301
|
-
|
|
342
|
+
_calculateAggregation(sourceStateId, targetStateId, getWindow, periodType) {
|
|
302
343
|
try {
|
|
303
344
|
const now = new Date();
|
|
304
345
|
const window = getWindow(now);
|
|
@@ -306,7 +347,7 @@ class statistics {
|
|
|
306
347
|
const toDate = window.to;
|
|
307
348
|
if (now < toDate) {
|
|
308
349
|
this.adapter.logger.debug(`statistics.js: Skipping ${periodType} aggregation because current time is before end of aggregation window`);
|
|
309
|
-
return;
|
|
350
|
+
return false;
|
|
310
351
|
}
|
|
311
352
|
const toStr = this._localIsoWithOffset(toDate);
|
|
312
353
|
|
|
@@ -322,7 +363,7 @@ class statistics {
|
|
|
322
363
|
|
|
323
364
|
// Avoid duplicates
|
|
324
365
|
const last = targetArray.length > 0 ? targetArray[targetArray.length - 1] : {};
|
|
325
|
-
if (last.to === toStr) return;
|
|
366
|
+
if (last.to === toStr) return false;
|
|
326
367
|
|
|
327
368
|
const target = {
|
|
328
369
|
from: this._localIsoWithOffset(fromDate),
|
|
@@ -379,30 +420,45 @@ class statistics {
|
|
|
379
420
|
targetArray.push(target);
|
|
380
421
|
}
|
|
381
422
|
|
|
382
|
-
// Retention: keep entries from retention start onwards
|
|
383
|
-
/*
|
|
384
|
-
const retentionStart = getRetentionStart(now);
|
|
385
|
-
targetArray = targetArray.filter(item => {
|
|
386
|
-
const ts = Date.parse(item.from);
|
|
387
|
-
return !Number.isNaN(ts) && ts >= retentionStart.getTime();
|
|
388
|
-
});
|
|
389
|
-
*/
|
|
390
|
-
|
|
391
423
|
targetArray.sort((a, b) => Date.parse(a.to) - Date.parse(b.to));
|
|
392
424
|
this.stateCache.set(targetStateId, JSON.stringify(targetArray), { type: 'string' });
|
|
393
425
|
this.adapter.logger.debug(`Appended ${periodType} statistic ${toStr} `);
|
|
394
|
-
return
|
|
426
|
+
return targetArray.length > 0;
|
|
395
427
|
} catch (err) {
|
|
396
428
|
this.adapter.logger.warn(`Error during ${periodType} aggregation: ${err.message}`);
|
|
397
429
|
}
|
|
398
430
|
}
|
|
399
431
|
|
|
432
|
+
/**
|
|
433
|
+
* Calculates and updates hourly consumption statistics.
|
|
434
|
+
*
|
|
435
|
+
* This function calculates the hourly consumption statistics based on the current day's data.
|
|
436
|
+
* It retrieves the consumption data and updates the hourly consumption JSON accordingly.
|
|
437
|
+
*
|
|
438
|
+
* @returns {void}
|
|
439
|
+
*/
|
|
440
|
+
_calculateHourly() {
|
|
441
|
+
const now = new Date();
|
|
442
|
+
if (this.testing) {
|
|
443
|
+
const state = this.adapter.getState('statistics.jsonHourly');
|
|
444
|
+
this.stateCache.set('statistics.jsonHourly', state?.val ?? '[]', { type: 'string', stored: true });
|
|
445
|
+
now.setDate(now.getDate() + 1); // set to start of day for testing to have consistent results
|
|
446
|
+
now.setHours(1, 0, 0, 1); // set to 1ms after midnight to trigger hourly calculation for the new day
|
|
447
|
+
}
|
|
448
|
+
const startOfDay = new Date(now.getFullYear(), now.getMonth(), now.getDate(), 0, 0, 0, 0);
|
|
449
|
+
const lastHour = new Date(now.getFullYear(), now.getMonth(), now.getDate(), now.getHours(), 0, 0, 0);
|
|
450
|
+
this.adapter.log.debug('### Hourly execution triggered ###');
|
|
451
|
+
if (this._calculateGeneric('statistics.jsonHourly', startOfDay, lastHour)) {
|
|
452
|
+
this._buildFlexchart('hourly');
|
|
453
|
+
}
|
|
454
|
+
}
|
|
455
|
+
|
|
400
456
|
/**
|
|
401
457
|
* Calculates and updates daily consumption statistics from hourly data.
|
|
402
458
|
*
|
|
403
459
|
* @returns {void}
|
|
404
460
|
*/
|
|
405
|
-
|
|
461
|
+
_calculateDaily() {
|
|
406
462
|
this.adapter.log.debug('### Daily execution triggered ###');
|
|
407
463
|
this._calculateAggregation(
|
|
408
464
|
'statistics.jsonHourly',
|
|
@@ -415,7 +471,7 @@ class statistics {
|
|
|
415
471
|
return { from: yesterday, to: today };
|
|
416
472
|
},
|
|
417
473
|
'daily',
|
|
418
|
-
) &&
|
|
474
|
+
) && this._buildFlexchart('daily'); // only update last execution time if aggregation was performed to avoid backfilling multiple days at startup
|
|
419
475
|
}
|
|
420
476
|
|
|
421
477
|
/**
|
|
@@ -423,22 +479,26 @@ class statistics {
|
|
|
423
479
|
*
|
|
424
480
|
* @returns {void}
|
|
425
481
|
*/
|
|
426
|
-
|
|
482
|
+
_calculateWeekly() {
|
|
427
483
|
this.adapter.log.debug('### Weekly execution triggered ###');
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
484
|
+
if (
|
|
485
|
+
this._calculateAggregation(
|
|
486
|
+
'statistics.jsonDaily',
|
|
487
|
+
'statistics.jsonWeekly',
|
|
488
|
+
now => {
|
|
489
|
+
// aggregation window: Monday to Sunday of the previous week (week that just ended)
|
|
490
|
+
const startday = new Date(now.getFullYear(), now.getMonth(), now.getDate(), 0, 0, 0, 0);
|
|
491
|
+
const lastday = new Date(startday);
|
|
492
|
+
// set to Monday of previous week
|
|
493
|
+
lastday.setDate(now.getDate() - (now.getDay() || 7) + 1); // set to Monday of actual week
|
|
494
|
+
startday.setDate(lastday.getDate() - 7); // set to Monday of previous week
|
|
495
|
+
return { from: startday, to: lastday };
|
|
496
|
+
},
|
|
497
|
+
'weekly',
|
|
498
|
+
)
|
|
499
|
+
) {
|
|
500
|
+
this._buildFlexchart('weekly');
|
|
501
|
+
} // only update last execution time if aggregation was performed to avoid backfilling multiple weeks at startup
|
|
442
502
|
}
|
|
443
503
|
|
|
444
504
|
/**
|
|
@@ -446,7 +506,7 @@ class statistics {
|
|
|
446
506
|
*
|
|
447
507
|
* @returns {void}
|
|
448
508
|
*/
|
|
449
|
-
|
|
509
|
+
_calculateMonthly() {
|
|
450
510
|
this.adapter.log.debug('### Monthly execution triggered ###');
|
|
451
511
|
this._calculateAggregation(
|
|
452
512
|
'statistics.jsonDaily',
|
|
@@ -458,7 +518,7 @@ class statistics {
|
|
|
458
518
|
return { from: prevMonth, to: thisMonth };
|
|
459
519
|
},
|
|
460
520
|
'monthly',
|
|
461
|
-
) &&
|
|
521
|
+
) && this._buildFlexchart('monthly'); // only update last execution time if aggregation was performed to avoid backfilling multiple months at startup
|
|
462
522
|
}
|
|
463
523
|
|
|
464
524
|
/**
|
|
@@ -466,7 +526,7 @@ class statistics {
|
|
|
466
526
|
*
|
|
467
527
|
* @returns {void}
|
|
468
528
|
*/
|
|
469
|
-
|
|
529
|
+
_calculateAnnual() {
|
|
470
530
|
this.adapter.log.debug('### Annual execution triggered ###');
|
|
471
531
|
this._calculateAggregation(
|
|
472
532
|
'statistics.jsonDaily',
|
|
@@ -478,14 +538,14 @@ class statistics {
|
|
|
478
538
|
return { from: prevYear, to: thisYear };
|
|
479
539
|
},
|
|
480
540
|
'annual',
|
|
481
|
-
) &&
|
|
541
|
+
) && this._buildFlexchart('annual'); // only update last execution time if aggregation was performed to avoid backfilling multiple years at startup
|
|
482
542
|
}
|
|
483
543
|
|
|
484
544
|
/**
|
|
485
545
|
* Initialize and schedule the unified task manager.
|
|
486
546
|
* This task runs every minute and checks which statistics need to be calculated.
|
|
487
547
|
*/
|
|
488
|
-
|
|
548
|
+
_initializeTask() {
|
|
489
549
|
const scheduleNextRun = () => {
|
|
490
550
|
const now = new Date();
|
|
491
551
|
const next = new Date(now);
|
|
@@ -505,16 +565,15 @@ class statistics {
|
|
|
505
565
|
this.adapter.clearTimeout(this.taskTimer);
|
|
506
566
|
}
|
|
507
567
|
|
|
508
|
-
this.taskTimer = this.adapter.setTimeout(
|
|
509
|
-
|
|
568
|
+
this.taskTimer = this.adapter.setTimeout(() => {
|
|
569
|
+
this._executeScheduledTasks();
|
|
510
570
|
scheduleNextRun(); // reschedule for next hour
|
|
511
571
|
}, msToNextHour);
|
|
512
572
|
};
|
|
513
|
-
//await this._executeScheduledTasks(); // execute immediately on startup to catch up on any missed runs while the adapter was not running
|
|
514
573
|
// Schedule the next run
|
|
515
574
|
scheduleNextRun();
|
|
516
575
|
}
|
|
517
|
-
|
|
576
|
+
_executeScheduledTasks() {
|
|
518
577
|
this._calculateHourly();
|
|
519
578
|
this._calculateDaily();
|
|
520
579
|
this._calculateWeekly();
|
|
@@ -527,15 +586,15 @@ class statistics {
|
|
|
527
586
|
* - Execute all scheduled tasks to ensure that statistics are up to date.
|
|
528
587
|
* - Clear old data based on retention policies.
|
|
529
588
|
*/
|
|
530
|
-
|
|
589
|
+
mitNightProcess() {
|
|
531
590
|
const now = new Date();
|
|
532
|
-
|
|
591
|
+
this._executeScheduledTasks();
|
|
533
592
|
// Clear old data based on retention policies
|
|
534
593
|
const startOfYear = new Date(now.getFullYear(), 0, 1, 0, 0, 0, 0);
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
594
|
+
this._clearGeneric('statistics.jsonDaily', startOfYear);
|
|
595
|
+
this._clearGeneric('statistics.jsonWeekly', startOfYear);
|
|
596
|
+
this._clearGeneric('statistics.jsonMonthly', startOfYear);
|
|
597
|
+
this._clearGeneric('statistics.jsonHourly', new Date(now.getFullYear(), now.getMonth(), now.getDate() - 1, 0, 0, 0, 0));
|
|
539
598
|
}
|
|
540
599
|
|
|
541
600
|
async initialize() {
|
|
@@ -547,7 +606,7 @@ class statistics {
|
|
|
547
606
|
this.stateCache.set('statistics.jsonDaily', state?.val ?? '[]', { type: 'string', stored: true });
|
|
548
607
|
|
|
549
608
|
state = await this.adapter.getState('statistics.jsonWeekly');
|
|
550
|
-
this.stateCache.set('statistics.jsonWeekly', state?.val ?? '[]', { type: 'string', stored: true });
|
|
609
|
+
this.stateCache.set('statistics.jsonWeekly', state?.val ?? '[]', { type: 'string', stored: true }); //is already stored
|
|
551
610
|
|
|
552
611
|
state = await this.adapter.getState('statistics.jsonMonthly');
|
|
553
612
|
this.stateCache.set('statistics.jsonMonthly', state?.val ?? '[]', { type: 'string', stored: true });
|
|
@@ -555,30 +614,28 @@ class statistics {
|
|
|
555
614
|
state = await this.adapter.getState('statistics.jsonAnnual');
|
|
556
615
|
this.stateCache.set('statistics.jsonAnnual', state?.val ?? '[]', { type: 'string', stored: true });
|
|
557
616
|
|
|
558
|
-
// load optional flexchart template
|
|
559
|
-
/*
|
|
560
|
-
state = await this.adapter.getState('statistics.flexChartTemplate');
|
|
561
|
-
this.stateCache.set('statistics.flexChartTemplate', state?.val ?? '{}', { type: 'string', stored: true });
|
|
562
|
-
*/
|
|
563
617
|
// wait until consumptionToday and so on is available to avoid running the task before the initial state is loaded
|
|
564
|
-
/*
|
|
565
|
-
await tools.waitForValue(() => this.stateCache.get('collected.consumptionToday')?.value, 60000);
|
|
566
|
-
await tools.waitForValue(() => this.stateCache.get('collected.dailySolarYield')?.value, 60000);
|
|
567
|
-
await tools.waitForValue(() => this.stateCache.get('collected.SOC')?.value, 60000);
|
|
568
|
-
*/
|
|
569
618
|
await tools.waitForValue(() => this.stateCache.get('collected.accumulatedEnergyYield')?.value, 60000);
|
|
619
|
+
|
|
620
|
+
// load templates — eines pro Chart-Typ
|
|
621
|
+
for (const chartType of ['hourly', 'daily', 'weekly', 'monthly', 'annual']) {
|
|
622
|
+
const templateStateId = `statistics.flexCharts.template.${chartType}`;
|
|
623
|
+
state = await this.adapter.getState(templateStateId);
|
|
624
|
+
this.stateCache.set(templateStateId, state?.val ?? '{}', { type: 'string', stored: true });
|
|
625
|
+
if (state?.ack === false) {
|
|
626
|
+
this.stateCache.set(templateStateId, state.val, { type: 'string' });
|
|
627
|
+
await this.adapter.setState(templateStateId, { val: state.val, ack: true });
|
|
628
|
+
this._buildFlexchart(chartType);
|
|
629
|
+
}
|
|
630
|
+
}
|
|
631
|
+
|
|
570
632
|
this.mitNightProcess(); // execute once on startup to catch up on any missed runs while the adapter was not running
|
|
571
633
|
this._initializeTask();
|
|
634
|
+
this.adapter.subscribeStates(`${this._path}.*`);
|
|
572
635
|
}
|
|
573
636
|
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
* The returned object may be sent to a callback for flexcharts' script source.
|
|
577
|
-
*
|
|
578
|
-
* @param {string} myChart - one of 'hourly','daily','weekly','monthly','annual'
|
|
579
|
-
* @returns {object} chart configuration
|
|
580
|
-
*/
|
|
581
|
-
_buildFlexchart(myChart) {
|
|
637
|
+
_buildFlexchart(myChart, chartStyle) {
|
|
638
|
+
chartStyle = chartStyle || (myChart === 'hourly' ? 'line' : 'bar'); // default styles: line for hourly (to better see the curve), bar for others
|
|
582
639
|
const IDS = {
|
|
583
640
|
hourly: 'statistics.jsonHourly',
|
|
584
641
|
daily: 'statistics.jsonDaily',
|
|
@@ -594,123 +651,447 @@ class statistics {
|
|
|
594
651
|
data = [];
|
|
595
652
|
}
|
|
596
653
|
|
|
597
|
-
//
|
|
598
|
-
const
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
654
|
+
// --- X-Axis labels ---
|
|
655
|
+
const xAxisData = data.map(entry => {
|
|
656
|
+
const from = new Date(entry.from);
|
|
657
|
+
const to = new Date(entry.to);
|
|
658
|
+
if (myChart === 'hourly') {
|
|
659
|
+
return `${to.toLocaleDateString('de-DE', { year: 'numeric', month: '2-digit', day: '2-digit' })} ${to.toLocaleTimeString('de-DE', {
|
|
660
|
+
hour12: false,
|
|
661
|
+
hour: '2-digit',
|
|
662
|
+
minute: '2-digit',
|
|
663
|
+
})}`;
|
|
664
|
+
}
|
|
665
|
+
if (myChart === 'weekly') {
|
|
666
|
+
const yesterday = new Date(to);
|
|
667
|
+
yesterday.setDate(yesterday.getDate() - 1);
|
|
668
|
+
return `${from.toLocaleDateString('de-DE', { month: '2-digit', day: '2-digit' })}..${yesterday.toLocaleTimeString('de-DE', { month: '2-digit', day: '2-digit' })}`;
|
|
669
|
+
}
|
|
670
|
+
if (myChart === 'monthly') {
|
|
671
|
+
return from.toLocaleDateString('de-DE', { year: 'numeric', month: '2-digit' });
|
|
672
|
+
}
|
|
673
|
+
if (myChart === 'annual') {
|
|
674
|
+
return from.toLocaleDateString('de-DE', { year: 'numeric' });
|
|
675
|
+
}
|
|
676
|
+
|
|
677
|
+
return from.toLocaleDateString('de-DE', { year: 'numeric', month: '2-digit', day: '2-digit' });
|
|
678
|
+
/*
|
|
679
|
+
return myChart === 'hourly'
|
|
680
|
+
? `${to.toLocaleDateString('de-DE', { year: 'numeric', month: '2-digit', day: '2-digit' })} ${to.toLocaleTimeString('de-DE', {
|
|
681
|
+
hour12: false,
|
|
682
|
+
hour: '2-digit',
|
|
683
|
+
minute: '2-digit',
|
|
684
|
+
})}`
|
|
685
|
+
: from.toLocaleDateString('de-DE', { year: 'numeric', month: '2-digit', day: '2-digit' });
|
|
686
|
+
*/
|
|
687
|
+
});
|
|
688
|
+
|
|
689
|
+
const xAxisDataShort = myChart === 'hourly' ? xAxisData.map(label => label.split(' ')[1]) : xAxisData;
|
|
690
|
+
|
|
691
|
+
// --- Tagesbereiche ---
|
|
692
|
+
const dayAreas = [];
|
|
693
|
+
if (myChart === 'hourly' && xAxisData.length > 0) {
|
|
694
|
+
const dayBoundaries = [0];
|
|
695
|
+
xAxisData.forEach((label, i) => {
|
|
696
|
+
if (i === 0) return;
|
|
697
|
+
const date = label.split(' ')[0];
|
|
698
|
+
const prevDate = xAxisData[i - 1].split(' ')[0];
|
|
699
|
+
if (date !== prevDate) dayBoundaries.push(i);
|
|
700
|
+
});
|
|
701
|
+
dayBoundaries.push(xAxisData.length);
|
|
702
|
+
|
|
703
|
+
dayBoundaries.forEach((startIdx, d) => {
|
|
704
|
+
if (d >= dayBoundaries.length - 1) return;
|
|
705
|
+
const endIdx = dayBoundaries[d + 1];
|
|
706
|
+
const date = xAxisData[startIdx].split(' ')[0];
|
|
707
|
+
const shaded = d % 2 === 1;
|
|
708
|
+
dayAreas.push([
|
|
709
|
+
{
|
|
710
|
+
xAxis: startIdx - 0.5,
|
|
711
|
+
label: {
|
|
712
|
+
show: true,
|
|
713
|
+
position: 'insideTop',
|
|
714
|
+
formatter: date,
|
|
715
|
+
color: '#555',
|
|
716
|
+
fontSize: 11,
|
|
717
|
+
fontWeight: 'bold',
|
|
718
|
+
backgroundColor: 'rgba(255,255,255,0.7)',
|
|
719
|
+
padding: [2, 4],
|
|
720
|
+
borderRadius: 3,
|
|
721
|
+
},
|
|
722
|
+
},
|
|
723
|
+
{
|
|
724
|
+
xAxis: endIdx - 0.5,
|
|
725
|
+
itemStyle: shaded
|
|
726
|
+
? { color: 'rgba(180,180,180,0.15)', borderColor: 'rgba(120,120,120,0.3)', borderWidth: 1, borderType: 'dashed' }
|
|
727
|
+
: { color: 'rgba(255,255,255,0)' },
|
|
728
|
+
},
|
|
729
|
+
]);
|
|
730
|
+
});
|
|
731
|
+
}
|
|
732
|
+
|
|
733
|
+
// --- Series data extraction ---
|
|
734
|
+
const extract = key => data.map(e => Number(Number(e[key]?.value ?? 0).toFixed(3)));
|
|
735
|
+
const negate = arr => arr.map(v => Number((-v).toFixed(3)));
|
|
736
|
+
|
|
737
|
+
const seriesData = {
|
|
738
|
+
solarYield: extract('solarYield'),
|
|
739
|
+
consumption: extract('consumption'),
|
|
740
|
+
gridExport: extract('gridExport'),
|
|
741
|
+
gridImport: extract('gridImport'),
|
|
742
|
+
chargeCapacity: extract('chargeCapacity'),
|
|
743
|
+
dischargeCapacity: extract('dischargeCapacity'),
|
|
744
|
+
SOC: extract('SOC'),
|
|
745
|
+
gridExportNeg: negate(extract('gridExport')),
|
|
746
|
+
chargeCapacityNeg: negate(extract('chargeCapacity')),
|
|
607
747
|
};
|
|
608
748
|
|
|
609
|
-
//
|
|
610
|
-
const
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
}
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
749
|
+
// --- Tooltip formatter (zeigt immer positive Werte, filtert DayBreak heraus) ---
|
|
750
|
+
const tooltipFormatter = params => {
|
|
751
|
+
if (!Array.isArray(params)) params = [params];
|
|
752
|
+
return params
|
|
753
|
+
.filter(p => p.seriesName !== 'DayBreak')
|
|
754
|
+
.map(p => {
|
|
755
|
+
const negatedSeries = ['Grid Export', 'Charge'];
|
|
756
|
+
const val = negatedSeries.includes(p.seriesName) ? Math.abs(p.value) : p.value;
|
|
757
|
+
const unit = p.seriesName === 'SOC' ? ' %' : ' kWh';
|
|
758
|
+
return `${p.marker}${p.seriesName}: <b>${val}${unit}</b>`;
|
|
759
|
+
})
|
|
760
|
+
.join('<br/>');
|
|
761
|
+
};
|
|
762
|
+
|
|
763
|
+
// --- Load chart-type specific template ---
|
|
764
|
+
const templateStateId = `statistics.flexCharts.template.${myChart}`;
|
|
765
|
+
const outputStateId = `statistics.flexCharts.jsonOutput.${myChart}`;
|
|
766
|
+
|
|
767
|
+
const templateStr = this.stateCache.get(templateStateId)?.value ?? '{}';
|
|
768
|
+
let chartStr = '{}';
|
|
769
|
+
|
|
770
|
+
try {
|
|
771
|
+
const templ = JSON.parse(templateStr);
|
|
772
|
+
|
|
773
|
+
if (Object.keys(templ).length === 0) {
|
|
774
|
+
// Kein Template → built-in Default
|
|
775
|
+
chartStr = this._buildDefaultChart(myChart, chartStyle, xAxisData, xAxisDataShort, dayAreas, seriesData);
|
|
776
|
+
} else {
|
|
777
|
+
// Template vorhanden → mit javascript-stringify serialisieren
|
|
778
|
+
chartStr = stringify(templ);
|
|
624
779
|
}
|
|
780
|
+
} catch (e) {
|
|
781
|
+
this.adapter.logger.warn(`statistics: invalid template for ${myChart}: ${e.message}`);
|
|
782
|
+
chartStr = this._buildDefaultChart(myChart, chartStyle, xAxisData, xAxisDataShort, dayAreas, seriesData);
|
|
625
783
|
}
|
|
626
784
|
|
|
627
|
-
//
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
785
|
+
// --- Replace data placeholders ---
|
|
786
|
+
chartStr = chartStr
|
|
787
|
+
// X-Achse
|
|
788
|
+
.replace("'%%xAxisData%%'", JSON.stringify(xAxisData))
|
|
789
|
+
.replace("'%%xAxisDataShort%%'", JSON.stringify(xAxisDataShort))
|
|
790
|
+
.replace("'%%xAxisMax%%'", String(xAxisData.length - 1))
|
|
791
|
+
// Originaldaten (immer positiv)
|
|
792
|
+
.replace("'%%solarYield%%'", JSON.stringify(seriesData.solarYield))
|
|
793
|
+
.replace("'%%consumption%%'", JSON.stringify(seriesData.consumption))
|
|
794
|
+
.replace("'%%gridExport%%'", JSON.stringify(seriesData.gridExport))
|
|
795
|
+
.replace("'%%gridImport%%'", JSON.stringify(seriesData.gridImport))
|
|
796
|
+
.replace("'%%chargeCapacity%%'", JSON.stringify(seriesData.chargeCapacity))
|
|
797
|
+
.replace("'%%dischargeCapacity%%'", JSON.stringify(seriesData.dischargeCapacity))
|
|
798
|
+
.replace("'%%SOC%%'", JSON.stringify(seriesData.SOC))
|
|
799
|
+
// Negierte Varianten für gegenläufige Darstellung
|
|
800
|
+
.replace("'%%gridExportNeg%%'", JSON.stringify(seriesData.gridExportNeg))
|
|
801
|
+
.replace("'%%chargeCapacityNeg%%'", JSON.stringify(seriesData.chargeCapacityNeg))
|
|
802
|
+
// Sonstiges
|
|
803
|
+
.replace("'%%dayAreas%%'", JSON.stringify(dayAreas))
|
|
804
|
+
.replace("'%%chartTitle%%'", JSON.stringify(`PV Statistics — ${myChart}`))
|
|
805
|
+
// Funktionen
|
|
806
|
+
.replace("'%%tooltipFormatter%%'", stringify(tooltipFormatter));
|
|
631
807
|
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
}
|
|
808
|
+
// --- In chart-type specific output state speichern ---
|
|
809
|
+
this.stateCache.set(outputStateId, chartStr, { type: 'string' });
|
|
810
|
+
this.adapter.logger.debug(`statistics: flexCharts built for ${myChart}/${chartStyle}`);
|
|
635
811
|
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
812
|
+
return chartStr;
|
|
813
|
+
}
|
|
814
|
+
|
|
815
|
+
/**
|
|
816
|
+
* Build the default chart configuration as javascript-stringify string.
|
|
817
|
+
* Used when no template is provided.
|
|
818
|
+
* @param myChart
|
|
819
|
+
* @param chartStyle
|
|
820
|
+
* @param xAxisData
|
|
821
|
+
* @param xAxisDataShort
|
|
822
|
+
* @param dayAreas
|
|
823
|
+
* @param seriesData
|
|
824
|
+
*/
|
|
825
|
+
_buildDefaultChart(myChart, chartStyle, xAxisData, xAxisDataShort, dayAreas, seriesData) {
|
|
826
|
+
const xAxisFormatterHourly = value => {
|
|
827
|
+
if (value.includes('|')) return value;
|
|
828
|
+
return value.split(' ')[1] ?? value;
|
|
829
|
+
};
|
|
830
|
+
|
|
831
|
+
const seriesType = chartStyle === 'line' ? 'line' : 'bar';
|
|
832
|
+
const lineOptions =
|
|
833
|
+
chartStyle === 'line' ? { smooth: true, symbol: 'circle', symbolSize: 4, lineStyle: { width: 2 }, areaStyle: { opacity: 0.15 } } : {};
|
|
834
|
+
|
|
835
|
+
const negate = arr => arr.map(v => Number((-v).toFixed(3)));
|
|
836
|
+
const showSOC = myChart === 'hourly';
|
|
837
|
+
|
|
838
|
+
// Tooltip formatter — zeigt immer positive Werte, filtert DayBreak heraus
|
|
839
|
+
const tooltipFormatter = params => {
|
|
840
|
+
if (!Array.isArray(params)) params = [params];
|
|
841
|
+
return params
|
|
842
|
+
.filter(p => p.seriesName !== 'DayBreak')
|
|
843
|
+
.map(p => {
|
|
844
|
+
const negatedSeries = ['Grid Export', 'Charge'];
|
|
845
|
+
const val = negatedSeries.includes(p.seriesName) ? Math.abs(p.value) : p.value;
|
|
846
|
+
const unit = p.seriesName === 'SOC' ? ' %' : ' kWh';
|
|
847
|
+
return `${p.marker}${p.seriesName}: <b>${val}${unit}</b>`;
|
|
848
|
+
})
|
|
849
|
+
.join('<br/>');
|
|
850
|
+
};
|
|
851
|
+
|
|
852
|
+
const chart = {
|
|
853
|
+
backgroundColor: '#fff',
|
|
854
|
+
animation: false,
|
|
855
|
+
title: {
|
|
856
|
+
left: 'center',
|
|
857
|
+
text: `SUN2000 - PV Statistics - ${myChart}`,
|
|
858
|
+
},
|
|
859
|
+
legend: {
|
|
860
|
+
top: 35,
|
|
861
|
+
left: 'center',
|
|
862
|
+
data: ['Solar Yield', 'Consumption', 'Grid Export', 'Grid Import', 'Charge', 'Discharge', ...(showSOC ? ['SOC'] : [])],
|
|
863
|
+
},
|
|
864
|
+
tooltip: {
|
|
865
|
+
trigger: 'axis',
|
|
866
|
+
axisPointer: { type: 'cross' },
|
|
867
|
+
backgroundColor: 'rgba(245,245,245,0.95)',
|
|
868
|
+
borderWidth: 1,
|
|
869
|
+
borderColor: '#ccc',
|
|
870
|
+
padding: 10,
|
|
871
|
+
textStyle: { color: '#000' },
|
|
872
|
+
formatter: '%%tooltipFormatter%%',
|
|
873
|
+
position: (pos, params, el, elRect, size) => {
|
|
874
|
+
const obj = { top: 10 };
|
|
875
|
+
obj[pos[0] < size.viewSize[0] / 2 ? 'left' : 'right'] = 30;
|
|
876
|
+
return obj;
|
|
877
|
+
},
|
|
878
|
+
},
|
|
879
|
+
axisPointer: {
|
|
880
|
+
link: [{ xAxisIndex: 'all' }],
|
|
881
|
+
label: { backgroundColor: '#777' },
|
|
882
|
+
},
|
|
883
|
+
toolbox: {
|
|
884
|
+
feature: {
|
|
885
|
+
dataZoom: { yAxisIndex: false },
|
|
886
|
+
dataView: { show: true, readOnly: false },
|
|
887
|
+
restore: { show: true },
|
|
888
|
+
saveAsImage: { show: true },
|
|
889
|
+
},
|
|
890
|
+
},
|
|
891
|
+
grid: [
|
|
892
|
+
{ left: '8%', right: showSOC ? '8%' : '4%', top: 80, height: '50%' },
|
|
893
|
+
{ left: '8%', right: showSOC ? '8%' : '4%', top: '75%', height: '15%' },
|
|
894
|
+
],
|
|
895
|
+
xAxis: [
|
|
896
|
+
{
|
|
897
|
+
type: 'category',
|
|
898
|
+
data: xAxisDataShort,
|
|
899
|
+
scale: true,
|
|
900
|
+
boundaryGap: chartStyle !== 'line',
|
|
901
|
+
axisLine: { onZero: false },
|
|
902
|
+
splitLine: { show: false },
|
|
903
|
+
axisPointer: { z: 100 },
|
|
904
|
+
min: 0,
|
|
905
|
+
max: xAxisDataShort.length - 1,
|
|
906
|
+
axisLabel: {
|
|
907
|
+
interval: 0,
|
|
908
|
+
lineHeight: 16,
|
|
909
|
+
fontSize: 11,
|
|
910
|
+
formatter: '%%xAxisFormatter%%',
|
|
911
|
+
},
|
|
912
|
+
},
|
|
913
|
+
{
|
|
914
|
+
type: 'category',
|
|
915
|
+
gridIndex: 1,
|
|
916
|
+
data: xAxisData,
|
|
917
|
+
scale: true,
|
|
918
|
+
boundaryGap: false,
|
|
919
|
+
axisLine: { onZero: false },
|
|
920
|
+
axisTick: { show: false },
|
|
921
|
+
splitLine: { show: false },
|
|
922
|
+
axisLabel: { show: false },
|
|
923
|
+
min: 0,
|
|
924
|
+
max: xAxisData.length - 1,
|
|
925
|
+
},
|
|
926
|
+
],
|
|
927
|
+
yAxis: [
|
|
928
|
+
// Index 0 — Energie links
|
|
929
|
+
{
|
|
930
|
+
scale: false,
|
|
931
|
+
splitArea: { show: true },
|
|
932
|
+
name: 'Energy (kWh)',
|
|
933
|
+
nameLocation: 'middle',
|
|
934
|
+
nameGap: 50,
|
|
935
|
+
axisLabel: { formatter: '{value} kWh' },
|
|
936
|
+
splitLine: { show: true },
|
|
937
|
+
axisLine: { show: true },
|
|
938
|
+
},
|
|
939
|
+
// Index 1 — SOC rechts (nur bei hourly)
|
|
940
|
+
...(showSOC
|
|
941
|
+
? [
|
|
942
|
+
{
|
|
943
|
+
type: 'value',
|
|
944
|
+
min: 0,
|
|
945
|
+
max: 100,
|
|
946
|
+
name: 'SOC (%)',
|
|
947
|
+
nameLocation: 'middle',
|
|
948
|
+
nameGap: 40,
|
|
949
|
+
axisLabel: { formatter: '{value} %' },
|
|
950
|
+
splitLine: { show: false },
|
|
951
|
+
axisLine: { show: true },
|
|
952
|
+
},
|
|
953
|
+
]
|
|
954
|
+
: []),
|
|
955
|
+
// Index 1 oder 2 — Consumption unteres Grid
|
|
956
|
+
{
|
|
957
|
+
scale: true,
|
|
958
|
+
gridIndex: 1,
|
|
959
|
+
splitNumber: 3,
|
|
960
|
+
axisLine: { show: false },
|
|
961
|
+
axisTick: { show: false },
|
|
962
|
+
splitLine: { show: false },
|
|
963
|
+
name: 'Consumption\n(kWh)',
|
|
964
|
+
nameLocation: 'middle',
|
|
965
|
+
nameGap: 50,
|
|
966
|
+
axisLabel: { formatter: '{value}' },
|
|
967
|
+
},
|
|
968
|
+
],
|
|
969
|
+
dataZoom: [
|
|
970
|
+
{ type: 'inside', xAxisIndex: [0, 1], start: 0, end: 100 },
|
|
971
|
+
{ show: true, xAxisIndex: [0, 1], type: 'slider', bottom: 5, start: 0, end: 100 },
|
|
972
|
+
],
|
|
973
|
+
series: [
|
|
974
|
+
// Positive Werte
|
|
975
|
+
{
|
|
976
|
+
name: 'Solar Yield',
|
|
977
|
+
type: seriesType,
|
|
978
|
+
data: seriesData.solarYield,
|
|
979
|
+
itemStyle: { color: '#f6c94e' },
|
|
980
|
+
emphasis: { focus: 'series' },
|
|
981
|
+
...lineOptions,
|
|
982
|
+
},
|
|
983
|
+
{
|
|
984
|
+
name: 'Grid Import',
|
|
985
|
+
type: seriesType,
|
|
986
|
+
data: seriesData.gridImport,
|
|
987
|
+
itemStyle: { color: '#ec0000' },
|
|
988
|
+
emphasis: { focus: 'series' },
|
|
989
|
+
...lineOptions,
|
|
990
|
+
},
|
|
991
|
+
{
|
|
992
|
+
name: 'Discharge',
|
|
993
|
+
type: seriesType,
|
|
994
|
+
data: seriesData.dischargeCapacity,
|
|
995
|
+
itemStyle: { color: '#ed50e0' },
|
|
996
|
+
emphasis: { focus: 'series' },
|
|
997
|
+
...lineOptions,
|
|
998
|
+
},
|
|
999
|
+
// Negative Werte (unterhalb Nulllinie)
|
|
1000
|
+
{
|
|
1001
|
+
name: 'Grid Export',
|
|
1002
|
+
type: seriesType,
|
|
1003
|
+
data: negate(seriesData.gridExport),
|
|
1004
|
+
itemStyle: { color: '#5cb85c' },
|
|
1005
|
+
emphasis: { focus: 'series' },
|
|
1006
|
+
...lineOptions,
|
|
1007
|
+
},
|
|
1008
|
+
{
|
|
1009
|
+
name: 'Charge',
|
|
1010
|
+
type: seriesType,
|
|
1011
|
+
data: negate(seriesData.chargeCapacity),
|
|
1012
|
+
itemStyle: { color: '#5bc0de' },
|
|
1013
|
+
emphasis: { focus: 'series' },
|
|
1014
|
+
...lineOptions,
|
|
1015
|
+
},
|
|
1016
|
+
// SOC — nur bei hourly
|
|
1017
|
+
...(showSOC
|
|
1018
|
+
? [
|
|
1019
|
+
{
|
|
1020
|
+
name: 'SOC',
|
|
1021
|
+
type: 'line',
|
|
1022
|
+
yAxisIndex: 1,
|
|
1023
|
+
data: seriesData.SOC,
|
|
1024
|
+
itemStyle: { color: '#985e24' },
|
|
1025
|
+
lineStyle: { width: 2, type: 'dashed' },
|
|
1026
|
+
symbol: 'none',
|
|
1027
|
+
smooth: true,
|
|
1028
|
+
},
|
|
1029
|
+
]
|
|
1030
|
+
: []),
|
|
1031
|
+
// Consumption im unteren Grid
|
|
1032
|
+
// yAxisIndex passt sich an: 2 wenn SOC vorhanden, sonst 1
|
|
1033
|
+
{
|
|
1034
|
+
name: 'Consumption',
|
|
1035
|
+
type: seriesType,
|
|
1036
|
+
data: seriesData.consumption,
|
|
1037
|
+
itemStyle: { color: '#337ab7' },
|
|
1038
|
+
xAxisIndex: 1,
|
|
1039
|
+
yAxisIndex: showSOC ? 2 : 1,
|
|
1040
|
+
...lineOptions,
|
|
1041
|
+
},
|
|
1042
|
+
// Tages-Bereiche
|
|
1043
|
+
...(dayAreas.length > 0
|
|
1044
|
+
? [
|
|
1045
|
+
{
|
|
1046
|
+
name: 'DayBreak',
|
|
1047
|
+
type: 'bar',
|
|
1048
|
+
barWidth: 0,
|
|
1049
|
+
data: [],
|
|
1050
|
+
legendHoverLink: false,
|
|
1051
|
+
silent: true,
|
|
1052
|
+
markArea: { silent: true, data: dayAreas },
|
|
1053
|
+
},
|
|
1054
|
+
]
|
|
1055
|
+
: []),
|
|
1056
|
+
],
|
|
1057
|
+
};
|
|
1058
|
+
|
|
1059
|
+
return stringify(chart).replace("'%%xAxisFormatter%%'", stringify(xAxisFormatterHourly)).replace("'%%tooltipFormatter%%'", stringify(tooltipFormatter));
|
|
1060
|
+
}
|
|
1061
|
+
|
|
1062
|
+
_deepMerge(target, source) {
|
|
1063
|
+
for (const key of Object.keys(source)) {
|
|
1064
|
+
if (
|
|
1065
|
+
source[key] !== null &&
|
|
1066
|
+
typeof source[key] === 'object' &&
|
|
1067
|
+
!Array.isArray(source[key]) &&
|
|
1068
|
+
target[key] !== null &&
|
|
1069
|
+
typeof target[key] === 'object' &&
|
|
1070
|
+
!Array.isArray(target[key])
|
|
1071
|
+
) {
|
|
1072
|
+
// Rekursiv für verschachtelte Objekte
|
|
1073
|
+
this._deepMerge(target[key], source[key]);
|
|
1074
|
+
} else {
|
|
1075
|
+
// Primitive, Arrays direkt überschreiben
|
|
1076
|
+
target[key] = source[key];
|
|
650
1077
|
}
|
|
651
1078
|
}
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
// apply unit information to axis/tooltip if there is a single unit or per-series
|
|
655
|
-
const units = new Set(Object.values(unitMap).filter(u => u));
|
|
656
|
-
if (units.size === 1) {
|
|
657
|
-
const singleUnit = [...units][0];
|
|
658
|
-
// update default yAxis label and tooltip formatter
|
|
659
|
-
chart.yAxis[0].name = singleUnit ? `(${singleUnit})` : '';
|
|
660
|
-
chart.yAxis[0].axisLabel.formatter = `{value}${singleUnit ? ` ${singleUnit}` : ''}`;
|
|
661
|
-
chart.tooltip.formatter = params => {
|
|
662
|
-
if (!Array.isArray(params)) params = [params];
|
|
663
|
-
return params.map(p => `${p.seriesName}: ${p.value}${singleUnit ? ` ${singleUnit}` : ''}`).join('<br/>');
|
|
664
|
-
};
|
|
665
|
-
} else if (units.size > 1) {
|
|
666
|
-
// multiple units – show per-series unit in tooltip
|
|
667
|
-
chart.tooltip.formatter = params => {
|
|
668
|
-
if (!Array.isArray(params)) params = [params];
|
|
669
|
-
return params
|
|
670
|
-
.map(p => {
|
|
671
|
-
const u = unitMap[p.seriesName] || '';
|
|
672
|
-
return `${p.seriesName}: ${p.value}${u ? ` ${u}` : ''}`;
|
|
673
|
-
})
|
|
674
|
-
.join('<br/>');
|
|
675
|
-
};
|
|
676
|
-
}
|
|
1079
|
+
return target;
|
|
1080
|
+
}
|
|
677
1081
|
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
} else {
|
|
691
|
-
s.data = [];
|
|
692
|
-
}
|
|
693
|
-
}
|
|
694
|
-
// ensure unit is added from stats definitions if not present
|
|
695
|
-
if (!s.unit) {
|
|
696
|
-
let unitVal = unitMap[key];
|
|
697
|
-
if (!unitVal) {
|
|
698
|
-
const foundu = Object.keys(unitMap).find(k => k.toLowerCase() === String(key).toLowerCase());
|
|
699
|
-
if (foundu) unitVal = unitMap[foundu];
|
|
700
|
-
}
|
|
701
|
-
s.unit = unitVal || '';
|
|
702
|
-
}
|
|
703
|
-
});
|
|
704
|
-
} else {
|
|
705
|
-
chart.series = Object.keys(seriesData).map(name => ({
|
|
706
|
-
name,
|
|
707
|
-
type: 'line',
|
|
708
|
-
data: seriesData[name],
|
|
709
|
-
unit: unitMap[name] || '',
|
|
710
|
-
}));
|
|
1082
|
+
async handleTemplateChange(chartType, state) {
|
|
1083
|
+
const templateStateId = `statistics.flexCharts.template.${chartType}`;
|
|
1084
|
+
const template = this.stateCache.get(templateStateId)?.value;
|
|
1085
|
+
if (template === null || template === undefined) {
|
|
1086
|
+
this.adapter.logger.warn(`Template state ${templateStateId} not found for handleTemplateChange`);
|
|
1087
|
+
return;
|
|
1088
|
+
}
|
|
1089
|
+
if (state?.val != null) {
|
|
1090
|
+
this.adapter.logger.debug(`statistics: Event - state: ${chartType} changed: ${state.val} ack: ${state.ack}`);
|
|
1091
|
+
this.stateCache.set(templateStateId, state.val, { type: 'string', stored: true });
|
|
1092
|
+
await this.adapter.setState(templateStateId, { val: state.val, ack: true });
|
|
1093
|
+
this._buildFlexchart(chartType);
|
|
711
1094
|
}
|
|
712
|
-
chart.title.text += myChart;
|
|
713
|
-
return chart;
|
|
714
1095
|
}
|
|
715
1096
|
|
|
716
1097
|
/**
|
|
@@ -721,7 +1102,8 @@ class statistics {
|
|
|
721
1102
|
*/
|
|
722
1103
|
handleFlexMessage(message, callback) {
|
|
723
1104
|
const chartType = message?.chart || 'hourly';
|
|
724
|
-
const
|
|
1105
|
+
const chartStyle = message?.style;
|
|
1106
|
+
const result = this._buildFlexchart(chartType, chartStyle);
|
|
725
1107
|
if (callback && typeof callback === 'function') {
|
|
726
1108
|
callback(result);
|
|
727
1109
|
}
|
package/main.js
CHANGED
|
@@ -659,6 +659,14 @@ class Sun2000 extends utils.Adapter {
|
|
|
659
659
|
emma.instance.control.set(serviceId, state);
|
|
660
660
|
}
|
|
661
661
|
}
|
|
662
|
+
|
|
663
|
+
//sun2000.0.statistics.flexCharts.template
|
|
664
|
+
if (idArray[2] == 'statistics' && idArray[3] == 'flexCharts' && idArray[4] == 'template') {
|
|
665
|
+
const chartType = idArray[5];
|
|
666
|
+
if (this.state.statistics && typeof this.state.statistics.handleTemplateChange === 'function') {
|
|
667
|
+
this.state.statistics.handleTemplateChange(chartType, state);
|
|
668
|
+
}
|
|
669
|
+
}
|
|
662
670
|
} else {
|
|
663
671
|
// The state was deleted
|
|
664
672
|
this.logger.info(`state ${id} deleted`);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "iobroker.sun2000",
|
|
3
|
-
"version": "2.4.
|
|
3
|
+
"version": "2.4.2",
|
|
4
4
|
"description": "sun2000",
|
|
5
5
|
"author": {
|
|
6
6
|
"name": "bolliy",
|
|
@@ -28,9 +28,10 @@
|
|
|
28
28
|
},
|
|
29
29
|
"dependencies": {
|
|
30
30
|
"@iobroker/adapter-core": "^3.3.2",
|
|
31
|
-
"modbus-serial": "^8.0.
|
|
31
|
+
"modbus-serial": "^8.0.25",
|
|
32
32
|
"suncalc2": "^1.8.1",
|
|
33
|
-
"tcp-port-used": "^1.0.2"
|
|
33
|
+
"tcp-port-used": "^1.0.2",
|
|
34
|
+
"javascript-stringify": "^2.1.0"
|
|
34
35
|
},
|
|
35
36
|
"devDependencies": {
|
|
36
37
|
"@alcalzone/release-script": "^5.1.1",
|
|
@@ -40,7 +41,7 @@
|
|
|
40
41
|
"@iobroker/adapter-dev": "^1.5.0",
|
|
41
42
|
"@iobroker/eslint-config": "^2.2.0",
|
|
42
43
|
"@iobroker/testing": "^5.2.2",
|
|
43
|
-
"@tsconfig/
|
|
44
|
+
"@tsconfig/node22": "^22.0.5",
|
|
44
45
|
"@types/node": "^25.5.0",
|
|
45
46
|
"globals": "^16.5.0",
|
|
46
47
|
"typescript": "~5.9.3"
|