node-red-contrib-energymeterplus 0.3.2 → 0.3.4
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 +96 -69
- package/package.json +1 -1
package/energyMeterPlus.js
CHANGED
|
@@ -11,88 +11,57 @@ module.exports = function(RED) {
|
|
|
11
11
|
let inputUnit = config.inputUnit || "W";
|
|
12
12
|
let currencyCode = config.currency || "USD";
|
|
13
13
|
|
|
14
|
-
//
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
14
|
+
// --- Editor-config delta apply (run on every deploy)
|
|
15
|
+
function toNum(v) { const n = Number(v); return isNaN(n) ? 0 : n; }
|
|
16
|
+
|
|
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)
|
|
20
26
|
};
|
|
21
27
|
|
|
22
|
-
//
|
|
23
|
-
let
|
|
24
|
-
daily:
|
|
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)
|
|
25
34
|
};
|
|
26
35
|
|
|
27
|
-
//
|
|
28
|
-
let
|
|
29
|
-
if (!(lastCheck instanceof Date)) {
|
|
30
|
-
lastCheck = new Date(lastCheck || Date.now());
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
function currencySymbol(code) {
|
|
34
|
-
switch (code) {
|
|
35
|
-
case "USD": return "$";
|
|
36
|
-
case "EUR": return "€";
|
|
37
|
-
case "NGN": return "₦";
|
|
38
|
-
default: return code;
|
|
39
|
-
}
|
|
40
|
-
}
|
|
41
|
-
function round2(val) {
|
|
42
|
-
if (val === null || val === undefined || isNaN(val)) return 0;
|
|
43
|
-
return Number(val.toFixed(2));
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
function archiveYearly(total, year) {
|
|
47
|
-
try {
|
|
48
|
-
const archivePath = path.join(path.dirname(filePath), "yearly_archive.json");
|
|
49
|
-
let archive = [];
|
|
50
|
-
if (fs.existsSync(archivePath)) {
|
|
51
|
-
archive = JSON.parse(fs.readFileSync(archivePath));
|
|
52
|
-
}
|
|
53
|
-
archive.push({ year, total, timestamp: new Date().toISOString() });
|
|
54
|
-
fs.writeFileSync(archivePath, JSON.stringify(archive, null, 2));
|
|
55
|
-
} catch (err) {
|
|
56
|
-
node.error("Failed to archive yearly total: " + err);
|
|
57
|
-
}
|
|
58
|
-
}
|
|
36
|
+
// ensure baseline exists
|
|
37
|
+
let baseline = node.context().get("baseline") || { daily:0, weekly:0, monthly:0, yearly:0 };
|
|
59
38
|
|
|
60
|
-
//
|
|
61
|
-
|
|
62
|
-
|
|
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;
|
|
63
44
|
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
45
|
+
// persist baseline and remember the editor values we just applied
|
|
46
|
+
node.context().set("baseline", baseline);
|
|
47
|
+
node.context().set("lastAppliedEditor", editor);
|
|
48
|
+
// -------------------------------------------------------------------------------
|
|
68
49
|
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
}
|
|
72
|
-
if (now.getDay() === 1 && lastCheck.getDay() !== 1) {
|
|
73
|
-
accumulated.weekly = 0; baseline.weekly = 0;
|
|
74
|
-
}
|
|
75
|
-
if (now.getMonth() !== lastCheck.getMonth()) {
|
|
76
|
-
accumulated.monthly = 0; baseline.monthly = 0;
|
|
77
|
-
}
|
|
78
|
-
if (now.getFullYear() !== lastCheck.getFullYear()) {
|
|
79
|
-
archiveYearly(baseline.yearly + accumulated.yearly, lastCheck.getFullYear());
|
|
80
|
-
accumulated.yearly = 0; baseline.yearly = 0;
|
|
81
|
-
}
|
|
50
|
+
// Accumulated values
|
|
51
|
+
let accumulated = node.context().get("accumulated") || { daily:0, weekly:0, monthly:0, yearly:0 };
|
|
82
52
|
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
53
|
+
// Last check timestamp
|
|
54
|
+
let lastCheck = node.context().get("lastCheck");
|
|
55
|
+
if (!(lastCheck instanceof Date)) {
|
|
56
|
+
lastCheck = new Date(lastCheck || Date.now());
|
|
87
57
|
}
|
|
88
|
-
setInterval(checkRollover, 60000);
|
|
89
58
|
|
|
90
|
-
// Input handler
|
|
59
|
+
// 1. Input handler: baseline updates + power increments
|
|
91
60
|
node.on('input', function(msg) {
|
|
92
61
|
let now = new Date();
|
|
93
62
|
let durationHours = (now - lastCheck) / (1000 * 3600);
|
|
94
63
|
|
|
95
|
-
// Handle baseline updates
|
|
64
|
+
// Handle baseline updates via message (runtime applyBaseline)
|
|
96
65
|
if (msg.topic === "applyBaseline" && msg.payload) {
|
|
97
66
|
baseline.daily += Number(msg.payload.daily) || 0;
|
|
98
67
|
baseline.weekly += Number(msg.payload.weekly) || 0;
|
|
@@ -117,6 +86,7 @@ module.exports = function(RED) {
|
|
|
117
86
|
node.context().set("lastCheck", lastCheck);
|
|
118
87
|
node.context().set("accumulated", accumulated);
|
|
119
88
|
|
|
89
|
+
// 3. Output payload
|
|
120
90
|
msg.payload = {
|
|
121
91
|
energyDaily: round2(baseline.daily + accumulated.daily),
|
|
122
92
|
energyWeekly: round2(baseline.weekly + accumulated.weekly),
|
|
@@ -129,6 +99,7 @@ module.exports = function(RED) {
|
|
|
129
99
|
currency: currencySymbol(currencyCode)
|
|
130
100
|
};
|
|
131
101
|
|
|
102
|
+
// 4. Persist to file
|
|
132
103
|
const dir = path.dirname(filePath);
|
|
133
104
|
if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
|
|
134
105
|
try { fs.writeFileSync(filePath, JSON.stringify(msg.payload, null, 2)); }
|
|
@@ -136,9 +107,65 @@ module.exports = function(RED) {
|
|
|
136
107
|
|
|
137
108
|
node.send(msg);
|
|
138
109
|
});
|
|
110
|
+
|
|
111
|
+
// 5. Rollover logic
|
|
112
|
+
function checkRollover() {
|
|
113
|
+
let now = new Date();
|
|
114
|
+
|
|
115
|
+
if (!(lastCheck instanceof Date)) {
|
|
116
|
+
lastCheck = new Date(lastCheck || Date.now());
|
|
117
|
+
}
|
|
118
|
+
|
|
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;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
lastCheck = now;
|
|
134
|
+
node.context().set("lastCheck", lastCheck);
|
|
135
|
+
node.context().set("accumulated", accumulated);
|
|
136
|
+
node.context().set("baseline", baseline);
|
|
137
|
+
}
|
|
138
|
+
setInterval(checkRollover, 60000);
|
|
139
|
+
|
|
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
|
+
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));
|
|
159
|
+
}
|
|
160
|
+
archive.push({ year, total, timestamp: new Date().toISOString() });
|
|
161
|
+
fs.writeFileSync(archivePath, JSON.stringify(archive, null, 2));
|
|
162
|
+
} catch (err) {
|
|
163
|
+
node.error("Failed to archive yearly total: " + err);
|
|
164
|
+
}
|
|
165
|
+
}
|
|
139
166
|
}
|
|
140
167
|
|
|
141
168
|
RED.nodes.registerType("energyMeterPlus", EnergyMeterPlus, {
|
|
142
169
|
color: "#f3a108fe"
|
|
143
170
|
});
|
|
144
|
-
}
|
|
171
|
+
};
|
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.4",
|
|
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",
|