data-solectrus 0.2.10

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,282 @@
1
+ 'use strict';
2
+
3
+ // Tick scheduler + runtime loop.
4
+ // Keeps timing and error-policy in one place, away from main.js.
5
+
6
+ const itemManager = require('./itemManager');
7
+ const snapshotService = require('./snapshot');
8
+ const evaluator = require('./evaluator');
9
+ const { getItemOutputId } = require('./itemIds');
10
+ const stateRegistry = require('./stateRegistry');
11
+
12
+ function getTickIntervalMs(adapter) {
13
+ const fallbackSeconds = 5;
14
+ const cfgSecondsRaw = adapter.config && adapter.config.pollIntervalSeconds !== undefined
15
+ ? adapter.config.pollIntervalSeconds
16
+ : fallbackSeconds;
17
+ const cfgSeconds = Number(cfgSecondsRaw);
18
+
19
+ // Keep it sane; Admin enforces min/max but we also guard here.
20
+ const seconds = Number.isFinite(cfgSeconds) && cfgSeconds > 0 ? cfgSeconds : fallbackSeconds;
21
+ return Math.round(seconds * 1000);
22
+ }
23
+
24
+ function getTickTimeBudgetMs(adapter) {
25
+ const interval = getTickIntervalMs(adapter);
26
+ const ratioRaw = Number(adapter.TICK_TIME_BUDGET_RATIO);
27
+ const ratio = Number.isFinite(ratioRaw) && ratioRaw > 0 && ratioRaw <= 1 ? ratioRaw : 0.8;
28
+ return Math.max(0, Math.floor(interval * ratio));
29
+ }
30
+
31
+ function getErrorRetriesBeforeZero(adapter) {
32
+ const raw = adapter.config && adapter.config.errorRetriesBeforeZero !== undefined
33
+ ? adapter.config.errorRetriesBeforeZero
34
+ : 3;
35
+ const n = Number(raw);
36
+ if (!Number.isFinite(n) || n < 0) return 3;
37
+ return Math.min(100, Math.round(n));
38
+ }
39
+
40
+ function isInputTsGapDiagnosticsEnabled(adapter) {
41
+ const raw = adapter.config && adapter.config.enableInputTsGapDiagnostics !== undefined
42
+ ? adapter.config.enableInputTsGapDiagnostics
43
+ : true;
44
+ if (raw === false || raw === 0 || raw === '0' || raw === 'false') return false;
45
+ return true;
46
+ }
47
+
48
+ async function runTick(adapter) {
49
+ const start = Date.now();
50
+ const items = Array.isArray(adapter.config.items) ? adapter.config.items : [];
51
+ const enabledItems = items.filter(it => it && typeof it === 'object' && it.enabled);
52
+ const retriesBeforeZero = getErrorRetriesBeforeZero(adapter);
53
+ const timeBudgetMs = getTickTimeBudgetMs(adapter);
54
+ let skippedItems = 0;
55
+
56
+ // If config changed (without restart), rebuild compiled cache + subscriptions.
57
+ try {
58
+ await itemManager.ensureCompiledForCurrentConfig(adapter, items);
59
+ } catch (e) {
60
+ const msg = e && e.message ? e.message : String(e);
61
+ adapter.log.warn(`Prepare items failed: ${msg}`);
62
+ try {
63
+ await adapter.setStateAsync('info.lastError', msg, true);
64
+ } catch {
65
+ // ignore
66
+ }
67
+ }
68
+
69
+ // Keep status in sync even if config changes without a restart
70
+ try {
71
+ await adapter.setStateAsync('info.diagnostics.itemsTotal', items.filter(it => it && typeof it === 'object').length, true);
72
+ await adapter.setStateAsync('info.itemsActive', enabledItems.length, true);
73
+ await adapter.setStateAsync('info.status', enabledItems.length ? 'ok' : 'no_items_enabled', true);
74
+ await adapter.setStateAsync('info.diagnostics.evalBudgetMs', timeBudgetMs, true);
75
+ await adapter.setStateAsync('info.diagnostics.evalSkipped', 0, true);
76
+ } catch {
77
+ // ignore
78
+ }
79
+
80
+ let snapshot = null;
81
+ try {
82
+ snapshot = await snapshotService.buildSnapshotForTick(adapter, items);
83
+ } catch (e) {
84
+ const msg = e && e.message ? e.message : String(e);
85
+ adapter.log.warn(`Snapshot build failed: ${msg}`);
86
+ try {
87
+ await adapter.setStateAsync('info.lastError', msg, true);
88
+ } catch {
89
+ // ignore
90
+ }
91
+ snapshot = new Map();
92
+ }
93
+ adapter.currentSnapshot = snapshot;
94
+
95
+ // Publish input timestamp skew diagnostics.
96
+ // This helps explain "impossible" transient combinations when snapshot is off or sources update slightly offset.
97
+ try {
98
+ const enabled = isInputTsGapDiagnosticsEnabled(adapter);
99
+ if (!enabled) {
100
+ // Keep other diagnostics untouched when disabled.
101
+ } else {
102
+ const ids = snapshot && typeof snapshot.keys === 'function' ? Array.from(snapshot.keys()) : [];
103
+ const now = Date.now();
104
+ let minTs = Infinity;
105
+ let maxTs = -Infinity;
106
+ let minId = '';
107
+ let maxId = '';
108
+ let withTs = 0;
109
+ let total = 0;
110
+ /** @type {Array<{id:string, ts:number, ageMs:number}>} */
111
+ const entries = [];
112
+ for (const id of ids) {
113
+ if (!id) continue;
114
+ total++;
115
+ const ts = adapter.cacheTs.get(id);
116
+ if (typeof ts !== 'number' || !Number.isFinite(ts)) continue;
117
+ withTs++;
118
+ const ageMs = Math.max(0, Math.round(now - ts));
119
+ entries.push({ id: String(id), ts, ageMs });
120
+ if (ts < minTs) {
121
+ minTs = ts;
122
+ minId = String(id);
123
+ }
124
+ if (ts > maxTs) {
125
+ maxTs = ts;
126
+ maxId = String(id);
127
+ }
128
+ }
129
+ const gapMs = withTs >= 2 ? Math.max(0, Math.round(maxTs - minTs)) : 0;
130
+ const intervalMs = getTickIntervalMs(adapter);
131
+ const thresholdMs = Math.max(200, Math.min(5000, Math.floor(intervalMs * 0.2)));
132
+ const ok = withTs <= 1 ? true : gapMs <= thresholdMs;
133
+
134
+ // "Active" inputs are those that updated recently; slow/sleeping inputs can otherwise dominate the gap.
135
+ const activeAgeThresholdMs = Math.max(intervalMs * 2, 30_000);
136
+ let activeMinTs = Infinity;
137
+ let activeMaxTs = -Infinity;
138
+ let activeCount = 0;
139
+ for (const e of entries) {
140
+ if (e.ageMs > activeAgeThresholdMs) continue;
141
+ activeCount++;
142
+ if (e.ts < activeMinTs) activeMinTs = e.ts;
143
+ if (e.ts > activeMaxTs) activeMaxTs = e.ts;
144
+ }
145
+ const activeGapMs = activeCount >= 2 ? Math.max(0, Math.round(activeMaxTs - activeMinTs)) : 0;
146
+ const activeOk = activeCount <= 1 ? true : activeGapMs <= thresholdMs;
147
+ const sleeping = Math.max(0, withTs - activeCount);
148
+
149
+ const oldestAgeMs = withTs >= 1 ? Math.max(0, Math.round(now - minTs)) : 0;
150
+ const newestAgeMs = withTs >= 1 ? Math.max(0, Math.round(now - maxTs)) : 0;
151
+
152
+ await adapter.setStateAsync('info.diagnostics.timing.gapMs', gapMs, true);
153
+ await adapter.setStateAsync('info.diagnostics.timing.gapOk', ok, true);
154
+ await adapter.setStateAsync('info.diagnostics.timing.gapActiveMs', activeGapMs, true);
155
+ await adapter.setStateAsync('info.diagnostics.timing.gapActiveOk', activeOk, true);
156
+ await adapter.setStateAsync('info.diagnostics.timing.sources', withTs, true);
157
+ await adapter.setStateAsync('info.diagnostics.timing.sourcesActive', activeCount, true);
158
+ await adapter.setStateAsync('info.diagnostics.timing.sourcesSleeping', sleeping, true);
159
+ await adapter.setStateAsync('info.diagnostics.timing.oldestId', minId, true);
160
+ await adapter.setStateAsync('info.diagnostics.timing.oldestAgeMs', oldestAgeMs, true);
161
+ await adapter.setStateAsync('info.diagnostics.timing.newestId', maxId, true);
162
+ await adapter.setStateAsync('info.diagnostics.timing.newestAgeMs', newestAgeMs, true);
163
+ }
164
+ } catch {
165
+ // ignore
166
+ }
167
+
168
+ for (let idx = 0; idx < enabledItems.length; idx++) {
169
+ const item = enabledItems[idx];
170
+ if (timeBudgetMs > 0 && (Date.now() - start) > timeBudgetMs) {
171
+ skippedItems = enabledItems.length - idx;
172
+ adapter.warnOnce(
173
+ `tick_budget_exceeded|${Math.floor(Date.now() / 60000)}`,
174
+ `Tick time budget exceeded (${timeBudgetMs}ms). Skipping ${skippedItems} remaining item(s) this tick.`
175
+ );
176
+ break;
177
+ }
178
+
179
+ const targetId = getItemOutputId(item);
180
+ if (!targetId) continue;
181
+ const itemStart = Date.now();
182
+ const itemInfoBase = stateRegistry.getItemInfoBaseId(targetId);
183
+
184
+ try {
185
+ const raw = await evaluator.computeItemValue(adapter, item, snapshot);
186
+ const shaped = evaluator.isNumericOutputItem(item)
187
+ ? evaluator.applyResultRules(adapter, item, raw)
188
+ : raw;
189
+ const value = evaluator.castValueForItemType(adapter, item, shaped);
190
+
191
+ await adapter.setStateAsync(targetId, value, true);
192
+ adapter.lastGoodValue.set(targetId, value);
193
+ adapter.lastGoodTs.set(targetId, Date.now());
194
+ adapter.consecutiveErrorCounts.set(targetId, 0);
195
+
196
+ // Per-item info states (best-effort; must never break tick)
197
+ try {
198
+ await adapter.setStateAsync(`${itemInfoBase}.lastOkTs`, new Date().toISOString(), true);
199
+ await adapter.setStateAsync(`${itemInfoBase}.lastEvalMs`, Date.now() - itemStart, true);
200
+ await adapter.setStateAsync(`${itemInfoBase}.lastError`, '', true);
201
+ await adapter.setStateAsync(`${itemInfoBase}.consecutiveErrors`, 0, true);
202
+ } catch {
203
+ // ignore
204
+ }
205
+ } catch (e) {
206
+ const name = item.name || targetId;
207
+ const errMsg = e && e.message ? e.message : String(e);
208
+ const msg = `${name}: ${errMsg}`;
209
+ adapter.warnOnce(`compute_failed|${targetId}`, `Compute failed (will retry/keep last): ${msg}`);
210
+ try {
211
+ await adapter.setStateAsync('info.lastError', msg, true);
212
+ } catch {
213
+ // ignore
214
+ }
215
+
216
+ const prev = adapter.consecutiveErrorCounts.get(targetId) || 0;
217
+ const next = prev + 1;
218
+ adapter.consecutiveErrorCounts.set(targetId, next);
219
+ try {
220
+ await adapter.setStateAsync(`${itemInfoBase}.lastError`, errMsg, true);
221
+ await adapter.setStateAsync(`${itemInfoBase}.lastEvalMs`, Date.now() - itemStart, true);
222
+ await adapter.setStateAsync(`${itemInfoBase}.consecutiveErrors`, next, true);
223
+ } catch {
224
+ // ignore
225
+ }
226
+
227
+ // Policy: keep last good value for N retries, then set to 0.
228
+ if (adapter.lastGoodValue.has(targetId) && next <= retriesBeforeZero) {
229
+ try {
230
+ await adapter.setStateAsync(targetId, adapter.lastGoodValue.get(targetId), true);
231
+ } catch {
232
+ // ignore write errors
233
+ }
234
+ } else if (next > retriesBeforeZero) {
235
+ try {
236
+ await adapter.setStateAsync(targetId, evaluator.getZeroValueForItem(item), true);
237
+ } catch {
238
+ // ignore write errors
239
+ }
240
+ }
241
+ }
242
+ }
243
+
244
+ adapter.currentSnapshot = null;
245
+
246
+ try {
247
+ await adapter.setStateAsync('info.diagnostics.evalSkipped', skippedItems, true);
248
+ await adapter.setStateAsync('info.lastRun', new Date().toISOString(), true);
249
+ await adapter.setStateAsync('info.lastRunMs', Date.now() - start, true);
250
+ } catch {
251
+ // ignore
252
+ }
253
+ }
254
+
255
+ function scheduleNextTick(adapter) {
256
+ if (adapter.isUnloading) return;
257
+ const interval = getTickIntervalMs(adapter);
258
+ const now = Date.now();
259
+ const delay = interval - (now % interval);
260
+
261
+ if (adapter.tickTimer) {
262
+ clearTimeout(adapter.tickTimer);
263
+ adapter.tickTimer = null;
264
+ }
265
+
266
+ adapter.tickTimer = setTimeout(() => {
267
+ runTick(adapter)
268
+ .catch(e => {
269
+ const msg = e && e.message ? e.message : String(e);
270
+ adapter.log.error(`Tick failed: ${msg}`);
271
+ adapter.setState('info.lastError', msg, true);
272
+ })
273
+ .finally(() => scheduleNextTick(adapter));
274
+ }, delay);
275
+ }
276
+
277
+ module.exports = {
278
+ scheduleNextTick,
279
+ runTick,
280
+ getTickIntervalMs,
281
+ getTickTimeBudgetMs,
282
+ };
package/main.js ADDED
@@ -0,0 +1,253 @@
1
+ 'use strict';
2
+
3
+ const utils = require('@iobroker/adapter-core');
4
+ const {
5
+ parseExpression,
6
+ normalizeFormulaExpression: normalizeFormulaExpressionImpl,
7
+ analyzeAst: analyzeAstImpl,
8
+ evalFormulaAst: evalFormulaAstImpl,
9
+ } = require('./lib/formula');
10
+ const {
11
+ applyJsonPath: applyJsonPathImpl,
12
+ getNumericFromJsonPath: getNumericFromJsonPathImpl,
13
+ getValueFromJsonPath: getValueFromJsonPathImpl,
14
+ } = require('./lib/jsonpath');
15
+
16
+ const stateRegistry = require('./lib/services/stateRegistry');
17
+ const itemManager = require('./lib/services/itemManager');
18
+ const tickRunner = require('./lib/services/tickRunner');
19
+ const evaluator = require('./lib/services/evaluator');
20
+
21
+ class DataSolectrus extends utils.Adapter {
22
+ constructor(options) {
23
+ super({
24
+ ...options,
25
+ name: 'data-solectrus',
26
+ });
27
+
28
+ // Hard limits to keep formula evaluation predictable even with hostile/mistyped configs.
29
+ this.MAX_FORMULA_LENGTH = 8000;
30
+ this.MAX_AST_NODES = 2000;
31
+ this.MAX_AST_DEPTH = 60;
32
+ this.MAX_DISCOVERED_STATE_IDS_PER_ITEM = 250;
33
+ // Global caps to keep runtime behavior predictable even with huge configs.
34
+ this.MAX_TOTAL_SOURCE_IDS = 5000;
35
+ this.TICK_TIME_BUDGET_RATIO = 0.8;
36
+
37
+ this.cache = new Map();
38
+ this.cacheTs = new Map();
39
+ // Precompiled per-item cache to keep tick evaluation fast and robust.
40
+ /** @type {Map<string, {ok:boolean, error?:string, item:any, outputId:string, mode:string, sourceIds:Set<string>, normalizedExpr?:string, ast?:any, constantValue?:any}>} */
41
+ this.compiledItems = new Map();
42
+ this.itemsConfigSignature = '';
43
+ this.subscribedIds = new Set();
44
+ this.lastGoodValue = new Map();
45
+ this.lastGoodTs = new Map();
46
+ this.consecutiveErrorCounts = new Map();
47
+
48
+ this.currentSnapshot = null;
49
+ this.tickTimer = null;
50
+ this.isUnloading = false;
51
+ this.jsonPathWarned = new Set();
52
+ this.debugOnceKeys = new Set();
53
+
54
+ // Formula helpers are intentionally small and deterministic.
55
+ // They can read from the tick snapshot (preferred) or the event cache.
56
+ this.formulaFunctions = evaluator.createFormulaFunctions(this);
57
+
58
+ this.on('ready', this.onReady.bind(this));
59
+ this.on('unload', this.onUnload.bind(this));
60
+ this.on('stateChange', this.onStateChange.bind(this));
61
+ this.on('message', this.onMessage.bind(this));
62
+ }
63
+
64
+ onMessage(obj) {
65
+ try {
66
+ if (!obj || !obj.command) return;
67
+ if (obj.command !== 'evalFormulaPreview') return;
68
+
69
+ const msg = obj.message && typeof obj.message === 'object' ? obj.message : {};
70
+ const expr = msg && msg.expr !== undefined ? String(msg.expr) : '';
71
+ const varsIn = msg && msg.vars && typeof msg.vars === 'object' ? msg.vars : {};
72
+
73
+ const safeVars = Object.create(null);
74
+ let keys = [];
75
+ try {
76
+ keys = Object.keys(varsIn);
77
+ } catch {
78
+ keys = [];
79
+ }
80
+
81
+ // Keep previews cheap and robust.
82
+ const MAX_PREVIEW_VARS = 200;
83
+ for (let i = 0; i < keys.length && i < MAX_PREVIEW_VARS; i++) {
84
+ const kRaw = keys[i];
85
+ const k = String(kRaw);
86
+ if (!/^[a-zA-Z_][a-zA-Z0-9_]*$/.test(k)) continue;
87
+ if (k === '__proto__' || k === 'prototype' || k === 'constructor') continue;
88
+
89
+ const v = varsIn[kRaw];
90
+ if (typeof v === 'string') {
91
+ safeVars[k] = v.length > 2000 ? v.slice(0, 2000) : v;
92
+ continue;
93
+ }
94
+ if (typeof v === 'number' || typeof v === 'boolean' || v === null || v === undefined) {
95
+ safeVars[k] = v;
96
+ continue;
97
+ }
98
+ // For objects/arrays, only allow reasonably-sized JSON.
99
+ try {
100
+ const json = JSON.stringify(v);
101
+ if (json && json.length <= 5000) {
102
+ safeVars[k] = v;
103
+ }
104
+ } catch {
105
+ // ignore
106
+ }
107
+ }
108
+
109
+ let result;
110
+ try {
111
+ result = this.evalFormula(expr, safeVars);
112
+ } catch (e) {
113
+ const err = e && e.message ? String(e.message) : String(e);
114
+ if (obj.callback) this.sendTo(obj.from, obj.command, { ok: false, error: err }, obj.callback);
115
+ return;
116
+ }
117
+
118
+ if (obj.callback) this.sendTo(obj.from, obj.command, { ok: true, value: result }, obj.callback);
119
+ } catch (e) {
120
+ try {
121
+ const err = e && e.message ? String(e.message) : String(e);
122
+ if (obj && obj.callback) this.sendTo(obj.from, obj.command, { ok: false, error: err }, obj.callback);
123
+ } catch {
124
+ // ignore
125
+ }
126
+ }
127
+ }
128
+
129
+ safeNum(val, fallback = 0) {
130
+ const n = Number(val);
131
+ return Number.isFinite(n) ? n : fallback;
132
+ }
133
+
134
+ warnOnce(key, msg) {
135
+ const k = String(key);
136
+ if (this.jsonPathWarned.size > 500) this.jsonPathWarned.clear();
137
+ if (this.jsonPathWarned.has(k)) return;
138
+ this.jsonPathWarned.add(k);
139
+ this.log.warn(msg);
140
+ }
141
+
142
+ debugOnce(key, msg) {
143
+ const k = String(key);
144
+ if (this.debugOnceKeys.size > 500) this.debugOnceKeys.clear();
145
+ if (this.debugOnceKeys.has(k)) return;
146
+ this.debugOnceKeys.add(k);
147
+ this.log.debug(msg);
148
+ }
149
+
150
+ /**
151
+ * Minimal JSONPath subset evaluator for typical IoT payloads.
152
+ * Supported examples:
153
+ * - $.apower
154
+ * - $.aenergy.by_minute[2]
155
+ * - $['temperature']['tC']
156
+ *
157
+ * Not supported: filters, wildcards, unions, recursive descent, functions.
158
+ */
159
+ applyJsonPath(obj, path) {
160
+ return applyJsonPathImpl(obj, path);
161
+ }
162
+
163
+ analyzeAst(ast) {
164
+ return analyzeAstImpl(ast, { maxNodes: this.MAX_AST_NODES, maxDepth: this.MAX_AST_DEPTH });
165
+ }
166
+
167
+ getNumericFromJsonPath(rawValue, jsonPath, warnKeyPrefix = '') {
168
+ return getNumericFromJsonPathImpl(rawValue, jsonPath, {
169
+ safeNum: this.safeNum.bind(this),
170
+ warnOnce: this.warnOnce.bind(this),
171
+ debugOnce: this.debugOnce.bind(this),
172
+ warnKeyPrefix,
173
+ });
174
+ }
175
+
176
+ getValueFromJsonPath(rawValue, jsonPath, warnKeyPrefix = '') {
177
+ return getValueFromJsonPathImpl(rawValue, jsonPath, {
178
+ warnOnce: this.warnOnce.bind(this),
179
+ warnKeyPrefix,
180
+ });
181
+ }
182
+
183
+ /**
184
+ * Normalizes some common non-JS formula syntax into the JS-like operators that `jsep` understands.
185
+ * - AND/OR/NOT (case-insensitive) -> && / || / !
186
+ * - single '=' (outside strings) -> '=='
187
+ *
188
+ * This is intentionally conservative and only runs outside quoted strings.
189
+ */
190
+ normalizeFormulaExpression(expr) {
191
+ return normalizeFormulaExpressionImpl(expr);
192
+ }
193
+
194
+ evalFormula(expr, vars) {
195
+ const normalized = this.normalizeFormulaExpression(expr);
196
+ if (normalized && normalized.length > this.MAX_FORMULA_LENGTH) {
197
+ throw new Error(`Formula too long (>${this.MAX_FORMULA_LENGTH} chars)`);
198
+ }
199
+ const ast = parseExpression(String(normalized));
200
+ this.analyzeAst(ast);
201
+ return this.evalFormulaAst(ast, vars);
202
+ }
203
+
204
+ evalFormulaAst(ast, vars) {
205
+ return evalFormulaAstImpl(ast, vars, this.formulaFunctions);
206
+ }
207
+
208
+ async onReady() {
209
+ this.isUnloading = false;
210
+ await stateRegistry.cleanupOldInfoStates(this);
211
+ await stateRegistry.createInfoStates(this);
212
+
213
+ // Config compatibility: some Admin/jsonConfig schema versions may store custom-control data
214
+ // under the control name (e.g. itemsEditor). Always prefer native.items but fall back.
215
+ const items = Array.isArray(this.config.items) ? this.config.items : [];
216
+ const itemsEditor = Array.isArray(this.config.itemsEditor) ? this.config.itemsEditor : [];
217
+ this.config.items = items.length ? items : itemsEditor;
218
+
219
+ await itemManager.ensureItemTitlesInInstanceConfig(this);
220
+ await itemManager.prepareItems(this);
221
+
222
+ this.log.info('Adapter started successfully');
223
+ tickRunner.scheduleNextTick(this);
224
+ }
225
+
226
+ onStateChange(id, state) {
227
+ if (!state) return;
228
+ if (id && String(id).startsWith(`${this.namespace}.`)) {
229
+ return;
230
+ }
231
+ this.cache.set(id, state.val);
232
+ this.cacheTs.set(id, typeof state.ts === 'number' ? state.ts : Date.now());
233
+ }
234
+
235
+ onUnload(callback) {
236
+ try {
237
+ this.isUnloading = true;
238
+ if (this.tickTimer) {
239
+ clearTimeout(this.tickTimer);
240
+ this.tickTimer = null;
241
+ }
242
+ callback();
243
+ } catch {
244
+ callback();
245
+ }
246
+ }
247
+ }
248
+
249
+ if (require.main !== module) {
250
+ module.exports = options => new DataSolectrus(options);
251
+ } else {
252
+ (() => new DataSolectrus())();
253
+ }
package/package.json ADDED
@@ -0,0 +1,36 @@
1
+ {
2
+ "name": "data-solectrus",
3
+ "version": "0.2.10",
4
+ "description": "ioBroker adapter to compute/mirror PV & consumption values for SOLECTRUS dashboards",
5
+ "author": "Sven Griese (Felliglanz)",
6
+ "main": "main.js",
7
+ "keywords": [
8
+ "iobroker",
9
+ "adapter",
10
+ "solectrus",
11
+ "pv",
12
+ "formula"
13
+ ],
14
+ "license": "MIT",
15
+ "repository": {
16
+ "type": "git",
17
+ "url": "git+https://github.com/Felliglanz/data-solectrus.git"
18
+ },
19
+ "bugs": {
20
+ "url": "https://github.com/Felliglanz/data-solectrus/issues"
21
+ },
22
+ "homepage": "https://github.com/Felliglanz/data-solectrus#readme",
23
+ "engines": {
24
+ "node": ">=18"
25
+ },
26
+ "dependencies": {
27
+ "@iobroker/adapter-core": "^3.2.3",
28
+ "jsep": "^1.3.8"
29
+ },
30
+ "scripts": {
31
+ "start": "node main.js",
32
+ "lint": "node scripts/lint-syntax.js",
33
+ "smoke": "node scripts/smoke-runtime.js",
34
+ "check:simulate": "node scripts/check-simulate-30s.js"
35
+ }
36
+ }