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.
Files changed (50) hide show
  1. package/LICENSE +21 -21
  2. package/README.md +59 -438
  3. package/admin/build/tab.js +133 -130
  4. package/admin/statechart.png +0 -0
  5. package/admin/{style.css → tab.css} +0 -2
  6. package/admin/tab_m.html +1 -1
  7. package/build/lib/chart-analysis.js +367 -0
  8. package/build/lib/chart-analysis.js.map +7 -0
  9. package/build/lib/chart-codegen.js +405 -0
  10. package/build/lib/chart-codegen.js.map +7 -0
  11. package/build/lib/chart-helpers.js +102 -0
  12. package/build/lib/chart-helpers.js.map +7 -0
  13. package/build/lib/chart-model.js +477 -0
  14. package/build/lib/chart-model.js.map +7 -0
  15. package/build/lib/chart-runtime.js +1521 -0
  16. package/build/lib/chart-runtime.js.map +7 -0
  17. package/build/lib/chart-structure.js +172 -0
  18. package/build/lib/chart-structure.js.map +7 -0
  19. package/build/lib/chart-validation.js +372 -0
  20. package/build/lib/chart-validation.js.map +7 -0
  21. package/build/main.js +638 -0
  22. package/build/main.js.map +7 -0
  23. package/io-package.json +31 -34
  24. package/package.json +21 -17
  25. package/admin/i18n/de.json +0 -4
  26. package/admin/i18n/en.json +0 -4
  27. package/admin/i18n/es.json +0 -4
  28. package/admin/i18n/fr.json +0 -4
  29. package/admin/i18n/it.json +0 -4
  30. package/admin/i18n/nl.json +0 -4
  31. package/admin/i18n/pl.json +0 -4
  32. package/admin/i18n/pt.json +0 -4
  33. package/admin/i18n/ru.json +0 -4
  34. package/admin/i18n/uk.json +0 -4
  35. package/admin/i18n/zh-cn.json +0 -4
  36. package/admin/jsonConfig.json +0 -19
  37. package/admin/statechart.svg +0 -38
  38. package/lib/adapter-config.d.ts +0 -19
  39. package/lib/chart-codegen.js +0 -436
  40. package/lib/chart-codegen.test.js +0 -132
  41. package/lib/chart-helpers.js +0 -74
  42. package/lib/chart-helpers.test.js +0 -38
  43. package/lib/chart-model.js +0 -577
  44. package/lib/chart-model.test.js +0 -189
  45. package/lib/chart-runtime.js +0 -797
  46. package/lib/chart-runtime.test.js +0 -699
  47. package/lib/chart-structure.js +0 -106
  48. package/lib/chart-validation.js +0 -328
  49. package/lib/chart-validation.test.js +0 -149
  50. package/main.js +0 -279
@@ -1,436 +0,0 @@
1
- "use strict";
2
-
3
- /*
4
- * Generates readable JavaScript from a chart. It shows how the runtime executes the chart:
5
- * the XState machine, and the functions which select the transitions in Stateflow order.
6
- * The code is only shown to the user, the runtime does not execute it.
7
- */
8
-
9
- const { HELPERS } = require("./chart-helpers");
10
- const { parseTransitionLabel } = require("./chart-model");
11
- const { ChartStructure } = require("./chart-structure");
12
-
13
- /**
14
- * @typedef {import("./chart-model").Chart} Chart
15
- * @typedef {import("./chart-model").ChartState} ChartState
16
- * @typedef {import("./chart-model").ChartTransition} ChartTransition
17
- * @typedef {import("./chart-model").TransitionLabel} TransitionLabel
18
- */
19
-
20
- const INDENT = " ";
21
-
22
- /**
23
- * @param {string} text
24
- * @param {number} level
25
- * @returns {string}
26
- */
27
- function indent(text, level) {
28
- const prefix = INDENT.repeat(level);
29
- return text
30
- .split("\n")
31
- .map(line => (line.trim() ? prefix + line : ""))
32
- .join("\n");
33
- }
34
-
35
- /**
36
- * Text for a // comment, on one line
37
- *
38
- * @param {string | undefined} text
39
- * @returns {string}
40
- */
41
- function oneLine(text) {
42
- return (text || "").replace(/\s+/g, " ").trim();
43
- }
44
-
45
- /**
46
- * A comment or note as // lines
47
- *
48
- * @param {string | undefined} text
49
- * @param {string} prefix Indentation and optional text before the first line, e.g. "Note: "
50
- * @param {string} [indentation]
51
- * @returns {string[]} no lines for an empty text
52
- */
53
- function commentLines(text, prefix = "", indentation = "") {
54
- if (!text?.trim()) {
55
- return [];
56
- }
57
- return text
58
- .trim()
59
- .split("\n")
60
- .map((line, i) => `${indentation}// ${i ? " ".repeat(prefix.length) : prefix}${line.trimEnd()}`.trimEnd());
61
- }
62
-
63
- /**
64
- * Converts a state ID to a valid identifier for the names of the generated functions
65
- *
66
- * @param {string} id
67
- * @returns {string}
68
- */
69
- function identifier(id) {
70
- const name = id.replace(/[^\w$]/g, "_");
71
- return /^\d/.test(name) ? `_${name}` : name;
72
- }
73
-
74
- /**
75
- * @param {string} key
76
- * @returns {string} the key for an object literal, in quotes only if necessary
77
- */
78
- function propertyKey(key) {
79
- return /^[A-Za-z_$][\w$]*$/.test(key) ? key : JSON.stringify(key);
80
- }
81
-
82
- /**
83
- * Removes the indentation all lines have in common, so the code can be indented again
84
- *
85
- * @param {string} code
86
- * @returns {string}
87
- */
88
- function dedent(code) {
89
- const lines = code
90
- .replace(/\s+$/, "")
91
- .replace(/^\s*\n/, "")
92
- .split("\n");
93
- const common = Math.min(...lines.filter(l => l.trim()).map(l => /^\s*/.exec(l)?.[0].length ?? 0));
94
- return lines.map(l => l.substring(Number.isFinite(common) ? common : 0)).join("\n");
95
- }
96
-
97
- /**
98
- * An arrow function executing an action, or null if there is no action
99
- *
100
- * @param {string | undefined} code
101
- * @param {number} level Indentation level of the line the function starts in
102
- * @returns {string | null}
103
- */
104
- function actionFunction(code, level) {
105
- if (!code?.trim()) {
106
- return null;
107
- }
108
- return `() => {\n${indent(dedent(code), level + 1)}\n${INDENT.repeat(level)}}`;
109
- }
110
-
111
- /**
112
- * Generates the code of a chart
113
- *
114
- * @param {Chart} chart
115
- * @param {string} chartId ID of the chart object, used for the machine ID like in the runtime
116
- * @returns {string}
117
- */
118
- function generateChartCode(chart, chartId) {
119
- const structure = new ChartStructure(chart);
120
- /** @param {string} id */
121
- const stateName = id => structure.statesById.get(id)?.name ?? id;
122
-
123
- /**
124
- * Parses a label, null if it is invalid or the target is unknown (the runtime ignores these transitions)
125
- *
126
- * @param {ChartTransition} transition
127
- * @returns {{ label: TransitionLabel } | { error: string }}
128
- */
129
- const parse = transition => {
130
- if (!structure.statesById.has(transition.target)) {
131
- return { error: "unknown target" };
132
- }
133
- try {
134
- return { label: parseTransitionLabel(transition.label) };
135
- } catch (err) {
136
- return { error: err.message };
137
- }
138
- };
139
-
140
- /**
141
- * @param {ChartTransition} transition
142
- * @returns {string} e.g. "Off -> On: motion[motion]"
143
- */
144
- const describe = transition =>
145
- `${stateName(transition.source)} -> ${stateName(transition.target)}${
146
- transition.label?.trim() ? `: ${oneLine(transition.label)}` : ""
147
- }`;
148
-
149
- const lines = [
150
- `// Code generated from the chart "${oneLine(chart.name)}" by the statechart adapter.`,
151
- "// It shows how the adapter executes the chart. It is not executed itself, changes here have no effect.",
152
- "",
153
- ...(chart.comment?.trim() ? [...commentLines(chart.comment), ""] : []),
154
- ...(chart.notes || []).flatMap(note => [...commentLines(note.text, "Note: "), ""]),
155
- 'const { createActor, createMachine } = require("xstate");',
156
- "",
157
- ];
158
-
159
- // variables
160
- lines.push("// ---------- Variables ----------", "");
161
- const outputs = chart.symbols.filter(s => s.scope === "output" && s.stateId);
162
- if (!chart.symbols.length) {
163
- lines.push("// no variables");
164
- }
165
- for (const symbol of chart.symbols) {
166
- if (symbol.scope === "local") {
167
- lines.push(`let ${symbol.name} = ${JSON.stringify(symbol.initial) ?? "undefined"}; // local`);
168
- } else if (!symbol.stateId) {
169
- lines.push(`let ${symbol.name}; // ${symbol.scope} without state`);
170
- } else if (symbol.scope === "input") {
171
- const trigger = [
172
- symbol.trigger === "change" ? "on value changes" : "on every update",
173
- symbol.ack === "ack" ? "only confirmed values (ack = true)" : "",
174
- symbol.ack === "command" ? "only commands (ack = false)" : "",
175
- symbol.debounceMs ? `after ${symbol.debounceMs} ms without updates` : "",
176
- ]
177
- .filter(Boolean)
178
- .join(", ");
179
- lines.push(
180
- `let ${symbol.name} = getState(${JSON.stringify(symbol.stateId)}); // input, wakes up the chart ${trigger}`,
181
- );
182
- } else {
183
- lines.push(
184
- `let ${symbol.name} = getState(${JSON.stringify(symbol.stateId)}); // output, see writeChangedOutputs()`,
185
- );
186
- }
187
- }
188
- if (outputs.length) {
189
- lines.push("", "// the last written value of each output");
190
- lines.push(`const written = { ${outputs.map(s => s.name).join(", ")} };`);
191
- }
192
- lines.push("");
193
-
194
- // function blocks
195
- for (const fn of chart.functions || []) {
196
- if (fn.code.trim()) {
197
- lines.push("// ---------- Function block ----------", "", ...commentLines(fn.comment), dedent(fn.code), "");
198
- }
199
- }
200
-
201
- // machine
202
- lines.push(
203
- "// ---------- State machine ----------",
204
- "",
205
- "// The transitions are selected by the process functions below (Stateflow order),",
206
- "// XState only executes them: exit actions, transition action, entry actions.",
207
- "const machine = createMachine({",
208
- `${INDENT}id: ${JSON.stringify(`chart_${chartId}`)},`,
209
- );
210
-
211
- /**
212
- * @param {ChartTransition} transition
213
- * @param {{ inner: boolean; reenter: boolean }} kind
214
- * @param {TransitionLabel} label
215
- * @param {number} level
216
- * @returns {string}
217
- */
218
- const transitionConfig = (transition, kind, label, level) => {
219
- const parts = [`target: ${JSON.stringify(`#${transition.target}`)}`];
220
- if (kind.reenter) {
221
- parts.push("reenter: true");
222
- }
223
- const action = actionFunction(label.action, level);
224
- if (action) {
225
- parts.push(`actions: ${action}`);
226
- }
227
- return `{ ${parts.join(", ")} }`;
228
- };
229
-
230
- /**
231
- * @param {ChartState} state
232
- * @param {number} level
233
- */
234
- const buildState = (state, level) => {
235
- const i1 = INDENT.repeat(level);
236
- const i2 = INDENT.repeat(level + 1);
237
- const i3 = INDENT.repeat(level + 2);
238
- lines.push(
239
- `${i1}// ${oneLine(state.name)}`,
240
- ...commentLines(state.comment, "", i1),
241
- `${i1}${propertyKey(state.id)}: {`,
242
- );
243
- lines.push(`${i2}id: ${JSON.stringify(state.id)},`);
244
- for (const field of /** @type {const} */ (["entry", "exit"])) {
245
- const action = actionFunction(state[field], level + 1);
246
- if (action) {
247
- lines.push(`${i2}${field}: ${action},`);
248
- }
249
- }
250
-
251
- const on = [];
252
- for (const transition of structure.getTransitions(state.id)) {
253
- const parsed = parse(transition);
254
- if ("error" in parsed) {
255
- on.push(`${i3}// ${describe(transition)}: ignored, ${parsed.error}`);
256
- continue;
257
- }
258
- const config = transitionConfig(
259
- transition,
260
- structure.getTransitionKind(transition),
261
- parsed.label,
262
- level + 2,
263
- );
264
- on.push(
265
- `${i3}// ${describe(transition)}`,
266
- ...commentLines(transition.comment, "", i3),
267
- `${i3}${JSON.stringify(`T.${transition.id}`)}: ${config},`,
268
- );
269
- }
270
- if (on.length) {
271
- lines.push(`${i2}on: {`, ...on, `${i2}},`);
272
- }
273
-
274
- const children = structure.getChildren(state.id);
275
- if (children.length) {
276
- lines.push(`${i2}initial: ${JSON.stringify(structure.getInitialChild(state.id))},`, `${i2}states: {`);
277
- for (const child of children) {
278
- buildState(child, level + 2);
279
- }
280
- lines.push(`${i2}},`);
281
- }
282
- lines.push(`${i1}},`);
283
- };
284
-
285
- const topLevel = structure.getChildren(null);
286
- if (topLevel.length) {
287
- lines.push(`${INDENT}initial: ${JSON.stringify(structure.getInitialChild(null))},`, `${INDENT}states: {`);
288
- for (const state of topLevel) {
289
- buildState(state, 2);
290
- }
291
- lines.push(`${INDENT}},`);
292
- }
293
- lines.push("});", "", "const actor = createActor(machine);", "actor.start();", "");
294
-
295
- // wakeup and process functions
296
- /**
297
- * @param {ChartState[]} states
298
- * @param {number} level
299
- * @param {string} prefix "return " inside process functions
300
- * @returns {string[]}
301
- */
302
- const callActive = (states, level, prefix) =>
303
- states.map(
304
- s =>
305
- `${INDENT.repeat(level)}if (isActive(${JSON.stringify(s.id)})) {\n` +
306
- `${INDENT.repeat(level + 1)}${prefix}process_${identifier(s.id)}(input);\n` +
307
- `${INDENT.repeat(level)}}`,
308
- );
309
-
310
- lines.push(
311
- "// ---------- Wakeup ----------",
312
- "",
313
- "/**",
314
- " * Called once for every change of an input, after its variable got the new value,",
315
- " * and when the time of an after(...) of an active state has come (input is null then).",
316
- " * Starting with the outermost active state, each active state is processed, see process_...().",
317
- " *",
318
- " * @param {string | null} input name of the changed input",
319
- " */",
320
- "function wakeup(input) {",
321
- // only one state of a level is active: "else if", so a state entered by a transition is not processed too
322
- ...topLevel.flatMap((s, i) => [
323
- `${INDENT}${i ? "} else " : ""}if (isActive(${JSON.stringify(s.id)})) {`,
324
- `${INDENT}${INDENT}process_${identifier(s.id)}(input);`,
325
- ]),
326
- ...(topLevel.length ? [`${INDENT}}`] : []),
327
- `${INDENT}writeChangedOutputs();`,
328
- "}",
329
- "",
330
- );
331
-
332
- /**
333
- * A check of a transition in a process function
334
- *
335
- * @param {ChartTransition} transition
336
- * @param {TransitionLabel} label
337
- * @returns {string[]}
338
- */
339
- const check = (transition, label) => {
340
- const conditions = [];
341
- if (label.event) {
342
- conditions.push(`input === ${JSON.stringify(label.event)}`);
343
- }
344
- if (label.afterMs !== undefined) {
345
- // valid from the moment on, not only when the timer expires
346
- conditions.push(`elapsed(${JSON.stringify(transition.source)}) >= ${label.afterMs}`);
347
- }
348
- if (label.condition) {
349
- conditions.push(`(${label.condition})`);
350
- }
351
- return [
352
- `${INDENT}// ${describe(transition)}`,
353
- ...commentLines(transition.comment, "", INDENT),
354
- `${INDENT}if (${conditions.join(" && ") || "true"}) {`,
355
- `${INDENT}${INDENT}actor.send({ type: ${JSON.stringify(`T.${transition.id}`)} });`,
356
- `${INDENT}${INDENT}return true;`,
357
- `${INDENT}}`,
358
- ];
359
- };
360
-
361
- for (const state of chart.states) {
362
- const outer = [];
363
- const inner = [];
364
- for (const transition of structure.getTransitions(state.id)) {
365
- const parsed = parse(transition);
366
- if ("error" in parsed) {
367
- continue;
368
- }
369
- (structure.getTransitionKind(transition).inner ? inner : outer).push(...check(transition, parsed.label));
370
- }
371
- const children = structure.getChildren(state.id);
372
- lines.push(
373
- "/**",
374
- ` * ${oneLine(state.name)}`,
375
- ...(state.comment?.trim()
376
- ? [
377
- " *",
378
- // "*/" would end the comment
379
- ...state.comment
380
- .trim()
381
- .split("\n")
382
- .map(line => ` * ${line.trimEnd().replace(/\*\//g, "* /")}`.trimEnd()),
383
- ]
384
- : []),
385
- " *",
386
- " * @param {string | null} input name of the changed input, null for timers",
387
- " * @returns {boolean} true if a transition was taken",
388
- " */",
389
- `function process_${identifier(state.id)}(input) {`,
390
- `${INDENT}// 1. transitions leaving the state`,
391
- ...(outer.length ? outer : [`${INDENT}// none`]),
392
- `${INDENT}// 2. during action`,
393
- state.during?.trim() ? `${INDENT}{\n${indent(dedent(state.during), 2)}\n${INDENT}}` : `${INDENT}// none`,
394
- `${INDENT}// 3. inner transitions (to own sub states, the state stays active)`,
395
- ...(inner.length ? inner : [`${INDENT}// none`]),
396
- `${INDENT}// 4. active sub state`,
397
- ...(children.length ? callActive(children, 1, "return ") : [`${INDENT}// none`]),
398
- `${INDENT}return false;`,
399
- "}",
400
- "",
401
- );
402
- }
403
-
404
- // helpers
405
- lines.push(
406
- "// ---------- Helpers ----------",
407
- "",
408
- "/** Writes every output whose value was changed by an action */",
409
- "function writeChangedOutputs() {",
410
- ...outputs.map(
411
- s =>
412
- `${INDENT}if (${s.name} !== written.${s.name}) {\n` +
413
- `${INDENT}${INDENT}written.${s.name} = ${s.name};\n` +
414
- `${INDENT}${INDENT}setState(${JSON.stringify(s.stateId)}, ${s.name});\n` +
415
- `${INDENT}}`,
416
- ),
417
- "}",
418
- "",
419
- "// Provided by the adapter:",
420
- "// getState(id) current value of an ioBroker state",
421
- "// isActive(stateId) true if the state of the chart is active",
422
- "// elapsed(stateId) milliseconds since the state was entered; when it reaches the delay of an",
423
- "// after(...) of the state, a timer calls wakeup(null)",
424
- "//",
425
- "// Helper functions for conditions, actions and function blocks:",
426
- ...Object.values(HELPERS).map(helper => `// ${helper.signature}\n// ${helper.description}`),
427
- "//",
428
- "// The adapter stores the active states, the times they were entered and the locals in the state",
429
- "// charts.<chart>.snapshot. After a restart of the adapter it continues with them, without executing the",
430
- "// entry actions again, if the chart was not changed meanwhile.",
431
- "",
432
- );
433
- return lines.join("\n");
434
- }
435
-
436
- module.exports = { generateChartCode };
@@ -1,132 +0,0 @@
1
- "use strict";
2
-
3
- const { expect } = require("chai");
4
- const acorn = require("acorn");
5
- const { generateChartCode } = require("./chart-codegen");
6
-
7
- /** @returns {import("./chart-model").Chart} */
8
- function createChart() {
9
- return {
10
- version: 1,
11
- name: "Heating",
12
- enabled: true,
13
- initial: "auto",
14
- symbols: [
15
- { name: "temp", scope: "input", stateId: "test.0.temp" },
16
- { name: "manual", scope: "input", stateId: "test.0.manual" },
17
- { name: "heating", scope: "output", stateId: "test.0.heating" },
18
- { name: "count", scope: "local", initial: 0 },
19
- ],
20
- states: [
21
- { id: "auto", name: "Auto", initial: "heat", during: "count++;", x: 0, y: 0 },
22
- { id: "idle", name: "Idle", parent: "auto", entry: "heating = false;", x: 0, y: 0 },
23
- { id: "heat", name: "Heat", parent: "auto", entry: "heating = true;\nlog('heating');", x: 0, y: 0 },
24
- { id: "manualState", name: "Manual", x: 0, y: 0 },
25
- ],
26
- transitions: [
27
- { id: "toManual", source: "auto", target: "manualState", label: "manual[manual]", priority: 1 },
28
- { id: "toAuto", source: "manualState", target: "auto", label: "after(5, min)[!manual]" },
29
- { id: "start", source: "idle", target: "heat", label: "[temp < 20]{count = 0;}" },
30
- { id: "restart", source: "auto", target: "idle", label: "[temp > 30]" },
31
- { id: "broken", source: "heat", target: "idle", label: "[temp > 22" },
32
- ],
33
- functions: [{ id: "f1", code: "function tooWarm(t) {\n return t > 22;\n}", x: 0, y: 0 }],
34
- };
35
- }
36
-
37
- describe("chart-codegen => generateChartCode", () => {
38
- it("generates valid JavaScript", () => {
39
- const code = generateChartCode(createChart(), "heating");
40
- expect(() => acorn.parse(code, { ecmaVersion: "latest", sourceType: "script" })).not.to.throw();
41
- });
42
-
43
- it("contains variables, function blocks and the machine", () => {
44
- const code = generateChartCode(createChart(), "heating");
45
- expect(code).to.include('let temp = getState("test.0.temp"); // input, wakes up the chart on every update');
46
- expect(code).to.include("let count = 0; // local");
47
- expect(code).to.include("const written = { heating };");
48
- expect(code).to.include("function tooWarm(t) {\n return t > 22;\n}");
49
- expect(code).to.include('id: "chart_heating"');
50
- expect(code).to.include('initial: "heat"');
51
- // temporal transitions are checked like the others, valid from the moment their time has come
52
- expect(code).to.include('if (elapsed("manualState") >= 300000 && (!manual)) {');
53
- expect(code).to.include('"T.toAuto": { target: "#auto" }');
54
- expect(code).not.to.include("after: {");
55
- });
56
-
57
- it("selects the transitions in Stateflow order", () => {
58
- const code = generateChartCode(createChart(), "heating");
59
- const auto = code.substring(code.indexOf("function process_auto("), code.indexOf("function process_idle("));
60
- const order = [
61
- 'input === "manual" && (manual)',
62
- "count++;",
63
- "(temp > 30)",
64
- 'isActive("idle")',
65
- 'isActive("heat")',
66
- ].map(part => auto.indexOf(part));
67
- expect(order.every(i => i >= 0)).to.equal(true, auto);
68
- expect([...order].sort((a, b) => a - b)).to.deep.equal(order);
69
- });
70
-
71
- it("marks inner transitions as not re-entering and ignores invalid transitions like the runtime", () => {
72
- const code = generateChartCode(createChart(), "heating");
73
- expect(code).to.include('"T.restart": { target: "#idle" }');
74
- expect(code).to.include('// Heat -> Idle: [temp > 22: ignored, Missing "]"');
75
- expect(code).not.to.include('"T.broken"');
76
- });
77
-
78
- it("processes only the state which was active when the chart woke up", () => {
79
- const code = generateChartCode(createChart(), "heating");
80
- expect(code).to.include(
81
- ' if (isActive("auto")) {\n process_auto(input);\n } else if (isActive("manualState")) {\n',
82
- );
83
- });
84
-
85
- it("indents multi line actions", () => {
86
- const code = generateChartCode(createChart(), "heating");
87
- expect(code).to.include(
88
- "entry: () => {\n heating = true;\n log('heating');\n",
89
- );
90
- });
91
-
92
- it("includes comments and notes as comments", () => {
93
- const chart = createChart();
94
- chart.comment = "Heating of the\nliving room";
95
- chart.notes = [{ id: "n1", text: "Check the\nthermostat", x: 0, y: 0 }];
96
- chart.states[0].comment = "Automatic mode */ with tricky text";
97
- chart.transitions[0].comment = "Manual has priority";
98
- /** @type {any[]} */ (chart.functions)[0].comment = "Helper";
99
- const code = generateChartCode(chart, "heating");
100
- expect(() => acorn.parse(code, { ecmaVersion: "latest" })).not.to.throw();
101
- expect(code).to.include("// Heating of the\n// living room\n");
102
- expect(code).to.include("// Note: Check the\n// thermostat\n");
103
- expect(code).to.include("// Helper\nfunction tooWarm(t)");
104
- expect(code).to.include(" // Auto\n // Automatic mode */ with tricky text\n auto: {");
105
- expect(code).to.include(" * Automatic mode * / with tricky text");
106
- // in the machine and in the process function
107
- expect(code).to.include(
108
- " // Auto -> Manual: manual[manual]\n // Manual has priority\n",
109
- );
110
- expect(code).to.include("\n // Auto -> Manual: manual[manual]\n // Manual has priority\n");
111
- });
112
-
113
- it("describes the triggers of inputs", () => {
114
- const chart = createChart();
115
- Object.assign(chart.symbols[0], { trigger: "change", ack: "ack", debounceMs: 500 });
116
- Object.assign(chart.symbols[1], { ack: "command" });
117
- const code = generateChartCode(chart, "heating");
118
- expect(code).to.include(
119
- "// input, wakes up the chart on value changes, only confirmed values (ack = true), after 500 ms without updates",
120
- );
121
- expect(code).to.include("// input, wakes up the chart on every update, only commands (ack = false)");
122
- });
123
-
124
- it("handles empty charts", () => {
125
- const code = generateChartCode(
126
- { version: 1, name: "Empty", enabled: true, symbols: [], states: [], transitions: [] },
127
- "empty",
128
- );
129
- expect(() => acorn.parse(code, { ecmaVersion: "latest" })).not.to.throw();
130
- expect(code).to.include("// no variables");
131
- });
132
- });
@@ -1,74 +0,0 @@
1
- "use strict";
2
-
3
- /*
4
- * Helper functions which can be used in conditions, actions and function blocks.
5
- * The descriptions are shared by the editor (completion), the validation and the code generator,
6
- * the runtime implements them (see ChartRuntime.createHelpers()).
7
- */
8
-
9
- /**
10
- * @typedef {object} HelperDescription
11
- * @property {string} signature
12
- * @property {string} description
13
- */
14
-
15
- /** @type {Record<string, HelperDescription>} */
16
- const HELPERS = {
17
- log: {
18
- signature: 'log(message, level = "info")',
19
- description: 'Writes a message to the ioBroker log, level "debug", "info", "warn" or "error"',
20
- },
21
- setState: {
22
- signature: "setState(id, value, ack = false)",
23
- description: "Writes a value to any ioBroker state",
24
- },
25
- setStateDelayed: {
26
- signature: "setStateDelayed(id, value, delayMs, ack = false)",
27
- description: "Writes a value after a delay, cancelled when the chart stops",
28
- },
29
- sendTo: {
30
- signature: "sendTo(instance, command, message)",
31
- description: 'Sends a message to an adapter, e.g. sendTo("telegram.0", "send", { text: "Hello" })',
32
- },
33
- isTimeBetween: {
34
- signature: 'isTimeBetween("22:00", "06:00")',
35
- description: "true if the current time is in the range, also over midnight",
36
- },
37
- isDaytime: {
38
- signature: "isDaytime()",
39
- description: "true between sunrise and sunset, at the location of the ioBroker system settings",
40
- },
41
- astroTime: {
42
- signature: 'astroTime("sunset", offsetMinutes = 0)',
43
- description:
44
- "Time of an astro event today: sunrise, sunset, dawn, dusk, solarNoon, nauticalDawn, nauticalDusk, goldenHour, goldenHourEnd, night, nightEnd",
45
- },
46
- };
47
-
48
- /**
49
- * @param {string} time "HH:mm" or "HH:mm:ss"
50
- * @returns {number} seconds since midnight
51
- * @throws {Error} if the time is invalid
52
- */
53
- function parseTime(time) {
54
- const match = /^(\d{1,2}):(\d{2})(?::(\d{2}))?$/.exec(String(time).trim());
55
- if (!match || Number(match[1]) > 23 || Number(match[2]) > 59 || Number(match[3] ?? 0) > 59) {
56
- throw new Error(`Invalid time "${time}", expected e.g. "22:30"`);
57
- }
58
- return Number(match[1]) * 3600 + Number(match[2]) * 60 + Number(match[3] ?? 0);
59
- }
60
-
61
- /**
62
- * @param {string} start "HH:mm", inclusive
63
- * @param {string} end "HH:mm", exclusive. A range over midnight ends on the next day.
64
- * @param {Date} [now]
65
- * @returns {boolean}
66
- */
67
- function isTimeBetween(start, end, now = new Date()) {
68
- const from = parseTime(start);
69
- const to = parseTime(end);
70
- const current = now.getHours() * 3600 + now.getMinutes() * 60 + now.getSeconds();
71
- return from <= to ? current >= from && current < to : current >= from || current < to;
72
- }
73
-
74
- module.exports = { HELPERS, isTimeBetween, parseTime };
@@ -1,38 +0,0 @@
1
- "use strict";
2
-
3
- const { expect } = require("chai");
4
- const { isTimeBetween, parseTime } = require("./chart-helpers");
5
-
6
- /**
7
- * @param {string} time "HH:mm"
8
- * @returns {Date} today at this time
9
- */
10
- const at = time => {
11
- const [h, m] = time.split(":").map(Number);
12
- const date = new Date();
13
- date.setHours(h, m, 0, 0);
14
- return date;
15
- };
16
-
17
- describe("chart-helpers => isTimeBetween", () => {
18
- it("checks ranges within a day, the end is exclusive", () => {
19
- expect(isTimeBetween("08:00", "17:30", at("08:00"))).to.equal(true);
20
- expect(isTimeBetween("08:00", "17:30", at("12:00"))).to.equal(true);
21
- expect(isTimeBetween("08:00", "17:30", at("17:30"))).to.equal(false);
22
- expect(isTimeBetween("08:00", "17:30", at("07:59"))).to.equal(false);
23
- });
24
-
25
- it("checks ranges over midnight", () => {
26
- expect(isTimeBetween("22:00", "06:00", at("23:30"))).to.equal(true);
27
- expect(isTimeBetween("22:00", "06:00", at("00:00"))).to.equal(true);
28
- expect(isTimeBetween("22:00", "06:00", at("05:59"))).to.equal(true);
29
- expect(isTimeBetween("22:00", "06:00", at("06:00"))).to.equal(false);
30
- expect(isTimeBetween("22:00", "06:00", at("12:00"))).to.equal(false);
31
- });
32
-
33
- it("rejects invalid times", () => {
34
- expect(() => parseTime("25:00")).to.throw(/Invalid time "25:00"/);
35
- expect(() => parseTime("8 Uhr")).to.throw(/Invalid time/);
36
- expect(parseTime("7:05:30")).to.equal(7 * 3600 + 5 * 60 + 30);
37
- });
38
- });