iobroker.sun2000 2.4.4 → 2.5.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 +10 -8
- package/admin/jsonConfig.json5 +33 -0
- package/io-package.json +29 -28
- package/lib/drivers/driver_emma.js +6 -6
- package/lib/drivers/driver_inverter.js +67 -23
- package/lib/json_helper.js +126 -0
- package/lib/modbus/modbus_server.js +1 -1
- package/lib/register.js +174 -26
- package/lib/statistics/breakdownChartHelper.js +131 -0
- package/lib/statistics/consumptionBreakdown.js +304 -0
- package/lib/statistics/statistics.js +1453 -0
- package/lib/tools.js +15 -3
- package/main.js +12 -0
- package/package.json +9 -10
- package/lib/statistics.js +0 -1477
|
@@ -0,0 +1,304 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* consumptionBreakdown.js
|
|
5
|
+
*
|
|
6
|
+
* Manages the user-defined consumption breakdown configuration.
|
|
7
|
+
*
|
|
8
|
+
* Responsibilities:
|
|
9
|
+
* - Defining the ioBroker state schema for the breakdown config
|
|
10
|
+
* - Parsing and validating the JSON configuration array
|
|
11
|
+
* - Reading breakdown source values from *foreign* ioBroker states
|
|
12
|
+
* (outside the adapter namespace) via getForeignStateAsync and caching
|
|
13
|
+
* them into the shared stateCache under internal keys so the existing
|
|
14
|
+
* statistics calculation code can reach them transparently
|
|
15
|
+
* - Handling live updates when the user edits the breakdown state at runtime
|
|
16
|
+
*
|
|
17
|
+
* This module is intentionally free of any chart / eCharts logic.
|
|
18
|
+
* It is used by statistics.js and consumed by breakdownChartHelper.js.
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
const { statisticsType } = require(`${__dirname}/../types.js`);
|
|
22
|
+
|
|
23
|
+
// ---------------------------------------------------------------------------
|
|
24
|
+
// Constants
|
|
25
|
+
// ---------------------------------------------------------------------------
|
|
26
|
+
|
|
27
|
+
/** State ID (relative to adapter namespace) of the user-editable config */
|
|
28
|
+
const BREAKDOWN_STATE_ID = 'statistics.dataDef.consumptionBreakdown';
|
|
29
|
+
|
|
30
|
+
/** Internal stateCache key prefix used to store fetched values */
|
|
31
|
+
const CACHE_KEY_PREFIX = 'statistics._consumptionBreakdown.';
|
|
32
|
+
|
|
33
|
+
/** Empty array serialised as the factory-default value */
|
|
34
|
+
const BREAKDOWN_DEFAULT = JSON.stringify([], null, 2);
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* ioBroker state definition for the breakdown configuration state.
|
|
38
|
+
* Intended to be merged into the postProcessHooks states array of statistics.js.
|
|
39
|
+
*
|
|
40
|
+
*/
|
|
41
|
+
const BREAKDOWN_STATE_DEF = {
|
|
42
|
+
id: BREAKDOWN_STATE_ID,
|
|
43
|
+
name: 'Consumption breakdown definition',
|
|
44
|
+
type: 'string',
|
|
45
|
+
role: 'json',
|
|
46
|
+
desc:
|
|
47
|
+
'JA JSON array that defines how total consumption, in "kWh", divided into subcategories ' +
|
|
48
|
+
'for stacked chart display. Each entry requires at minimum: ' +
|
|
49
|
+
'"sourceId" (foreign ioBroker state path), "targetPath" (internal key), "name" (legend label). ' +
|
|
50
|
+
'Optional: "gain" (divisor, default 1), ' +
|
|
51
|
+
'"color" (hex string), "type" ("deltaReset"|"delta"|"level").',
|
|
52
|
+
write: true,
|
|
53
|
+
initVal: BREAKDOWN_DEFAULT,
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
// ---------------------------------------------------------------------------
|
|
57
|
+
// Typedef
|
|
58
|
+
// ---------------------------------------------------------------------------
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* sourceId - Foreign ioBroker state ID, read via getForeignStateAsync
|
|
62
|
+
* targetPath - Internal key in statistics data entries and chart series
|
|
63
|
+
* name - Human-readable chart legend label
|
|
64
|
+
* unit - Unit shown in tooltip (default: "kWh")
|
|
65
|
+
* gain - Divisor applied to the raw value before storing (default: 1)
|
|
66
|
+
* color - Optional hex colour for the eCharts series
|
|
67
|
+
* type - statisticsType value: deltaReset | delta | level
|
|
68
|
+
*/
|
|
69
|
+
|
|
70
|
+
// ---------------------------------------------------------------------------
|
|
71
|
+
// Class
|
|
72
|
+
// ---------------------------------------------------------------------------
|
|
73
|
+
|
|
74
|
+
class ConsumptionBreakdown {
|
|
75
|
+
/**
|
|
76
|
+
* @param {object} adapterInstance - ioBroker adapter instance (needs getForeignStateAsync, logger)
|
|
77
|
+
* @param {object} stateCache - Shared state cache (get / set interface)
|
|
78
|
+
* @param {string[]} builtinPaths - targetPath values already used by statistics.stats,
|
|
79
|
+
* used to prevent collisions during validation
|
|
80
|
+
*/
|
|
81
|
+
constructor(adapterInstance, stateCache, builtinPaths) {
|
|
82
|
+
this.adapter = adapterInstance;
|
|
83
|
+
this.stateCache = stateCache;
|
|
84
|
+
this._builtinPaths = builtinPaths || [];
|
|
85
|
+
|
|
86
|
+
this._entries = [];
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// -----------------------------------------------------------------------
|
|
90
|
+
// Public API
|
|
91
|
+
// -----------------------------------------------------------------------
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* The ioBroker state definition object that must be registered by statistics.js.
|
|
95
|
+
* @returns {object}
|
|
96
|
+
*/
|
|
97
|
+
get stateDef() {
|
|
98
|
+
return BREAKDOWN_STATE_DEF;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* The state ID of the breakdown configuration state.
|
|
103
|
+
* @returns {string}
|
|
104
|
+
*/
|
|
105
|
+
get stateId() {
|
|
106
|
+
return BREAKDOWN_STATE_ID;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* The currently active (validated) breakdown entries.
|
|
111
|
+
* @returns {ConsumptionBreakdownEntry[]}
|
|
112
|
+
*/
|
|
113
|
+
get entries() {
|
|
114
|
+
return this._entries;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Returns each active entry mapped to the schema expected by the statistics
|
|
119
|
+
* calculation pipeline (_calculateGeneric / _calculateAggregation):
|
|
120
|
+
*
|
|
121
|
+
* { sourceId, targetPath, unit, type, _isBreakdown: true }
|
|
122
|
+
*
|
|
123
|
+
* The sourceId points into the stateCache (CACHE_KEY_PREFIX + targetPath)
|
|
124
|
+
* where _refreshValues() stores the fetched foreign-state value.
|
|
125
|
+
*
|
|
126
|
+
* @returns {Array}
|
|
127
|
+
*/
|
|
128
|
+
get statsEntries() {
|
|
129
|
+
return this._entries.map(bd => ({
|
|
130
|
+
sourceId: `${CACHE_KEY_PREFIX}${bd.targetPath}`,
|
|
131
|
+
targetPath: bd.targetPath,
|
|
132
|
+
unit: bd.unit,
|
|
133
|
+
type: bd.type,
|
|
134
|
+
_isBreakdown: true,
|
|
135
|
+
}));
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* Loads the persisted configuration from the adapter state and parses it.
|
|
140
|
+
* Call this once during statistics.initialize().
|
|
141
|
+
*
|
|
142
|
+
* @returns {Promise<void>}
|
|
143
|
+
*/
|
|
144
|
+
async initialize() {
|
|
145
|
+
const state = await this.adapter.getStateAsync(BREAKDOWN_STATE_ID);
|
|
146
|
+
const raw = state?.val ?? BREAKDOWN_DEFAULT;
|
|
147
|
+
this.stateCache.set(BREAKDOWN_STATE_ID, raw, { type: 'string', stored: true });
|
|
148
|
+
if (state?.ack === false) {
|
|
149
|
+
await this.adapter.setState(BREAKDOWN_STATE_ID, { val: state.val, ack: true });
|
|
150
|
+
}
|
|
151
|
+
this._entries = this._parse(raw);
|
|
152
|
+
this._logLoad();
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* Fetches fresh values from all configured foreign states and writes them
|
|
157
|
+
* into the stateCache so that the statistics calculation code can access them.
|
|
158
|
+
*
|
|
159
|
+
* Must be called:
|
|
160
|
+
* - once after load() during initialisation, before the first calculation run
|
|
161
|
+
* - at the start of every scheduled hourly task (_executeScheduledTasks)
|
|
162
|
+
*
|
|
163
|
+
* @returns {Promise<void>}
|
|
164
|
+
*/
|
|
165
|
+
async refreshValues() {
|
|
166
|
+
for (const bd of this._entries) {
|
|
167
|
+
const cacheKey = `${CACHE_KEY_PREFIX}${bd.targetPath}`;
|
|
168
|
+
try {
|
|
169
|
+
const state = await this.adapter.getForeignStateAsync(bd.sourceId);
|
|
170
|
+
if (state && state?.val != null) {
|
|
171
|
+
const raw = Number(state.val);
|
|
172
|
+
if (!isNaN(raw)) {
|
|
173
|
+
this.stateCache.set(cacheKey, raw / bd.gain || 1, { type: 'number', stored: true });
|
|
174
|
+
} else {
|
|
175
|
+
this.adapter.logger.debug(`consumptionBreakdown: '${bd.targetPath}' value is not a number: ${state.val}`);
|
|
176
|
+
}
|
|
177
|
+
} else {
|
|
178
|
+
this.adapter.logger.debug(`consumptionBreakdown: '${bd.targetPath}' state '${bd.sourceId}' not available.`);
|
|
179
|
+
}
|
|
180
|
+
} catch (err) {
|
|
181
|
+
this.adapter.logger.warn(`consumptionBreakdown: '${bd.targetPath}' - error reading '${bd.sourceId}': ${err.message}`);
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
/**
|
|
187
|
+
* Handles a live change of the breakdown configuration state (ack: false).
|
|
188
|
+
* Validates the new JSON, updates the internal entry list and cache,
|
|
189
|
+
* acknowledges the state, then triggers a value refresh.
|
|
190
|
+
*
|
|
191
|
+
* The caller (statistics.js) is responsible for rebuilding the charts afterwards.
|
|
192
|
+
*
|
|
193
|
+
* @param {object} state - ioBroker state object with the new val
|
|
194
|
+
* @returns {Promise<boolean>} true if the config was updated, false on no-op
|
|
195
|
+
*/
|
|
196
|
+
async handleStateChange(state) {
|
|
197
|
+
if (state?.val == null) return false;
|
|
198
|
+
|
|
199
|
+
this.adapter.logger.info('consumptionBreakdown: configuration changed - reloading.');
|
|
200
|
+
|
|
201
|
+
const newEntries = this._parse(state.val);
|
|
202
|
+
this._entries = newEntries;
|
|
203
|
+
this.stateCache.set(BREAKDOWN_STATE_ID, state.val, { type: 'string', stored: true });
|
|
204
|
+
|
|
205
|
+
await this.adapter.setState(BREAKDOWN_STATE_ID, { val: state.val, ack: true });
|
|
206
|
+
//await this.refreshValues();
|
|
207
|
+
|
|
208
|
+
this._logLoad();
|
|
209
|
+
return true;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
// -----------------------------------------------------------------------
|
|
213
|
+
// Private helpers
|
|
214
|
+
// -----------------------------------------------------------------------
|
|
215
|
+
|
|
216
|
+
/**
|
|
217
|
+
* Parses, validates and normalises a raw JSON string into an entry array.
|
|
218
|
+
* Invalid entries are skipped with a warning; the rest are returned.
|
|
219
|
+
*
|
|
220
|
+
* @param {string} rawJson
|
|
221
|
+
* @returns {ConsumptionBreakdownEntry[]}
|
|
222
|
+
*/
|
|
223
|
+
_parse(rawJson) {
|
|
224
|
+
let arr = [];
|
|
225
|
+
try {
|
|
226
|
+
arr = JSON.parse(rawJson);
|
|
227
|
+
} catch (e) {
|
|
228
|
+
this.adapter.logger.warn(`consumptionBreakdown: JSON parse error: ${e.message}`);
|
|
229
|
+
return [];
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
if (!Array.isArray(arr)) {
|
|
233
|
+
this.adapter.logger.warn('consumptionBreakdown: root value must be a JSON array – ignoring.');
|
|
234
|
+
return [];
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
const valid = [];
|
|
238
|
+
for (const raw of arr) {
|
|
239
|
+
// --- Required fields ---
|
|
240
|
+
if (typeof raw.sourceId !== 'string' || !raw.sourceId.trim()) {
|
|
241
|
+
this.adapter.logger.warn(`consumptionBreakdown: entry missing 'sourceId' – skipping: ${JSON.stringify(raw)}`);
|
|
242
|
+
continue;
|
|
243
|
+
}
|
|
244
|
+
if (typeof raw.targetPath !== 'string' || !raw.targetPath.trim()) {
|
|
245
|
+
this.adapter.logger.warn(`consumptionBreakdown: entry missing 'targetPath' – skipping: ${JSON.stringify(raw)}`);
|
|
246
|
+
continue;
|
|
247
|
+
}
|
|
248
|
+
if (typeof raw.name !== 'string' || !raw.name.trim()) {
|
|
249
|
+
this.adapter.logger.warn(`consumptionBreakdown: entry missing 'name' – skipping: ${JSON.stringify(raw)}`);
|
|
250
|
+
continue;
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
// --- Collision check ---
|
|
254
|
+
if (this._builtinPaths.includes(raw.targetPath)) {
|
|
255
|
+
this.adapter.logger.warn(`consumptionBreakdown: targetPath '${raw.targetPath}' collides with a built-in stat – skipping.`);
|
|
256
|
+
continue;
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
// --- Duplicate targetPath within the breakdown list ---
|
|
260
|
+
if (valid.some(e => e.targetPath === raw.targetPath)) {
|
|
261
|
+
this.adapter.logger.warn(`consumptionBreakdown: duplicate targetPath '${raw.targetPath}' – skipping second occurrence.`);
|
|
262
|
+
continue;
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
// --- Normalise optional fields ---
|
|
266
|
+
const allowedTypes = [statisticsType.deltaReset, statisticsType.delta, statisticsType.level];
|
|
267
|
+
const entry = {
|
|
268
|
+
sourceId: raw.sourceId.trim(),
|
|
269
|
+
targetPath: raw.targetPath.trim(),
|
|
270
|
+
name: raw.name.trim(),
|
|
271
|
+
//unit: typeof raw.unit === 'string' && raw.unit ? raw.unit : 'kWh',
|
|
272
|
+
unit: 'kWh',
|
|
273
|
+
gain: typeof raw.gain === 'number' && raw.gain !== 0 ? raw.gain : 1,
|
|
274
|
+
color: typeof raw.color === 'string' && raw.color ? raw.color : null,
|
|
275
|
+
type: allowedTypes.includes(raw.type) ? raw.type : statisticsType.delta,
|
|
276
|
+
};
|
|
277
|
+
valid.push(entry);
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
return valid;
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
/** Logs a summary of the currently loaded entries at info level. */
|
|
284
|
+
_logLoad() {
|
|
285
|
+
const n = this._entries.length;
|
|
286
|
+
const noun = n === 1 ? 'entry' : 'entries';
|
|
287
|
+
this.adapter.logger.debug(`statistics: consumptionBreakdown loaded ${n} ${noun}.`);
|
|
288
|
+
if (n > 0) {
|
|
289
|
+
this.adapter.logger.debug(
|
|
290
|
+
`statistics: consumptionBreakdown entries: ${this._entries.map(e => `${e.name} (${e.sourceId} → ${e.targetPath})`).join(', ')}`,
|
|
291
|
+
);
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
// ---------------------------------------------------------------------------
|
|
297
|
+
// Exports
|
|
298
|
+
// ---------------------------------------------------------------------------
|
|
299
|
+
|
|
300
|
+
module.exports = {
|
|
301
|
+
ConsumptionBreakdown,
|
|
302
|
+
BREAKDOWN_STATE_ID,
|
|
303
|
+
BREAKDOWN_DEFAULT,
|
|
304
|
+
};
|