data-solectrus 0.2.10

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.
@@ -0,0 +1,32 @@
1
+ name: Publish to NPM
2
+
3
+ on:
4
+ release:
5
+ types: [published]
6
+
7
+ permissions:
8
+ id-token: write
9
+ contents: read
10
+
11
+ jobs:
12
+ publish:
13
+ runs-on: ubuntu-latest
14
+ environment: npm-publish
15
+ steps:
16
+ - name: Checkout code
17
+ uses: actions/checkout@v4
18
+
19
+ - name: Setup Node.js
20
+ uses: actions/setup-node@v4
21
+ with:
22
+ node-version: '18'
23
+ registry-url: 'https://registry.npmjs.org'
24
+
25
+ - name: Install dependencies
26
+ run: npm ci
27
+
28
+ - name: Run tests
29
+ run: npm run lint
30
+
31
+ - name: Publish to NPM
32
+ run: npm publish --provenance --access public
package/CHANGELOG.md ADDED
@@ -0,0 +1,138 @@
1
+ # Changelog
2
+
3
+ ## 0.2.9 - 2026-01-31
4
+
5
+ ### Changed
6
+
7
+ - **BREAKING:** Package renamed from `iobroker.data-solectrus` to `data-solectrus` for NPM namespace compatibility
8
+ - Enabled GitHub Actions automated publishing via Trusted Publishing (OIDC)
9
+
10
+ ## 0.2.8 - 2026-01-31
11
+
12
+ ### Changed
13
+
14
+ - Official NPM release with automated publishing via GitHub Actions
15
+ - Added NPM version and download badges to README
16
+ - Configured trusted publisher for secure automated releases
17
+ - Restructured info states: 6 core + hierarchical diagnostics (diagnostics.*, diagnostics.timing.*)
18
+
19
+ ## 0.2.7 - 2026-01-30
20
+
21
+ ### Added
22
+
23
+ - New sync diagnostics under `info.*`: input timestamp gap telemetry (`info.inputTsGapMs`, `info.inputTsGapOk`, `info.inputTsGapThresholdMs`, `info.inputTsSources`, `info.inputTsMissing`).
24
+ - Expanded sync diagnostics: active-only gap and culprit helpers (`info.inputTsGapActiveMs`, `info.inputTsSleepingSources`, `info.inputTsOldestId`, `info.inputTsOldestAgeMs`).
25
+ - Runtime setting to enable/disable the input timestamp gap diagnostics (`enableInputTsGapDiagnostics`).
26
+ - New deterministic regression check script: `npm run check:simulate` (30s / 6 ticks) to validate PV + signed grid meter scenarios.
27
+
28
+ ### Changed
29
+
30
+ - Admin UI: clearer wording + tooltip for “Ergebnis negativ → 0” (mode-aware hint: source vs formula).
31
+ - Docs: README + Wiki updated to clarify output clamp vs per-input clamp.
32
+
33
+ ### Fixed
34
+
35
+ - Formula evaluation: item-level `noNegative` no longer clamps negative *inputs* (important for signed meters where export is negative). Only per-input `noNegative` clamps that input; item-level `noNegative` clamps the final result.
36
+ - Source mode: output datatype handling corrected (string/boolean/mixed items no longer force numeric parsing; primitives are mirrored correctly).
37
+ - Sync diagnostics robustness: initial reads now populate the timestamp cache so `info.inputTsGap*` works immediately after startup.
38
+
39
+ ## 0.2.6 - 2026-01-29
40
+ - **Info states structure**: Reorganized `info.*` states for better readability and logical grouping (Variante D):
41
+ - Core states: `info.status`, `info.itemsActive`, `info.lastError`, `info.lastRun`, `info.lastRunMs`
42
+ - Diagnostics moved to: `info.diagnostics.*` (eval stats) and `info.diagnostics.timing.*` (timestamp skew)
43
+ - Old info states are automatically cleaned up on adapter restart
44
+ - Renamed: `evalTimeMs` → `lastRunMs`, `itemsEnabled` → `itemsActive`, `itemsConfigured` → `diagnostics.itemsTotal`
45
+
46
+ ### Added
47
+
48
+ - Admin UI: Formula Builder popup for building formulas with a palette (operators/functions/state pickers).
49
+ - Admin UI: Live input values inside the builder (polling while popup is open).
50
+ - Admin UI: Local in-browser formula preview pill (updates automatically while popup is open).
51
+
52
+ ### Fixed
53
+
54
+ - Admin UI: Builder crash on open (runtime error in custom UI script).
55
+ - Admin UI: Formula Builder live values now respect JSONPath and input clamping (neg→0) in the preview.
56
+
57
+ ## 0.2.5 - 2026-01-29
58
+
59
+ ### Changed
60
+
61
+ - Internal refactor: extracted formula parsing/evaluation and JSONPath helpers into separate modules under `lib/` to keep `main.js` smaller and easier to maintain.
62
+ - No functional changes intended.
63
+
64
+ ## 0.2.4 - 2026-01-29
65
+
66
+ ### Added
67
+
68
+ - Subscription management hardening: subscriptions are now derived from enabled items and kept in sync (unsubscribe removed ids). A global cap prevents runaway subscription counts.
69
+ - Tick time budget to avoid long ticks piling up; new telemetry states: `info.timeBudgetMs` and `info.skippedItems`.
70
+
71
+ ### Fixed
72
+
73
+ - Output type consistency: formula results are no longer forced numeric for `string`/`boolean`/`mixed` outputs.
74
+ - Fallback after repeated errors is now type-appropriate (e.g. `''` for string outputs instead of `'0'`).
75
+
76
+ ## 0.2.3 - 2026-01-29
77
+
78
+ ### Fixed
79
+
80
+ - Formula inputs with JSONPath can now yield strings/booleans (not only numbers). This enables conditions like `IF(opMode == 'Heating', ..., 0)` when `opMode` is an input extracted from a JSON payload.
81
+ - Numeric-like strings (e.g. `"12.2"`) are still treated as numbers to keep strict comparisons (`===`) working as expected.
82
+
83
+ ## 0.2.2 - 2026-01-29
84
+
85
+ ### Added
86
+
87
+ - New formula helper `jp("state.id", "jsonPath")` to read primitive values (string/number/boolean) from JSON payload states via the built-in minimal JSONPath.
88
+ - This enables conditions like `IF(jp("...", "$['Operation Mode']") == 'Heating', ..., 0)` without requiring separate input states.
89
+ - Per-item diagnostics states under `data-solectrus.0.items.<outputId>.*`:
90
+ - `compiledOk`, `compileError`, `lastError`, `lastOkTs`, `lastEvalMs`, `consecutiveErrors`.
91
+
92
+ ### Changed
93
+
94
+ - Robust tick behavior: formula/compile/snapshot failures no longer stop the adapter; errors are handled per item.
95
+ - Fallback behavior on errors: keep the last good value for a few retries, then set the output to `0` (config key `errorRetriesBeforeZero`, default: 3).
96
+ - Performance: formulas are compiled once per item (normalized expression + AST) and reused during ticks; snapshot/subscriptions use compiled source ids.
97
+ - Config change detection: when items change without restart, the adapter rebuilds compiled caches on the next tick.
98
+
99
+ ## 0.2.1 - 2026-01-29
100
+
101
+ ### Added
102
+
103
+ - Formula function `IF(condition, valueIfTrue, valueIfFalse)` (alias: `if(...)`).
104
+ - Helper `v("state.id")` to read raw foreign state values (string/boolean/number) from cache/snapshot.
105
+ - Compatibility normalization in formulas (outside strings): `AND`/`OR`/`NOT` and single `=`.
106
+
107
+ ### Changed
108
+
109
+ - Expression engine: re-introduced `==` and `!=` for compatibility with existing formula styles (use `===`/`!==` when you want strict matching).
110
+ - `s("...")` / `v("...")` state ids are now discovered from formulas and included in snapshot/subscriptions.
111
+
112
+ ## 0.2.0 - 2026-01-29
113
+
114
+ ### Added
115
+
116
+ - Formula function `pow(a, b)` (use this instead of an exponent operator).
117
+ - One-time debug logs for blocked dangerous input keys and for skipped JSONPath when the source value is already numeric.
118
+
119
+ ### Changed
120
+
121
+ - Security hardening: formula input variables use a null-prototype map and block dangerous keys (`__proto__`, `prototype`, `constructor`).
122
+ - Expression engine: removed support for `**` and for loose equality operators `==` / `!=` (use `===` / `!==`).
123
+ - Bounded internal "log once" caches to avoid unbounded growth.
124
+
125
+ ## 0.1.22 - 2026-01-28
126
+
127
+ ### Added
128
+
129
+ - Optional JSON extraction for `source` items and formula inputs via **JSONPath (optional)** (e.g. `$.apower`, `$.aenergy.by_minute[2]`).
130
+
131
+ ### Changed
132
+
133
+ - Wiki/README updated: JSON payloads no longer require a separate alias/script when JSONPath is configured.
134
+ - When `Datatype` is set to `boolean` or `string`, the adapter now writes real booleans/strings to the output state (not 0/1).
135
+
136
+ ### Notes
137
+
138
+ - JSONPath support is intentionally limited (dot access, bracket keys, array indexes). Unsupported expressions fall back to `0` and log a warning once.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Sven Griese
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,229 @@
1
+ # ioBroker.Data-SOLECTRUS
2
+
3
+ ![Version](https://img.shields.io/github/package-json/v/Felliglanz/data-solectrus?label=version)
4
+ ![NPM Version](https://img.shields.io/npm/v/data-solectrus?label=npm)
5
+ ![NPM Downloads](https://img.shields.io/npm/dt/data-solectrus)
6
+
7
+ <img src="admin/data-solectrus.png" alt="SOLECTRUS" width="120" />
8
+
9
+ Ein kleiner ioBroker-Adapter, der eigene States unter `data-solectrus.0.*` anlegt und im festen Intervall (Standard: 5s, **wall-clock aligned**) mit berechneten oder gespiegelten Werten befüllt.
10
+
11
+ Kurz gesagt: 🧮 **Formeln** + 🔌 **beliebige ioBroker-States** → 📦 **saubere, adapter-eigene Ziel-States** (z.B. für SOLECTRUS-Dashboards).
12
+
13
+ ## 📑 Inhaltsverzeichnis
14
+
15
+ - [✨ Highlights](#-highlights)
16
+ - [📦 Installation](#-installation)
17
+ - [🚀 Quickstart](#-quickstart)
18
+ - [⚡ Wichtige Semantik (Vorzeichen & Clamps)](#-wichtige-semantik-vorzeichen--clamps)
19
+ - [📚 Wiki & Anwendungsbeispiele](#-wiki--anwendungsbeispiele)
20
+ - [📊 Diagnose-States](#-diagnose-states)
21
+ - [🔧 Entwicklung & Tests](#-entwicklung--tests)
22
+ - [⚙️ Konfiguration](#-konfiguration)
23
+ - [🧮 Formeln](#-formeln)
24
+
25
+ ## ✨ Highlights
26
+
27
+ - 🔗 **Quell-Items**: 1:1 spiegeln (optional mit JSONPath)
28
+ - 🧮 **Formel-Items**: Werte aus vielen Quellen zusammenrechnen
29
+ - 📸 **Snapshot-Modus**: Reduziert Timing-Effekte bei zeitversetzten Quell-Updates
30
+ - 🎚️ **Ergebnis-Regeln**: Clamps am Ergebnis (z.B. negativ → 0, Min/Max)
31
+ - 📊 **Diagnose-States**: Laufzeit, Fehler und Sync-Statistiken
32
+
33
+ ## 📦 Installation
34
+
35
+ Der Adapter kann lokal als `.tgz` gebaut und in ioBroker installiert werden (oder via GitHub-Release, falls vorhanden).
36
+
37
+ - Paket bauen: `npm pack`
38
+ - Installation in ioBroker: Admin → Adapter → „Benutzerdefiniert" / URL/Datei → `data-solectrus-<version>.tgz` (z.B. `data-solectrus-0.2.9.tgz`)
39
+ - Oder direkt von NPM: `npm install data-solectrus`
40
+
41
+ Hinweis: Adaptername in ioBroker ist `data-solectrus` (Instanz: `data-solectrus.0`).
42
+
43
+ ## 🚀 Quickstart
44
+
45
+ Der Adapter ist absichtlich „leer" – du legst nur die Werte an, die du brauchst.
46
+
47
+ ### 1️⃣ Werte anlegen
48
+ **Admin → Adapter → data-solectrus → Werte**
49
+
50
+ - **Modus „Quelle"**: Genau einen State 1:1 spiegeln
51
+ - **Modus „Formel"**: Mehrere Inputs zusammenrechnen
52
+
53
+ ### 2️⃣ Snapshot aktivieren (optional)
54
+ **Globale Einstellungen**
55
+
56
+ Wenn deine Quellen zeitversetzt updaten und du kurzzeitig unplausible Kombinationen siehst, aktiviere den Snapshot-Modus.
57
+
58
+ ## ⚡ Wichtige Semantik (Vorzeichen & Clamps)
59
+
60
+ ### Ergebnis negativ → 0
61
+
62
+ Die Option **„Ergebnis negativ → 0“** wirkt nur auf das **Ergebnis** des Items (Output).
63
+
64
+ - Wenn du nur einzelne Inputs bereinigen willst (z.B. PV darf nie negativ sein, aber Netzleistung ist signed), nutze dafür pro Input **„neg→0“** oder `max(0, …)` in der Formel.
65
+
66
+ ### Beispiel: Hausverbrauch aus PV + signed Netzleistung
67
+
68
+ - `gridSigned`: Import positiv, Export negativ
69
+ - Hausverbrauch: `pvTotal + gridSigned`
70
+
71
+ Wenn PV=4639W und Export=-2514W, ergibt sich Hausverbrauch ≈ 2125W.
72
+
73
+ ## 📚 Wiki & Anwendungsbeispiele
74
+
75
+ Ausführliche Beispiele und Erklärungen findest du im Wiki:
76
+ 👉 https://github.com/Felliglanz/data-solectrus/wiki
77
+
78
+ **Direktlinks:**
79
+
80
+ - 🏠 [Hausverbrauch berechnen](https://github.com/Felliglanz/data-solectrus/wiki/Hausverbrauch)
81
+ - 🎚️ [Werte begrenzen](https://github.com/Felliglanz/data-solectrus/wiki/Werte-begrenzen)
82
+ - 🔧 [Formel-Builder nutzen](https://github.com/Felliglanz/data-solectrus/wiki/Formel-Builder)
83
+ - 📖 [Alle Anwendungsfälle](https://github.com/Felliglanz/data-solectrus/wiki/Use-Cases)
84
+
85
+ ## 📊 Diagnose-States
86
+
87
+ Der Adapter legt strukturierte States unter `data-solectrus.0.info.*` an:
88
+
89
+ ### 🎯 Kern-Infos (immer sichtbar)
90
+
91
+ - `info.connection` – Standard ioBroker Verbindungsstatus
92
+ - `info.itemsActive` – Anzahl aktivierter Werte
93
+ - `info.lastError` – Letzter Fehler (leer wenn alles OK)
94
+ - `info.lastRun` – Zeitstempel letzter Durchlauf (ISO 8601)
95
+ - `info.lastRunMs` – Dauer letzter Durchlauf (ms)
96
+ - `info.status` – Adapter-Status (`starting`, `ok`, `no_items_enabled`)
97
+
98
+ ### 🔍 Diagnose (eingeklappt im Objektbaum)
99
+
100
+ **`info.diagnostics.*`**
101
+
102
+ - `evalBudgetMs` – Verfügbare Zeit pro Durchlauf (ms)
103
+ - `evalSkipped` – Übersprungene Werte im letzten Durchlauf
104
+ - `itemsTotal` – Gesamtzahl konfigurierter Werte (aktiv + inaktiv)
105
+
106
+ **`info.diagnostics.timing.*`** *(Timestamp-Analyse für zeitversetzte Quellen)*
107
+
108
+ - `gapMs` – Differenz zwischen ältestem und neuestem Eingabe-Zeitstempel (ms)
109
+ - `gapOk` – ✅ wenn Differenz unter Schwellwert
110
+ - `gapActiveMs` – Differenz nur für aktive Quellen (ms)
111
+ - `gapActiveOk` – ✅ wenn aktive Differenz unter Schwellwert
112
+ - `sources` – Anzahl Eingaben mit Zeitstempel
113
+ - `sourcesActive` – Anzahl aktiver Eingaben (kürzlich aktualisiert)
114
+ - `sourcesSleeping` – Anzahl schlafender Eingaben (alte Zeitstempel)
115
+ - `oldestId` – State-ID mit ältestem Zeitstempel
116
+ - `oldestAgeMs` – Alter der ältesten Eingabe (ms)
117
+ - `newestId` – State-ID mit neuestem Zeitstempel
118
+ - `newestAgeMs` – Alter der neuesten Eingabe (ms)
119
+
120
+ ### 📈 Pro-Wert Diagnose
121
+
122
+ Unter `data-solectrus.0.items.<outputId>.*`:
123
+
124
+ - `compiledOk` – ✅ Kompilierung erfolgreich
125
+ - `compileError` – Kompilierungs-Fehler (leer bei OK)
126
+ - `lastError` – Letzter Laufzeit-Fehler
127
+ - `lastOkTs` – Zeitstempel letzte erfolgreiche Berechnung
128
+ - `lastEvalMs` – Dauer letzte Berechnung (ms)
129
+ - `consecutiveErrors` – Anzahl aufeinanderfolgender Fehler
130
+
131
+ ## 🔧 Entwicklung & Tests
132
+
133
+ Für schnelle Checks (z.B. nach Refactorings) gibt es einen Runtime-Smoke-Test, der **ohne** ioBroker-Controller läuft.
134
+ Er mockt die minimal benötigte Adapter-API und führt einmalig diese Phasen aus:
135
+
136
+ - `cleanupOldInfoStates()` (räumt alte Struktur auf)
137
+ - `createInfoStates()` (legt neue Struktur an)
138
+ - `prepareItems()` (inkl. Formel-Compile, Source-Discovery, Subscriptions)
139
+ - `runTick()` (ein Tick mit Snapshot + Berechnung + Output-States)
140
+
141
+ Ausführen:
142
+
143
+ - `npm run smoke`
144
+
145
+ ## ⚙️ Konfiguration
146
+
147
+ Die Konfiguration ist absichtlich **leer** – du fügst nur die Werte hinzu, die du brauchst.
148
+
149
+ ### 🌍 Globale Einstellungen
150
+
151
+ **⏱️ Aktualisierungs-Intervall**
152
+ - Intervall in Sekunden (min. 1 Sekunde)
153
+ - Läuft synchron zur Uhr: Bei 5s → `...:00, ...:05, ...:10, ...`
154
+
155
+ **📸 Snapshot-Modus (optional)**
156
+ - **Eingaben synchron einlesen**: Liest alle benötigten States einmal pro Durchlauf aktiv ein
157
+ - Reduziert Abweichungen, wenn Quellen zeitversetzt aktualisieren
158
+ - **Snapshot-Verzögerung**: Optionaler Delay in ms (z.B. 100–300ms) nach dem Durchlauf-Start
159
+
160
+ **🛡️ Fehlerbehandlung**
161
+ - `errorRetriesBeforeZero` – Anzahl Fehlversuche, bevor Output auf `0` gesetzt wird (Standard: 3)
162
+
163
+ ### 📝 Werte konfigurieren
164
+
165
+ Jeder Eintrag erzeugt genau **einen Output-State**.
166
+
167
+ Felder:
168
+
169
+ - **Enabled**: aktiviert/deaktiviert.
170
+ - **Name**: Anzeigename (optional).
171
+ - **Folder/Group**: optionaler Ordner/Channel-Prefix.
172
+ - Beispiel: `pv` + Target ID `leistung` → Output wird `data-solectrus.0.pv.leistung`.
173
+ - **Target ID**: Ziel-State innerhalb des Adapters (relativ). Beispiel: `leistung`, `pv.gesamt`.
174
+ - Erlaubt sind nur Segmente mit `A-Z`, `a-z`, `0-9`, `_`, `-` und `.` (keine absoluten IDs, kein `..`).
175
+ - **Mode**:
176
+ - `source`: 1:1 Spiegelung eines ioBroker-States (mit optionaler Nachbearbeitung).
177
+ - `formula`: Berechnung aus mehreren Inputs.
178
+ - **ioBroker Source State**:
179
+ - bei `mode=source`: der Quell-State (vollqualifiziert, z.B. `some.adapter.0.channel.state`).
180
+ - bei `mode=formula`: pro Input ein Source-State.
181
+ - **JSONPath (optional)**:
182
+ - Wenn der Source-State (oder ein Input) statt einer Zahl ein JSON als Text enthält, kann hier ein JSONPath angegeben werden, um daraus einen numerischen Wert zu extrahieren.
183
+ - Beispiele: `$.apower`, `$.aenergy.by_minute[2]`
184
+ - **Inputs** (nur `mode=formula`): Liste aus (Key, Source State).
185
+ - Optional pro Input: **Input negativ auf 0** (klemmt nur diesen Input vor der Rechnung).
186
+ - Optional pro Input: **JSONPath**
187
+ - Wenn JSONPath auf einen String/Boolean zeigt, wird dieser Wert als Variable bereitgestellt (z.B. für `IF(opMode == 'Heating', ...)`).
188
+ - Wenn JSONPath auf eine Zahl zeigt (oder einen numerischen String wie `"12.2"`), wird der Wert als Zahl bereitgestellt.
189
+ - **Wichtig zu Keys**: In Formeln sind `-` und Leerzeichen Operatoren/Trenner.
190
+ - Verwende daher am besten nur `a-z`, `0-9`, `_` (z.B. `bkw_garage`, `enpal`, `zendure`).
191
+ - Intern werden ungültige Zeichen im Key zu `_` umgewandelt.
192
+ - **Formula expression**: Formel-String.
193
+ - **Datatype**: optional (Standard: number).
194
+ - **Role**, **Unit**: optional (für Metadaten).
195
+
196
+ Nachbearbeitung:
197
+
198
+ - **Clamp negative to 0**: negative Werte werden auf `0` gesetzt.
199
+ - wirkt auf das **Ergebnis** des Items (Output).
200
+ - wenn du nur einzelne Quellen/Inputs „bereinigen“ willst (z.B. PV darf nie negativ sein, aber Netzleistung ist signed), nutze dafür **Input negativ auf 0** direkt am jeweiligen Input oder `max(0, …)` in der Formel.
201
+ - **Clamp result**: Ergebnis begrenzen (Min/Max). Leere Felder bedeuten „nicht begrenzen“.
202
+
203
+ ## 🧮 Formeln
204
+
205
+ ### Variablen
206
+
207
+ Variablen kommen aus den **Eingaben** (Schlüssel → Quell-State).
208
+
209
+ **Beispiel:**
210
+
211
+ ```javascript
212
+ // Eingaben definiert:
213
+ pv1 = 1000
214
+ pv2 = 1500
215
+ pv3 = 800
216
+
217
+ // Formel:
218
+ pv1 + pv2 + pv3 // = 3300
219
+ ```
220
+
221
+ **🔧 Verfügbare Funktionen:**
222
+
223
+ Siehe [Formel-Builder Wiki](https://github.com/Felliglanz/data-solectrus/wiki/Formel-Builder) für alle mathematischen Funktionen, Operatoren und Beispiele.
224
+
225
+ **✅ Tests ausführen:**
226
+
227
+ - `npm run lint` – Syntax-Prüfung
228
+ - `npm run smoke` – Runtime-Test (mockt ioBroker)
229
+ - `npm run check:simulate` – 30s Regressions-Test (PV + signed Meter)