@haydendonald/node-red-contrib-hass-stuff 0.0.1 → 1.0.1
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
CHANGED
|
@@ -541,4 +541,15 @@ A node that provides control of a cover
|
|
|
541
541
|
* Have curtain be closed at certain hours
|
|
542
542
|
* Have curtain be half at certain hours
|
|
543
543
|
|
|
544
|
+
## EV Charging Price
|
|
545
|
+
|
|
546
|
+
A node that keeps track of how much i spend on charging my EV (polestar 2) while at home.
|
|
547
|
+
|
|
548
|
+
It works by checking if the car started charging while i'm at home. When the car stops charging it will add the price of the session to a sensor.
|
|
549
|
+
This just estimates the value, useful for if you don't have the ability to monitor the charge rate etc.
|
|
550
|
+
|
|
551
|
+
### Features
|
|
552
|
+
* Calculates how much money per month i spend on charging my EV
|
|
553
|
+
* Detects if the car was charged at home, ignoring fast charging sessions
|
|
554
|
+
|
|
544
555
|
// MIMO //
|
|
@@ -86,11 +86,19 @@ module.exports = function ConnectionsConfigNode(RED) {
|
|
|
86
86
|
self.sendHASSAPI("http", "post", "/api/states/" + entityId, callback, undefined, data);
|
|
87
87
|
};
|
|
88
88
|
self.addHASSInputBoolean = function (options) {
|
|
89
|
-
let currentState = options.state || "unknown";
|
|
89
|
+
let currentState = options.state || options.state || options.defaultState || "unknown";
|
|
90
90
|
const entityId = options.id ? options.id : (0, utility_1.getEntityId)("input_boolean", options.friendlyName);
|
|
91
91
|
const data = (state) => {
|
|
92
|
+
//If the state is not set, set it to the default state, otherwise "unknown"
|
|
93
|
+
let setState = state;
|
|
94
|
+
if (setState === undefined || setState == "unknown") {
|
|
95
|
+
setState = options.defaultState;
|
|
96
|
+
}
|
|
97
|
+
if (setState === undefined) {
|
|
98
|
+
setState = "unknown";
|
|
99
|
+
}
|
|
92
100
|
return {
|
|
93
|
-
state:
|
|
101
|
+
state: setState,
|
|
94
102
|
attributes: {
|
|
95
103
|
friendly_name: options.friendlyName
|
|
96
104
|
}
|
|
@@ -102,7 +110,7 @@ module.exports = function ConnectionsConfigNode(RED) {
|
|
|
102
110
|
self.error(`Found more than 1 entity for ${entityId}`);
|
|
103
111
|
return;
|
|
104
112
|
}
|
|
105
|
-
const previousState = entities.length == 1 ? entities[0].state :
|
|
113
|
+
const previousState = entities.length == 1 ? entities[0].state : undefined;
|
|
106
114
|
self.addHASSEntity(entityId, data(options.state || previousState), options.creationCallback ? (response) => {
|
|
107
115
|
options.creationCallback(response.payload.state, response);
|
|
108
116
|
} : undefined);
|
|
@@ -133,8 +141,16 @@ module.exports = function ConnectionsConfigNode(RED) {
|
|
|
133
141
|
var _a;
|
|
134
142
|
const entityId = (_a = options.id) !== null && _a !== void 0 ? _a : (0, utility_1.getEntityId)("button", options.friendlyName);
|
|
135
143
|
const data = (state) => {
|
|
144
|
+
//If the state is not set, set it to the default state, otherwise "unknown"
|
|
145
|
+
let setState = state;
|
|
146
|
+
if (setState === undefined || setState == "unknown") {
|
|
147
|
+
setState = options.defaultState;
|
|
148
|
+
}
|
|
149
|
+
if (setState === undefined) {
|
|
150
|
+
setState = "unknown";
|
|
151
|
+
}
|
|
136
152
|
return {
|
|
137
|
-
state:
|
|
153
|
+
state: setState,
|
|
138
154
|
attributes: {
|
|
139
155
|
friendly_name: options.friendlyName
|
|
140
156
|
}
|
|
@@ -146,7 +162,7 @@ module.exports = function ConnectionsConfigNode(RED) {
|
|
|
146
162
|
self.error(`Found more than 1 entity for ${entityId}`);
|
|
147
163
|
return;
|
|
148
164
|
}
|
|
149
|
-
const previousState = entities.length == 1 ? entities[0].state :
|
|
165
|
+
const previousState = entities.length == 1 ? entities[0].state : undefined;
|
|
150
166
|
self.addHASSEntity(entityId, data(options.state || previousState), options.creationCallback ? (response) => {
|
|
151
167
|
options.creationCallback(response.payload.state, response);
|
|
152
168
|
} : undefined);
|
|
@@ -166,8 +182,16 @@ module.exports = function ConnectionsConfigNode(RED) {
|
|
|
166
182
|
self.addHASSScene = function (options) {
|
|
167
183
|
const entityId = options.id || (0, utility_1.getEntityId)("scene", options.friendlyName);
|
|
168
184
|
const data = (state) => {
|
|
185
|
+
//If the state is not set, set it to the default state, otherwise "unknown"
|
|
186
|
+
let setState = state;
|
|
187
|
+
if (setState === undefined || setState == "unknown") {
|
|
188
|
+
setState = options.defaultState;
|
|
189
|
+
}
|
|
190
|
+
if (setState === undefined) {
|
|
191
|
+
setState = "unknown";
|
|
192
|
+
}
|
|
169
193
|
return {
|
|
170
|
-
state:
|
|
194
|
+
state: setState,
|
|
171
195
|
attributes: {
|
|
172
196
|
friendly_name: options.friendlyName
|
|
173
197
|
}
|
|
@@ -179,7 +203,7 @@ module.exports = function ConnectionsConfigNode(RED) {
|
|
|
179
203
|
self.error(`Found more than 1 entity for ${entityId}`);
|
|
180
204
|
return;
|
|
181
205
|
}
|
|
182
|
-
const previousState = entities.length == 1 ? entities[0].state :
|
|
206
|
+
const previousState = entities.length == 1 ? entities[0].state : undefined;
|
|
183
207
|
self.addHASSEntity(entityId, data(options.state || previousState), options.creationCallback ? (response) => {
|
|
184
208
|
options.creationCallback(response.payload.state, response);
|
|
185
209
|
} : undefined);
|
|
@@ -203,8 +227,16 @@ module.exports = function ConnectionsConfigNode(RED) {
|
|
|
203
227
|
self.addHASSSelect = function (options) {
|
|
204
228
|
const entityId = options.id || (0, utility_1.getEntityId)("select", options.friendlyName);
|
|
205
229
|
const data = (state) => {
|
|
230
|
+
//If the state is not set, set it to the default state, otherwise "unknown"
|
|
231
|
+
let setState = state;
|
|
232
|
+
if (setState === undefined || setState == "unknown") {
|
|
233
|
+
setState = options.defaultState;
|
|
234
|
+
}
|
|
235
|
+
if (setState === undefined) {
|
|
236
|
+
setState = "unknown";
|
|
237
|
+
}
|
|
206
238
|
return {
|
|
207
|
-
state:
|
|
239
|
+
state: setState,
|
|
208
240
|
attributes: {
|
|
209
241
|
friendly_name: options.friendlyName,
|
|
210
242
|
options: options.options
|
|
@@ -217,7 +249,7 @@ module.exports = function ConnectionsConfigNode(RED) {
|
|
|
217
249
|
self.error(`Found more than 1 entity for ${entityId}`);
|
|
218
250
|
return;
|
|
219
251
|
}
|
|
220
|
-
const previousState = entities.length == 1 ? entities[0].state :
|
|
252
|
+
const previousState = entities.length == 1 ? entities[0].state : undefined;
|
|
221
253
|
self.addHASSEntity(entityId, data(options.state || previousState), options.creationCallback ? (response) => {
|
|
222
254
|
options.creationCallback(response.payload.state, response);
|
|
223
255
|
} : undefined);
|
|
@@ -271,6 +303,53 @@ module.exports = function ConnectionsConfigNode(RED) {
|
|
|
271
303
|
};
|
|
272
304
|
}
|
|
273
305
|
};
|
|
306
|
+
self.addHASSSensor = function (options) {
|
|
307
|
+
var _a;
|
|
308
|
+
const entityId = (_a = options.id) !== null && _a !== void 0 ? _a : (0, utility_1.getEntityId)("number", options.friendlyName);
|
|
309
|
+
const data = (state) => {
|
|
310
|
+
//If the state is not set, set it to the default state, otherwise "unknown"
|
|
311
|
+
let setState = state;
|
|
312
|
+
if (setState === undefined || setState == "unknown") {
|
|
313
|
+
setState = options.defaultState;
|
|
314
|
+
}
|
|
315
|
+
if (setState === undefined) {
|
|
316
|
+
setState = "unknown";
|
|
317
|
+
}
|
|
318
|
+
return {
|
|
319
|
+
state: setState,
|
|
320
|
+
attributes: {
|
|
321
|
+
friendly_name: options.friendlyName
|
|
322
|
+
}
|
|
323
|
+
};
|
|
324
|
+
};
|
|
325
|
+
//Add the button to HASS
|
|
326
|
+
self.getHASSEntities([{ property: "entity_id", logic: "is", value: entityId }], (entities) => {
|
|
327
|
+
if (entities.length > 1) {
|
|
328
|
+
self.error(`Found more than 1 entity for ${entityId}`);
|
|
329
|
+
return;
|
|
330
|
+
}
|
|
331
|
+
const previousState = entities.length == 1 ? entities[0].state : undefined;
|
|
332
|
+
self.addHASSEntity(entityId, data(options.state || previousState), options.creationCallback ? (response) => {
|
|
333
|
+
options.creationCallback(response.payload.state, response);
|
|
334
|
+
} : undefined);
|
|
335
|
+
});
|
|
336
|
+
return (state) => {
|
|
337
|
+
self.sendHASSAPI("http", "post", "/api/states/" + entityId, undefined, undefined, data(state));
|
|
338
|
+
if (options.changedCallback) {
|
|
339
|
+
options.changedCallback(state);
|
|
340
|
+
}
|
|
341
|
+
};
|
|
342
|
+
};
|
|
343
|
+
self.sendCompanionNotification = function (options) {
|
|
344
|
+
for (const entityId of options.entityIds) {
|
|
345
|
+
self.sendHASSAction(entityId, undefined, {
|
|
346
|
+
message: options.message,
|
|
347
|
+
title: options.title,
|
|
348
|
+
target: options.target,
|
|
349
|
+
data: options.data
|
|
350
|
+
});
|
|
351
|
+
}
|
|
352
|
+
};
|
|
274
353
|
self.handleCallback = function (callbacks, callbackId, ...args) {
|
|
275
354
|
var _a;
|
|
276
355
|
//This is a specific callback id
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
<script type="text/javascript" id="node-ev-charging-price-node">
|
|
2
|
+
RED.nodes.registerType("ev-charging-price-node", {
|
|
3
|
+
category: "HASS Stuff",
|
|
4
|
+
color: "#ffcc00",
|
|
5
|
+
inputs: 0,
|
|
6
|
+
outputs: 0,
|
|
7
|
+
icon: "debug.svg",
|
|
8
|
+
paletteLabel: "EV Charging Price",
|
|
9
|
+
defaults: {
|
|
10
|
+
name: { value: "" },
|
|
11
|
+
connectionsConfigNode: { value: '', type: "connections-config-node", required: true },
|
|
12
|
+
chargeRate: { value: 0.0, required: true, validate: RED.validators.number() },
|
|
13
|
+
peakRate: { value: 0.0, required: true, validate: RED.validators.number() },
|
|
14
|
+
offPeakRate: { value: 0.0, required: true, validate: RED.validators.number() },
|
|
15
|
+
peakRateHours: { value: "7,8,9,10,11,12,13,14,15,16,17,18,19", required: true },
|
|
16
|
+
peakRateDays: { value: "0,1,2,3,4", required: true },
|
|
17
|
+
chargingEntityId: { value: "", required: true },
|
|
18
|
+
homeEntityId: { value: "", required: true },
|
|
19
|
+
notificationId: { value: "", required: false }
|
|
20
|
+
},
|
|
21
|
+
label: function () {
|
|
22
|
+
return this.name || "EV Charging Price"
|
|
23
|
+
}
|
|
24
|
+
});
|
|
25
|
+
</script>
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
<script type="text/html" data-template-name="ev-charging-price-node">
|
|
29
|
+
<div class="form-row">
|
|
30
|
+
<label for="node-input-name"><i class="fa fa-tag"></i> Name</label>
|
|
31
|
+
<input type="text" id="node-input-name" placeholder="Name">
|
|
32
|
+
</div>
|
|
33
|
+
<div class="form-row">
|
|
34
|
+
<label for="node-input-connectionsConfigNode">Config</label>
|
|
35
|
+
<input type="text" id="node-input-connectionsConfigNode" />
|
|
36
|
+
</div>
|
|
37
|
+
<div class="form-row">
|
|
38
|
+
<p>The charge rate in KW</p>
|
|
39
|
+
<label for="node-input-chargeRate">Charge Rate</label>
|
|
40
|
+
<input type="number" step="0.01" id="node-input-chargeRate" />
|
|
41
|
+
</div>
|
|
42
|
+
<div class="form-row">
|
|
43
|
+
<p>The cost of charging during the peak hours in $/kWh</p>
|
|
44
|
+
<label for="node-input-peakRate">Peak Rate</label>
|
|
45
|
+
<input type="number" step="0.01" id="node-input-peakRate" />
|
|
46
|
+
</div>
|
|
47
|
+
<div class="form-row">
|
|
48
|
+
<p>The cost of charging during the off-peak hours in $/kWh</p>
|
|
49
|
+
<label for="node-input-offPeakRate">Off-Peak Rate</label>
|
|
50
|
+
<input type="number" step="0.01" id="node-input-offPeakRate" />
|
|
51
|
+
</div>
|
|
52
|
+
<div class="form-row">
|
|
53
|
+
<p>The hours during which the peak rate applies. For example 7,8,9,10,11,12,13,14,15,16,17,18,19</p>
|
|
54
|
+
<label for="node-input-peakRateHours">Peak Rate Hours</label>
|
|
55
|
+
<input type="text" id="node-input-peakRateHours" />
|
|
56
|
+
</div>
|
|
57
|
+
<div class="form-row">
|
|
58
|
+
<p>The days during which the peak rate applies. For example 0,1,2,3,4 for Monday to Friday</p>
|
|
59
|
+
<label for="node-input-peakRateDays">Peak Rate Days</label>
|
|
60
|
+
<input type="text" id="node-input-peakRateDays" />
|
|
61
|
+
</div>
|
|
62
|
+
<div class="form-row">
|
|
63
|
+
<p>The entity ID for the charging sensor for example sensor.polestar2_charging_state</p>
|
|
64
|
+
<label for="node-input-chargingEntityId">Charging Entity ID</label>
|
|
65
|
+
<input type="text" id="node-input-chargingEntityId" />
|
|
66
|
+
</div>
|
|
67
|
+
<div class="form-row">
|
|
68
|
+
<p>The entity ID for the home presence sensor for example person.hayden</p>
|
|
69
|
+
<label for="node-input-homeEntityId">Home Entity ID</label>
|
|
70
|
+
<input type="text" id="node-input-homeEntityId" />
|
|
71
|
+
</div>
|
|
72
|
+
<div class="form-row">
|
|
73
|
+
<p>The entity IDs for the notification target for example notify.mobile_app_haydens_iphone</p>
|
|
74
|
+
<label for="node-input-notificationId">Notification ID</label>
|
|
75
|
+
<input type="text" id="node-input-notificationId" />
|
|
76
|
+
</div>
|
|
77
|
+
</script>
|
|
78
|
+
<script type="text/html" data-help-name="ev-charging-price-node">
|
|
79
|
+
</script>
|
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
const baseNode_1 = require("../baseNode");
|
|
3
|
+
const utility_1 = require("../utility");
|
|
4
|
+
module.exports = function EVChargingPriceNode(RED) {
|
|
5
|
+
function register(config) {
|
|
6
|
+
const self = this;
|
|
7
|
+
//Validate the config
|
|
8
|
+
let hasValidationError = false;
|
|
9
|
+
if (!config.connectionsConfigNode) {
|
|
10
|
+
self.error("Connections config node is required");
|
|
11
|
+
hasValidationError = true;
|
|
12
|
+
}
|
|
13
|
+
if (!config.chargingEntityId) {
|
|
14
|
+
self.error("Charging entity ID is required");
|
|
15
|
+
hasValidationError = true;
|
|
16
|
+
}
|
|
17
|
+
if (!config.homeEntityId) {
|
|
18
|
+
self.error("Home entity ID is required");
|
|
19
|
+
hasValidationError = true;
|
|
20
|
+
}
|
|
21
|
+
if (!config.chargeRate || parseFloat(config.chargeRate) <= 0) {
|
|
22
|
+
self.error("Charge rate must be a positive number");
|
|
23
|
+
hasValidationError = true;
|
|
24
|
+
}
|
|
25
|
+
if (!config.peakRate || parseFloat(config.peakRate) <= 0) {
|
|
26
|
+
self.error("Peak rate must be a positive number");
|
|
27
|
+
hasValidationError = true;
|
|
28
|
+
}
|
|
29
|
+
if (!config.offPeakRate || parseFloat(config.offPeakRate) <= 0) {
|
|
30
|
+
self.error("Off-peak rate must be a positive number");
|
|
31
|
+
hasValidationError = true;
|
|
32
|
+
}
|
|
33
|
+
if (!config.peakRateHours) {
|
|
34
|
+
self.error("Peak rate hours are required");
|
|
35
|
+
hasValidationError = true;
|
|
36
|
+
}
|
|
37
|
+
if (!config.peakRateDays) {
|
|
38
|
+
self.error("Peak rate days are required");
|
|
39
|
+
hasValidationError = true;
|
|
40
|
+
}
|
|
41
|
+
if (hasValidationError) {
|
|
42
|
+
return;
|
|
43
|
+
}
|
|
44
|
+
let chargingState;
|
|
45
|
+
let homeState;
|
|
46
|
+
let priceState;
|
|
47
|
+
let startedChargingAtHomeState;
|
|
48
|
+
let startedChargingAt;
|
|
49
|
+
let finishedChargingAt;
|
|
50
|
+
//Set the price sensor
|
|
51
|
+
let setPrice;
|
|
52
|
+
RED.nodes.createNode(this, config);
|
|
53
|
+
(0, baseNode_1.assignBaseNode)(this);
|
|
54
|
+
const connectionsConfigNode = RED.nodes.getNode(config.connectionsConfigNode);
|
|
55
|
+
//When HASS is ready
|
|
56
|
+
connectionsConfigNode.hassEventReadyCallbacks[this.id] = function (msg) {
|
|
57
|
+
// Get the current state of the charger
|
|
58
|
+
connectionsConfigNode.getHASSEntityState(config.chargingEntityId, (payload, data) => {
|
|
59
|
+
chargingState = data.data.state;
|
|
60
|
+
if (chargingState == "Charging") {
|
|
61
|
+
startedChargingAt = data.data.last_changed;
|
|
62
|
+
}
|
|
63
|
+
else {
|
|
64
|
+
finishedChargingAt = data.data.last_changed;
|
|
65
|
+
}
|
|
66
|
+
});
|
|
67
|
+
// Get the current state of the home
|
|
68
|
+
connectionsConfigNode.getHASSEntityState(config.homeEntityId, (payload, data) => { homeState = data.data.state; });
|
|
69
|
+
//Add a boolean to keep track if the charger started while at home
|
|
70
|
+
connectionsConfigNode.addHASSInputBoolean({
|
|
71
|
+
friendlyName: `${self.name} - Started Charging at Home`,
|
|
72
|
+
id: (0, utility_1.getEntityId)("input_boolean", `${self.name}_started_charging_at_home`),
|
|
73
|
+
defaultState: "off",
|
|
74
|
+
creationCallback: (state) => {
|
|
75
|
+
startedChargingAtHomeState = state;
|
|
76
|
+
},
|
|
77
|
+
changedCallback: (state) => {
|
|
78
|
+
startedChargingAtHomeState = state;
|
|
79
|
+
}
|
|
80
|
+
});
|
|
81
|
+
//Add our sensor to track the cost
|
|
82
|
+
setPrice = connectionsConfigNode.addHASSSensor({
|
|
83
|
+
friendlyName: `${self.name} - Price`,
|
|
84
|
+
id: (0, utility_1.getEntityId)("sensor", `${self.name}_price`),
|
|
85
|
+
defaultState: 0.0,
|
|
86
|
+
creationCallback: (state) => {
|
|
87
|
+
priceState = state;
|
|
88
|
+
},
|
|
89
|
+
changedCallback: (state) => {
|
|
90
|
+
priceState = state;
|
|
91
|
+
}
|
|
92
|
+
});
|
|
93
|
+
//Add a button to reset the price
|
|
94
|
+
connectionsConfigNode.addHASSButton({
|
|
95
|
+
friendlyName: `${self.name} - Reset Price`,
|
|
96
|
+
id: (0, utility_1.getEntityId)("button", `${self.name}_reset_price`),
|
|
97
|
+
pressedCallback: () => {
|
|
98
|
+
setPrice(0.0);
|
|
99
|
+
}
|
|
100
|
+
});
|
|
101
|
+
};
|
|
102
|
+
//When a state change happens in home assistant
|
|
103
|
+
connectionsConfigNode.hassEventStateChangeCallbacks[this.id] = function (entityId, oldState, newState) {
|
|
104
|
+
switch (entityId) {
|
|
105
|
+
case config.homeEntityId: {
|
|
106
|
+
homeState = newState.state;
|
|
107
|
+
handle();
|
|
108
|
+
break;
|
|
109
|
+
}
|
|
110
|
+
case config.chargingEntityId: {
|
|
111
|
+
chargingState = newState.state;
|
|
112
|
+
if (chargingState == "Charging") {
|
|
113
|
+
startedChargingAt = newState.last_changed;
|
|
114
|
+
finishedChargingAt = undefined;
|
|
115
|
+
}
|
|
116
|
+
else {
|
|
117
|
+
startedChargingAt = oldState.last_changed;
|
|
118
|
+
finishedChargingAt = newState.last_changed;
|
|
119
|
+
}
|
|
120
|
+
handle();
|
|
121
|
+
break;
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
};
|
|
125
|
+
function sendNotification(title, message) {
|
|
126
|
+
if (config.notificationId) {
|
|
127
|
+
connectionsConfigNode.sendCompanionNotification({
|
|
128
|
+
entityIds: [config.notificationId],
|
|
129
|
+
message,
|
|
130
|
+
title
|
|
131
|
+
});
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
function handle() {
|
|
135
|
+
//Set started charging at home if user is home and car is charging
|
|
136
|
+
if (startedChargingAtHomeState == "off" && (chargingState == "Charging" && homeState == "home")) {
|
|
137
|
+
connectionsConfigNode.sendHASSAction("input_boolean.turn_on", { entity_id: [(0, utility_1.getEntityId)("input_boolean", `${self.name}_started_charging_at_home`)] });
|
|
138
|
+
sendNotification(`Charging has started for ${self.name}`, `Charging has started for ${self.name}`);
|
|
139
|
+
}
|
|
140
|
+
//Car has stopped charging and had started while at home
|
|
141
|
+
if (chargingState != "Charging" && startedChargingAtHomeState == "on") {
|
|
142
|
+
connectionsConfigNode.sendHASSAction("input_boolean.turn_off", { entity_id: [(0, utility_1.getEntityId)("input_boolean", `${self.name}_started_charging_at_home`)] });
|
|
143
|
+
const powerKW = parseFloat(config.chargeRate);
|
|
144
|
+
const peakHours = config.peakRateHours.split(",").map((h) => parseInt(h.trim()));
|
|
145
|
+
const peakDays = config.peakRateDays.split(",").map((d) => parseInt(d.trim()));
|
|
146
|
+
const pricePeak = parseFloat(config.peakRate);
|
|
147
|
+
const priceOffPeak = parseFloat(config.offPeakRate);
|
|
148
|
+
const startDate = new Date(startedChargingAt);
|
|
149
|
+
const endDate = new Date(finishedChargingAt);
|
|
150
|
+
if (endDate < startDate) {
|
|
151
|
+
self.error("Finished charging time is before started charging time. Cannot calculate");
|
|
152
|
+
return;
|
|
153
|
+
}
|
|
154
|
+
// Calculate time spent in peak and off-peak periods
|
|
155
|
+
// Split charging period at hour boundaries for accurate rate calculation
|
|
156
|
+
let timeInPeakMs = 0;
|
|
157
|
+
let timeInOffPeakMs = 0;
|
|
158
|
+
let currentTime = new Date();
|
|
159
|
+
while (currentTime < endDate) {
|
|
160
|
+
// Find the next hour boundary or end of charging period
|
|
161
|
+
const nextHourBoundary = new Date(currentTime);
|
|
162
|
+
nextHourBoundary.setHours(currentTime.getHours() + 1, 0, 0, 0);
|
|
163
|
+
const segmentEnd = nextHourBoundary > endDate ? endDate : nextHourBoundary;
|
|
164
|
+
const duration = segmentEnd.getTime() - currentTime.getTime();
|
|
165
|
+
const currentHour = currentTime.getHours();
|
|
166
|
+
const currentDay = currentTime.getDay();
|
|
167
|
+
// Check if current time falls in peak period
|
|
168
|
+
const isPeak = peakHours.includes(currentHour) && peakDays.includes(currentDay);
|
|
169
|
+
if (isPeak) {
|
|
170
|
+
timeInPeakMs += duration;
|
|
171
|
+
}
|
|
172
|
+
else {
|
|
173
|
+
timeInOffPeakMs += duration;
|
|
174
|
+
}
|
|
175
|
+
currentTime = segmentEnd;
|
|
176
|
+
}
|
|
177
|
+
// Calculate the price
|
|
178
|
+
const timeInPeakHours = timeInPeakMs / 3600000;
|
|
179
|
+
const timeInOffPeakHours = timeInOffPeakMs / 3600000;
|
|
180
|
+
const totalHours = timeInPeakHours + timeInOffPeakHours;
|
|
181
|
+
const peakPrice = powerKW * timeInPeakHours * pricePeak;
|
|
182
|
+
const offPeakPrice = powerKW * timeInOffPeakHours * priceOffPeak;
|
|
183
|
+
const totalPrice = peakPrice + offPeakPrice;
|
|
184
|
+
const rollingPrice = parseFloat((parseFloat(priceState) + totalPrice).toFixed(2));
|
|
185
|
+
// Update the price
|
|
186
|
+
setPrice(rollingPrice);
|
|
187
|
+
sendNotification(`Charging has finished for ${self.name}`, `Charging finished for ${self.name}. Was charging for ${totalHours.toFixed(2)} hours. The charge session was $${totalPrice.toFixed(2)}, the total so far is $${rollingPrice}`);
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
RED.nodes.registerType("ev-charging-price-node", register);
|
|
192
|
+
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@haydendonald/node-red-contrib-hass-stuff",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "1.0.1",
|
|
4
4
|
"description": "A collection of stuff I use on my Node Red + Home Assistant server. This could be of use for others, i don't know..",
|
|
5
5
|
"devDependencies": {
|
|
6
6
|
"@types/node": "^18.14.0",
|
|
@@ -27,7 +27,8 @@
|
|
|
27
27
|
"ConnectionsConfigNode": "dist/Connections/connectionsConfigNode.js",
|
|
28
28
|
"ConnectionsNode": "dist/Connections/connectionsNode.js",
|
|
29
29
|
"LightControlConfigNode": "dist/LightControl/lightControlConfigNode.js",
|
|
30
|
-
"LightControlNode": "dist/LightControl/lightControlNode.js"
|
|
30
|
+
"LightControlNode": "dist/LightControl/lightControlNode.js",
|
|
31
|
+
"EVChargingPriceNode": "dist/EVChargingPrice/EVChargingPriceNode.js"
|
|
31
32
|
}
|
|
32
33
|
},
|
|
33
34
|
"publishConfig": {
|