iobroker.statechart 0.1.1 → 0.2.0
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 +130 -21
- package/admin/build/tab.js +132 -130
- package/admin/statechart.png +0 -0
- package/build/lib/chart-codegen.js +338 -0
- package/build/lib/chart-codegen.js.map +7 -0
- package/build/lib/chart-helpers.js +90 -0
- package/build/lib/chart-helpers.js.map +7 -0
- package/build/lib/chart-model.js +353 -0
- package/build/lib/chart-model.js.map +7 -0
- package/build/lib/chart-runtime.js +876 -0
- package/build/lib/chart-runtime.js.map +7 -0
- package/build/lib/chart-structure.js +101 -0
- package/build/lib/chart-structure.js.map +7 -0
- package/build/lib/chart-validation.js +303 -0
- package/build/lib/chart-validation.js.map +7 -0
- package/build/main.js +342 -0
- package/build/main.js.map +7 -0
- package/io-package.json +14 -1
- package/package.json +20 -17
- package/lib/adapter-config.d.ts +0 -19
- package/lib/chart-codegen.js +0 -436
- package/lib/chart-codegen.test.js +0 -132
- package/lib/chart-helpers.js +0 -74
- package/lib/chart-helpers.test.js +0 -38
- package/lib/chart-model.js +0 -577
- package/lib/chart-model.test.js +0 -189
- package/lib/chart-runtime.js +0 -797
- package/lib/chart-runtime.test.js +0 -699
- package/lib/chart-structure.js +0 -106
- package/lib/chart-validation.js +0 -328
- package/lib/chart-validation.test.js +0 -149
- package/main.js +0 -279
|
@@ -0,0 +1,876 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __create = Object.create;
|
|
3
|
+
var __defProp = Object.defineProperty;
|
|
4
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
5
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
6
|
+
var __getProtoOf = Object.getPrototypeOf;
|
|
7
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
8
|
+
var __export = (target, all) => {
|
|
9
|
+
for (var name in all)
|
|
10
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
11
|
+
};
|
|
12
|
+
var __copyProps = (to, from, except, desc) => {
|
|
13
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
14
|
+
for (let key of __getOwnPropNames(from))
|
|
15
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
16
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
17
|
+
}
|
|
18
|
+
return to;
|
|
19
|
+
};
|
|
20
|
+
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
|
|
21
|
+
// If the importer is in node compatibility mode or this is not an ESM
|
|
22
|
+
// file that has been converted to a CommonJS file using a Babel-
|
|
23
|
+
// compatible transform (i.e. "__esModule" has not been set), then set
|
|
24
|
+
// "default" to the CommonJS "module.exports" for node compatibility.
|
|
25
|
+
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
|
|
26
|
+
mod
|
|
27
|
+
));
|
|
28
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
29
|
+
var chart_runtime_exports = {};
|
|
30
|
+
__export(chart_runtime_exports, {
|
|
31
|
+
ChartRuntime: () => ChartRuntime,
|
|
32
|
+
getChartHash: () => getChartHash
|
|
33
|
+
});
|
|
34
|
+
module.exports = __toCommonJS(chart_runtime_exports);
|
|
35
|
+
var import_node_crypto = __toESM(require("node:crypto"));
|
|
36
|
+
var import_node_util = require("node:util");
|
|
37
|
+
var import_node_vm = __toESM(require("node:vm"));
|
|
38
|
+
var import_suncalc = __toESM(require("suncalc"));
|
|
39
|
+
var import_xstate = require("xstate");
|
|
40
|
+
var import_chart_helpers = require("./chart-helpers");
|
|
41
|
+
var import_chart_model = require("./chart-model");
|
|
42
|
+
var import_chart_structure = require("./chart-structure");
|
|
43
|
+
var import_chart_validation = require("./chart-validation");
|
|
44
|
+
const SCRIPT_TIMEOUT_MS = 1e3;
|
|
45
|
+
const MAX_TIMEOUT_MS = 2 ** 31 - 1;
|
|
46
|
+
const PERSISTED_VERSION = 1;
|
|
47
|
+
function getChartHash(chart) {
|
|
48
|
+
const logic = {
|
|
49
|
+
initial: chart.initial,
|
|
50
|
+
symbols: chart.symbols,
|
|
51
|
+
// comments and notes are only documentation
|
|
52
|
+
states: chart.states.map(({ x: _x, y: _y, width: _w, height: _h, comment: _c, ...state }) => state),
|
|
53
|
+
transitions: chart.transitions.map(
|
|
54
|
+
({
|
|
55
|
+
sourceHandle: _s,
|
|
56
|
+
targetHandle: _t,
|
|
57
|
+
sourceAnchor: _sa,
|
|
58
|
+
targetAnchor: _ta,
|
|
59
|
+
comment: _c,
|
|
60
|
+
...transition
|
|
61
|
+
}) => transition
|
|
62
|
+
),
|
|
63
|
+
functions: (chart.functions || []).map((fn) => fn.code)
|
|
64
|
+
};
|
|
65
|
+
return import_node_crypto.default.createHash("sha1").update(JSON.stringify(logic)).digest("hex");
|
|
66
|
+
}
|
|
67
|
+
class ChartRuntime {
|
|
68
|
+
constructor(chartId, chart, host) {
|
|
69
|
+
this.chartId = chartId;
|
|
70
|
+
this.chart = chart;
|
|
71
|
+
this.host = host;
|
|
72
|
+
this.structure = new import_chart_structure.ChartStructure(chart);
|
|
73
|
+
this.statesById = this.structure.statesById;
|
|
74
|
+
this.sandbox = import_node_vm.default.createContext(this.createHelpers());
|
|
75
|
+
this.hash = getChartHash(chart);
|
|
76
|
+
this.prepareAllCode();
|
|
77
|
+
}
|
|
78
|
+
structure;
|
|
79
|
+
statesById;
|
|
80
|
+
/** timers of setStateDelayed() */
|
|
81
|
+
delayedWrites = /* @__PURE__ */ new Set();
|
|
82
|
+
/** values of all symbols, this is also the global scope of conditions and actions */
|
|
83
|
+
sandbox;
|
|
84
|
+
/** last value written to (or read from) each output */
|
|
85
|
+
outputValues = /* @__PURE__ */ new Map();
|
|
86
|
+
/** transitions which leave the source state, by source, sorted */
|
|
87
|
+
outerTransitions = /* @__PURE__ */ new Map();
|
|
88
|
+
/** transitions to children of the source state, by source, sorted */
|
|
89
|
+
innerTransitions = /* @__PURE__ */ new Map();
|
|
90
|
+
/** temporal transitions by source */
|
|
91
|
+
temporalTransitions = /* @__PURE__ */ new Map();
|
|
92
|
+
/** all active states, parents before children */
|
|
93
|
+
activeIds = [];
|
|
94
|
+
/** time each active state was entered */
|
|
95
|
+
enteredAt = /* @__PURE__ */ new Map();
|
|
96
|
+
/** timers of the temporal transitions by state */
|
|
97
|
+
timers = /* @__PURE__ */ new Map();
|
|
98
|
+
/** temporal transitions whose time has come */
|
|
99
|
+
expired = /* @__PURE__ */ new Set();
|
|
100
|
+
/** timers of inputs with debounce, by variable name */
|
|
101
|
+
debounceTimers = /* @__PURE__ */ new Map();
|
|
102
|
+
/** JSON of the last persisted data, to persist only changes */
|
|
103
|
+
lastPersisted = "";
|
|
104
|
+
/** name paths of the active leaf states, like the "activeState" state */
|
|
105
|
+
activeNames = "";
|
|
106
|
+
/** events which are not yet passed to host.onHistory() */
|
|
107
|
+
pendingHistory = [];
|
|
108
|
+
/** JSON of the last values passed to host.onValues(), to pass only changes */
|
|
109
|
+
lastValues = "";
|
|
110
|
+
/** what caused the current wakeup, for the history */
|
|
111
|
+
wakeupCause = "input";
|
|
112
|
+
/** name of the input which caused the current wakeup, for the history */
|
|
113
|
+
wakeupInput = null;
|
|
114
|
+
/** the code of conditions, actions and function blocks prepared for the change detection, by kind and code */
|
|
115
|
+
preparedCode = /* @__PURE__ */ new Map();
|
|
116
|
+
/** the variables used by hasChanged() etc. */
|
|
117
|
+
changeVariables = /* @__PURE__ */ new Set();
|
|
118
|
+
/** their values at the start of the previous wakeup, null before the second wakeup */
|
|
119
|
+
previousValues = null;
|
|
120
|
+
/** their values at the start of the current wakeup, null before the first one */
|
|
121
|
+
currentValues = null;
|
|
122
|
+
hash;
|
|
123
|
+
actor = null;
|
|
124
|
+
/** the current time, can be replaced in tests */
|
|
125
|
+
now = () => Date.now();
|
|
126
|
+
/**
|
|
127
|
+
* Prepares all code of the chart for the change detection, so its variables are known from the first wakeup on
|
|
128
|
+
*/
|
|
129
|
+
prepareAllCode() {
|
|
130
|
+
for (const state of this.chart.states) {
|
|
131
|
+
for (const code of [state.entry, state.during, state.exit]) {
|
|
132
|
+
this.prepareCode(code, "statements");
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
for (const transition of this.chart.transitions) {
|
|
136
|
+
try {
|
|
137
|
+
const label = (0, import_chart_model.parseTransitionLabel)(transition.label);
|
|
138
|
+
this.prepareCode(label.condition, "expression");
|
|
139
|
+
this.prepareCode(label.action, "statements");
|
|
140
|
+
} catch {
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
for (const fn of this.chart.functions || []) {
|
|
144
|
+
this.prepareCode(fn.code, "statements");
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
/**
|
|
148
|
+
* @param code A condition, action or function block
|
|
149
|
+
* @param kind Expression or statements
|
|
150
|
+
* @returns the code with the names of the variables in hasChanged() etc., see quoteChangeArguments()
|
|
151
|
+
*/
|
|
152
|
+
prepareCode(code, kind) {
|
|
153
|
+
if (!code) {
|
|
154
|
+
return "";
|
|
155
|
+
}
|
|
156
|
+
const key = `${kind}:${code}`;
|
|
157
|
+
let prepared = this.preparedCode.get(key);
|
|
158
|
+
if (prepared === void 0) {
|
|
159
|
+
const quoted = (0, import_chart_validation.quoteChangeArguments)(code, kind);
|
|
160
|
+
prepared = quoted.code;
|
|
161
|
+
for (const name of quoted.names) {
|
|
162
|
+
this.changeVariables.add(name);
|
|
163
|
+
}
|
|
164
|
+
this.preparedCode.set(key, prepared);
|
|
165
|
+
}
|
|
166
|
+
return prepared;
|
|
167
|
+
}
|
|
168
|
+
/**
|
|
169
|
+
* IDs of all states this chart reads
|
|
170
|
+
*/
|
|
171
|
+
get inputStateIds() {
|
|
172
|
+
return this.chart.symbols.filter((s) => s.scope === "input" && s.stateId).map((s) => s.stateId);
|
|
173
|
+
}
|
|
174
|
+
/**
|
|
175
|
+
* @param options Start options
|
|
176
|
+
* @param options.restore Continue with the persisted state, if the chart was not changed since
|
|
177
|
+
*/
|
|
178
|
+
async start(options = {}) {
|
|
179
|
+
this.logProblems();
|
|
180
|
+
for (const symbol of this.chart.symbols) {
|
|
181
|
+
if (symbol.scope === "local") {
|
|
182
|
+
this.sandbox[symbol.name] = structuredClone(symbol.initial);
|
|
183
|
+
} else if (symbol.stateId) {
|
|
184
|
+
const val = await this.host.readState(symbol.stateId);
|
|
185
|
+
this.sandbox[symbol.name] = val;
|
|
186
|
+
if (symbol.scope === "output") {
|
|
187
|
+
this.outputValues.set(symbol.name, val);
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
for (const fn of this.chart.functions || []) {
|
|
192
|
+
const where = `function block ${(0, import_chart_validation.getDeclarations)(fn.code).map((d) => d.name).join(", ")}`;
|
|
193
|
+
this.run(this.compile(this.prepareCode(fn.code, "statements"), where), where);
|
|
194
|
+
}
|
|
195
|
+
this.writeChangedOutputs();
|
|
196
|
+
const machine = (0, import_xstate.createMachine)(this.buildMachineConfig());
|
|
197
|
+
const persisted = options.restore && this.chart.restoreState !== false ? await this.loadPersisted() : null;
|
|
198
|
+
let restored = false;
|
|
199
|
+
if (persisted) {
|
|
200
|
+
try {
|
|
201
|
+
this.restore(machine, persisted);
|
|
202
|
+
restored = true;
|
|
203
|
+
this.host.log.info(`[${this.chart.name}] Restored the state from before the restart`);
|
|
204
|
+
} catch (err) {
|
|
205
|
+
this.host.log.warn(`[${this.chart.name}] Cannot restore the state, starting anew: ${err.message}`);
|
|
206
|
+
this.stop();
|
|
207
|
+
this.startActor(machine);
|
|
208
|
+
}
|
|
209
|
+
} else {
|
|
210
|
+
this.startActor(machine);
|
|
211
|
+
}
|
|
212
|
+
this.pendingHistory.push({ ts: this.now(), kind: "start", restored, active: this.activeNames });
|
|
213
|
+
this.wakeup(null, "start");
|
|
214
|
+
}
|
|
215
|
+
/**
|
|
216
|
+
* Starts the chart in its initial state, the entry actions are executed
|
|
217
|
+
*
|
|
218
|
+
* @param machine The XState machine of the chart
|
|
219
|
+
*/
|
|
220
|
+
startActor(machine) {
|
|
221
|
+
this.activeIds = [];
|
|
222
|
+
this.enteredAt.clear();
|
|
223
|
+
this.actor = (0, import_xstate.createActor)(machine);
|
|
224
|
+
this.subscribe();
|
|
225
|
+
this.actor.start();
|
|
226
|
+
}
|
|
227
|
+
/**
|
|
228
|
+
* Continues with a persisted state: the entry actions are not executed again,
|
|
229
|
+
* the timers of the temporal transitions continue with their remaining time
|
|
230
|
+
*
|
|
231
|
+
* @param machine The XState machine of the chart
|
|
232
|
+
* @param persisted The stored state
|
|
233
|
+
*/
|
|
234
|
+
restore(machine, persisted) {
|
|
235
|
+
for (const symbol of this.chart.symbols) {
|
|
236
|
+
if (symbol.scope === "local" && symbol.name in persisted.locals) {
|
|
237
|
+
this.sandbox[symbol.name] = persisted.locals[symbol.name];
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
this.actor = (0, import_xstate.createActor)(machine, { snapshot: persisted.machine });
|
|
241
|
+
this.subscribe();
|
|
242
|
+
this.actor.start();
|
|
243
|
+
this.publishActiveStates(this.actor.getSnapshot().value);
|
|
244
|
+
for (const id of this.activeIds) {
|
|
245
|
+
this.enteredAt.set(id, persisted.entered[id] ?? this.now());
|
|
246
|
+
this.startTimers(id);
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
subscribe() {
|
|
250
|
+
this.actor?.subscribe({
|
|
251
|
+
next: (snapshot) => this.publishActiveStates(snapshot.value),
|
|
252
|
+
error: (err) => this.host.log.error(`[${this.chart.name}] ${String(err)}`)
|
|
253
|
+
});
|
|
254
|
+
}
|
|
255
|
+
/**
|
|
256
|
+
* @returns the persisted data, if it fits to this chart
|
|
257
|
+
*/
|
|
258
|
+
async loadPersisted() {
|
|
259
|
+
let persisted;
|
|
260
|
+
try {
|
|
261
|
+
persisted = await this.host.loadPersisted?.();
|
|
262
|
+
} catch (err) {
|
|
263
|
+
this.host.log.warn(`[${this.chart.name}] Cannot load the stored state: ${err.message}`);
|
|
264
|
+
return null;
|
|
265
|
+
}
|
|
266
|
+
if (!persisted?.machine || persisted.version !== PERSISTED_VERSION) {
|
|
267
|
+
return null;
|
|
268
|
+
}
|
|
269
|
+
if (persisted.hash !== this.hash) {
|
|
270
|
+
this.host.log.info(`[${this.chart.name}] The chart was changed, starting in the initial state`);
|
|
271
|
+
return null;
|
|
272
|
+
}
|
|
273
|
+
return { ...persisted, entered: persisted.entered || {}, locals: persisted.locals || {} };
|
|
274
|
+
}
|
|
275
|
+
/**
|
|
276
|
+
* Stores the current state of the chart, if it changed since the last time
|
|
277
|
+
*/
|
|
278
|
+
persist() {
|
|
279
|
+
if (!this.actor || !this.host.persist) {
|
|
280
|
+
return;
|
|
281
|
+
}
|
|
282
|
+
const data = {
|
|
283
|
+
version: PERSISTED_VERSION,
|
|
284
|
+
hash: this.hash,
|
|
285
|
+
machine: this.actor.getPersistedSnapshot(),
|
|
286
|
+
entered: Object.fromEntries(this.enteredAt),
|
|
287
|
+
locals: Object.fromEntries(
|
|
288
|
+
this.chart.symbols.filter((s) => s.scope === "local").map((s) => [s.name, this.sandbox[s.name]])
|
|
289
|
+
)
|
|
290
|
+
};
|
|
291
|
+
let json;
|
|
292
|
+
try {
|
|
293
|
+
json = JSON.stringify(data);
|
|
294
|
+
} catch (err) {
|
|
295
|
+
this.host.log.warn(`[${this.chart.name}] Cannot store the state: ${err.message}`);
|
|
296
|
+
return;
|
|
297
|
+
}
|
|
298
|
+
if (json !== this.lastPersisted) {
|
|
299
|
+
this.lastPersisted = json;
|
|
300
|
+
this.host.persist(data);
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
/**
|
|
304
|
+
* Called by XState before the entry action of a state
|
|
305
|
+
*
|
|
306
|
+
* @param id ID of the state
|
|
307
|
+
*/
|
|
308
|
+
onEnter(id) {
|
|
309
|
+
this.enteredAt.set(id, this.now());
|
|
310
|
+
this.startTimers(id);
|
|
311
|
+
}
|
|
312
|
+
/**
|
|
313
|
+
* Called by XState after the exit action of a state
|
|
314
|
+
*
|
|
315
|
+
* @param id ID of the state
|
|
316
|
+
*/
|
|
317
|
+
onExit(id) {
|
|
318
|
+
this.enteredAt.delete(id);
|
|
319
|
+
for (const timer of this.timers.get(id) || []) {
|
|
320
|
+
clearTimeout(timer.handle);
|
|
321
|
+
}
|
|
322
|
+
this.timers.delete(id);
|
|
323
|
+
for (const transition of this.temporalTransitions.get(id) || []) {
|
|
324
|
+
this.expired.delete(transition.id);
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
/**
|
|
328
|
+
* Starts a timer for every temporal transition of a state, which wakes up the chart when its time has come
|
|
329
|
+
*
|
|
330
|
+
* @param id ID of the state
|
|
331
|
+
*/
|
|
332
|
+
startTimers(id) {
|
|
333
|
+
const entered = this.enteredAt.get(id) ?? this.now();
|
|
334
|
+
const timers = [];
|
|
335
|
+
for (const transition of this.temporalTransitions.get(id) || []) {
|
|
336
|
+
const timer = {};
|
|
337
|
+
const due = entered + transition.afterMs;
|
|
338
|
+
const wait = () => {
|
|
339
|
+
const remaining = due - this.now();
|
|
340
|
+
timer.handle = setTimeout(
|
|
341
|
+
() => {
|
|
342
|
+
if (due - this.now() > 0 && remaining > MAX_TIMEOUT_MS) {
|
|
343
|
+
wait();
|
|
344
|
+
return;
|
|
345
|
+
}
|
|
346
|
+
this.expired.add(transition.id);
|
|
347
|
+
this.wakeup(null);
|
|
348
|
+
},
|
|
349
|
+
Math.min(Math.max(remaining, 0), MAX_TIMEOUT_MS)
|
|
350
|
+
);
|
|
351
|
+
};
|
|
352
|
+
wait();
|
|
353
|
+
timers.push(timer);
|
|
354
|
+
}
|
|
355
|
+
this.timers.set(id, timers);
|
|
356
|
+
}
|
|
357
|
+
/**
|
|
358
|
+
* Logs the problems found by the validation, the chart is started anyway
|
|
359
|
+
*/
|
|
360
|
+
logProblems() {
|
|
361
|
+
for (const { kind, id, field, diagnostic } of (0, import_chart_validation.validateChart)(this.chart)) {
|
|
362
|
+
let where;
|
|
363
|
+
if (kind === "state") {
|
|
364
|
+
where = `${this.statesById.get(id)?.name ?? id}/${field}`;
|
|
365
|
+
} else if (kind === "function") {
|
|
366
|
+
const fn = this.chart.functions?.find((f) => f.id === id);
|
|
367
|
+
where = `Function block ${(0, import_chart_validation.getDeclarations)(fn?.code).map((d) => d.name).join(", ")}`;
|
|
368
|
+
} else {
|
|
369
|
+
const transition = this.chart.transitions.find((t) => t.id === id);
|
|
370
|
+
const source = this.statesById.get(transition?.source ?? "")?.name;
|
|
371
|
+
const target = this.statesById.get(transition?.target ?? "")?.name;
|
|
372
|
+
where = `Transition ${source} -> ${target}`;
|
|
373
|
+
}
|
|
374
|
+
const log = diagnostic.severity === "error" ? this.host.log.error : this.host.log.warn;
|
|
375
|
+
log(`[${this.chart.name}] ${where}: ${diagnostic.message}`);
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
stop() {
|
|
379
|
+
this.actor?.stop();
|
|
380
|
+
this.actor = null;
|
|
381
|
+
for (const timers of this.timers.values()) {
|
|
382
|
+
for (const timer of timers) {
|
|
383
|
+
clearTimeout(timer.handle);
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
this.timers.clear();
|
|
387
|
+
this.expired.clear();
|
|
388
|
+
for (const timer of this.debounceTimers.values()) {
|
|
389
|
+
clearTimeout(timer);
|
|
390
|
+
}
|
|
391
|
+
this.debounceTimers.clear();
|
|
392
|
+
for (const timer of this.delayedWrites) {
|
|
393
|
+
clearTimeout(timer);
|
|
394
|
+
}
|
|
395
|
+
this.delayedWrites.clear();
|
|
396
|
+
}
|
|
397
|
+
/**
|
|
398
|
+
* The helper functions which can be used in conditions and actions, see chart-helpers.ts
|
|
399
|
+
*/
|
|
400
|
+
createHelpers() {
|
|
401
|
+
const prefix = `[${this.chart.name}]`;
|
|
402
|
+
const write = (id, value, ack) => this.host.writeState(String(id), value, !!ack).catch((err) => this.host.log.warn(`${prefix} Cannot write ${id}: ${err}`));
|
|
403
|
+
return {
|
|
404
|
+
log: (message, level = "info") => {
|
|
405
|
+
const method = ["debug", "info", "warn", "error"].includes(level) ? level : "info";
|
|
406
|
+
this.host.log[method](`${prefix} ${message}`);
|
|
407
|
+
},
|
|
408
|
+
setState: (id, value, ack = false) => {
|
|
409
|
+
void write(id, value, ack);
|
|
410
|
+
},
|
|
411
|
+
setStateDelayed: (id, value, delayMs, ack = false) => {
|
|
412
|
+
const timer = setTimeout(
|
|
413
|
+
() => {
|
|
414
|
+
this.delayedWrites.delete(timer);
|
|
415
|
+
void write(id, value, ack);
|
|
416
|
+
},
|
|
417
|
+
Math.min(Math.max(Number(delayMs) || 0, 0), MAX_TIMEOUT_MS)
|
|
418
|
+
);
|
|
419
|
+
this.delayedWrites.add(timer);
|
|
420
|
+
},
|
|
421
|
+
sendTo: (instance, command, message) => {
|
|
422
|
+
if (!this.host.sendTo) {
|
|
423
|
+
throw new Error("sendTo() is not available");
|
|
424
|
+
}
|
|
425
|
+
this.host.sendTo(String(instance), String(command), message);
|
|
426
|
+
},
|
|
427
|
+
isTimeBetween: (start, end) => (0, import_chart_helpers.isTimeBetween)(start, end, new Date(this.now())),
|
|
428
|
+
isDaytime: () => {
|
|
429
|
+
const now = new Date(this.now());
|
|
430
|
+
const { sunrise, sunset, alwaysUp } = this.getSunTimes(now);
|
|
431
|
+
if (!sunrise || !sunset) {
|
|
432
|
+
return !!alwaysUp;
|
|
433
|
+
}
|
|
434
|
+
return now >= sunrise && now < sunset;
|
|
435
|
+
},
|
|
436
|
+
astroTime: (name, offsetMinutes = 0) => {
|
|
437
|
+
const times = this.getSunTimes(new Date(this.now()));
|
|
438
|
+
const time = times[name];
|
|
439
|
+
if (!(time instanceof Date)) {
|
|
440
|
+
const known = name in times && name !== "alwaysUp" && name !== "alwaysDown";
|
|
441
|
+
throw new Error(
|
|
442
|
+
known ? `There is no "${name}" today at the location of the system settings` : `Unknown astro event "${name}"`
|
|
443
|
+
);
|
|
444
|
+
}
|
|
445
|
+
return new Date(time.getTime() + (Number(offsetMinutes) || 0) * 6e4);
|
|
446
|
+
},
|
|
447
|
+
hasChanged: (name) => this.getChange(name, "hasChanged").changed,
|
|
448
|
+
hasChangedFrom: (name, value) => {
|
|
449
|
+
const { changed, previous } = this.getChange(name, "hasChangedFrom");
|
|
450
|
+
return changed && (0, import_node_util.isDeepStrictEqual)(previous, value);
|
|
451
|
+
},
|
|
452
|
+
hasChangedTo: (name, value) => {
|
|
453
|
+
const { changed, current } = this.getChange(name, "hasChangedTo");
|
|
454
|
+
return changed && (0, import_node_util.isDeepStrictEqual)(current, value);
|
|
455
|
+
}
|
|
456
|
+
};
|
|
457
|
+
}
|
|
458
|
+
/**
|
|
459
|
+
* @param date The day
|
|
460
|
+
* @returns the astro events of the day, null for events which do not happen on that day (polar regions)
|
|
461
|
+
* @throws {Error} if no location is configured
|
|
462
|
+
*/
|
|
463
|
+
getSunTimes(date) {
|
|
464
|
+
const location = this.host.getLocation?.();
|
|
465
|
+
if (!location) {
|
|
466
|
+
throw new Error("No location (latitude/longitude) configured in the ioBroker system settings");
|
|
467
|
+
}
|
|
468
|
+
return import_suncalc.default.getTimes(date, location.latitude, location.longitude);
|
|
469
|
+
}
|
|
470
|
+
/**
|
|
471
|
+
* Must be called when an input state is updated
|
|
472
|
+
*
|
|
473
|
+
* @param stateId ID of the updated state
|
|
474
|
+
* @param val The new value
|
|
475
|
+
* @param options The ack flag of the update
|
|
476
|
+
* @param options.ack The ack flag of the update
|
|
477
|
+
*/
|
|
478
|
+
handleInput(stateId, val, options = {}) {
|
|
479
|
+
if (!this.actor) {
|
|
480
|
+
return;
|
|
481
|
+
}
|
|
482
|
+
for (const symbol of this.chart.symbols) {
|
|
483
|
+
if (symbol.scope !== "input" || symbol.stateId !== stateId) {
|
|
484
|
+
continue;
|
|
485
|
+
}
|
|
486
|
+
if (symbol.ack === "ack" && options.ack !== true || symbol.ack === "command" && options.ack !== false) {
|
|
487
|
+
continue;
|
|
488
|
+
}
|
|
489
|
+
const debounceMs = symbol.debounceMs || 0;
|
|
490
|
+
if (debounceMs > 0) {
|
|
491
|
+
clearTimeout(this.debounceTimers.get(symbol.name));
|
|
492
|
+
this.debounceTimers.set(
|
|
493
|
+
symbol.name,
|
|
494
|
+
setTimeout(() => {
|
|
495
|
+
this.debounceTimers.delete(symbol.name);
|
|
496
|
+
this.applyInput(symbol, val);
|
|
497
|
+
}, debounceMs)
|
|
498
|
+
);
|
|
499
|
+
} else {
|
|
500
|
+
this.applyInput(symbol, val);
|
|
501
|
+
}
|
|
502
|
+
}
|
|
503
|
+
}
|
|
504
|
+
/**
|
|
505
|
+
* Sets the variable of an input and wakes up the chart
|
|
506
|
+
*
|
|
507
|
+
* @param symbol The input
|
|
508
|
+
* @param val The new value
|
|
509
|
+
*/
|
|
510
|
+
applyInput(symbol, val) {
|
|
511
|
+
if (symbol.trigger === "change" && val === this.sandbox[symbol.name]) {
|
|
512
|
+
return;
|
|
513
|
+
}
|
|
514
|
+
this.sandbox[symbol.name] = val;
|
|
515
|
+
this.wakeup(symbol.name);
|
|
516
|
+
}
|
|
517
|
+
/**
|
|
518
|
+
* @param input Name of the input which caused the wakeup, null for timers and the start
|
|
519
|
+
* @param cause What caused the wakeup
|
|
520
|
+
*/
|
|
521
|
+
wakeup(input, cause = input === null ? "timer" : "input") {
|
|
522
|
+
if (!this.actor) {
|
|
523
|
+
return;
|
|
524
|
+
}
|
|
525
|
+
this.wakeupCause = cause;
|
|
526
|
+
this.wakeupInput = input;
|
|
527
|
+
this.rememberValues();
|
|
528
|
+
if (!this.chart.superStep) {
|
|
529
|
+
this.step(input);
|
|
530
|
+
} else {
|
|
531
|
+
let steps = 0;
|
|
532
|
+
let stepInput = input;
|
|
533
|
+
while (steps < import_chart_model.MAX_SUPER_STEPS && this.step(stepInput)) {
|
|
534
|
+
steps++;
|
|
535
|
+
stepInput = null;
|
|
536
|
+
}
|
|
537
|
+
if (steps >= import_chart_model.MAX_SUPER_STEPS) {
|
|
538
|
+
this.host.log.warn(
|
|
539
|
+
`[${this.chart.name}] Stopped after ${import_chart_model.MAX_SUPER_STEPS} transitions in one wakeup, the transitions probably form a loop. The chart continues on the next wakeup.`
|
|
540
|
+
);
|
|
541
|
+
}
|
|
542
|
+
}
|
|
543
|
+
this.writeChangedOutputs();
|
|
544
|
+
this.persist();
|
|
545
|
+
this.publishLive();
|
|
546
|
+
}
|
|
547
|
+
/**
|
|
548
|
+
* Stores the values of the variables used by the change detection at the start of a wakeup
|
|
549
|
+
*/
|
|
550
|
+
rememberValues() {
|
|
551
|
+
if (!this.changeVariables.size) {
|
|
552
|
+
return;
|
|
553
|
+
}
|
|
554
|
+
this.previousValues = this.currentValues;
|
|
555
|
+
this.currentValues = /* @__PURE__ */ new Map();
|
|
556
|
+
for (const name of this.changeVariables) {
|
|
557
|
+
const value = this.sandbox[name];
|
|
558
|
+
let copy;
|
|
559
|
+
try {
|
|
560
|
+
copy = structuredClone(value);
|
|
561
|
+
} catch {
|
|
562
|
+
copy = value;
|
|
563
|
+
}
|
|
564
|
+
this.currentValues.set(name, copy);
|
|
565
|
+
}
|
|
566
|
+
}
|
|
567
|
+
/**
|
|
568
|
+
* @param name Name of the variable
|
|
569
|
+
* @param helper Name of the helper, for the error message
|
|
570
|
+
* @returns whether the variable has changed between the start of the previous and of the current wakeup
|
|
571
|
+
* @throws {Error} if the variable is unknown
|
|
572
|
+
*/
|
|
573
|
+
getChange(name, helper) {
|
|
574
|
+
if (!this.changeVariables.has(name) || !this.chart.symbols.some((s) => s.name === name)) {
|
|
575
|
+
throw new Error(`${helper}() needs a variable of the chart as first argument`);
|
|
576
|
+
}
|
|
577
|
+
if (!this.previousValues || !this.currentValues) {
|
|
578
|
+
return { changed: false };
|
|
579
|
+
}
|
|
580
|
+
const previous = this.previousValues.get(name);
|
|
581
|
+
const current = this.currentValues.get(name);
|
|
582
|
+
return { changed: !(0, import_node_util.isDeepStrictEqual)(previous, current), previous, current };
|
|
583
|
+
}
|
|
584
|
+
/**
|
|
585
|
+
* Processes the active states once, see the class description
|
|
586
|
+
*
|
|
587
|
+
* @param input Name of the input which caused the wakeup, null if the step has no event
|
|
588
|
+
* @returns true if a transition was taken
|
|
589
|
+
*/
|
|
590
|
+
step(input) {
|
|
591
|
+
for (const id of this.getActiveChildren(null)) {
|
|
592
|
+
if (this.processState(id, input)) {
|
|
593
|
+
return true;
|
|
594
|
+
}
|
|
595
|
+
}
|
|
596
|
+
return false;
|
|
597
|
+
}
|
|
598
|
+
/**
|
|
599
|
+
* Passes the events since the last call and the changed values of the variables to the host
|
|
600
|
+
*/
|
|
601
|
+
publishLive() {
|
|
602
|
+
if (this.pendingHistory.length) {
|
|
603
|
+
const entries = this.pendingHistory;
|
|
604
|
+
this.pendingHistory = [];
|
|
605
|
+
this.host.onHistory?.(entries);
|
|
606
|
+
}
|
|
607
|
+
if (!this.host.onValues) {
|
|
608
|
+
return;
|
|
609
|
+
}
|
|
610
|
+
const values = Object.fromEntries(this.chart.symbols.map((s) => [s.name, this.sandbox[s.name]]));
|
|
611
|
+
let json;
|
|
612
|
+
try {
|
|
613
|
+
json = JSON.stringify(values);
|
|
614
|
+
} catch {
|
|
615
|
+
for (const [name, value] of Object.entries(values)) {
|
|
616
|
+
try {
|
|
617
|
+
JSON.stringify(value);
|
|
618
|
+
} catch {
|
|
619
|
+
values[name] = String(value);
|
|
620
|
+
}
|
|
621
|
+
}
|
|
622
|
+
json = JSON.stringify(values);
|
|
623
|
+
}
|
|
624
|
+
if (json !== this.lastValues) {
|
|
625
|
+
this.lastValues = json;
|
|
626
|
+
this.host.onValues(values);
|
|
627
|
+
}
|
|
628
|
+
}
|
|
629
|
+
/**
|
|
630
|
+
* Processes an active state and its active children, see the class description
|
|
631
|
+
*
|
|
632
|
+
* @param id ID of the state
|
|
633
|
+
* @param input Name of the input which caused the wakeup, null for timers
|
|
634
|
+
* @returns true if a transition was taken
|
|
635
|
+
*/
|
|
636
|
+
processState(id, input) {
|
|
637
|
+
if (this.takeFirstValid(this.outerTransitions.get(id), input)) {
|
|
638
|
+
return true;
|
|
639
|
+
}
|
|
640
|
+
const state = this.statesById.get(id);
|
|
641
|
+
if (state?.during) {
|
|
642
|
+
this.runAction(state.during, `${state.name}/during`);
|
|
643
|
+
}
|
|
644
|
+
if (this.takeFirstValid(this.innerTransitions.get(id), input)) {
|
|
645
|
+
return true;
|
|
646
|
+
}
|
|
647
|
+
for (const child of this.getActiveChildren(id)) {
|
|
648
|
+
if (this.processState(child, input)) {
|
|
649
|
+
return true;
|
|
650
|
+
}
|
|
651
|
+
}
|
|
652
|
+
return false;
|
|
653
|
+
}
|
|
654
|
+
/**
|
|
655
|
+
* @param parent null for the top level
|
|
656
|
+
*/
|
|
657
|
+
getActiveChildren(parent) {
|
|
658
|
+
const children = new Set(this.structure.getChildren(parent).map((s) => s.id));
|
|
659
|
+
return this.activeIds.filter((id) => children.has(id));
|
|
660
|
+
}
|
|
661
|
+
/**
|
|
662
|
+
* Takes the first transition whose event and condition match
|
|
663
|
+
*
|
|
664
|
+
* @param transitions The transitions to check, sorted
|
|
665
|
+
* @param input Name of the input which caused the wakeup, null for timers
|
|
666
|
+
* @returns true if a transition was taken
|
|
667
|
+
*/
|
|
668
|
+
takeFirstValid(transitions, input) {
|
|
669
|
+
for (const transition of transitions || []) {
|
|
670
|
+
if (transition.event && transition.event !== input) {
|
|
671
|
+
continue;
|
|
672
|
+
}
|
|
673
|
+
if (transition.afterMs !== void 0 && !this.isExpired(transition)) {
|
|
674
|
+
continue;
|
|
675
|
+
}
|
|
676
|
+
if (transition.condition && !this.run(transition.condition, `${transition.where}/condition`)) {
|
|
677
|
+
continue;
|
|
678
|
+
}
|
|
679
|
+
this.actor?.send({ type: `T.${transition.id}` });
|
|
680
|
+
this.pendingHistory.push({
|
|
681
|
+
ts: this.now(),
|
|
682
|
+
kind: "transition",
|
|
683
|
+
transition: transition.id,
|
|
684
|
+
from: transition.from,
|
|
685
|
+
to: transition.to,
|
|
686
|
+
// in a super step, all transitions of the wakeup were caused by its input
|
|
687
|
+
trigger: this.wakeupInput,
|
|
688
|
+
cause: this.wakeupCause,
|
|
689
|
+
active: this.activeNames
|
|
690
|
+
});
|
|
691
|
+
return true;
|
|
692
|
+
}
|
|
693
|
+
return false;
|
|
694
|
+
}
|
|
695
|
+
/**
|
|
696
|
+
* @param transition A temporal transition
|
|
697
|
+
* @returns true if its source state is active for at least its delay
|
|
698
|
+
*/
|
|
699
|
+
isExpired(transition) {
|
|
700
|
+
if (this.expired.has(transition.id)) {
|
|
701
|
+
return true;
|
|
702
|
+
}
|
|
703
|
+
const entered = this.enteredAt.get(transition.source);
|
|
704
|
+
return entered !== void 0 && this.now() - entered >= transition.afterMs;
|
|
705
|
+
}
|
|
706
|
+
buildMachineConfig() {
|
|
707
|
+
const structure = this.structure;
|
|
708
|
+
const buildState = (state) => {
|
|
709
|
+
const node = {
|
|
710
|
+
id: state.id,
|
|
711
|
+
entry: [
|
|
712
|
+
() => this.onEnter(state.id),
|
|
713
|
+
...state.entry ? [() => this.runAction(state.entry, `${state.name}/entry`)] : []
|
|
714
|
+
],
|
|
715
|
+
exit: [
|
|
716
|
+
...state.exit ? [() => this.runAction(state.exit, `${state.name}/exit`)] : [],
|
|
717
|
+
() => this.onExit(state.id)
|
|
718
|
+
],
|
|
719
|
+
on: {}
|
|
720
|
+
};
|
|
721
|
+
const outer = [];
|
|
722
|
+
const inner = [];
|
|
723
|
+
for (const transition of structure.getTransitions(state.id)) {
|
|
724
|
+
const built = this.buildTransition(transition, state);
|
|
725
|
+
if (!built) {
|
|
726
|
+
continue;
|
|
727
|
+
}
|
|
728
|
+
node.on[`T.${transition.id}`] = built.config;
|
|
729
|
+
(built.inner ? inner : outer).push(built.checked);
|
|
730
|
+
}
|
|
731
|
+
this.outerTransitions.set(state.id, outer);
|
|
732
|
+
this.innerTransitions.set(state.id, inner);
|
|
733
|
+
this.temporalTransitions.set(
|
|
734
|
+
state.id,
|
|
735
|
+
[...outer, ...inner].filter((t) => t.afterMs !== void 0)
|
|
736
|
+
);
|
|
737
|
+
const subStates = structure.getChildren(state.id);
|
|
738
|
+
if (subStates.length) {
|
|
739
|
+
node.initial = structure.getInitialChild(state.id);
|
|
740
|
+
node.states = Object.fromEntries(subStates.map((s) => [s.id, buildState(s)]));
|
|
741
|
+
}
|
|
742
|
+
return node;
|
|
743
|
+
};
|
|
744
|
+
const topLevel = structure.getChildren(null);
|
|
745
|
+
if (!topLevel.length) {
|
|
746
|
+
throw new Error("The chart has no states");
|
|
747
|
+
}
|
|
748
|
+
return {
|
|
749
|
+
id: `chart_${this.chartId}`,
|
|
750
|
+
initial: structure.getInitialChild(null),
|
|
751
|
+
states: Object.fromEntries(topLevel.map((s) => [s.id, buildState(s)]))
|
|
752
|
+
};
|
|
753
|
+
}
|
|
754
|
+
/**
|
|
755
|
+
* @param transition The transition
|
|
756
|
+
* @param source Its source state
|
|
757
|
+
*/
|
|
758
|
+
buildTransition(transition, source) {
|
|
759
|
+
const target = this.statesById.get(transition.target);
|
|
760
|
+
const where = `${source.name} -> ${target?.name ?? transition.target}`;
|
|
761
|
+
if (!target) {
|
|
762
|
+
this.host.log.warn(`[${this.chart.name}] Transition ${where}: unknown target, ignored`);
|
|
763
|
+
return null;
|
|
764
|
+
}
|
|
765
|
+
let label;
|
|
766
|
+
try {
|
|
767
|
+
label = (0, import_chart_model.parseTransitionLabel)(transition.label);
|
|
768
|
+
} catch (err) {
|
|
769
|
+
this.host.log.error(`[${this.chart.name}] Transition ${where}: ${err.message}, ignored`);
|
|
770
|
+
return null;
|
|
771
|
+
}
|
|
772
|
+
const condition = label.condition ? this.compile(`(${this.prepareCode(label.condition, "expression")})`, `${where}/condition`) : null;
|
|
773
|
+
const { inner, reenter } = this.structure.getTransitionKind(transition);
|
|
774
|
+
const config = {
|
|
775
|
+
target: `#${target.id}`,
|
|
776
|
+
reenter,
|
|
777
|
+
actions: label.action ? [() => this.runAction(label.action, `${where}/action`)] : []
|
|
778
|
+
};
|
|
779
|
+
return {
|
|
780
|
+
inner,
|
|
781
|
+
config,
|
|
782
|
+
checked: {
|
|
783
|
+
id: transition.id,
|
|
784
|
+
source: source.id,
|
|
785
|
+
where,
|
|
786
|
+
from: source.name,
|
|
787
|
+
to: target.name,
|
|
788
|
+
event: label.event,
|
|
789
|
+
afterMs: label.afterMs,
|
|
790
|
+
condition
|
|
791
|
+
}
|
|
792
|
+
};
|
|
793
|
+
}
|
|
794
|
+
/**
|
|
795
|
+
* @param code The code
|
|
796
|
+
* @param where Used in error messages
|
|
797
|
+
*/
|
|
798
|
+
compile(code, where) {
|
|
799
|
+
try {
|
|
800
|
+
return new import_node_vm.default.Script(code, { filename: `${this.chart.name}/${where}` });
|
|
801
|
+
} catch (err) {
|
|
802
|
+
this.host.log.error(`[${this.chart.name}] ${where}: ${err.message}`);
|
|
803
|
+
return null;
|
|
804
|
+
}
|
|
805
|
+
}
|
|
806
|
+
/**
|
|
807
|
+
* @param script The compiled code
|
|
808
|
+
* @param where Used in error messages
|
|
809
|
+
*/
|
|
810
|
+
run(script, where) {
|
|
811
|
+
if (!script) {
|
|
812
|
+
return void 0;
|
|
813
|
+
}
|
|
814
|
+
try {
|
|
815
|
+
return script.runInContext(this.sandbox, { timeout: SCRIPT_TIMEOUT_MS });
|
|
816
|
+
} catch (err) {
|
|
817
|
+
this.host.log.warn(`[${this.chart.name}] ${where}: ${err.message}`);
|
|
818
|
+
return void 0;
|
|
819
|
+
}
|
|
820
|
+
}
|
|
821
|
+
/**
|
|
822
|
+
* Executes an action and writes all changed outputs
|
|
823
|
+
*
|
|
824
|
+
* @param code The action
|
|
825
|
+
* @param where Used in error messages
|
|
826
|
+
*/
|
|
827
|
+
runAction(code, where) {
|
|
828
|
+
this.run(this.compile(`{
|
|
829
|
+
${this.prepareCode(code, "statements")}
|
|
830
|
+
}`, where), where);
|
|
831
|
+
this.writeChangedOutputs();
|
|
832
|
+
}
|
|
833
|
+
writeChangedOutputs() {
|
|
834
|
+
for (const symbol of this.chart.symbols) {
|
|
835
|
+
if (symbol.scope !== "output" || !symbol.stateId) {
|
|
836
|
+
continue;
|
|
837
|
+
}
|
|
838
|
+
const val = this.sandbox[symbol.name];
|
|
839
|
+
if (val !== this.outputValues.get(symbol.name)) {
|
|
840
|
+
this.outputValues.set(symbol.name, val);
|
|
841
|
+
this.host.writeState(symbol.stateId, val).catch((err) => this.host.log.warn(`[${this.chart.name}] Cannot write ${symbol.stateId}: ${err}`));
|
|
842
|
+
}
|
|
843
|
+
}
|
|
844
|
+
}
|
|
845
|
+
/**
|
|
846
|
+
* @param value XState state value
|
|
847
|
+
*/
|
|
848
|
+
publishActiveStates(value) {
|
|
849
|
+
const ids = [];
|
|
850
|
+
const names = [];
|
|
851
|
+
const walk = (val, path) => {
|
|
852
|
+
const keys = typeof val === "string" ? [val] : Object.keys(val || {});
|
|
853
|
+
for (const key of keys) {
|
|
854
|
+
const name = [...path, this.statesById.get(key)?.name ?? key];
|
|
855
|
+
ids.push(key);
|
|
856
|
+
if (typeof val === "string" || !val[key] || !Object.keys(val[key]).length) {
|
|
857
|
+
names.push(name.join("."));
|
|
858
|
+
} else {
|
|
859
|
+
walk(val[key], name);
|
|
860
|
+
}
|
|
861
|
+
}
|
|
862
|
+
};
|
|
863
|
+
walk(value, []);
|
|
864
|
+
if (ids.join() !== this.activeIds.join()) {
|
|
865
|
+
this.activeIds = ids;
|
|
866
|
+
this.activeNames = names.join(", ");
|
|
867
|
+
this.host.onActiveChange({ names: this.activeNames, ids });
|
|
868
|
+
}
|
|
869
|
+
}
|
|
870
|
+
}
|
|
871
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
872
|
+
0 && (module.exports = {
|
|
873
|
+
ChartRuntime,
|
|
874
|
+
getChartHash
|
|
875
|
+
});
|
|
876
|
+
//# sourceMappingURL=chart-runtime.js.map
|