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.
Files changed (2) hide show
  1. package/energyMeterPlus.js +210 -100
  2. package/package.json +1 -1
@@ -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
- // Baseline initialization + merge editor config
15
- let baseline = node.context().get("baseline") || { daily:0, weekly:0, monthly:0, yearly:0 };
16
- baseline.daily += Number(config.baselineDaily) || 0;
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",
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",