@haydendonald/node-red-contrib-hass-stuff 1.0.0 → 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: state || "unknown",
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 : "unknown";
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: state || "unknown",
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 : "unknown";
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: state || "unknown",
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 : "unknown";
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: state || "unknown",
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 : "unknown";
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
+ };
@@ -0,0 +1,2 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@haydendonald/node-red-contrib-hass-stuff",
3
- "version": "1.0.0",
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": {