node-red-contrib-energymeterplus 0.1.0 → 0.1.2
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/README.md +166 -0
- package/energyMeterPlus.html +11 -2
- package/energyMeterPlus.js +49 -29
- package/package.json +3 -2
package/README.md
ADDED
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
#### **EnergyMeterPlus Node Version 0.1.2**
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
##### **Overview:**
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
EnergyMeterPlus is a custom Node‑RED function node designed to convert instantaneous power readings (Watts) into accumulated energy values (kWh) over time. It integrates power based on elapsed time between samples, applies baseline corrections, calculates costs, and persists totals across restarts.
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
##### **Features:**
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
* Converts power (W) into energy (kWh) using time integration.
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
* Tracks daily, weekly, monthly, and yearly energy totals.
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
* Calculates costs based on a configurable unit cost.
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
* Unit cost configurable in 3 different currencies
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
* Supports baseline corrections to start counters from existing values.
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
* Handles rollover at day, week, month, and year boundaries.
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
* Persists counters and timestamps in Node‑RED context and writes snapshots to file.
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
* Outputs clean values rounded to two decimal places for dashboards.
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
###### **Configuration:**
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
Unit Cost: Cost per kWh (e.g., 0.15 for $0.15/kWh).
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
Currency: Choose USD, EUR, or NGN (fixed values, not convertible)
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
File Path: Path to store snapshots (default: /config/node\_red/solargen\_data.json).
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
Input Unit: "W" for Watts (default) or "kW" if your source already provides kilowatts.
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
Baselines: Initial values for daily, weekly, monthly, and yearly counters. Can also be used to correct baseline values or left blank if not needed.
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
###### **Output:**
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
The node outputs a payload object:
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
json
|
|
88
|
+
|
|
89
|
+
{
|
|
90
|
+
|
|
91
|
+
  "daily\_kWh": 4.53,
|
|
92
|
+
|
|
93
|
+
  "weekly\_kWh": 32.18,
|
|
94
|
+
|
|
95
|
+
  "monthly\_kWh": 128.74,
|
|
96
|
+
|
|
97
|
+
  "yearly\_kWh": 1024.56,
|
|
98
|
+
|
|
99
|
+
  "daily\_cost": 0.68,
|
|
100
|
+
|
|
101
|
+
  "weekly\_cost": 4.83,
|
|
102
|
+
|
|
103
|
+
  "monthly\_cost": 19.31,
|
|
104
|
+
|
|
105
|
+
  "yearly\_cost": 153.68
|
|
106
|
+
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
All values are rounded to two decimal places.
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
###### **Rollover Logic:**
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
* Daily resets at midnight.
|
|
120
|
+
*
|
|
121
|
+
* Weekly resets on Sunday.
|
|
122
|
+
*
|
|
123
|
+
* Monthly resets on the first of the month.
|
|
124
|
+
*
|
|
125
|
+
* Yearly resets on January 1st and archives the previous year’s totals.
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
###### **Persistence:**
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
Counters and timestamps are stored in Node‑RED context.
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
Snapshots are written to the configured JSON file.
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
Directories are auto‑created if missing, ensuring no ENOENT errors.
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
##### **Example:**
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
If your solar system outputs \~4526 W continuously:
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
After 1 hour → \~4.53 kWh added to daily total.
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
After 24 hours → \~108.6 kWh added to daily total.
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
Costs scale automatically with your configured unit cost.
|
|
166
|
+
|
package/energyMeterPlus.html
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
<script type="text/javascript">
|
|
2
2
|
RED.nodes.registerType('energyMeterPlus', {
|
|
3
3
|
category: 'energy',
|
|
4
|
-
color: '#
|
|
4
|
+
color: '#f3a108fe', <!-- Dutch Orange -->
|
|
5
5
|
defaults: {
|
|
6
6
|
name: {value:""},
|
|
7
7
|
baselineDaily: {value:0},
|
|
@@ -9,7 +9,8 @@
|
|
|
9
9
|
baselineMonthly: {value:0},
|
|
10
10
|
baselineYearly: {value:0},
|
|
11
11
|
unitCost: {value:0.15},
|
|
12
|
-
|
|
12
|
+
currency: {value:"USD"}, // NEW field
|
|
13
|
+
filePath: {value:"/config/node_red/solargen_data.json"},
|
|
13
14
|
inputUnit: {value:"kW"}
|
|
14
15
|
},
|
|
15
16
|
inputs:1,
|
|
@@ -46,6 +47,14 @@
|
|
|
46
47
|
<label for="node-input-unitCost">Cost per Unit</label>
|
|
47
48
|
<input type="number" id="node-input-unitCost" step="0.01">
|
|
48
49
|
</div>
|
|
50
|
+
<div class="form-row">
|
|
51
|
+
<label for="node-input-currency"><i class="fa fa-money"></i> Currency</label>
|
|
52
|
+
<select id="node-input-currency">
|
|
53
|
+
<option value="USD">USD ($)</option>
|
|
54
|
+
<option value="EUR">Euro (€)</option>
|
|
55
|
+
<option value="NGN">Naira (₦)</option>
|
|
56
|
+
</select>
|
|
57
|
+
</div>
|
|
49
58
|
<div class="form-row">
|
|
50
59
|
<label for="node-input-filePath">Filepath</label>
|
|
51
60
|
<input type="text" id="node-input-filePath">
|
package/energyMeterPlus.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
const fs = require("fs");
|
|
2
|
+
const path = require("path");
|
|
2
3
|
|
|
3
4
|
module.exports = function(RED) {
|
|
4
5
|
function EnergyMeterPlus(config) {
|
|
@@ -6,18 +7,36 @@ module.exports = function(RED) {
|
|
|
6
7
|
var node = this;
|
|
7
8
|
|
|
8
9
|
let unitCost = Number(config.unitCost) || 0;
|
|
9
|
-
let filePath = config.filePath || "/
|
|
10
|
-
let inputUnit = config.inputUnit || "W";
|
|
10
|
+
let filePath = config.filePath || "/config/node_red/solargen_data.json";
|
|
11
|
+
let inputUnit = config.inputUnit || "W";
|
|
12
|
+
let currencyCode = config.currency || "USD";
|
|
11
13
|
|
|
12
|
-
//
|
|
14
|
+
// Baseline corrections
|
|
13
15
|
let baselineDaily = Number(config.baselineDaily) || 0;
|
|
14
16
|
let baselineWeekly = Number(config.baselineWeekly) || 0;
|
|
15
17
|
let baselineMonthly = Number(config.baselineMonthly) || 0;
|
|
16
18
|
let baselineYearly = Number(config.baselineYearly) || 0;
|
|
17
19
|
|
|
18
|
-
//
|
|
19
|
-
let baseline =
|
|
20
|
-
|
|
20
|
+
// Load persisted baseline or initialize
|
|
21
|
+
let baseline = node.context().get("baseline") || {
|
|
22
|
+
daily: baselineDaily,
|
|
23
|
+
weekly: baselineWeekly,
|
|
24
|
+
monthly: baselineMonthly,
|
|
25
|
+
yearly: baselineYearly
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
// Load last timestamp or initialize
|
|
29
|
+
let lastCheck = node.context().get("lastCheck") || new Date();
|
|
30
|
+
|
|
31
|
+
// Currency symbol helper
|
|
32
|
+
function currencySymbol(code) {
|
|
33
|
+
switch (code) {
|
|
34
|
+
case "USD": return "$";
|
|
35
|
+
case "EUR": return "€";
|
|
36
|
+
case "NGN": return "₦";
|
|
37
|
+
default: return code;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
21
40
|
|
|
22
41
|
// Rollover logic
|
|
23
42
|
function checkRollover() {
|
|
@@ -25,40 +44,31 @@ module.exports = function(RED) {
|
|
|
25
44
|
if (now.getDate() !== lastCheck.getDate()) baseline.daily = baselineDaily;
|
|
26
45
|
if (now.getDay() === 0 && lastCheck.getDay() !== 0) baseline.weekly = baselineWeekly;
|
|
27
46
|
if (now.getMonth() !== lastCheck.getMonth()) baseline.monthly = baselineMonthly;
|
|
28
|
-
if (now.getFullYear() !== lastCheck.getFullYear())
|
|
29
|
-
let archiveName = filePath.replace(".json", `_${lastCheck.getFullYear()}.json`);
|
|
30
|
-
try { fs.writeFileSync(archiveName, JSON.stringify(baseline, null, 2)); }
|
|
31
|
-
catch (err) { node.error("Failed to archive yearly file: " + err); }
|
|
32
|
-
baseline.yearly = baselineYearly;
|
|
33
|
-
}
|
|
47
|
+
if (now.getFullYear() !== lastCheck.getFullYear()) baseline.yearly = baselineYearly;
|
|
34
48
|
lastCheck = now;
|
|
49
|
+
node.context().set("baseline", baseline);
|
|
50
|
+
node.context().set("lastCheck", lastCheck);
|
|
35
51
|
}
|
|
36
52
|
setInterval(checkRollover, 60000);
|
|
37
53
|
|
|
38
54
|
node.on('input', function(msg) {
|
|
39
55
|
let now = new Date();
|
|
40
|
-
let durationHours = (now - lastCheck) / (1000 * 3600);
|
|
56
|
+
let durationHours = (now - lastCheck) / (1000 * 3600);
|
|
41
57
|
lastCheck = now;
|
|
58
|
+
node.context().set("lastCheck", lastCheck);
|
|
42
59
|
|
|
43
60
|
let power = Number(msg.payload) || 0;
|
|
44
61
|
let power_kW = (inputUnit === "W") ? power / 1000 : power;
|
|
45
62
|
|
|
46
|
-
// Energy increment = Power × Time
|
|
47
63
|
let energyIncrement = power_kW * durationHours;
|
|
48
64
|
|
|
49
|
-
// Add to counters
|
|
50
65
|
baseline.daily += energyIncrement;
|
|
51
66
|
baseline.weekly += energyIncrement;
|
|
52
67
|
baseline.monthly += energyIncrement;
|
|
53
68
|
baseline.yearly += energyIncrement;
|
|
54
69
|
|
|
55
|
-
|
|
56
|
-
let daily_cost = baseline.daily * unitCost;
|
|
57
|
-
let weekly_cost = baseline.weekly * unitCost;
|
|
58
|
-
let monthly_cost = baseline.monthly * unitCost;
|
|
59
|
-
let yearly_cost = baseline.yearly * unitCost;
|
|
70
|
+
node.context().set("baseline", baseline);
|
|
60
71
|
|
|
61
|
-
// Round all outputs to 2 decimal places
|
|
62
72
|
function round2(val) { return Number(val.toFixed(2)); }
|
|
63
73
|
|
|
64
74
|
msg.payload = {
|
|
@@ -66,21 +76,31 @@ module.exports = function(RED) {
|
|
|
66
76
|
weekly_kWh: round2(baseline.weekly),
|
|
67
77
|
monthly_kWh: round2(baseline.monthly),
|
|
68
78
|
yearly_kWh: round2(baseline.yearly),
|
|
69
|
-
daily_cost: round2(
|
|
70
|
-
weekly_cost: round2(
|
|
71
|
-
monthly_cost: round2(
|
|
72
|
-
yearly_cost: round2(
|
|
79
|
+
daily_cost: round2(baseline.daily * unitCost),
|
|
80
|
+
weekly_cost: round2(baseline.weekly * unitCost),
|
|
81
|
+
monthly_cost: round2(baseline.monthly * unitCost),
|
|
82
|
+
yearly_cost: round2(baseline.yearly * unitCost),
|
|
83
|
+
currency: currencySymbol(currencyCode)
|
|
73
84
|
};
|
|
74
85
|
|
|
75
|
-
//
|
|
76
|
-
|
|
77
|
-
|
|
86
|
+
// Ensure directory exists
|
|
87
|
+
const dir = path.dirname(filePath);
|
|
88
|
+
if (!fs.existsSync(dir)) {
|
|
89
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// Write snapshot safely
|
|
93
|
+
try {
|
|
94
|
+
fs.writeFileSync(filePath, JSON.stringify(msg.payload, null, 2));
|
|
95
|
+
} catch (err) {
|
|
96
|
+
node.error("Failed to write to file: " + err);
|
|
97
|
+
}
|
|
78
98
|
|
|
79
99
|
node.send(msg);
|
|
80
100
|
});
|
|
81
101
|
}
|
|
82
102
|
|
|
83
103
|
RED.nodes.registerType("energyMeterPlus", EnergyMeterPlus, {
|
|
84
|
-
color: "#
|
|
104
|
+
color: "#f3a108fe"
|
|
85
105
|
});
|
|
86
106
|
}
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "node-red-contrib-energymeterplus",
|
|
3
|
-
"version": "0.1.
|
|
4
|
-
"description": "A
|
|
3
|
+
"version": "0.1.2",
|
|
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",
|
|
7
7
|
"keywords": [
|
|
@@ -13,3 +13,4 @@
|
|
|
13
13
|
}
|
|
14
14
|
}
|
|
15
15
|
}
|
|
16
|
+
|