node-red-contrib-energymeterplus 0.3.3 → 0.3.5
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/energyMeterPlus.js +210 -100
- package/package.json +1 -1
package/energyMeterPlus.js
CHANGED
|
@@ -6,111 +6,15 @@ module.exports = function(RED) {
|
|
|
6
6
|
RED.nodes.createNode(this, config);
|
|
7
7
|
var node = this;
|
|
8
8
|
|
|
9
|
+
// Config
|
|
9
10
|
let unitCost = Number(config.unitCost) || 0;
|
|
10
11
|
let filePath = config.filePath || "/config/node_red/solargen_data.json";
|
|
11
12
|
let inputUnit = config.inputUnit || "W";
|
|
12
13
|
let currencyCode = config.currency || "USD";
|
|
13
14
|
|
|
14
|
-
//
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
baseline.weekly += Number(config.baselineWeekly) || 0;
|
|
18
|
-
baseline.monthly += Number(config.baselineMonthly) || 0;
|
|
19
|
-
baseline.yearly += Number(config.baselineYearly) || 0;
|
|
20
|
-
node.context().set("baseline", baseline);
|
|
21
|
-
|
|
22
|
-
// Accumulated values
|
|
23
|
-
let accumulated = node.context().get("accumulated") || { daily:0, weekly:0, monthly:0, yearly:0 };
|
|
24
|
-
|
|
25
|
-
// Last check timestamp
|
|
26
|
-
let lastCheck = node.context().get("lastCheck");
|
|
27
|
-
if (!(lastCheck instanceof Date)) {
|
|
28
|
-
lastCheck = new Date(lastCheck || Date.now());
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
// 1. Input handler: baseline updates + power increments
|
|
32
|
-
node.on('input', function(msg) {
|
|
33
|
-
let now = new Date();
|
|
34
|
-
let durationHours = (now - lastCheck) / (1000 * 3600);
|
|
35
|
-
|
|
36
|
-
// Handle baseline updates via message
|
|
37
|
-
if (msg.topic === "applyBaseline" && msg.payload) {
|
|
38
|
-
baseline.daily += Number(msg.payload.daily) || 0;
|
|
39
|
-
baseline.weekly += Number(msg.payload.weekly) || 0;
|
|
40
|
-
baseline.monthly += Number(msg.payload.monthly) || 0;
|
|
41
|
-
baseline.yearly += Number(msg.payload.yearly) || 0;
|
|
42
|
-
node.context().set("baseline", baseline);
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
// 2. Handle power input
|
|
46
|
-
let power = Number(msg.payload);
|
|
47
|
-
if (!isNaN(power) && durationHours > 0) {
|
|
48
|
-
let power_kW = (inputUnit === "W") ? power / 1000 : power;
|
|
49
|
-
let energyIncrement = power_kW * durationHours;
|
|
50
|
-
|
|
51
|
-
accumulated.daily += energyIncrement;
|
|
52
|
-
accumulated.weekly += energyIncrement;
|
|
53
|
-
accumulated.monthly += energyIncrement;
|
|
54
|
-
accumulated.yearly += energyIncrement;
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
lastCheck = now;
|
|
58
|
-
node.context().set("lastCheck", lastCheck);
|
|
59
|
-
node.context().set("accumulated", accumulated);
|
|
60
|
-
|
|
61
|
-
// 3. Output payload
|
|
62
|
-
msg.payload = {
|
|
63
|
-
energyDaily: round2(baseline.daily + accumulated.daily),
|
|
64
|
-
energyWeekly: round2(baseline.weekly + accumulated.weekly),
|
|
65
|
-
energyMonthly: round2(baseline.monthly + accumulated.monthly),
|
|
66
|
-
energyYearly: round2(baseline.yearly + accumulated.yearly),
|
|
67
|
-
daily_cost: round2((baseline.daily + accumulated.daily) * unitCost),
|
|
68
|
-
weekly_cost: round2((baseline.weekly + accumulated.weekly) * unitCost),
|
|
69
|
-
monthly_cost: round2((baseline.monthly + accumulated.monthly) * unitCost),
|
|
70
|
-
yearly_cost: round2((baseline.yearly + accumulated.yearly) * unitCost),
|
|
71
|
-
currency: currencySymbol(currencyCode)
|
|
72
|
-
};
|
|
73
|
-
|
|
74
|
-
// 4. Persist to file
|
|
75
|
-
const dir = path.dirname(filePath);
|
|
76
|
-
if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
|
|
77
|
-
try { fs.writeFileSync(filePath, JSON.stringify(msg.payload, null, 2)); }
|
|
78
|
-
catch (err) { node.error("Failed to write to file: " + err); }
|
|
79
|
-
|
|
80
|
-
node.send(msg);
|
|
81
|
-
});
|
|
82
|
-
|
|
83
|
-
// 5. Rollover logic
|
|
84
|
-
function checkRollover() {
|
|
85
|
-
let now = new Date();
|
|
86
|
-
|
|
87
|
-
if (!(lastCheck instanceof Date)) {
|
|
88
|
-
lastCheck = new Date(lastCheck || Date.now());
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
if (now.getDate() !== lastCheck.getDate()) {
|
|
92
|
-
accumulated.daily = 0; baseline.daily = 0;
|
|
93
|
-
}
|
|
94
|
-
if (now.getDay() === 1 && lastCheck.getDay() !== 1) {
|
|
95
|
-
accumulated.weekly = 0; baseline.weekly = 0;
|
|
96
|
-
}
|
|
97
|
-
if (now.getMonth() !== lastCheck.getMonth()) {
|
|
98
|
-
accumulated.monthly = 0; baseline.monthly = 0;
|
|
99
|
-
}
|
|
100
|
-
if (now.getFullYear() !== lastCheck.getFullYear()) {
|
|
101
|
-
archiveYearly(baseline.yearly + accumulated.yearly, lastCheck.getFullYear());
|
|
102
|
-
accumulated.yearly = 0; baseline.yearly = 0;
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
lastCheck = now;
|
|
106
|
-
node.context().set("lastCheck", lastCheck);
|
|
107
|
-
node.context().set("accumulated", accumulated);
|
|
108
|
-
node.context().set("baseline", baseline);
|
|
109
|
-
}
|
|
110
|
-
setInterval(checkRollover, 60000);
|
|
111
|
-
|
|
112
|
-
// 6. Helpers
|
|
113
|
-
function currencySymbol(code) {
|
|
15
|
+
// Helpers
|
|
16
|
+
function toNum(v) { const n = Number(v); return isNaN(n) ? 0 : n; }
|
|
17
|
+
function currencySymbol(code) {
|
|
114
18
|
switch (code) {
|
|
115
19
|
case "USD": return "$";
|
|
116
20
|
case "EUR": return "€";
|
|
@@ -131,10 +35,216 @@ module.exports = function(RED) {
|
|
|
131
35
|
}
|
|
132
36
|
archive.push({ year, total, timestamp: new Date().toISOString() });
|
|
133
37
|
fs.writeFileSync(archivePath, JSON.stringify(archive, null, 2));
|
|
38
|
+
node.log(`Archived yearly total for ${year}: ${total}`);
|
|
134
39
|
} catch (err) {
|
|
135
40
|
node.error("Failed to archive yearly total: " + err);
|
|
136
41
|
}
|
|
137
42
|
}
|
|
43
|
+
|
|
44
|
+
// --- Editor-config delta apply (run on every deploy)
|
|
45
|
+
try {
|
|
46
|
+
let lastApplied = node.context().get("lastAppliedEditor") || { daily:0, weekly:0, monthly:0, yearly:0 };
|
|
47
|
+
|
|
48
|
+
let editor = {
|
|
49
|
+
daily: toNum(config.baselineDaily),
|
|
50
|
+
weekly: toNum(config.baselineWeekly),
|
|
51
|
+
monthly: toNum(config.baselineMonthly),
|
|
52
|
+
yearly: toNum(config.baselineYearly)
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
let delta = {
|
|
56
|
+
daily: editor.daily - (toNum(lastApplied.daily) || 0),
|
|
57
|
+
weekly: editor.weekly - (toNum(lastApplied.weekly) || 0),
|
|
58
|
+
monthly: editor.monthly - (toNum(lastApplied.monthly) || 0),
|
|
59
|
+
yearly: editor.yearly - (toNum(lastApplied.yearly) || 0)
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
// Ensure baseline exists
|
|
63
|
+
let baseline = node.context().get("baseline") || { daily:0, weekly:0, monthly:0, yearly:0 };
|
|
64
|
+
|
|
65
|
+
// Apply delta only if non-zero (keeps logs clean)
|
|
66
|
+
if (delta.daily || delta.weekly || delta.monthly || delta.yearly) {
|
|
67
|
+
baseline.daily += delta.daily;
|
|
68
|
+
baseline.weekly += delta.weekly;
|
|
69
|
+
baseline.monthly += delta.monthly;
|
|
70
|
+
baseline.yearly += delta.yearly;
|
|
71
|
+
|
|
72
|
+
node.context().set("baseline", baseline);
|
|
73
|
+
node.context().set("lastAppliedEditor", editor);
|
|
74
|
+
|
|
75
|
+
node.log(`Editor baseline applied (delta): daily ${delta.daily}, weekly ${delta.weekly}, monthly ${delta.monthly}, yearly ${delta.yearly}`);
|
|
76
|
+
} else {
|
|
77
|
+
// Still ensure lastAppliedEditor exists in context
|
|
78
|
+
if (!node.context().get("lastAppliedEditor")) {
|
|
79
|
+
node.context().set("lastAppliedEditor", editor);
|
|
80
|
+
}
|
|
81
|
+
node.log("Editor baseline: no change to apply on deploy.");
|
|
82
|
+
}
|
|
83
|
+
} catch (err) {
|
|
84
|
+
node.error("Error applying editor baseline delta: " + err);
|
|
85
|
+
}
|
|
86
|
+
// -------------------------------------------------------------------------------
|
|
87
|
+
|
|
88
|
+
// Accumulated values
|
|
89
|
+
let accumulated = node.context().get("accumulated") || { daily:0, weekly:0, monthly:0, yearly:0 };
|
|
90
|
+
|
|
91
|
+
// Last check timestamp
|
|
92
|
+
let lastCheck = node.context().get("lastCheck");
|
|
93
|
+
if (!(lastCheck instanceof Date)) {
|
|
94
|
+
lastCheck = new Date(lastCheck || Date.now());
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// Rollover interval handle
|
|
98
|
+
let rolloverInterval = null;
|
|
99
|
+
|
|
100
|
+
// Rollover logic
|
|
101
|
+
function checkRollover() {
|
|
102
|
+
try {
|
|
103
|
+
let now = new Date();
|
|
104
|
+
|
|
105
|
+
if (!(lastCheck instanceof Date)) {
|
|
106
|
+
lastCheck = new Date(lastCheck || Date.now());
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// daily
|
|
110
|
+
if (now.getDate() !== lastCheck.getDate() || now.toDateString() !== lastCheck.toDateString()) {
|
|
111
|
+
accumulated.daily = 0;
|
|
112
|
+
let baseline = node.context().get("baseline") || { daily:0, weekly:0, monthly:0, yearly:0 };
|
|
113
|
+
baseline.daily = 0;
|
|
114
|
+
node.context().set("baseline", baseline);
|
|
115
|
+
node.log("Daily rollover executed.");
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// weekly (assume week starts Monday)
|
|
119
|
+
if (now.getDay() === 1 && lastCheck.getDay() !== 1) {
|
|
120
|
+
accumulated.weekly = 0;
|
|
121
|
+
let baseline = node.context().get("baseline") || { daily:0, weekly:0, monthly:0, yearly:0 };
|
|
122
|
+
baseline.weekly = 0;
|
|
123
|
+
node.context().set("baseline", baseline);
|
|
124
|
+
node.log("Weekly rollover executed.");
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// monthly
|
|
128
|
+
if (now.getMonth() !== lastCheck.getMonth()) {
|
|
129
|
+
accumulated.monthly = 0;
|
|
130
|
+
let baseline = node.context().get("baseline") || { daily:0, weekly:0, monthly:0, yearly:0 };
|
|
131
|
+
baseline.monthly = 0;
|
|
132
|
+
node.context().set("baseline", baseline);
|
|
133
|
+
node.log("Monthly rollover executed.");
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// yearly
|
|
137
|
+
if (now.getFullYear() !== lastCheck.getFullYear()) {
|
|
138
|
+
let baseline = node.context().get("baseline") || { daily:0, weekly:0, monthly:0, yearly:0 };
|
|
139
|
+
archiveYearly(baseline.yearly + accumulated.yearly, lastCheck.getFullYear());
|
|
140
|
+
accumulated.yearly = 0;
|
|
141
|
+
baseline.yearly = 0;
|
|
142
|
+
node.context().set("baseline", baseline);
|
|
143
|
+
node.log("Yearly rollover executed and archived.");
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
lastCheck = now;
|
|
147
|
+
node.context().set("lastCheck", lastCheck);
|
|
148
|
+
node.context().set("accumulated", accumulated);
|
|
149
|
+
} catch (err) {
|
|
150
|
+
node.error("Rollover error: " + err);
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
// Start rollover timer
|
|
155
|
+
rolloverInterval = setInterval(checkRollover, 60000);
|
|
156
|
+
|
|
157
|
+
// Main input handler
|
|
158
|
+
node.on('input', function(msg) {
|
|
159
|
+
try {
|
|
160
|
+
// Allow manual reset of lastAppliedEditor via message
|
|
161
|
+
if (msg.topic === "resetEditorApplied") {
|
|
162
|
+
node.context().set("lastAppliedEditor", { daily:0, weekly:0, monthly:0, yearly:0 });
|
|
163
|
+
node.log("lastAppliedEditor cleared by message.");
|
|
164
|
+
return;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
// Rehydrate baseline & accumulated from context (in case other code changed them)
|
|
168
|
+
let baseline = node.context().get("baseline") || { daily:0, weekly:0, monthly:0, yearly:0 };
|
|
169
|
+
accumulated = node.context().get("accumulated") || accumulated;
|
|
170
|
+
|
|
171
|
+
// Ensure lastCheck is a Date
|
|
172
|
+
if (!(lastCheck instanceof Date)) {
|
|
173
|
+
lastCheck = new Date(lastCheck || Date.now());
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
let now = new Date();
|
|
177
|
+
let durationHours = (now - lastCheck) / (1000 * 3600);
|
|
178
|
+
|
|
179
|
+
// Runtime applyBaseline message (adds values immediately)
|
|
180
|
+
if (msg.topic === "applyBaseline" && msg.payload && typeof msg.payload === "object") {
|
|
181
|
+
let dDaily = toNum(msg.payload.daily);
|
|
182
|
+
let dWeekly = toNum(msg.payload.weekly);
|
|
183
|
+
let dMonthly = toNum(msg.payload.monthly);
|
|
184
|
+
let dYearly = toNum(msg.payload.yearly);
|
|
185
|
+
|
|
186
|
+
baseline.daily += dDaily;
|
|
187
|
+
baseline.weekly += dWeekly;
|
|
188
|
+
baseline.monthly += dMonthly;
|
|
189
|
+
baseline.yearly += dYearly;
|
|
190
|
+
|
|
191
|
+
node.context().set("baseline", baseline);
|
|
192
|
+
node.log(`Runtime applyBaseline: daily ${dDaily}, weekly ${dWeekly}, monthly ${dMonthly}, yearly ${dYearly}`);
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
// Power input handling (numeric payload)
|
|
196
|
+
let power = Number(msg.payload);
|
|
197
|
+
if (!isNaN(power) && durationHours > 0) {
|
|
198
|
+
let power_kW = (inputUnit === "W") ? power / 1000 : power;
|
|
199
|
+
let energyIncrement = power_kW * durationHours;
|
|
200
|
+
|
|
201
|
+
accumulated.daily += energyIncrement;
|
|
202
|
+
accumulated.weekly += energyIncrement;
|
|
203
|
+
accumulated.monthly += energyIncrement;
|
|
204
|
+
accumulated.yearly += energyIncrement;
|
|
205
|
+
|
|
206
|
+
node.log(`Power input processed: ${power} ${inputUnit} -> +${energyIncrement.toFixed(6)} kWh over ${durationHours.toFixed(6)} h`);
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
// Update timestamps and persist
|
|
210
|
+
lastCheck = now;
|
|
211
|
+
node.context().set("lastCheck", lastCheck);
|
|
212
|
+
node.context().set("accumulated", accumulated);
|
|
213
|
+
node.context().set("baseline", baseline);
|
|
214
|
+
|
|
215
|
+
// Build output payload
|
|
216
|
+
msg.payload = {
|
|
217
|
+
energyDaily: round2(baseline.daily + accumulated.daily),
|
|
218
|
+
energyWeekly: round2(baseline.weekly + accumulated.weekly),
|
|
219
|
+
energyMonthly: round2(baseline.monthly + accumulated.monthly),
|
|
220
|
+
energyYearly: round2(baseline.yearly + accumulated.yearly),
|
|
221
|
+
daily_cost: round2((baseline.daily + accumulated.daily) * unitCost),
|
|
222
|
+
weekly_cost: round2((baseline.weekly + accumulated.weekly) * unitCost),
|
|
223
|
+
monthly_cost: round2((baseline.monthly + accumulated.monthly) * unitCost),
|
|
224
|
+
yearly_cost: round2((baseline.yearly + accumulated.yearly) * unitCost),
|
|
225
|
+
currency: currencySymbol(currencyCode)
|
|
226
|
+
};
|
|
227
|
+
|
|
228
|
+
// Persist to file
|
|
229
|
+
try {
|
|
230
|
+
const dir = path.dirname(filePath);
|
|
231
|
+
if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
|
|
232
|
+
fs.writeFileSync(filePath, JSON.stringify(msg.payload, null, 2));
|
|
233
|
+
} catch (err) {
|
|
234
|
+
node.error("Failed to write to file: " + err);
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
node.send(msg);
|
|
238
|
+
} catch (err) {
|
|
239
|
+
node.error("Input handler error: " + err);
|
|
240
|
+
}
|
|
241
|
+
});
|
|
242
|
+
|
|
243
|
+
// Clean up on close
|
|
244
|
+
node.on('close', function(removed, done) {
|
|
245
|
+
if (rolloverInterval) clearInterval(rolloverInterval);
|
|
246
|
+
done();
|
|
247
|
+
});
|
|
138
248
|
}
|
|
139
249
|
|
|
140
250
|
RED.nodes.registerType("energyMeterPlus", EnergyMeterPlus, {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "node-red-contrib-energymeterplus",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.5",
|
|
4
4
|
"description": "A custom Node-RED node that integrates power readings into energy totals with persistence and cost calculation.",
|
|
5
5
|
"author": "Arcfrankye",
|
|
6
6
|
"license": "MIT",
|