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.
Files changed (2) hide show
  1. package/energyMeterPlus.js +207 -125
  2. package/package.json +1 -1
@@ -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
- // --- Editor-config delta apply (run on every deploy)
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
- // read stored "last applied editor values" (so we can compute deltas)
18
- let lastApplied = node.context().get("lastAppliedEditor") || { daily:0, weekly:0, monthly:0, yearly:0 };
19
-
20
- // current editor values (always read from config)
21
- let editor = {
22
- daily: toNum(config.baselineDaily),
23
- weekly: toNum(config.baselineWeekly),
24
- monthly: toNum(config.baselineMonthly),
25
- yearly: toNum(config.baselineYearly)
26
- };
27
-
28
- // compute delta = editor - lastApplied
29
- let delta = {
30
- daily: editor.daily - (toNum(lastApplied.daily) || 0),
31
- weekly: editor.weekly - (toNum(lastApplied.weekly) || 0),
32
- monthly: editor.monthly - (toNum(lastApplied.monthly) || 0),
33
- yearly: editor.yearly - (toNum(lastApplied.yearly) || 0)
34
- };
35
-
36
- // ensure baseline exists
37
- let baseline = node.context().get("baseline") || { daily:0, weekly:0, monthly:0, yearly:0 };
38
-
39
- // apply delta (this adds positive or negative changes)
40
- baseline.daily += delta.daily;
41
- baseline.weekly += delta.weekly;
42
- baseline.monthly += delta.monthly;
43
- baseline.yearly += delta.yearly;
44
-
45
- // persist baseline and remember the editor values we just applied
46
- node.context().set("baseline", baseline);
47
- node.context().set("lastAppliedEditor", editor);
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
- // 1. Input handler: baseline updates + power increments
60
- node.on('input', function(msg) {
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
- accumulated.daily += energyIncrement;
80
- accumulated.weekly += energyIncrement;
81
- accumulated.monthly += energyIncrement;
82
- accumulated.yearly += energyIncrement;
83
- }
100
+ // Rollover logic
101
+ function checkRollover() {
102
+ try {
103
+ let now = new Date();
84
104
 
85
- lastCheck = now;
86
- node.context().set("lastCheck", lastCheck);
87
- node.context().set("accumulated", accumulated);
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
- // 4. Persist to file
103
- const dir = path.dirname(filePath);
104
- if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
105
- try { fs.writeFileSync(filePath, JSON.stringify(msg.payload, null, 2)); }
106
- catch (err) { node.error("Failed to write to file: " + err); }
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
- node.send(msg);
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
- // 5. Rollover logic
112
- function checkRollover() {
113
- let now = new Date();
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
- if (!(lastCheck instanceof Date)) {
116
- lastCheck = new Date(lastCheck || Date.now());
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
- if (now.getDate() !== lastCheck.getDate()) {
120
- accumulated.daily = 0; baseline.daily = 0;
121
- }
122
- if (now.getDay() === 1 && lastCheck.getDay() !== 1) {
123
- accumulated.weekly = 0; baseline.weekly = 0;
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
- // 6. Helpers
141
- function currencySymbol(code) {
142
- switch (code) {
143
- case "USD": return "$";
144
- case "EUR": return "€";
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
- const archivePath = path.join(path.dirname(filePath), "yearly_archive.json");
156
- let archive = [];
157
- if (fs.existsSync(archivePath)) {
158
- archive = JSON.parse(fs.readFileSync(archivePath));
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
- archive.push({ year, total, timestamp: new Date().toISOString() });
161
- fs.writeFileSync(archivePath, JSON.stringify(archive, null, 2));
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("Failed to archive yearly total: " + err);
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.4",
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",