node-red-contrib-energymeterplus 0.1.0 → 0.2.0

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 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
+
@@ -1,7 +1,7 @@
1
1
  <script type="text/javascript">
2
2
  RED.nodes.registerType('energyMeterPlus', {
3
3
  category: 'energy',
4
- color: '#d9910cfe', <!-- Dutch Orange -->
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
- filePath: {value:"/addon_configs/a0d7b954_nodered/node_red/solarGen_data.json"},
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">
@@ -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,59 +7,115 @@ module.exports = function(RED) {
6
7
  var node = this;
7
8
 
8
9
  let unitCost = Number(config.unitCost) || 0;
9
- let filePath = config.filePath || "/addon_configs/a0d7b954_nodered/node_red/solarGen_data.json";
10
- let inputUnit = config.inputUnit || "W"; // default to Watts
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
- // Numeric baseline corrections
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
- // Baseline counters
19
- let baseline = { daily: baselineDaily, weekly: baselineWeekly, monthly: baselineMonthly, yearly: baselineYearly };
20
- let lastCheck = new Date();
20
+ // Load persisted baseline or initialize
21
+ let baseline = node.context().get("baseline") || {
22
+ daily: 0,
23
+ weekly: 0,
24
+ monthly: 0,
25
+ yearly: 0
26
+ };
27
+
28
+ // Apply baselines if new values entered
29
+ let baselineApplied = node.context().get("baselineApplied") || false;
30
+ if (!baselineApplied || baselineDaily || baselineWeekly || baselineMonthly || baselineYearly) {
31
+ baseline.daily += baselineDaily;
32
+ baseline.weekly += baselineWeekly;
33
+ baseline.monthly += baselineMonthly;
34
+ baseline.yearly += baselineYearly;
35
+
36
+ node.context().set("baselineApplied", true);
37
+
38
+ // Cosmetic blanking: clear config fields so they show empty in UI
39
+ config.baselineDaily = "";
40
+ config.baselineWeekly = "";
41
+ config.baselineMonthly = "";
42
+ config.baselineYearly = "";
43
+ }
44
+
45
+ // Load last timestamp or initialize
46
+ let lastCheck = node.context().get("lastCheck") || new Date();
47
+
48
+ // Currency symbol helper
49
+ function currencySymbol(code) {
50
+ switch (code) {
51
+ case "USD": return "$";
52
+ case "EUR": return "€";
53
+ case "NGN": return "₦";
54
+ default: return code;
55
+ }
56
+ }
21
57
 
22
58
  // Rollover logic
23
59
  function checkRollover() {
24
60
  let now = new Date();
25
- if (now.getDate() !== lastCheck.getDate()) baseline.daily = baselineDaily;
26
- if (now.getDay() === 0 && lastCheck.getDay() !== 0) baseline.weekly = baselineWeekly;
27
- if (now.getMonth() !== lastCheck.getMonth()) baseline.monthly = baselineMonthly;
61
+
62
+ // Daily rollover (midnight)
63
+ if (now.getDate() !== lastCheck.getDate()) {
64
+ baseline.weekly += baseline.daily; // absorb yesterday
65
+ baseline.daily = 0;
66
+ }
67
+
68
+ // Weekly rollover (Sunday → Monday)
69
+ if (now.getDay() === 1 && lastCheck.getDay() !== 1) {
70
+ baseline.monthly += baseline.weekly; // absorb last week
71
+ baseline.weekly = 0;
72
+ }
73
+
74
+ // Monthly rollover (1st of month)
75
+ if (now.getMonth() !== lastCheck.getMonth()) {
76
+ baseline.yearly += baseline.monthly; // absorb last month
77
+ baseline.monthly = 0;
78
+ }
79
+
80
+ // Yearly rollover (Jan 1)
28
81
  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;
82
+ try {
83
+ const archivePath = path.join(path.dirname(filePath), "yearly_archive.json");
84
+ let archive = [];
85
+ if (fs.existsSync(archivePath)) {
86
+ archive = JSON.parse(fs.readFileSync(archivePath));
87
+ }
88
+ archive.push({
89
+ year: lastCheck.getFullYear(),
90
+ total_kWh: baseline.yearly,
91
+ timestamp: new Date().toISOString()
92
+ });
93
+ fs.writeFileSync(archivePath, JSON.stringify(archive, null, 2));
94
+ } catch (err) {
95
+ node.error("Failed to archive yearly total: " + err);
96
+ }
97
+ baseline.yearly = 0;
33
98
  }
99
+
34
100
  lastCheck = now;
101
+ node.context().set("baseline", baseline);
102
+ node.context().set("lastCheck", lastCheck);
35
103
  }
36
104
  setInterval(checkRollover, 60000);
37
105
 
38
106
  node.on('input', function(msg) {
39
107
  let now = new Date();
40
- let durationHours = (now - lastCheck) / (1000 * 3600); // elapsed time in hours
108
+ let durationHours = (now - lastCheck) / (1000 * 3600);
41
109
  lastCheck = now;
110
+ node.context().set("lastCheck", lastCheck);
42
111
 
43
112
  let power = Number(msg.payload) || 0;
44
113
  let power_kW = (inputUnit === "W") ? power / 1000 : power;
45
114
 
46
- // Energy increment = Power × Time
47
115
  let energyIncrement = power_kW * durationHours;
48
116
 
49
- // Add to counters
50
- baseline.daily += energyIncrement;
51
- baseline.weekly += energyIncrement;
52
- baseline.monthly += energyIncrement;
53
- baseline.yearly += energyIncrement;
54
-
55
- // Calculate costs
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;
117
+ baseline.daily += energyIncrement;
60
118
 
61
- // Round all outputs to 2 decimal places
62
119
  function round2(val) { return Number(val.toFixed(2)); }
63
120
 
64
121
  msg.payload = {
@@ -66,21 +123,31 @@ module.exports = function(RED) {
66
123
  weekly_kWh: round2(baseline.weekly),
67
124
  monthly_kWh: round2(baseline.monthly),
68
125
  yearly_kWh: round2(baseline.yearly),
69
- daily_cost: round2(daily_cost),
70
- weekly_cost: round2(weekly_cost),
71
- monthly_cost: round2(monthly_cost),
72
- yearly_cost: round2(yearly_cost)
126
+ daily_cost: round2(baseline.daily * unitCost),
127
+ weekly_cost: round2(baseline.weekly * unitCost),
128
+ monthly_cost: round2(baseline.monthly * unitCost),
129
+ yearly_cost: round2(baseline.yearly * unitCost),
130
+ currency: currencySymbol(currencyCode)
73
131
  };
74
132
 
75
- // Save snapshot
76
- try { fs.writeFileSync(filePath, JSON.stringify(msg.payload, null, 2)); }
77
- catch (err) { node.error("Failed to write to file: " + err); }
133
+ // Ensure directory exists
134
+ const dir = path.dirname(filePath);
135
+ if (!fs.existsSync(dir)) {
136
+ fs.mkdirSync(dir, { recursive: true });
137
+ }
138
+
139
+ // Write snapshot safely
140
+ try {
141
+ fs.writeFileSync(filePath, JSON.stringify(msg.payload, null, 2));
142
+ } catch (err) {
143
+ node.error("Failed to write to file: " + err);
144
+ }
78
145
 
79
146
  node.send(msg);
80
147
  });
81
148
  }
82
149
 
83
150
  RED.nodes.registerType("energyMeterPlus", EnergyMeterPlus, {
84
- color: "#d9910cfe" // Dutch Orange
151
+ color: "#f3a108fe"
85
152
  });
86
153
  }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "node-red-contrib-energymeterplus",
3
- "version": "0.1.0",
4
- "description": "A node-red energy meter node with editable baseline data, milestone summary, and cost calculation",
3
+ "version": "0.2.0",
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
+