iobroker.statechart 0.0.0-stage → 0.1.1

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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 IggePop
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 CHANGED
@@ -1,3 +1,560 @@
1
- # Temporary Holding Version
1
+ ![Logo](admin/statechart.png)
2
+ # ioBroker.statechart
2
3
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
4
+ [![NPM version](https://img.shields.io/npm/v/iobroker.statechart.svg)](https://www.npmjs.com/package/iobroker.statechart)
5
+ [![Downloads](https://img.shields.io/npm/dm/iobroker.statechart.svg)](https://www.npmjs.com/package/iobroker.statechart)
6
+ ![Number of Installations](https://iobroker.live/badges/statechart-installed.svg)
7
+ ![Current version in stable repository](https://iobroker.live/badges/statechart-stable.svg)
8
+
9
+ [![NPM](https://nodei.co/npm/iobroker.statechart.png?downloads=true)](https://nodei.co/npm/iobroker.statechart/)
10
+
11
+ **Tests:** ![Test and Release](https://github.com/IggePop/ioBroker.statechart/workflows/Test%20and%20Release/badge.svg)
12
+
13
+ **[English](#english) | [Deutsch](#deutsch)**
14
+
15
+ ---
16
+
17
+ <a id="english"></a>
18
+
19
+ ## State machine adapter for ioBroker
20
+
21
+ Model your automations as **state machines** (statecharts) in a graphical editor, similar to the
22
+ chart editors known from model based development. States, transitions with events, conditions and
23
+ actions, timers and function blocks are drawn in the ioBroker admin and executed by the adapter.
24
+
25
+ ### Features
26
+
27
+ - Graphical editor as its own entry in the admin side bar
28
+ - States with `entry`, `during` and `exit` actions
29
+ - Transitions with labels in the well-known syntax `event[condition]{action}`
30
+ - Timers: `after(5, sec)`
31
+ - Function blocks with your own JavaScript functions, usable in all conditions and actions
32
+ - Inputs and outputs mapped to any ioBroker states
33
+ - Live view: the active state is highlighted in the editor
34
+ - Code editor with syntax highlighting, error checking (e.g. unknown variables) and completion
35
+ - Several independent charts per instance
36
+ - Import and export of charts as JSON files
37
+ - Comments on states, transitions and function blocks, and notes on the canvas
38
+ - Per input: wake up on every update or only on changes, filter by ack, debounce
39
+ - Helper functions: setState, setStateDelayed, sendTo, isTimeBetween, isDaytime, astroTime
40
+ - Undo/redo (Ctrl+Z, Ctrl+Y), copy/cut/paste of states and function blocks (Ctrl+C, Ctrl+X, Ctrl+V), also between charts, and a warning about unsaved changes
41
+ - **Show code**: the JavaScript generated from the chart, showing exactly how the adapter executes it
42
+
43
+ ### Installation
44
+
45
+ The adapter is not in the ioBroker repositories yet, but can be installed directly from GitHub:
46
+
47
+ - **Admin:** tab **Adapters** → icon **Install from custom URL** (GitHub cat) → tab **Custom** →
48
+ URL `https://github.com/IggePop/ioBroker.statechart` → **Install**
49
+ - **or on the console** of the ioBroker system:
50
+ ```bash
51
+ iobroker url https://github.com/IggePop/ioBroker.statechart
52
+ ```
53
+
54
+ Then add an instance of **State Machine**. This installs the current development version, which may
55
+ still change incompatibly.
56
+
57
+ Requirements: Node.js 22 or newer, js-controller 6.0.11 or newer, admin 7.6.17 or newer.
58
+
59
+ ### Getting started
60
+
61
+ 1. Open **State Machine** in the admin side bar.
62
+ 2. Create a chart with **+** in the list on the left.
63
+ 3. Define the variables in the tab **Variables** on the right:
64
+ - **Input**: read from an ioBroker state, every change wakes up the chart
65
+ - **Output**: written to an ioBroker state when an action changes it
66
+ - **Local**: only exists inside the chart, with an initial value
67
+ 4. Add states with **Add state**. The state marked with the dot and arrow is the initial state
68
+ (can be changed in the properties of a state).
69
+ 5. Draw transitions by dragging from the border of a state to another state (or to itself).
70
+ 6. Select a state or transition (or double click it) to edit its actions or label on the right.
71
+ 7. Click **Save**. New charts are paused: start the chart with ▶ in the chart list. After saving, the adapter
72
+ restarts a running chart immediately.
73
+
74
+ ### Example: light with motion detector
75
+
76
+ | Variable | Scope | State |
77
+ |---|---|---|
78
+ | `motion` | Input | e.g. `hm-rpc.0.ABC123.1.MOTION` |
79
+ | `light` | Output | e.g. `hue.0.hallway.on` |
80
+
81
+ | State | entry |
82
+ |---|---|
83
+ | Off (initial state) | `light = false;` |
84
+ | On | `light = true;` |
85
+
86
+ | Transition | Label |
87
+ |---|---|
88
+ | Off → On | `motion[motion == true]` |
89
+ | On → Off | `after(2, min)[!motion]` |
90
+
91
+ ### Transition labels
92
+
93
+ ```
94
+ event[condition]{action}
95
+ ```
96
+
97
+ All parts are optional and may span several lines.
98
+
99
+ | Part | Meaning | Example |
100
+ |---|---|---|
101
+ | `event` | Name of an input: the transition is only checked when this input changes | `motion` |
102
+ | `after(n, unit)` | Instead of an event: valid from *n* units after entering the state on. Then a timer wakes up the chart, and the condition is checked again on every following wakeup. Units: `msec`, `sec`, `min` | `after(30, sec)` |
103
+ | `[condition]` | JavaScript expression, the transition is only taken if it is true | `[temp > 21 && !window]` |
104
+ | `{action}` | JavaScript, executed when the transition is taken | `{count++; light = true;}` |
105
+
106
+ Without event, a transition is checked whenever any input changes. If a state has several outgoing
107
+ transitions, they are checked in the order of their **priority** (shown at the arrow).
108
+
109
+ ### When inputs wake up the chart
110
+
111
+ Every input has three settings in the tab **Variables**:
112
+
113
+ | Setting | Values | Default |
114
+ |---|---|---|
115
+ | **Wake up on** | every update / value changes only | every update |
116
+ | **Acknowledged (ack)** | any / confirmed values only (`ack=true`) / commands only (`ack=false`) | any |
117
+ | **Debounce (ms)** | the chart wakes up only after the input had no update for this time, with its last value | 0 (off) |
118
+
119
+ Updates filtered out by ack do not change the variable, so it e.g. only contains confirmed values.
120
+
121
+ ### Actions
122
+
123
+ Actions and conditions are JavaScript. All variables of the chart can be used directly by their name,
124
+ plus `Math`, `Date`, `JSON` etc. and `log("text")` to write to the ioBroker log.
125
+
126
+ | Action | Executed |
127
+ |---|---|
128
+ | `entry` | when the state is entered |
129
+ | `during` | on every wakeup of the chart while the state is active and none of its outgoing transitions is taken |
130
+ | `exit` | when the state is left |
131
+
132
+ ### Nested states
133
+
134
+ Drop a state into another state to make it a **sub state**, drag it out again to undo it. While dragging,
135
+ the state it would be moved into is highlighted. Every level has its own initial state (dot with arrow).
136
+
137
+ - Entering a super state also enters its initial sub state (or the sub state the transition points to).
138
+ Leaving it leaves the active sub state first.
139
+ - On every wakeup, each active state is processed in this order, starting with the outermost one:
140
+ 1. transitions leaving the state, 2. its `during` action, 3. its **inner transitions**, 4. its active sub state.
141
+ So the transitions of a super state have priority over those of its sub states.
142
+ - A transition from a state to one of its own sub states is an **inner transition** (dot at its start):
143
+ the state stays active, only its active sub state is left, its own `entry`/`exit` are not executed.
144
+ With the option **Leave and enter the parent state again**, it is left and entered again instead.
145
+
146
+ ### Helper functions
147
+
148
+ In conditions, actions and function blocks:
149
+
150
+ | Function | Purpose |
151
+ |---|---|
152
+ | `log(message, level)` | writes to the ioBroker log, level `"debug"`, `"info"` (default), `"warn"` or `"error"` |
153
+ | `setState(id, value, ack)` | writes any ioBroker state, `ack` is `false` by default |
154
+ | `setStateDelayed(id, value, delayMs, ack)` | writes after a delay, cancelled when the chart stops |
155
+ | `sendTo(instance, command, message)` | sends a message to an adapter, e.g. `sendTo("telegram.0", "send", { text: "Window open" })` |
156
+ | `isTimeBetween("22:00", "06:00")` | true if the current time is in the range, also over midnight |
157
+ | `isDaytime()` | true between sunrise and sunset |
158
+ | `astroTime("sunset", offsetMinutes)` | time of an astro event today as `Date`: `sunrise`, `sunset`, `dawn`, `dusk`, `solarNoon`, ... |
159
+
160
+ `isDaytime()` and `astroTime()` use the location of the ioBroker system settings. To read other states, add
161
+ them as inputs: their variables are always up to date and wake up the chart. Time based functions do not wake up
162
+ the chart by themselves, they are evaluated when it wakes up.
163
+
164
+ ### Function blocks
165
+
166
+ **Add function** creates a block for your own JavaScript. It is executed once when the chart starts,
167
+ all functions and constants declared in it can be used in every condition and action of the chart:
168
+
169
+ ```js
170
+ const LIMIT = 22;
171
+
172
+ function tooWarm(temp) {
173
+ return temp > LIMIT;
174
+ }
175
+ ```
176
+
177
+ Transition label: `[tooWarm(temperature)]{heating = false;}`
178
+
179
+ ### Comments and notes
180
+
181
+ - **Comment:** states, transitions, function blocks and the chart itself have a field **Comment** in their properties.
182
+ A speech bubble on the canvas shows that there is a comment, the comment is shown as tooltip.
183
+ - **Notes** (icon **Add note** in the toolbar) are free texts on the canvas, like sticky notes. Drop a note into a
184
+ state, so it moves together with it.
185
+
186
+ Comments and notes have no influence on the execution. They are included in the generated code and in exported
187
+ files, and changing them does not prevent restoring the state after a restart.
188
+
189
+ ### Import and export
190
+
191
+ - **Export chart** (last icon in the toolbar of the editor) saves the chart as JSON file, including unsaved changes.
192
+ - **Export all charts** (icon above the chart list) saves all saved charts in one file.
193
+ - **Import charts** (icon above the chart list) adds the charts of such a file as new charts, existing charts are not
194
+ changed. The file is checked before (structure, IDs, references between states and transitions).
195
+
196
+ Imported charts are **paused**: their variables may refer to states of another system. Check them in the tab
197
+ **Variables**, then start the chart with the button in the chart list.
198
+
199
+ ### States created by the adapter
200
+
201
+ For every chart, the adapter creates below `statechart.<instance>.charts.<chart>`:
202
+
203
+ | State | Content |
204
+ |---|---|
205
+ | `activeState` | Name of the active state, nested states as `Parent.Child` |
206
+ | `activeStateIds` | IDs of all active states as JSON |
207
+ | `snapshot` | Stored state of the chart (active states, when they were entered, locals), see below |
208
+
209
+ The chart itself is stored in the object `statechart.<instance>.charts.<chart>`.
210
+
211
+ ### Restart of the adapter
212
+
213
+ After a restart of the adapter or of ioBroker, every chart continues in the states which were active
214
+ before, with its local variables. The entry actions are not executed again, and timers continue with
215
+ their remaining time. A timer which expired while the adapter was stopped is taken right after the start.
216
+
217
+ The chart starts in its initial state instead if it was changed meanwhile (positions in the editor do not
218
+ count), after saving it in the editor, after pausing it, and if the option **Restore the state after a
219
+ restart** in the chart properties is switched off. Variables declared in function blocks are not restored.
220
+
221
+ ### Execution model and differences to other statechart tools
222
+
223
+ - The adapter uses [XState](https://stately.ai/docs/xstate) to execute the charts.
224
+ - Every change of an input wakes up the chart once. Transitions without event are checked on every wakeup.
225
+ - `{action}` is executed while the transition is taken, i.e. after `exit` of the old and before `entry` of
226
+ the new state. A separate condition action and `/action` are not supported (yet).
227
+ - Transitions are selected like in Stateflow (outer before inner, see above), XState only executes them.
228
+ Pure XState would check the transitions of sub states first.
229
+ - Temporal logic is limited to `after(n, msec|sec|min)`, measured in real time.
230
+ - Conditions and actions use JavaScript, not MATLAB/C syntax.
231
+
232
+ ### Limitations
233
+
234
+ - Parallel states (AND), history junctions and connective junctions are not supported yet.
235
+ - Deleting a super state also deletes its sub states.
236
+ - Unsaved changes are lost when you select another chart.
237
+ - Conditions and actions run in a separate JavaScript context with a time limit of 1 second, but this
238
+ is **no security sandbox**: everyone who may edit charts can execute any code in the adapter,
239
+ just like with the JavaScript adapter.
240
+
241
+ ---
242
+
243
+ <a id="deutsch"></a>
244
+
245
+ ## Zustandsautomaten-Adapter für ioBroker
246
+
247
+ Bilde deine Automatisierungen als **Zustandsautomaten** (Statecharts) in einem grafischen Editor ab,
248
+ ähnlich den Chart-Editoren aus der modellbasierten Entwicklung. Zustände, Übergänge mit Events,
249
+ Bedingungen und Aktionen, Timer und Funktionsblöcke werden im ioBroker-Admin gezeichnet und vom
250
+ Adapter ausgeführt.
251
+
252
+ ### Funktionen
253
+
254
+ - Grafischer Editor als eigener Eintrag in der Seitenleiste des Admins
255
+ - Zustände mit `entry`-, `during`- und `exit`-Aktionen
256
+ - Übergänge mit Beschriftungen in der bekannten Syntax `event[bedingung]{aktion}`
257
+ - Timer: `after(5, sec)`
258
+ - Funktionsblöcke mit eigenen JavaScript-Funktionen, nutzbar in allen Bedingungen und Aktionen
259
+ - Eingänge und Ausgänge, die mit beliebigen ioBroker-Datenpunkten verbunden sind
260
+ - Live-Ansicht: Der aktive Zustand wird im Editor hervorgehoben
261
+ - Code-Editor mit Syntax-Hervorhebung, Fehlerprüfung (z.B. unbekannte Variablen) und Autovervollständigung
262
+ - Mehrere unabhängige Charts pro Instanz
263
+ - Import und Export von Charts als JSON-Dateien
264
+ - Kommentare zu Zuständen, Übergängen und Funktionsblöcken sowie Notizen auf der Zeichenfläche
265
+ - Pro Eingang: Aufwecken bei jeder Aktualisierung oder nur bei Änderung, Filter nach ack, Entprellen
266
+ - Hilfsfunktionen: setState, setStateDelayed, sendTo, isTimeBetween, isDaytime, astroTime
267
+ - Rückgängig/Wiederholen (Strg+Z, Strg+Y), Kopieren/Ausschneiden/Einfügen von Zuständen und Funktionsblöcken (Strg+C, Strg+X, Strg+V), auch zwischen Charts, und eine Warnung bei ungespeicherten Änderungen
268
+ - **Code anzeigen**: das aus dem Chart generierte JavaScript, das genau zeigt, wie der Adapter es ausführt
269
+
270
+ ### Installation
271
+
272
+ Der Adapter ist noch nicht in den ioBroker-Repositories, lässt sich aber direkt von GitHub installieren:
273
+
274
+ - **Admin:** Reiter **Adapter** → Symbol **Aus eigener URL installieren** (GitHub-Katze) → Reiter
275
+ **Benutzerdefiniert** → URL `https://github.com/IggePop/ioBroker.statechart` → **Installieren**
276
+ - **oder auf der Konsole** des ioBroker-Systems:
277
+ ```bash
278
+ iobroker url https://github.com/IggePop/ioBroker.statechart
279
+ ```
280
+
281
+ Danach eine Instanz von **Zustandsmaschine** hinzufügen. Installiert wird der aktuelle
282
+ Entwicklungsstand, der sich noch inkompatibel ändern kann.
283
+
284
+ Voraussetzungen: Node.js 22 oder neuer, js-controller 6.0.11 oder neuer, Admin 7.6.17 oder neuer.
285
+
286
+ ### Erste Schritte
287
+
288
+ 1. In der Seitenleiste des Admins **Zustandsmaschine** öffnen.
289
+ 2. Links in der Liste mit **+** ein Chart anlegen.
290
+ 3. Rechts im Reiter **Variablen** die Variablen festlegen:
291
+ - **Eingang**: wird aus einem ioBroker-Datenpunkt gelesen, jede Änderung weckt das Chart auf
292
+ - **Ausgang**: wird in einen ioBroker-Datenpunkt geschrieben, wenn eine Aktion ihn ändert
293
+ - **Lokal**: gibt es nur im Chart, mit einem Startwert
294
+ 4. Mit **Zustand hinzufügen** Zustände anlegen. Der Zustand mit Punkt und Pfeil ist der Startzustand
295
+ (änderbar in den Eigenschaften eines Zustands).
296
+ 5. Übergänge zeichnen: vom Rand eines Zustands zu einem anderen Zustand (oder zu sich selbst) ziehen.
297
+ 6. Einen Zustand oder Übergang anklicken (oder doppelklicken), um rechts seine Aktionen bzw. seine
298
+ Beschriftung zu bearbeiten.
299
+ 7. **Speichern** klicken. Neue Charts sind angehalten: Starte das Chart mit ▶ in der Chart-Liste. Nach dem
300
+ Speichern startet der Adapter ein laufendes Chart sofort neu.
301
+
302
+ ### Beispiel: Licht mit Bewegungsmelder
303
+
304
+ | Variable | Art | Datenpunkt |
305
+ |---|---|---|
306
+ | `motion` | Eingang | z.B. `hm-rpc.0.ABC123.1.MOTION` |
307
+ | `light` | Ausgang | z.B. `hue.0.flur.on` |
308
+
309
+ | Zustand | entry |
310
+ |---|---|
311
+ | Aus (Startzustand) | `light = false;` |
312
+ | An | `light = true;` |
313
+
314
+ | Übergang | Beschriftung |
315
+ |---|---|
316
+ | Aus → An | `motion[motion == true]` |
317
+ | An → Aus | `after(2, min)[!motion]` |
318
+
319
+ ### Beschriftung von Übergängen
320
+
321
+ ```
322
+ event[bedingung]{aktion}
323
+ ```
324
+
325
+ Alle Teile sind optional und dürfen über mehrere Zeilen gehen.
326
+
327
+ | Teil | Bedeutung | Beispiel |
328
+ |---|---|---|
329
+ | `event` | Name eines Eingangs: Der Übergang wird nur geprüft, wenn sich dieser Eingang ändert | `motion` |
330
+ | `after(n, einheit)` | Statt eines Events: gültig ab *n* Einheiten nach Betreten des Zustands. Dann weckt ein Timer das Chart, und die Bedingung wird bei jedem weiteren Aufwecken erneut geprüft. Einheiten: `msec`, `sec`, `min` | `after(30, sec)` |
331
+ | `[bedingung]` | JavaScript-Ausdruck, der Übergang wird nur genommen, wenn er wahr ist | `[temp > 21 && !fenster]` |
332
+ | `{aktion}` | JavaScript, wird ausgeführt, wenn der Übergang genommen wird | `{zaehler++; licht = true;}` |
333
+
334
+ Ohne Event wird ein Übergang bei jeder Änderung eines beliebigen Eingangs geprüft. Hat ein Zustand
335
+ mehrere ausgehende Übergänge, werden sie in der Reihenfolge ihrer **Priorität** geprüft (steht am Pfeil).
336
+
337
+ ### Wann Eingänge das Chart aufwecken
338
+
339
+ Jeder Eingang hat im Reiter **Variablen** drei Einstellungen:
340
+
341
+ | Einstellung | Werte | Standard |
342
+ |---|---|---|
343
+ | **Aufwecken bei** | jeder Aktualisierung / nur bei Wertänderung | jeder Aktualisierung |
344
+ | **Bestätigt (ack)** | egal / nur bestätigte Werte (`ack=true`) / nur Befehle (`ack=false`) | egal |
345
+ | **Entprellen (ms)** | das Chart wird erst geweckt, wenn der Eingang so lange keine Aktualisierung bekommen hat, mit dem letzten Wert | 0 (aus) |
346
+
347
+ Aktualisierungen, die wegen ack aussortiert werden, ändern die Variable nicht. Sie enthält dann z.B. nur bestätigte Werte.
348
+
349
+ ### Aktionen
350
+
351
+ Aktionen und Bedingungen sind JavaScript. Alle Variablen des Charts sind direkt über ihren Namen
352
+ verfügbar, dazu `Math`, `Date`, `JSON` usw. sowie `log("Text")` zum Schreiben ins ioBroker-Log.
353
+
354
+ | Aktion | Wird ausgeführt |
355
+ |---|---|
356
+ | `entry` | beim Betreten des Zustands |
357
+ | `during` | bei jedem Aufwecken des Charts, solange der Zustand aktiv ist und keiner seiner ausgehenden Übergänge genommen wird |
358
+ | `exit` | beim Verlassen des Zustands |
359
+
360
+ ### Verschachtelte Zustände
361
+
362
+ Lass einen Zustand über einem anderen los, um ihn zum **Unterzustand** zu machen, und zieh ihn wieder
363
+ heraus, um das rückgängig zu machen. Beim Ziehen wird der Zustand hervorgehoben, in den er verschoben
364
+ würde. Jede Ebene hat ihren eigenen Startzustand (Punkt mit Pfeil).
365
+
366
+ - Beim Betreten eines Superstates wird auch sein Start-Unterzustand betreten (oder der Unterzustand,
367
+ auf den der Übergang zeigt). Beim Verlassen wird zuerst der aktive Unterzustand verlassen.
368
+ - Bei jedem Aufwecken wird jeder aktive Zustand in dieser Reihenfolge bearbeitet, beginnend mit dem äußersten:
369
+ 1. Übergänge, die den Zustand verlassen, 2. seine `during`-Aktion, 3. seine **inneren Übergänge**,
370
+ 4. sein aktiver Unterzustand. Die Übergänge eines Superstates haben also Vorrang vor denen seiner Unterzustände.
371
+ - Ein Übergang von einem Zustand zu einem seiner eigenen Unterzustände ist ein **innerer Übergang**
372
+ (Punkt am Anfang): Der Zustand bleibt aktiv, nur sein aktiver Unterzustand wird verlassen, seine
373
+ eigenen `entry`/`exit` laufen nicht. Mit der Option **Elternzustand verlassen und neu betreten** wird
374
+ er stattdessen verlassen und neu betreten.
375
+
376
+ ### Hilfsfunktionen
377
+
378
+ In Bedingungen, Aktionen und Funktionsblöcken:
379
+
380
+ | Funktion | Zweck |
381
+ |---|---|
382
+ | `log(text, stufe)` | schreibt ins ioBroker-Log, Stufe `"debug"`, `"info"` (Standard), `"warn"` oder `"error"` |
383
+ | `setState(id, wert, ack)` | schreibt einen beliebigen Datenpunkt, `ack` ist standardmäßig `false` |
384
+ | `setStateDelayed(id, wert, ms, ack)` | schreibt verzögert, wird beim Anhalten des Charts verworfen |
385
+ | `sendTo(instanz, befehl, nachricht)` | sendet eine Nachricht an einen Adapter, z.B. `sendTo("telegram.0", "send", { text: "Fenster offen" })` |
386
+ | `isTimeBetween("22:00", "06:00")` | wahr, wenn die aktuelle Uhrzeit im Bereich liegt, auch über Mitternacht |
387
+ | `isDaytime()` | wahr zwischen Sonnenaufgang und Sonnenuntergang |
388
+ | `astroTime("sunset", minuten)` | Zeitpunkt eines Astro-Ereignisses heute als `Date`: `sunrise`, `sunset`, `dawn`, `dusk`, `solarNoon`, ... |
389
+
390
+ `isDaytime()` und `astroTime()` nutzen den Standort aus den ioBroker-Systemeinstellungen. Andere Datenpunkte liest
391
+ man über Eingänge: Ihre Variablen sind immer aktuell und wecken das Chart. Zeitabhängige Funktionen wecken das
392
+ Chart nicht von selbst, sie werden beim Aufwecken ausgewertet.
393
+
394
+ ### Funktionsblöcke
395
+
396
+ **Funktion hinzufügen** legt einen Block für eigenes JavaScript an. Er wird beim Start des Charts
397
+ einmal ausgeführt. Alle darin deklarierten Funktionen und Konstanten können in jeder Bedingung und
398
+ Aktion des Charts verwendet werden:
399
+
400
+ ```js
401
+ const LIMIT = 22;
402
+
403
+ function zuWarm(temp) {
404
+ return temp > LIMIT;
405
+ }
406
+ ```
407
+
408
+ Beschriftung eines Übergangs: `[zuWarm(temperatur)]{heizung = false;}`
409
+
410
+ ### Kommentare und Notizen
411
+
412
+ - **Kommentar:** Zustände, Übergänge, Funktionsblöcke und das Chart selbst haben in ihren Eigenschaften ein Feld
413
+ **Kommentar**. Eine Sprechblase auf der Zeichenfläche zeigt, dass es einen Kommentar gibt, der Kommentar erscheint
414
+ als Tooltip.
415
+ - **Notizen** (Symbol **Notiz hinzufügen** in der Werkzeugleiste) sind freie Texte auf der Zeichenfläche, wie
416
+ Haftzettel. Lass eine Notiz über einem Zustand los, dann bewegt sie sich mit ihm.
417
+
418
+ Kommentare und Notizen haben keinen Einfluss auf die Ausführung. Sie stehen im generierten Code und in exportierten
419
+ Dateien, und wenn sie sich ändern, wird der Zustand nach einem Neustart trotzdem wiederhergestellt.
420
+
421
+ ### Import und Export
422
+
423
+ - **Chart exportieren** (letztes Symbol in der Werkzeugleiste des Editors) speichert das Chart als JSON-Datei, mit
424
+ ungespeicherten Änderungen.
425
+ - **Alle Charts exportieren** (Symbol über der Chart-Liste) speichert alle gespeicherten Charts in einer Datei.
426
+ - **Charts importieren** (Symbol über der Chart-Liste) fügt die Charts einer solchen Datei als neue Charts hinzu,
427
+ vorhandene Charts werden nicht verändert. Die Datei wird vorher geprüft (Aufbau, IDs, Verweise zwischen Zuständen
428
+ und Übergängen).
429
+
430
+ Importierte Charts sind **angehalten**: Ihre Variablen können auf Datenpunkte eines anderen Systems verweisen. Prüfe sie
431
+ im Reiter **Variablen** und starte das Chart dann mit dem Knopf in der Chart-Liste.
432
+
433
+ ### Vom Adapter angelegte Datenpunkte
434
+
435
+ Für jedes Chart legt der Adapter unter `statechart.<instanz>.charts.<chart>` an:
436
+
437
+ | Datenpunkt | Inhalt |
438
+ |---|---|
439
+ | `activeState` | Name des aktiven Zustands, verschachtelte Zustände als `Eltern.Kind` |
440
+ | `activeStateIds` | IDs aller aktiven Zustände als JSON |
441
+ | `snapshot` | Gespeicherter Zustand des Charts (aktive Zustände, wann sie betreten wurden, lokale Variablen), siehe unten |
442
+
443
+ Das Chart selbst ist im Objekt `statechart.<instanz>.charts.<chart>` gespeichert.
444
+
445
+ ### Neustart des Adapters
446
+
447
+ Nach einem Neustart des Adapters oder von ioBroker macht jedes Chart in den Zuständen weiter, die
448
+ vorher aktiv waren, mit seinen lokalen Variablen. Die entry-Aktionen laufen nicht noch einmal, und Timer
449
+ laufen mit ihrer Restzeit weiter. Ein Timer, der abgelaufen ist, während der Adapter gestoppt war, greift
450
+ gleich nach dem Start.
451
+
452
+ Im Startzustand beginnt das Chart stattdessen, wenn es zwischendurch geändert wurde (Positionen im
453
+ Editor zählen nicht), nach dem Speichern im Editor, nach dem Anhalten und wenn die Option **Zustand nach
454
+ Neustart wiederherstellen** in den Chart-Eigenschaften ausgeschaltet ist. Variablen, die in
455
+ Funktionsblöcken deklariert sind, werden nicht wiederhergestellt.
456
+
457
+ ### Ausführung und Unterschiede zu anderen Statechart-Werkzeugen
458
+
459
+ - Der Adapter führt die Charts mit [XState](https://stately.ai/docs/xstate) aus.
460
+ - Jede Änderung eines Eingangs weckt das Chart einmal auf. Übergänge ohne Event werden bei jedem Aufwecken geprüft.
461
+ - `{aktion}` wird während des Übergangs ausgeführt, also nach `exit` des alten und vor `entry` des
462
+ neuen Zustands. Eine eigene Condition Action und `/aktion` werden (noch) nicht unterstützt.
463
+ - Übergänge werden wie bei Stateflow ausgewählt (außen vor innen, siehe oben), XState führt sie nur aus.
464
+ Reines XState würde die Übergänge der Unterzustände zuerst prüfen.
465
+ - Zeitlogik gibt es nur als `after(n, msec|sec|min)`, gemessen in Echtzeit.
466
+ - Bedingungen und Aktionen sind JavaScript, nicht MATLAB- oder C-Syntax.
467
+
468
+ ### Einschränkungen
469
+
470
+ - Parallele Zustände (AND), History- und Verbindungspunkte werden noch nicht unterstützt.
471
+ - Beim Löschen eines Superstates werden auch seine Unterzustände gelöscht.
472
+ - Ungespeicherte Änderungen gehen verloren, wenn ein anderes Chart ausgewählt wird.
473
+ - Bedingungen und Aktionen laufen in einem eigenen JavaScript-Kontext mit 1 Sekunde Zeitlimit, das
474
+ ist aber **keine Sicherheits-Sandbox**: Wer Charts bearbeiten darf, kann im Adapter beliebigen Code
475
+ ausführen, wie beim JavaScript-Adapter auch.
476
+
477
+ ---
478
+
479
+ ## Development / Entwicklung
480
+
481
+ ```bash
482
+ npm install
483
+ npm run build # build the admin tab
484
+ npm run dev-server setup # once: local ioBroker test instance in .dev-server/
485
+ npm run dev-server watch # run it, admin on http://127.0.0.1:8081/
486
+ npm run upload:tab # in a second terminal: upload the tab into the test instance
487
+ ```
488
+
489
+ | Script | Description |
490
+ |---|---|
491
+ | `dev-server` | Runs [`dev-server`](https://github.com/ioBroker/dev-server) with the given command. Before, it applies the patches in `patches/`: the dev-server uses `wmic.exe` to find the adapter process, which no longer exists in Windows 11 25H2 |
492
+ | `build` / `watch` | Build the React sources of the tab (`admin/src`) to `admin/build` |
493
+ | `upload:tab` | Build the tab and write the admin files into the running dev-server instance (works while `dev-server watch` runs) |
494
+ | `test` | Unit tests (`lib/**/*.test.js`) and package tests |
495
+ | `check` | Type check of the adapter (`npx tsc -p admin/tsconfig.json --noEmit` for the tab) |
496
+ | `lint` | ESLint |
497
+ | `translate` | Translate the texts of the adapter |
498
+ | `release` | Create a new release |
499
+
500
+ After changes of `io-package.json`, run `npm run dev-server upload` and restart `npm run dev-server watch`.
501
+
502
+ **The built tab (`admin/build`) is committed**, because installations from GitHub do not run a build.
503
+ After changes in `admin/src`, run `npm run build` and commit `admin/build` as well. The workflow job
504
+ `check-admin-build` fails if it is outdated. The source maps (`admin/build/*.map`) are not committed.
505
+
506
+ | Path | Content |
507
+ |---|---|
508
+ | `main.js` | Adapter: loads the charts, subscribes the inputs, restarts a chart when it is saved |
509
+ | `lib/chart-model.js` | Chart model (shared by adapter and editor) and parser of the transition labels |
510
+ | `lib/chart-runtime.js` | Execution of a chart with XState |
511
+ | `lib/chart-validation.js` | Checks of conditions, actions and function blocks |
512
+ | `admin/src/` | Editor (React, React Flow, CodeMirror) |
513
+ | `admin/jsonConfig.json` | Instance settings |
514
+
515
+ ## Disclaimer
516
+
517
+ MATLAB and Stateflow are registered trademarks of The MathWorks, Inc. This adapter is not affiliated
518
+ with, endorsed or sponsored by The MathWorks, Inc.
519
+
520
+ ## Changelog
521
+ <!--
522
+ Placeholder for the next version (at the beginning of the line):
523
+ ### **WORK IN PROGRESS**
524
+ -->
525
+ ### 0.1.1 (2026-10-25)
526
+ * (Igge) renamed from "state-machine" to "statechart", because npm does not accept the old name (too similar to an existing package)
527
+
528
+ ### 0.1.0 (2026-10-25)
529
+ * (Igge) initial release
530
+ * (Igge) graphical chart editor in the admin side bar: states, transitions, nested states, function blocks, notes
531
+ * (Igge) transition labels in Stateflow syntax event[condition]{action}, timers with after(n, unit)
532
+ * (Igge) execution with Stateflow semantics (outer before inner transitions, inner transitions)
533
+ * (Igge) charts continue in their state after a restart of the adapter
534
+ * (Igge) per input: wake up on updates or changes, filter by ack, debounce
535
+ * (Igge) helper functions: setState, setStateDelayed, sendTo, isTimeBetween, isDaytime, astroTime
536
+ * (Igge) code editor with syntax highlighting, error checks and completion, generated code view
537
+ * (Igge) undo/redo, copy/paste, import/export of charts as JSON
538
+
539
+ ## License
540
+ MIT License
541
+
542
+ Copyright (c) 2026 Igge <mail@igge.org>
543
+
544
+ Permission is hereby granted, free of charge, to any person obtaining a copy
545
+ of this software and associated documentation files (the "Software"), to deal
546
+ in the Software without restriction, including without limitation the rights
547
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
548
+ copies of the Software, and to permit persons to whom the Software is
549
+ furnished to do so, subject to the following conditions:
550
+
551
+ The above copyright notice and this permission notice shall be included in all
552
+ copies or substantial portions of the Software.
553
+
554
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
555
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
556
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
557
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
558
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
559
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
560
+ SOFTWARE.
@@ -0,0 +1,2 @@
1
+ .react-flow{direction:ltr;--xy-edge-stroke-default: #b1b1b7;--xy-edge-stroke-width-default: 1;--xy-edge-stroke-selected-default: #555;--xy-connectionline-stroke-default: #b1b1b7;--xy-connectionline-stroke-width-default: 1;--xy-attribution-background-color-default: rgba(255, 255, 255, .5);--xy-minimap-background-color-default: #fff;--xy-minimap-mask-background-color-default: rgba(240, 240, 240, .6);--xy-minimap-mask-stroke-color-default: transparent;--xy-minimap-mask-stroke-width-default: 1;--xy-minimap-node-background-color-default: #e2e2e2;--xy-minimap-node-stroke-color-default: transparent;--xy-minimap-node-stroke-width-default: 2;--xy-background-color-default: transparent;--xy-background-pattern-dots-color-default: #91919a;--xy-background-pattern-lines-color-default: #eee;--xy-background-pattern-cross-color-default: #e2e2e2;background-color:var(--xy-background-color, var(--xy-background-color-default));--xy-node-color-default: inherit;--xy-node-border-default: 1px solid #1a192b;--xy-node-background-color-default: #fff;--xy-node-group-background-color-default: rgba(240, 240, 240, .25);--xy-node-boxshadow-hover-default: 0 1px 4px 1px rgba(0, 0, 0, .08);--xy-node-boxshadow-selected-default: 0 0 0 .5px #1a192b;--xy-node-border-radius-default: 3px;--xy-handle-background-color-default: #1a192b;--xy-handle-border-color-default: #fff;--xy-selection-background-color-default: rgba(0, 89, 220, .08);--xy-selection-border-default: 1px dotted rgba(0, 89, 220, .8);--xy-controls-button-background-color-default: #fefefe;--xy-controls-button-background-color-hover-default: #f4f4f4;--xy-controls-button-color-default: inherit;--xy-controls-button-color-hover-default: inherit;--xy-controls-button-border-color-default: #eee;--xy-controls-box-shadow-default: 0 0 2px 1px rgba(0, 0, 0, .08);--xy-edge-label-background-color-default: #ffffff;--xy-edge-label-color-default: inherit;--xy-resize-background-color-default: #3367d9}.react-flow.dark{--xy-edge-stroke-default: #3e3e3e;--xy-edge-stroke-width-default: 1;--xy-edge-stroke-selected-default: #727272;--xy-connectionline-stroke-default: #b1b1b7;--xy-connectionline-stroke-width-default: 1;--xy-attribution-background-color-default: rgba(150, 150, 150, .25);--xy-minimap-background-color-default: #141414;--xy-minimap-mask-background-color-default: rgba(60, 60, 60, .6);--xy-minimap-mask-stroke-color-default: transparent;--xy-minimap-mask-stroke-width-default: 1;--xy-minimap-node-background-color-default: #2b2b2b;--xy-minimap-node-stroke-color-default: transparent;--xy-minimap-node-stroke-width-default: 2;--xy-background-color-default: #141414;--xy-background-pattern-dots-color-default: #555;--xy-background-pattern-lines-color-default: #333;--xy-background-pattern-cross-color-default: #333;--xy-node-color-default: #f8f8f8;--xy-node-border-default: 1px solid #3c3c3c;--xy-node-background-color-default: #1e1e1e;--xy-node-group-background-color-default: rgba(240, 240, 240, .25);--xy-node-boxshadow-hover-default: 0 1px 4px 1px rgba(255, 255, 255, .08);--xy-node-boxshadow-selected-default: 0 0 0 .5px #999;--xy-handle-background-color-default: #bebebe;--xy-handle-border-color-default: #1e1e1e;--xy-selection-background-color-default: rgba(200, 200, 220, .08);--xy-selection-border-default: 1px dotted rgba(200, 200, 220, .8);--xy-controls-button-background-color-default: #2b2b2b;--xy-controls-button-background-color-hover-default: #3e3e3e;--xy-controls-button-color-default: #f8f8f8;--xy-controls-button-color-hover-default: #fff;--xy-controls-button-border-color-default: #5b5b5b;--xy-controls-box-shadow-default: 0 0 2px 1px rgba(0, 0, 0, .08);--xy-edge-label-background-color-default: #141414;--xy-edge-label-color-default: #f8f8f8}.react-flow__background{background-color:var(--xy-background-color-props, var(--xy-background-color, var(--xy-background-color-default)));pointer-events:none;z-index:-1}.react-flow__container{position:absolute;width:100%;height:100%;top:0;left:0}.react-flow__pane{z-index:1;touch-action:none}.react-flow__pane.draggable{cursor:grab}.react-flow__pane.dragging{cursor:grabbing}.react-flow__pane.selection{cursor:pointer}.react-flow__viewport{transform-origin:0 0;z-index:2;pointer-events:none}.react-flow__renderer{z-index:4}.react-flow__selection{z-index:6}.react-flow__nodesselection-rect:focus,.react-flow__nodesselection-rect:focus-visible{outline:none}.react-flow__edge-path{stroke:var(--xy-edge-stroke, var(--xy-edge-stroke-default));stroke-width:var(--xy-edge-stroke-width, var(--xy-edge-stroke-width-default));fill:none}.react-flow__connection-path{stroke:var(--xy-connectionline-stroke, var(--xy-connectionline-stroke-default));stroke-width:var(--xy-connectionline-stroke-width, var(--xy-connectionline-stroke-width-default));fill:none}.react-flow .react-flow__edges{position:absolute}.react-flow .react-flow__edges svg{overflow:visible;position:absolute;pointer-events:none}.react-flow__edge{pointer-events:visibleStroke}.react-flow__edge.selectable{cursor:pointer}.react-flow__edge.animated path{stroke-dasharray:5;animation:dashdraw .5s linear infinite}.react-flow__edge.animated path.react-flow__edge-interaction{stroke-dasharray:none;animation:none}.react-flow__edge.inactive{pointer-events:none}.react-flow__edge.selected,.react-flow__edge:focus,.react-flow__edge:focus-visible{outline:none}.react-flow__edge.selected .react-flow__edge-path,.react-flow__edge.selectable:focus .react-flow__edge-path,.react-flow__edge.selectable:focus-visible .react-flow__edge-path{stroke:var(--xy-edge-stroke-selected, var(--xy-edge-stroke-selected-default))}.react-flow__edge-textwrapper{pointer-events:all}.react-flow__edge .react-flow__edge-text{pointer-events:none;-webkit-user-select:none;-moz-user-select:none;user-select:none}.react-flow__arrowhead polyline{stroke:var(--xy-edge-stroke, var(--xy-edge-stroke-default))}.react-flow__arrowhead polyline.arrowclosed{fill:var(--xy-edge-stroke, var(--xy-edge-stroke-default))}.react-flow__connection{pointer-events:none}.react-flow__connection .animated{stroke-dasharray:5;animation:dashdraw .5s linear infinite}svg.react-flow__connectionline{z-index:1001;overflow:visible;position:absolute}.react-flow__nodes{pointer-events:none;transform-origin:0 0}.react-flow__node{position:absolute;-webkit-user-select:none;-moz-user-select:none;user-select:none;pointer-events:all;transform-origin:0 0;box-sizing:border-box;cursor:default}.react-flow__node.selectable{cursor:pointer}.react-flow__node.draggable{cursor:grab;pointer-events:all}.react-flow__node.draggable.dragging{cursor:grabbing}.react-flow__nodesselection{z-index:3;transform-origin:left top;pointer-events:none}.react-flow__nodesselection-rect{position:absolute;pointer-events:all;cursor:grab}.react-flow__handle{position:absolute;pointer-events:none;min-width:5px;min-height:5px;width:6px;height:6px;background-color:var(--xy-handle-background-color, var(--xy-handle-background-color-default));border:1px solid var(--xy-handle-border-color, var(--xy-handle-border-color-default));border-radius:100%}.react-flow__handle.connectingfrom{pointer-events:all}.react-flow__handle.connectionindicator{pointer-events:all;cursor:crosshair}.react-flow__handle-bottom{top:auto;left:50%;bottom:0;transform:translate(-50%,50%)}.react-flow__handle-top{top:0;left:50%;transform:translate(-50%,-50%)}.react-flow__handle-left{top:50%;left:0;transform:translate(-50%,-50%)}.react-flow__handle-right{top:50%;right:0;transform:translate(50%,-50%)}.react-flow__edgeupdater{cursor:move;pointer-events:all}.react-flow__pane.selection .react-flow__panel{pointer-events:none}.react-flow__panel{position:absolute;z-index:5;margin:15px}.react-flow__panel.top{top:0}.react-flow__panel.bottom{bottom:0}.react-flow__panel.top.center,.react-flow__panel.bottom.center{left:50%;transform:translate(-15px) translate(-50%)}.react-flow__panel.left{left:0}.react-flow__panel.right{right:0}.react-flow__panel.left.center,.react-flow__panel.right.center{top:50%;transform:translateY(-15px) translateY(-50%)}.react-flow__attribution{font-size:10px;background:var(--xy-attribution-background-color, var(--xy-attribution-background-color-default));padding:2px 3px;margin:0}.react-flow__attribution a{text-decoration:none;color:#999}@keyframes dashdraw{0%{stroke-dashoffset:10}}.react-flow__edgelabel-renderer{position:absolute;width:100%;height:100%;pointer-events:none;-webkit-user-select:none;-moz-user-select:none;user-select:none;left:0;top:0}.react-flow__viewport-portal{position:absolute;width:100%;height:100%;left:0;top:0;-webkit-user-select:none;-moz-user-select:none;user-select:none}.react-flow__minimap{background:var( --xy-minimap-background-color-props, var(--xy-minimap-background-color, var(--xy-minimap-background-color-default)) )}.react-flow__minimap-svg{display:block}.react-flow__minimap-mask{fill:var( --xy-minimap-mask-background-color-props, var(--xy-minimap-mask-background-color, var(--xy-minimap-mask-background-color-default)) );stroke:var( --xy-minimap-mask-stroke-color-props, var(--xy-minimap-mask-stroke-color, var(--xy-minimap-mask-stroke-color-default)) );stroke-width:var( --xy-minimap-mask-stroke-width-props, var(--xy-minimap-mask-stroke-width, var(--xy-minimap-mask-stroke-width-default)) )}.react-flow__minimap-node{fill:var( --xy-minimap-node-background-color-props, var(--xy-minimap-node-background-color, var(--xy-minimap-node-background-color-default)) );stroke:var( --xy-minimap-node-stroke-color-props, var(--xy-minimap-node-stroke-color, var(--xy-minimap-node-stroke-color-default)) );stroke-width:var( --xy-minimap-node-stroke-width-props, var(--xy-minimap-node-stroke-width, var(--xy-minimap-node-stroke-width-default)) )}.react-flow__background-pattern.dots{fill:var( --xy-background-pattern-color-props, var(--xy-background-pattern-color, var(--xy-background-pattern-dots-color-default)) )}.react-flow__background-pattern.lines{stroke:var( --xy-background-pattern-color-props, var(--xy-background-pattern-color, var(--xy-background-pattern-lines-color-default)) )}.react-flow__background-pattern.cross{stroke:var( --xy-background-pattern-color-props, var(--xy-background-pattern-color, var(--xy-background-pattern-cross-color-default)) )}.react-flow__controls{display:flex;flex-direction:column;box-shadow:var(--xy-controls-box-shadow, var(--xy-controls-box-shadow-default))}.react-flow__controls.horizontal{flex-direction:row}.react-flow__controls-button{display:flex;justify-content:center;align-items:center;height:26px;width:26px;padding:4px;border:none;background:var(--xy-controls-button-background-color, var(--xy-controls-button-background-color-default));border-bottom:1px solid var( --xy-controls-button-border-color-props, var(--xy-controls-button-border-color, var(--xy-controls-button-border-color-default)) );color:var( --xy-controls-button-color-props, var(--xy-controls-button-color, var(--xy-controls-button-color-default)) );cursor:pointer;-webkit-user-select:none;-moz-user-select:none;user-select:none}.react-flow__controls-button svg{width:100%;max-width:12px;max-height:12px;fill:currentColor}.react-flow__edge.updating .react-flow__edge-path{stroke:#777}.react-flow__edge-text{font-size:10px}.react-flow__node.selectable:focus,.react-flow__node.selectable:focus-visible{outline:none}.react-flow__node-input,.react-flow__node-default,.react-flow__node-output,.react-flow__node-group{padding:10px;border-radius:var(--xy-node-border-radius, var(--xy-node-border-radius-default));width:150px;font-size:12px;color:var(--xy-node-color, var(--xy-node-color-default));text-align:center;border:var(--xy-node-border, var(--xy-node-border-default));background-color:var(--xy-node-background-color, var(--xy-node-background-color-default))}.react-flow__node-input.selectable:hover,.react-flow__node-default.selectable:hover,.react-flow__node-output.selectable:hover,.react-flow__node-group.selectable:hover{box-shadow:var(--xy-node-boxshadow-hover, var(--xy-node-boxshadow-hover-default))}.react-flow__node-input.selectable.selected,.react-flow__node-input.selectable:focus,.react-flow__node-input.selectable:focus-visible,.react-flow__node-default.selectable.selected,.react-flow__node-default.selectable:focus,.react-flow__node-default.selectable:focus-visible,.react-flow__node-output.selectable.selected,.react-flow__node-output.selectable:focus,.react-flow__node-output.selectable:focus-visible,.react-flow__node-group.selectable.selected,.react-flow__node-group.selectable:focus,.react-flow__node-group.selectable:focus-visible{box-shadow:var(--xy-node-boxshadow-selected, var(--xy-node-boxshadow-selected-default))}.react-flow__node-group{background-color:var(--xy-node-group-background-color, var(--xy-node-group-background-color-default))}.react-flow__nodesselection-rect,.react-flow__selection{background:var(--xy-selection-background-color, var(--xy-selection-background-color-default));border:var(--xy-selection-border, var(--xy-selection-border-default))}.react-flow__nodesselection-rect:focus,.react-flow__nodesselection-rect:focus-visible,.react-flow__selection:focus,.react-flow__selection:focus-visible{outline:none}.react-flow__controls-button:hover{background:var( --xy-controls-button-background-color-hover-props, var(--xy-controls-button-background-color-hover, var(--xy-controls-button-background-color-hover-default)) );color:var( --xy-controls-button-color-hover-props, var(--xy-controls-button-color-hover, var(--xy-controls-button-color-hover-default)) )}.react-flow__controls-button:disabled{pointer-events:none}.react-flow__controls-button:disabled svg{fill-opacity:.4}.react-flow__controls-button:last-child{border-bottom:none}.react-flow__controls.horizontal .react-flow__controls-button{border-bottom:none;border-right:1px solid var( --xy-controls-button-border-color-props, var(--xy-controls-button-border-color, var(--xy-controls-button-border-color-default)) )}.react-flow__controls.horizontal .react-flow__controls-button:last-child{border-right:none}.react-flow__resize-control{position:absolute}.react-flow__resize-control.left,.react-flow__resize-control.right{cursor:ew-resize}.react-flow__resize-control.top,.react-flow__resize-control.bottom{cursor:ns-resize}.react-flow__resize-control.top.left,.react-flow__resize-control.bottom.right{cursor:nwse-resize}.react-flow__resize-control.bottom.left,.react-flow__resize-control.top.right{cursor:nesw-resize}.react-flow__resize-control.handle{width:5px;height:5px;border:1px solid #fff;border-radius:1px;background-color:var(--xy-resize-background-color, var(--xy-resize-background-color-default));translate:-50% -50%}.react-flow__resize-control.handle.left{left:0;top:50%}.react-flow__resize-control.handle.right{left:100%;top:50%}.react-flow__resize-control.handle.top{left:50%;top:0}.react-flow__resize-control.handle.bottom{left:50%;top:100%}.react-flow__resize-control.handle.top.left,.react-flow__resize-control.handle.bottom.left{left:0}.react-flow__resize-control.handle.top.right,.react-flow__resize-control.handle.bottom.right{left:100%}.react-flow__resize-control.line{border-color:var(--xy-resize-background-color, var(--xy-resize-background-color-default));border-width:0;border-style:solid}.react-flow__resize-control.line.left,.react-flow__resize-control.line.right{width:1px;transform:translate(-50%);top:0;height:100%}.react-flow__resize-control.line.left{left:0;border-left-width:1px}.react-flow__resize-control.line.right{left:100%;border-right-width:1px}.react-flow__resize-control.line.top,.react-flow__resize-control.line.bottom{height:1px;transform:translateY(-50%);left:0;width:100%}.react-flow__resize-control.line.top{top:0;border-top-width:1px}.react-flow__resize-control.line.bottom{border-bottom-width:1px;top:100%}.react-flow__edge-textbg{fill:var(--xy-edge-label-background-color, var(--xy-edge-label-background-color-default))}.react-flow__edge-text{fill:var(--xy-edge-label-color, var(--xy-edge-label-color-default))}
2
+ /*# sourceMappingURL=tab.css.map */