node-red-contrib-energymeterplus 0.3.4 → 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 +207 -125
- package/package.json +1 -1
package/energyMeterPlus.js
CHANGED
|
@@ -6,45 +6,83 @@ 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
|
+
// Helpers
|
|
15
16
|
function toNum(v) { const n = Number(v); return isNaN(n) ? 0 : n; }
|
|
17
|
+
function currencySymbol(code) {
|
|
18
|
+
switch (code) {
|
|
19
|
+
case "USD": return "$";
|
|
20
|
+
case "EUR": return "€";
|
|
21
|
+
case "NGN": return "₦";
|
|
22
|
+
default: return code;
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
function round2(val) {
|
|
26
|
+
if (val === null || val === undefined || isNaN(val)) return 0;
|
|
27
|
+
return Number(val.toFixed(2));
|
|
28
|
+
}
|
|
29
|
+
function archiveYearly(total, year) {
|
|
30
|
+
try {
|
|
31
|
+
const archivePath = path.join(path.dirname(filePath), "yearly_archive.json");
|
|
32
|
+
let archive = [];
|
|
33
|
+
if (fs.existsSync(archivePath)) {
|
|
34
|
+
archive = JSON.parse(fs.readFileSync(archivePath));
|
|
35
|
+
}
|
|
36
|
+
archive.push({ year, total, timestamp: new Date().toISOString() });
|
|
37
|
+
fs.writeFileSync(archivePath, JSON.stringify(archive, null, 2));
|
|
38
|
+
node.log(`Archived yearly total for ${year}: ${total}`);
|
|
39
|
+
} catch (err) {
|
|
40
|
+
node.error("Failed to archive yearly total: " + err);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
16
43
|
|
|
17
|
-
//
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
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
|
+
}
|
|
48
86
|
// -------------------------------------------------------------------------------
|
|
49
87
|
|
|
50
88
|
// Accumulated values
|
|
@@ -56,113 +94,157 @@ module.exports = function(RED) {
|
|
|
56
94
|
lastCheck = new Date(lastCheck || Date.now());
|
|
57
95
|
}
|
|
58
96
|
|
|
59
|
-
//
|
|
60
|
-
|
|
61
|
-
let now = new Date();
|
|
62
|
-
let durationHours = (now - lastCheck) / (1000 * 3600);
|
|
63
|
-
|
|
64
|
-
// Handle baseline updates via message (runtime applyBaseline)
|
|
65
|
-
if (msg.topic === "applyBaseline" && msg.payload) {
|
|
66
|
-
baseline.daily += Number(msg.payload.daily) || 0;
|
|
67
|
-
baseline.weekly += Number(msg.payload.weekly) || 0;
|
|
68
|
-
baseline.monthly += Number(msg.payload.monthly) || 0;
|
|
69
|
-
baseline.yearly += Number(msg.payload.yearly) || 0;
|
|
70
|
-
node.context().set("baseline", baseline);
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
// Handle power input
|
|
74
|
-
let power = Number(msg.payload);
|
|
75
|
-
if (!isNaN(power) && durationHours > 0) {
|
|
76
|
-
let power_kW = (inputUnit === "W") ? power / 1000 : power;
|
|
77
|
-
let energyIncrement = power_kW * durationHours;
|
|
97
|
+
// Rollover interval handle
|
|
98
|
+
let rolloverInterval = null;
|
|
78
99
|
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
}
|
|
100
|
+
// Rollover logic
|
|
101
|
+
function checkRollover() {
|
|
102
|
+
try {
|
|
103
|
+
let now = new Date();
|
|
84
104
|
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
// 3. Output payload
|
|
90
|
-
msg.payload = {
|
|
91
|
-
energyDaily: round2(baseline.daily + accumulated.daily),
|
|
92
|
-
energyWeekly: round2(baseline.weekly + accumulated.weekly),
|
|
93
|
-
energyMonthly: round2(baseline.monthly + accumulated.monthly),
|
|
94
|
-
energyYearly: round2(baseline.yearly + accumulated.yearly),
|
|
95
|
-
daily_cost: round2((baseline.daily + accumulated.daily) * unitCost),
|
|
96
|
-
weekly_cost: round2((baseline.weekly + accumulated.weekly) * unitCost),
|
|
97
|
-
monthly_cost: round2((baseline.monthly + accumulated.monthly) * unitCost),
|
|
98
|
-
yearly_cost: round2((baseline.yearly + accumulated.yearly) * unitCost),
|
|
99
|
-
currency: currencySymbol(currencyCode)
|
|
100
|
-
};
|
|
105
|
+
if (!(lastCheck instanceof Date)) {
|
|
106
|
+
lastCheck = new Date(lastCheck || Date.now());
|
|
107
|
+
}
|
|
101
108
|
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
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
|
+
}
|
|
107
117
|
|
|
108
|
-
|
|
109
|
-
|
|
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
|
+
}
|
|
110
126
|
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
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
|
+
}
|
|
114
135
|
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
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
|
+
}
|
|
118
145
|
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
}
|
|
125
|
-
if (now.getMonth() !== lastCheck.getMonth()) {
|
|
126
|
-
accumulated.monthly = 0; baseline.monthly = 0;
|
|
127
|
-
}
|
|
128
|
-
if (now.getFullYear() !== lastCheck.getFullYear()) {
|
|
129
|
-
archiveYearly(baseline.yearly + accumulated.yearly, lastCheck.getFullYear());
|
|
130
|
-
accumulated.yearly = 0; baseline.yearly = 0;
|
|
146
|
+
lastCheck = now;
|
|
147
|
+
node.context().set("lastCheck", lastCheck);
|
|
148
|
+
node.context().set("accumulated", accumulated);
|
|
149
|
+
} catch (err) {
|
|
150
|
+
node.error("Rollover error: " + err);
|
|
131
151
|
}
|
|
132
|
-
|
|
133
|
-
lastCheck = now;
|
|
134
|
-
node.context().set("lastCheck", lastCheck);
|
|
135
|
-
node.context().set("accumulated", accumulated);
|
|
136
|
-
node.context().set("baseline", baseline);
|
|
137
152
|
}
|
|
138
|
-
setInterval(checkRollover, 60000);
|
|
139
153
|
|
|
140
|
-
//
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
case "NGN": return "₦";
|
|
146
|
-
default: return code;
|
|
147
|
-
}
|
|
148
|
-
}
|
|
149
|
-
function round2(val) {
|
|
150
|
-
if (val === null || val === undefined || isNaN(val)) return 0;
|
|
151
|
-
return Number(val.toFixed(2));
|
|
152
|
-
}
|
|
153
|
-
function archiveYearly(total, year) {
|
|
154
|
+
// Start rollover timer
|
|
155
|
+
rolloverInterval = setInterval(checkRollover, 60000);
|
|
156
|
+
|
|
157
|
+
// Main input handler
|
|
158
|
+
node.on('input', function(msg) {
|
|
154
159
|
try {
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
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;
|
|
159
165
|
}
|
|
160
|
-
|
|
161
|
-
|
|
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);
|
|
162
238
|
} catch (err) {
|
|
163
|
-
node.error("
|
|
239
|
+
node.error("Input handler error: " + err);
|
|
164
240
|
}
|
|
165
|
-
}
|
|
241
|
+
});
|
|
242
|
+
|
|
243
|
+
// Clean up on close
|
|
244
|
+
node.on('close', function(removed, done) {
|
|
245
|
+
if (rolloverInterval) clearInterval(rolloverInterval);
|
|
246
|
+
done();
|
|
247
|
+
});
|
|
166
248
|
}
|
|
167
249
|
|
|
168
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",
|