iobroker.statechart 0.1.1 → 0.3.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/LICENSE +21 -21
- package/README.md +59 -438
- package/admin/build/tab.js +133 -130
- package/admin/statechart.png +0 -0
- package/admin/{style.css → tab.css} +0 -2
- package/admin/tab_m.html +1 -1
- package/build/lib/chart-analysis.js +367 -0
- package/build/lib/chart-analysis.js.map +7 -0
- package/build/lib/chart-codegen.js +405 -0
- package/build/lib/chart-codegen.js.map +7 -0
- package/build/lib/chart-helpers.js +102 -0
- package/build/lib/chart-helpers.js.map +7 -0
- package/build/lib/chart-model.js +477 -0
- package/build/lib/chart-model.js.map +7 -0
- package/build/lib/chart-runtime.js +1521 -0
- package/build/lib/chart-runtime.js.map +7 -0
- package/build/lib/chart-structure.js +172 -0
- package/build/lib/chart-structure.js.map +7 -0
- package/build/lib/chart-validation.js +372 -0
- package/build/lib/chart-validation.js.map +7 -0
- package/build/main.js +638 -0
- package/build/main.js.map +7 -0
- package/io-package.json +31 -34
- package/package.json +21 -17
- package/admin/i18n/de.json +0 -4
- package/admin/i18n/en.json +0 -4
- package/admin/i18n/es.json +0 -4
- package/admin/i18n/fr.json +0 -4
- package/admin/i18n/it.json +0 -4
- package/admin/i18n/nl.json +0 -4
- package/admin/i18n/pl.json +0 -4
- package/admin/i18n/pt.json +0 -4
- package/admin/i18n/ru.json +0 -4
- package/admin/i18n/uk.json +0 -4
- package/admin/i18n/zh-cn.json +0 -4
- package/admin/jsonConfig.json +0 -19
- package/admin/statechart.svg +0 -38
- 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
package/build/main.js
ADDED
|
@@ -0,0 +1,638 @@
|
|
|
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 __copyProps = (to, from, except, desc) => {
|
|
9
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
10
|
+
for (let key of __getOwnPropNames(from))
|
|
11
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
12
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
13
|
+
}
|
|
14
|
+
return to;
|
|
15
|
+
};
|
|
16
|
+
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
|
|
17
|
+
// If the importer is in node compatibility mode or this is not an ESM
|
|
18
|
+
// file that has been converted to a CommonJS file using a Babel-
|
|
19
|
+
// compatible transform (i.e. "__esModule" has not been set), then set
|
|
20
|
+
// "default" to the CommonJS "module.exports" for node compatibility.
|
|
21
|
+
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
|
|
22
|
+
mod
|
|
23
|
+
));
|
|
24
|
+
var utils = __toESM(require("@iobroker/adapter-core"));
|
|
25
|
+
var import_chart_runtime = require("./lib/chart-runtime");
|
|
26
|
+
const HISTORY_LENGTH = 200;
|
|
27
|
+
const SIMULATION_TIMEOUT_MS = 2 * 60 * 1e3;
|
|
28
|
+
const CONTROLS = ["enabled", "reset", "event"];
|
|
29
|
+
class Statechart extends utils.Adapter {
|
|
30
|
+
/** running charts by chart ID */
|
|
31
|
+
runtimes = /* @__PURE__ */ new Map();
|
|
32
|
+
/** foreign states we are subscribed to */
|
|
33
|
+
subscribedInputs = /* @__PURE__ */ new Set();
|
|
34
|
+
/** chart (re)starts are executed one after another */
|
|
35
|
+
chartQueue = Promise.resolve();
|
|
36
|
+
/** for the astro helper functions */
|
|
37
|
+
location = null;
|
|
38
|
+
/** the last events of each chart by chart ID, newest last, also stored in charts.<chart>.history */
|
|
39
|
+
histories = /* @__PURE__ */ new Map();
|
|
40
|
+
/** charts which run as simulation (their runtime is in runtimes), with the timer which ends the simulation */
|
|
41
|
+
simulations = /* @__PURE__ */ new Map();
|
|
42
|
+
constructor(options = {}) {
|
|
43
|
+
super({
|
|
44
|
+
...options,
|
|
45
|
+
name: "statechart"
|
|
46
|
+
});
|
|
47
|
+
this.on("ready", this.onReady.bind(this));
|
|
48
|
+
this.on("stateChange", this.onStateChange.bind(this));
|
|
49
|
+
this.on("objectChange", this.onObjectChange.bind(this));
|
|
50
|
+
this.on("message", this.onMessage.bind(this));
|
|
51
|
+
this.on("unload", this.onUnload.bind(this));
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* Is called when databases are connected and adapter received configuration.
|
|
55
|
+
*/
|
|
56
|
+
async onReady() {
|
|
57
|
+
await this.setObjectNotExistsAsync("charts", {
|
|
58
|
+
type: "folder",
|
|
59
|
+
common: { name: "Charts" },
|
|
60
|
+
native: {}
|
|
61
|
+
});
|
|
62
|
+
await this.loadLocation();
|
|
63
|
+
const objects = await this.getForeignObjectsAsync(`${this.namespace}.charts.*`, "channel");
|
|
64
|
+
for (const [id, obj] of Object.entries(objects)) {
|
|
65
|
+
const chartId = this.getChartId(id);
|
|
66
|
+
if (chartId) {
|
|
67
|
+
await this.startChart(chartId, obj, true);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
this.subscribeObjects("charts.*");
|
|
71
|
+
this.subscribeStates("charts.*.history");
|
|
72
|
+
for (const control of CONTROLS) {
|
|
73
|
+
this.subscribeStates(`charts.*.${control}`);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* Reads the location of the ioBroker system settings, used by isDaytime() and astroTime()
|
|
78
|
+
*/
|
|
79
|
+
async loadLocation() {
|
|
80
|
+
const config = await this.getForeignObjectAsync("system.config");
|
|
81
|
+
const latitude = parseFloat(String(config?.common?.latitude ?? ""));
|
|
82
|
+
const longitude = parseFloat(String(config?.common?.longitude ?? ""));
|
|
83
|
+
if (Number.isFinite(latitude) && Number.isFinite(longitude)) {
|
|
84
|
+
this.location = { latitude, longitude };
|
|
85
|
+
} else {
|
|
86
|
+
this.log.info("No location configured in the system settings, isDaytime() and astroTime() do not work");
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
/**
|
|
90
|
+
* Extracts the chart ID from the ID of a chart object
|
|
91
|
+
*
|
|
92
|
+
* @param id Object ID
|
|
93
|
+
* @returns undefined if the ID is no chart object
|
|
94
|
+
*/
|
|
95
|
+
getChartId(id) {
|
|
96
|
+
const prefix = `${this.namespace}.charts.`;
|
|
97
|
+
if (!id.startsWith(prefix)) {
|
|
98
|
+
return void 0;
|
|
99
|
+
}
|
|
100
|
+
const chartId = id.substring(prefix.length);
|
|
101
|
+
return chartId.includes(".") ? void 0 : chartId;
|
|
102
|
+
}
|
|
103
|
+
/**
|
|
104
|
+
* @param id State ID
|
|
105
|
+
* @returns the chart ID and the name of the state if it is a state of a chart, e.g. "history"
|
|
106
|
+
*/
|
|
107
|
+
parseChartStateId(id) {
|
|
108
|
+
const match = /^charts\.([^.]+)\.([^.]+)$/.exec(id.substring(this.namespace.length + 1));
|
|
109
|
+
return id.startsWith(`${this.namespace}.`) && match ? { chartId: match[1], name: match[2] } : void 0;
|
|
110
|
+
}
|
|
111
|
+
/**
|
|
112
|
+
* @param chartId Chart ID
|
|
113
|
+
* @param obj Chart object
|
|
114
|
+
* @param restore Continue with the persisted state instead of the initial state
|
|
115
|
+
*/
|
|
116
|
+
async startChart(chartId, obj, restore = false) {
|
|
117
|
+
const chart = obj?.type === "channel" ? obj.native?.chart : void 0;
|
|
118
|
+
if (!chart) {
|
|
119
|
+
this.histories.delete(chartId);
|
|
120
|
+
return;
|
|
121
|
+
}
|
|
122
|
+
await this.createChartStates(chartId);
|
|
123
|
+
await this.loadHistory(chartId);
|
|
124
|
+
await this.setState(`charts.${chartId}.enabled`, chart.enabled !== false, true);
|
|
125
|
+
await this.setState(`charts.${chartId}.simulation`, false, true);
|
|
126
|
+
await this.setState(`charts.${chartId}.simulationStatus`, "", true);
|
|
127
|
+
const empty = !chart.states?.length;
|
|
128
|
+
if (!chart.enabled || empty) {
|
|
129
|
+
if (empty) {
|
|
130
|
+
this.log.warn(`Chart "${chart.name}" has no states, it is not started`);
|
|
131
|
+
} else {
|
|
132
|
+
this.log.info(`Chart "${chart.name}" is paused`);
|
|
133
|
+
}
|
|
134
|
+
await this.setState(`charts.${chartId}.activeState`, "", true);
|
|
135
|
+
await this.setState(`charts.${chartId}.activeStateIds`, "[]", true);
|
|
136
|
+
await this.setState(`charts.${chartId}.values`, "{}", true);
|
|
137
|
+
return;
|
|
138
|
+
}
|
|
139
|
+
const runtime = this.createRuntime(chartId, chart);
|
|
140
|
+
try {
|
|
141
|
+
await runtime.start({ restore });
|
|
142
|
+
} catch (err) {
|
|
143
|
+
this.log.error(`Cannot start chart "${chart.name}": ${err.message}`);
|
|
144
|
+
return;
|
|
145
|
+
}
|
|
146
|
+
this.runtimes.set(chartId, runtime);
|
|
147
|
+
this.updateSubscriptions();
|
|
148
|
+
this.log.info(`Chart "${chart.name}" started`);
|
|
149
|
+
}
|
|
150
|
+
/**
|
|
151
|
+
* @param chartId Chart ID
|
|
152
|
+
* @param chart The chart
|
|
153
|
+
* @param simulated Run it as simulation, see ChartRuntime
|
|
154
|
+
*/
|
|
155
|
+
createRuntime(chartId, chart, simulated = false) {
|
|
156
|
+
return new import_chart_runtime.ChartRuntime(
|
|
157
|
+
chartId,
|
|
158
|
+
chart,
|
|
159
|
+
{
|
|
160
|
+
readState: async (id) => (await this.getForeignStateAsync(id))?.val ?? null,
|
|
161
|
+
getState: (id) => this.getForeignStateAsync(id),
|
|
162
|
+
writeState: async (id, val, ack = false) => {
|
|
163
|
+
await this.setForeignStateAsync(id, val, ack);
|
|
164
|
+
},
|
|
165
|
+
sendTo: (instance, command, message) => this.sendTo(instance, command, message),
|
|
166
|
+
getLocation: () => this.location,
|
|
167
|
+
onActiveChange: ({ names, ids }) => {
|
|
168
|
+
void this.setState(`charts.${chartId}.activeState`, names, true);
|
|
169
|
+
void this.setState(`charts.${chartId}.activeStateIds`, JSON.stringify(ids), true);
|
|
170
|
+
},
|
|
171
|
+
persist: (data) => {
|
|
172
|
+
void this.setState(`charts.${chartId}.snapshot`, JSON.stringify(data), true);
|
|
173
|
+
},
|
|
174
|
+
onValues: (values) => {
|
|
175
|
+
void this.setState(`charts.${chartId}.values`, JSON.stringify(values), true);
|
|
176
|
+
},
|
|
177
|
+
onHistory: (entries) => this.addHistory(chartId, entries),
|
|
178
|
+
onSimulationStatus: (status) => {
|
|
179
|
+
void this.setState(`charts.${chartId}.simulationStatus`, JSON.stringify(status), true);
|
|
180
|
+
},
|
|
181
|
+
loadPersisted: async () => {
|
|
182
|
+
const state = await this.getStateAsync(`charts.${chartId}.snapshot`);
|
|
183
|
+
return typeof state?.val === "string" && state.val ? JSON.parse(state.val) : null;
|
|
184
|
+
},
|
|
185
|
+
log: this.log,
|
|
186
|
+
// the timers of the adapter are cleared when it is unloaded
|
|
187
|
+
setTimeout: (callback, ms) => this.setTimeout(callback, ms),
|
|
188
|
+
clearTimeout: (handle) => this.clearTimeout(handle)
|
|
189
|
+
},
|
|
190
|
+
simulated
|
|
191
|
+
);
|
|
192
|
+
}
|
|
193
|
+
/**
|
|
194
|
+
* Stops the chart and runs the given version of it as simulation, see ChartRuntime
|
|
195
|
+
*
|
|
196
|
+
* @param chartId Chart ID
|
|
197
|
+
* @param chart The chart as shown in the editor, may have unsaved changes
|
|
198
|
+
* @param breakpoints IDs of the states and transitions with a breakpoint
|
|
199
|
+
*/
|
|
200
|
+
async startSimulation(chartId, chart, breakpoints = []) {
|
|
201
|
+
if (!chart?.states?.length) {
|
|
202
|
+
throw new Error("The chart has no states");
|
|
203
|
+
}
|
|
204
|
+
this.stopChart(chartId);
|
|
205
|
+
this.endSimulationTimer(chartId);
|
|
206
|
+
const runtime = this.createRuntime(chartId, chart, true);
|
|
207
|
+
runtime.breakpoints = new Set(breakpoints);
|
|
208
|
+
this.simulations.set(
|
|
209
|
+
chartId,
|
|
210
|
+
this.setTimeout(() => this.onSimulationTimeout(chartId), SIMULATION_TIMEOUT_MS)
|
|
211
|
+
);
|
|
212
|
+
try {
|
|
213
|
+
await runtime.start();
|
|
214
|
+
} catch (err) {
|
|
215
|
+
this.endSimulationTimer(chartId);
|
|
216
|
+
await this.restartChart(chartId);
|
|
217
|
+
throw err;
|
|
218
|
+
}
|
|
219
|
+
this.runtimes.set(chartId, runtime);
|
|
220
|
+
this.updateSubscriptions();
|
|
221
|
+
await this.setState(`charts.${chartId}.simulation`, true, true);
|
|
222
|
+
this.log.info(`Chart "${chart.name}" runs as simulation, its outputs are not written`);
|
|
223
|
+
}
|
|
224
|
+
/**
|
|
225
|
+
* Ends the simulation of a chart, the real chart continues where it was before
|
|
226
|
+
*
|
|
227
|
+
* @param chartId Chart ID
|
|
228
|
+
*/
|
|
229
|
+
async stopSimulation(chartId) {
|
|
230
|
+
if (!this.simulations.has(chartId)) {
|
|
231
|
+
return;
|
|
232
|
+
}
|
|
233
|
+
this.endSimulationTimer(chartId);
|
|
234
|
+
this.stopChart(chartId);
|
|
235
|
+
this.log.info(`Simulation of chart ${chartId} ended`);
|
|
236
|
+
await this.restartChart(chartId, true);
|
|
237
|
+
}
|
|
238
|
+
/**
|
|
239
|
+
* @param chartId Chart ID
|
|
240
|
+
*/
|
|
241
|
+
endSimulationTimer(chartId) {
|
|
242
|
+
this.clearTimeout(this.simulations.get(chartId));
|
|
243
|
+
this.simulations.delete(chartId);
|
|
244
|
+
}
|
|
245
|
+
/**
|
|
246
|
+
* @param chartId Chart ID
|
|
247
|
+
*/
|
|
248
|
+
onSimulationTimeout(chartId) {
|
|
249
|
+
this.log.info(`Simulation of chart ${chartId} ended, the editor did not answer anymore`);
|
|
250
|
+
this.chartQueue = this.chartQueue.then(() => this.stopSimulation(chartId)).catch((err) => this.log.error(`Cannot end the simulation of chart ${chartId}: ${err.message}`));
|
|
251
|
+
}
|
|
252
|
+
/**
|
|
253
|
+
* Starts a chart with the stored version
|
|
254
|
+
*
|
|
255
|
+
* @param chartId Chart ID
|
|
256
|
+
* @param restore Continue with the persisted state instead of the initial state
|
|
257
|
+
*/
|
|
258
|
+
async restartChart(chartId, restore = false) {
|
|
259
|
+
await this.startChart(chartId, await this.getObjectAsync(`charts.${chartId}`), restore);
|
|
260
|
+
}
|
|
261
|
+
/**
|
|
262
|
+
* Messages of the editor, for the simulation
|
|
263
|
+
*
|
|
264
|
+
* @param obj The message
|
|
265
|
+
*/
|
|
266
|
+
onMessage(obj) {
|
|
267
|
+
if (typeof obj !== "object" || !obj.command) {
|
|
268
|
+
return;
|
|
269
|
+
}
|
|
270
|
+
const message = obj.message || {};
|
|
271
|
+
const chartId = String(message.chartId || "");
|
|
272
|
+
const reply = (response) => {
|
|
273
|
+
if (obj.callback) {
|
|
274
|
+
this.sendTo(obj.from, obj.command, response, obj.callback);
|
|
275
|
+
}
|
|
276
|
+
};
|
|
277
|
+
const simulating = this.simulations.has(chartId);
|
|
278
|
+
if (simulating) {
|
|
279
|
+
this.clearTimeout(this.simulations.get(chartId));
|
|
280
|
+
this.simulations.set(
|
|
281
|
+
chartId,
|
|
282
|
+
this.setTimeout(() => this.onSimulationTimeout(chartId), SIMULATION_TIMEOUT_MS)
|
|
283
|
+
);
|
|
284
|
+
}
|
|
285
|
+
const runtime = this.runtimes.get(chartId);
|
|
286
|
+
switch (obj.command) {
|
|
287
|
+
case "simulationStart":
|
|
288
|
+
this.chartQueue = this.chartQueue.then(() => this.startSimulation(chartId, message.chart, message.breakpoints)).then(() => reply({ ok: true })).catch((err) => reply({ error: err.message }));
|
|
289
|
+
return;
|
|
290
|
+
case "simulationStop":
|
|
291
|
+
this.chartQueue = this.chartQueue.then(() => this.stopSimulation(chartId)).then(() => reply({ ok: true })).catch((err) => reply({ error: err.message }));
|
|
292
|
+
return;
|
|
293
|
+
case "simulationInput":
|
|
294
|
+
if (!simulating || !runtime) {
|
|
295
|
+
reply({ error: "No simulation is running" });
|
|
296
|
+
} else if (!runtime.simulateInput(String(message.name), message.value)) {
|
|
297
|
+
reply({ error: `Unknown input "${message.name}"` });
|
|
298
|
+
} else {
|
|
299
|
+
reply({ ok: true });
|
|
300
|
+
}
|
|
301
|
+
return;
|
|
302
|
+
case "simulationEvent":
|
|
303
|
+
if (!simulating || !runtime) {
|
|
304
|
+
reply({ error: "No simulation is running" });
|
|
305
|
+
} else if (!runtime.handleEvent(String(message.name))) {
|
|
306
|
+
reply({ error: `Unknown input "${message.name}"` });
|
|
307
|
+
} else {
|
|
308
|
+
reply({ ok: true });
|
|
309
|
+
}
|
|
310
|
+
return;
|
|
311
|
+
case "simulationPing":
|
|
312
|
+
reply({ ok: simulating });
|
|
313
|
+
return;
|
|
314
|
+
case "simulationBreakpoints":
|
|
315
|
+
case "simulationPause":
|
|
316
|
+
case "simulationResume":
|
|
317
|
+
case "simulationStep":
|
|
318
|
+
case "simulationAdvance":
|
|
319
|
+
if (!simulating || !runtime) {
|
|
320
|
+
reply({ error: "No simulation is running" });
|
|
321
|
+
return;
|
|
322
|
+
}
|
|
323
|
+
if (obj.command === "simulationBreakpoints") {
|
|
324
|
+
runtime.breakpoints = new Set(Array.isArray(message.breakpoints) ? message.breakpoints : []);
|
|
325
|
+
} else if (obj.command === "simulationPause") {
|
|
326
|
+
runtime.pauseSimulation();
|
|
327
|
+
} else if (obj.command === "simulationResume") {
|
|
328
|
+
runtime.resumeSimulation();
|
|
329
|
+
} else if (obj.command === "simulationStep") {
|
|
330
|
+
runtime.stepSimulation();
|
|
331
|
+
} else if (typeof message.ms === "number" && message.ms > 0) {
|
|
332
|
+
runtime.advanceTime(message.ms);
|
|
333
|
+
} else {
|
|
334
|
+
runtime.advanceToNextTimer();
|
|
335
|
+
}
|
|
336
|
+
reply({ ok: true });
|
|
337
|
+
return;
|
|
338
|
+
default:
|
|
339
|
+
reply({ error: `Unknown command "${obj.command}"` });
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
/**
|
|
343
|
+
* Reads the stored history of a chart, if it is not known yet
|
|
344
|
+
*
|
|
345
|
+
* @param chartId Chart ID
|
|
346
|
+
*/
|
|
347
|
+
async loadHistory(chartId) {
|
|
348
|
+
if (this.histories.has(chartId)) {
|
|
349
|
+
return;
|
|
350
|
+
}
|
|
351
|
+
let history = [];
|
|
352
|
+
try {
|
|
353
|
+
const state = await this.getStateAsync(`charts.${chartId}.history`);
|
|
354
|
+
const parsed = typeof state?.val === "string" && state.val ? JSON.parse(state.val) : [];
|
|
355
|
+
history = Array.isArray(parsed) ? parsed : [];
|
|
356
|
+
} catch {
|
|
357
|
+
}
|
|
358
|
+
this.histories.set(chartId, history);
|
|
359
|
+
}
|
|
360
|
+
/**
|
|
361
|
+
* Appends events to the history of a chart and stores it
|
|
362
|
+
*
|
|
363
|
+
* @param chartId Chart ID
|
|
364
|
+
* @param entries The new events
|
|
365
|
+
*/
|
|
366
|
+
addHistory(chartId, entries) {
|
|
367
|
+
const history = [...this.histories.get(chartId) || [], ...entries].slice(-HISTORY_LENGTH);
|
|
368
|
+
this.histories.set(chartId, history);
|
|
369
|
+
void this.setState(`charts.${chartId}.history`, JSON.stringify(history), true);
|
|
370
|
+
}
|
|
371
|
+
/**
|
|
372
|
+
* @param chartId Chart ID
|
|
373
|
+
*/
|
|
374
|
+
stopChart(chartId) {
|
|
375
|
+
const runtime = this.runtimes.get(chartId);
|
|
376
|
+
if (runtime) {
|
|
377
|
+
runtime.stop();
|
|
378
|
+
this.runtimes.delete(chartId);
|
|
379
|
+
this.updateSubscriptions();
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
/**
|
|
383
|
+
* Creates the states which show the current status of a chart
|
|
384
|
+
*
|
|
385
|
+
* @param chartId Chart ID
|
|
386
|
+
*/
|
|
387
|
+
async createChartStates(chartId) {
|
|
388
|
+
await this.setObjectNotExistsAsync(`charts.${chartId}.activeState`, {
|
|
389
|
+
type: "state",
|
|
390
|
+
common: {
|
|
391
|
+
name: "Active state",
|
|
392
|
+
type: "string",
|
|
393
|
+
role: "text",
|
|
394
|
+
read: true,
|
|
395
|
+
write: false
|
|
396
|
+
},
|
|
397
|
+
native: {}
|
|
398
|
+
});
|
|
399
|
+
await this.setObjectNotExistsAsync(`charts.${chartId}.snapshot`, {
|
|
400
|
+
type: "state",
|
|
401
|
+
common: {
|
|
402
|
+
name: "Stored state of the chart, restored after a restart of the adapter",
|
|
403
|
+
type: "string",
|
|
404
|
+
role: "json",
|
|
405
|
+
read: true,
|
|
406
|
+
write: false
|
|
407
|
+
},
|
|
408
|
+
native: {}
|
|
409
|
+
});
|
|
410
|
+
await this.setObjectNotExistsAsync(`charts.${chartId}.activeStateIds`, {
|
|
411
|
+
type: "state",
|
|
412
|
+
common: {
|
|
413
|
+
name: "IDs of all active states",
|
|
414
|
+
type: "string",
|
|
415
|
+
role: "json",
|
|
416
|
+
read: true,
|
|
417
|
+
write: false
|
|
418
|
+
},
|
|
419
|
+
native: {}
|
|
420
|
+
});
|
|
421
|
+
await this.setObjectNotExistsAsync(`charts.${chartId}.values`, {
|
|
422
|
+
type: "state",
|
|
423
|
+
common: {
|
|
424
|
+
name: "Current values of all variables",
|
|
425
|
+
type: "string",
|
|
426
|
+
role: "json",
|
|
427
|
+
read: true,
|
|
428
|
+
write: false
|
|
429
|
+
},
|
|
430
|
+
native: {}
|
|
431
|
+
});
|
|
432
|
+
await this.setObjectNotExistsAsync(`charts.${chartId}.history`, {
|
|
433
|
+
type: "state",
|
|
434
|
+
common: {
|
|
435
|
+
name: `Last ${HISTORY_LENGTH} events: starts and transitions taken, write anything to clear it`,
|
|
436
|
+
type: "string",
|
|
437
|
+
role: "json",
|
|
438
|
+
read: true,
|
|
439
|
+
write: true
|
|
440
|
+
},
|
|
441
|
+
native: {}
|
|
442
|
+
});
|
|
443
|
+
await this.setObjectNotExistsAsync(`charts.${chartId}.enabled`, {
|
|
444
|
+
type: "state",
|
|
445
|
+
common: {
|
|
446
|
+
name: "Chart runs: false pauses it, true starts it in its initial state",
|
|
447
|
+
type: "boolean",
|
|
448
|
+
role: "switch.enable",
|
|
449
|
+
read: true,
|
|
450
|
+
write: true
|
|
451
|
+
},
|
|
452
|
+
native: {}
|
|
453
|
+
});
|
|
454
|
+
await this.setObjectNotExistsAsync(`charts.${chartId}.reset`, {
|
|
455
|
+
type: "state",
|
|
456
|
+
common: {
|
|
457
|
+
name: "Restart the running chart in its initial state",
|
|
458
|
+
type: "boolean",
|
|
459
|
+
role: "button",
|
|
460
|
+
read: false,
|
|
461
|
+
write: true
|
|
462
|
+
},
|
|
463
|
+
native: {}
|
|
464
|
+
});
|
|
465
|
+
await this.setObjectNotExistsAsync(`charts.${chartId}.simulation`, {
|
|
466
|
+
type: "state",
|
|
467
|
+
common: {
|
|
468
|
+
name: "The chart runs as simulation from the editor, its outputs are not written",
|
|
469
|
+
type: "boolean",
|
|
470
|
+
role: "indicator",
|
|
471
|
+
read: true,
|
|
472
|
+
write: false
|
|
473
|
+
},
|
|
474
|
+
native: {}
|
|
475
|
+
});
|
|
476
|
+
await this.setObjectNotExistsAsync(`charts.${chartId}.simulationStatus`, {
|
|
477
|
+
type: "state",
|
|
478
|
+
common: {
|
|
479
|
+
name: "Status of the simulation as JSON: paused, time offset, queued wakeups, next timer, breakpoint",
|
|
480
|
+
type: "string",
|
|
481
|
+
role: "json",
|
|
482
|
+
read: true,
|
|
483
|
+
write: false
|
|
484
|
+
},
|
|
485
|
+
native: {}
|
|
486
|
+
});
|
|
487
|
+
await this.setObjectNotExistsAsync(`charts.${chartId}.event`, {
|
|
488
|
+
type: "state",
|
|
489
|
+
common: {
|
|
490
|
+
name: "Name of an input: wakes up the chart with this event, without changing the value of the input",
|
|
491
|
+
type: "string",
|
|
492
|
+
role: "text",
|
|
493
|
+
read: true,
|
|
494
|
+
write: true
|
|
495
|
+
},
|
|
496
|
+
native: {}
|
|
497
|
+
});
|
|
498
|
+
}
|
|
499
|
+
/**
|
|
500
|
+
* Executes a command written to a control state of a chart
|
|
501
|
+
*
|
|
502
|
+
* @param chartId Chart ID
|
|
503
|
+
* @param control Name of the state
|
|
504
|
+
* @param val The written value
|
|
505
|
+
*/
|
|
506
|
+
async handleControl(chartId, control, val) {
|
|
507
|
+
const id = `charts.${chartId}.${control}`;
|
|
508
|
+
const obj = await this.getObjectAsync(`charts.${chartId}`);
|
|
509
|
+
const chart = obj?.type === "channel" ? obj.native?.chart : void 0;
|
|
510
|
+
if (!chart) {
|
|
511
|
+
return;
|
|
512
|
+
}
|
|
513
|
+
if (control === "enabled") {
|
|
514
|
+
const enabled = val === true || val === "true" || val === 1;
|
|
515
|
+
if (enabled === (chart.enabled !== false)) {
|
|
516
|
+
await this.setState(id, enabled, true);
|
|
517
|
+
return;
|
|
518
|
+
}
|
|
519
|
+
await this.extendObjectAsync(`charts.${chartId}`, { native: { chart: { enabled } } });
|
|
520
|
+
return;
|
|
521
|
+
}
|
|
522
|
+
const runtime = this.runtimes.get(chartId);
|
|
523
|
+
if (control === "reset") {
|
|
524
|
+
if (this.simulations.has(chartId)) {
|
|
525
|
+
this.log.info(`Chart "${chart.name}" is not reset, it runs as simulation`);
|
|
526
|
+
} else if (runtime) {
|
|
527
|
+
this.log.info(`Chart "${chart.name}" is reset`);
|
|
528
|
+
this.chartQueue = this.chartQueue.then(async () => {
|
|
529
|
+
this.stopChart(chartId);
|
|
530
|
+
await this.startChart(chartId, obj);
|
|
531
|
+
}).catch((err) => this.log.error(`Cannot reset chart ${chartId}: ${err.message}`));
|
|
532
|
+
await this.chartQueue;
|
|
533
|
+
} else {
|
|
534
|
+
this.log.info(`Chart "${chart.name}" is not reset, it does not run`);
|
|
535
|
+
}
|
|
536
|
+
await this.setState(id, false, true);
|
|
537
|
+
return;
|
|
538
|
+
}
|
|
539
|
+
const name = String(val ?? "").trim();
|
|
540
|
+
if (!runtime) {
|
|
541
|
+
this.log.info(`Event "${name}" ignored, chart "${chart.name}" does not run`);
|
|
542
|
+
} else if (!runtime.handleEvent(name)) {
|
|
543
|
+
this.log.warn(`Chart "${chart.name}": unknown event "${name}", use the name of an input`);
|
|
544
|
+
}
|
|
545
|
+
await this.setState(id, name, true);
|
|
546
|
+
}
|
|
547
|
+
/**
|
|
548
|
+
* Subscribes to all inputs of the running charts and unsubscribes from the ones no longer used
|
|
549
|
+
*/
|
|
550
|
+
updateSubscriptions() {
|
|
551
|
+
const needed = new Set([...this.runtimes.values()].flatMap((runtime) => runtime.subscribedStateIds));
|
|
552
|
+
for (const id of needed) {
|
|
553
|
+
if (!this.subscribedInputs.has(id)) {
|
|
554
|
+
this.subscribeForeignStates(id);
|
|
555
|
+
}
|
|
556
|
+
}
|
|
557
|
+
for (const id of this.subscribedInputs) {
|
|
558
|
+
if (!needed.has(id)) {
|
|
559
|
+
this.unsubscribeForeignStates(id);
|
|
560
|
+
}
|
|
561
|
+
}
|
|
562
|
+
this.subscribedInputs = needed;
|
|
563
|
+
}
|
|
564
|
+
/**
|
|
565
|
+
* Is called when adapter shuts down - callback has to be called under any circumstances!
|
|
566
|
+
*
|
|
567
|
+
* @param callback Callback function
|
|
568
|
+
*/
|
|
569
|
+
onUnload(callback) {
|
|
570
|
+
try {
|
|
571
|
+
for (const timer of this.simulations.values()) {
|
|
572
|
+
this.clearTimeout(timer);
|
|
573
|
+
}
|
|
574
|
+
this.simulations.clear();
|
|
575
|
+
for (const runtime of this.runtimes.values()) {
|
|
576
|
+
runtime.stop();
|
|
577
|
+
}
|
|
578
|
+
this.runtimes.clear();
|
|
579
|
+
callback();
|
|
580
|
+
} catch (error) {
|
|
581
|
+
this.log.error(`Error during unloading: ${error.message}`);
|
|
582
|
+
callback();
|
|
583
|
+
}
|
|
584
|
+
}
|
|
585
|
+
/**
|
|
586
|
+
* Is called if a subscribed object changes
|
|
587
|
+
*
|
|
588
|
+
* @param id Object ID
|
|
589
|
+
* @param obj Object
|
|
590
|
+
*/
|
|
591
|
+
onObjectChange(id, obj) {
|
|
592
|
+
const chartId = this.getChartId(id);
|
|
593
|
+
if (!chartId) {
|
|
594
|
+
return;
|
|
595
|
+
}
|
|
596
|
+
this.chartQueue = this.chartQueue.then(async () => {
|
|
597
|
+
this.endSimulationTimer(chartId);
|
|
598
|
+
this.stopChart(chartId);
|
|
599
|
+
await this.startChart(chartId, obj);
|
|
600
|
+
}).catch((err) => this.log.error(`Cannot restart chart ${chartId}: ${err.message}`));
|
|
601
|
+
}
|
|
602
|
+
/**
|
|
603
|
+
* Is called if a subscribed state changes
|
|
604
|
+
*
|
|
605
|
+
* @param id State ID
|
|
606
|
+
* @param state State object
|
|
607
|
+
*/
|
|
608
|
+
onStateChange(id, state) {
|
|
609
|
+
if (!state) {
|
|
610
|
+
return;
|
|
611
|
+
}
|
|
612
|
+
const own = this.parseChartStateId(id);
|
|
613
|
+
if (own?.name === "history") {
|
|
614
|
+
if (!state.ack) {
|
|
615
|
+
this.histories.set(own.chartId, []);
|
|
616
|
+
void this.setState(`charts.${own.chartId}.history`, "[]", true);
|
|
617
|
+
}
|
|
618
|
+
return;
|
|
619
|
+
}
|
|
620
|
+
if (own && CONTROLS.includes(own.name)) {
|
|
621
|
+
if (!state.ack) {
|
|
622
|
+
this.handleControl(own.chartId, own.name, state.val).catch(
|
|
623
|
+
(err) => this.log.error(`Cannot execute ${id}: ${err.message}`)
|
|
624
|
+
);
|
|
625
|
+
}
|
|
626
|
+
return;
|
|
627
|
+
}
|
|
628
|
+
for (const runtime of this.runtimes.values()) {
|
|
629
|
+
runtime.handleStateChange(id, state);
|
|
630
|
+
}
|
|
631
|
+
}
|
|
632
|
+
}
|
|
633
|
+
if (require.main !== module) {
|
|
634
|
+
module.exports = (options) => new Statechart(options);
|
|
635
|
+
} else {
|
|
636
|
+
(() => new Statechart())();
|
|
637
|
+
}
|
|
638
|
+
//# sourceMappingURL=main.js.map
|