node-red-contrib-power-saver 3.0.5 → 3.0.9
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/docs/.vuepress/config.js +1 -1
- package/docs/.vuepress/dist/404.html +3 -3
- package/docs/.vuepress/dist/assets/js/app.190d977f.js +1 -0
- package/docs/.vuepress/dist/assets/js/runtime~app.36bc5048.js +1 -0
- package/docs/.vuepress/dist/assets/js/{v-510ed0d4.129ef915.js → v-510ed0d4.01d07aab.js} +1 -1
- package/docs/.vuepress/dist/changelog/index.html +4 -4
- package/docs/.vuepress/dist/contribute/index.html +3 -3
- package/docs/.vuepress/dist/examples/example-nordpool-current-state.html +3 -3
- package/docs/.vuepress/dist/examples/example-nordpool-events-state.html +3 -3
- package/docs/.vuepress/dist/examples/example-tibber-mqtt.html +3 -3
- package/docs/.vuepress/dist/examples/index.html +3 -3
- package/docs/.vuepress/dist/guide/index.html +3 -3
- package/docs/.vuepress/dist/index.html +3 -3
- package/docs/.vuepress/dist/nodes/index.html +3 -3
- package/docs/.vuepress/dist/nodes/old-power-saver-doc.html +3 -3
- package/docs/.vuepress/dist/nodes/power-saver.html +3 -3
- package/docs/.vuepress/dist/nodes/ps-elvia-add-tariff.html +3 -3
- package/docs/.vuepress/dist/nodes/ps-receive-price.html +3 -3
- package/docs/.vuepress/dist/nodes/ps-strategy-best-save.html +3 -3
- package/docs/.vuepress/dist/nodes/ps-strategy-lowest-price.html +3 -3
- package/docs/.vuepress/dist/nodes/strategy-input.html +3 -3
- package/docs/changelog/README.md +17 -0
- package/docs/examples/example-nordpool-current-state.md +172 -152
- package/docs/nodes/ps-receive-price.md +4 -0
- package/package.json +2 -3
- package/src/handle-input.js +1 -7
- package/src/power-saver.js +1 -5
- package/src/receive-price-functions.js +9 -17
- package/src/strategy-lowest-price.js +2 -1
- package/src/utils.js +6 -0
- package/test/data/lowest-price-input-missing-end.json +197 -0
- package/test/data/lowest-price-result-missing-end.json +319 -0
- package/test/data/nordpool-events-state-2.json +288 -0
- package/test/data/nordpool-prices-in-payload.json +287 -0
- package/test/receive-price.test.js +27 -4
- package/test/strategy-lowest-price.test.js +33 -0
- package/docs/.vuepress/dist/assets/js/app.342dc054.js +0 -1
- package/docs/.vuepress/dist/assets/js/runtime~app.0d53f24f.js +0 -1
|
@@ -10,157 +10,177 @@ In this example, data is read from the Nord Pool sensor in HA via the `current s
|
|
|
10
10
|
|
|
11
11
|
```json:no-line-numbers
|
|
12
12
|
[
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
"
|
|
53
|
-
"
|
|
54
|
-
"
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
13
|
+
{
|
|
14
|
+
"id": "e2578f6a.210a8",
|
|
15
|
+
"type": "debug",
|
|
16
|
+
"z": "d938c47f.3398f8",
|
|
17
|
+
"name": "Nord Pool result",
|
|
18
|
+
"active": false,
|
|
19
|
+
"tosidebar": true,
|
|
20
|
+
"console": false,
|
|
21
|
+
"tostatus": false,
|
|
22
|
+
"complete": "true",
|
|
23
|
+
"targetType": "full",
|
|
24
|
+
"statusVal": "",
|
|
25
|
+
"statusType": "auto",
|
|
26
|
+
"x": 820,
|
|
27
|
+
"y": 440,
|
|
28
|
+
"wires": []
|
|
29
|
+
},
|
|
30
|
+
{
|
|
31
|
+
"id": "48bcdcca.fe42a4",
|
|
32
|
+
"type": "api-current-state",
|
|
33
|
+
"z": "d938c47f.3398f8",
|
|
34
|
+
"name": "Read Nord Pool",
|
|
35
|
+
"server": "ec4a12a1.b2be9",
|
|
36
|
+
"version": 2,
|
|
37
|
+
"outputs": 1,
|
|
38
|
+
"halt_if": "",
|
|
39
|
+
"halt_if_type": "str",
|
|
40
|
+
"halt_if_compare": "is",
|
|
41
|
+
"entity_id": "sensor.nordpool_kwh_trheim_nok_3_095_025",
|
|
42
|
+
"state_type": "str",
|
|
43
|
+
"blockInputOverrides": false,
|
|
44
|
+
"outputProperties": [
|
|
45
|
+
{
|
|
46
|
+
"property": "payload",
|
|
47
|
+
"propertyType": "msg",
|
|
48
|
+
"value": "",
|
|
49
|
+
"valueType": "entity"
|
|
50
|
+
}
|
|
51
|
+
],
|
|
52
|
+
"x": 280,
|
|
53
|
+
"y": 380,
|
|
54
|
+
"wires": [
|
|
55
|
+
[
|
|
56
|
+
"428d7c7ca88db95f"
|
|
57
|
+
]
|
|
58
|
+
]
|
|
59
|
+
},
|
|
60
|
+
{
|
|
61
|
+
"id": "97cc8e58.4247a",
|
|
62
|
+
"type": "inject",
|
|
63
|
+
"z": "d938c47f.3398f8",
|
|
64
|
+
"name": "",
|
|
65
|
+
"props": [
|
|
66
|
+
{
|
|
67
|
+
"p": "payload"
|
|
68
|
+
},
|
|
69
|
+
{
|
|
70
|
+
"p": "topic",
|
|
71
|
+
"vt": "str"
|
|
72
|
+
}
|
|
73
|
+
],
|
|
74
|
+
"repeat": "3600",
|
|
75
|
+
"crontab": "",
|
|
76
|
+
"once": true,
|
|
77
|
+
"onceDelay": 0.1,
|
|
78
|
+
"topic": "",
|
|
79
|
+
"payloadType": "date",
|
|
80
|
+
"x": 110,
|
|
81
|
+
"y": 380,
|
|
82
|
+
"wires": [
|
|
83
|
+
[
|
|
84
|
+
"48bcdcca.fe42a4"
|
|
85
|
+
]
|
|
86
|
+
]
|
|
87
|
+
},
|
|
88
|
+
{
|
|
89
|
+
"id": "a6f2769b.1a62a8",
|
|
90
|
+
"type": "api-call-service",
|
|
91
|
+
"z": "d938c47f.3398f8",
|
|
92
|
+
"name": "Turn on VVB",
|
|
93
|
+
"server": "ec4a12a1.b2be9",
|
|
94
|
+
"version": 3,
|
|
95
|
+
"debugenabled": false,
|
|
96
|
+
"service_domain": "switch",
|
|
97
|
+
"service": "turn_on",
|
|
98
|
+
"entityId": "switch.varmtvannsbereder_switch",
|
|
99
|
+
"data": "",
|
|
100
|
+
"dataType": "jsonata",
|
|
101
|
+
"mergecontext": "",
|
|
102
|
+
"mustacheAltTags": false,
|
|
103
|
+
"outputProperties": [],
|
|
104
|
+
"queue": "none",
|
|
105
|
+
"x": 810,
|
|
106
|
+
"y": 340,
|
|
107
|
+
"wires": [
|
|
108
|
+
[]
|
|
109
|
+
]
|
|
110
|
+
},
|
|
111
|
+
{
|
|
112
|
+
"id": "9fc75126.65dd3",
|
|
113
|
+
"type": "api-call-service",
|
|
114
|
+
"z": "d938c47f.3398f8",
|
|
115
|
+
"name": "Turn off VVB",
|
|
116
|
+
"server": "ec4a12a1.b2be9",
|
|
117
|
+
"version": 3,
|
|
118
|
+
"debugenabled": true,
|
|
119
|
+
"service_domain": "switch",
|
|
120
|
+
"service": "turn_off",
|
|
121
|
+
"entityId": "switch.varmtvannsbereder_switch",
|
|
122
|
+
"data": "",
|
|
123
|
+
"dataType": "json",
|
|
124
|
+
"mergecontext": "",
|
|
125
|
+
"mustacheAltTags": false,
|
|
126
|
+
"outputProperties": [],
|
|
127
|
+
"queue": "none",
|
|
128
|
+
"x": 810,
|
|
129
|
+
"y": 380,
|
|
130
|
+
"wires": [
|
|
131
|
+
[]
|
|
132
|
+
]
|
|
133
|
+
},
|
|
134
|
+
{
|
|
135
|
+
"id": "428d7c7ca88db95f",
|
|
136
|
+
"type": "ps-receive-price",
|
|
137
|
+
"z": "d938c47f.3398f8",
|
|
138
|
+
"name": "Price Receiver",
|
|
139
|
+
"x": 460,
|
|
140
|
+
"y": 380,
|
|
141
|
+
"wires": [
|
|
142
|
+
[
|
|
143
|
+
"4147bf0b99fe626f"
|
|
144
|
+
]
|
|
145
|
+
]
|
|
146
|
+
},
|
|
147
|
+
{
|
|
148
|
+
"id": "4147bf0b99fe626f",
|
|
149
|
+
"type": "ps-strategy-best-save",
|
|
150
|
+
"z": "d938c47f.3398f8",
|
|
151
|
+
"name": "Best Save",
|
|
152
|
+
"maxHoursToSaveInSequence": "4",
|
|
153
|
+
"minHoursOnAfterMaxSequenceSaved": "1",
|
|
154
|
+
"minSaving": "0.03",
|
|
155
|
+
"sendCurrentValueWhenRescheduling": true,
|
|
156
|
+
"outputIfNoSchedule": "true",
|
|
157
|
+
"scheduleOnlyFromCurrentTime": "false",
|
|
158
|
+
"x": 630,
|
|
159
|
+
"y": 380,
|
|
160
|
+
"wires": [
|
|
161
|
+
[
|
|
162
|
+
"a6f2769b.1a62a8"
|
|
163
|
+
],
|
|
164
|
+
[
|
|
165
|
+
"9fc75126.65dd3"
|
|
166
|
+
],
|
|
167
|
+
[
|
|
168
|
+
"e2578f6a.210a8"
|
|
169
|
+
]
|
|
170
|
+
]
|
|
171
|
+
},
|
|
172
|
+
{
|
|
173
|
+
"id": "ec4a12a1.b2be9",
|
|
174
|
+
"type": "server",
|
|
175
|
+
"name": "Home Assistant",
|
|
176
|
+
"version": 2,
|
|
177
|
+
"addon": true,
|
|
178
|
+
"rejectUnauthorizedCerts": true,
|
|
179
|
+
"ha_boolean": "y|yes|true|on|home|open",
|
|
180
|
+
"connectionDelay": true,
|
|
181
|
+
"cacheJson": true,
|
|
182
|
+
"heartbeat": false,
|
|
183
|
+
"heartbeatInterval": 30
|
|
184
|
+
}
|
|
165
185
|
]
|
|
166
186
|
```
|
|
@@ -125,6 +125,10 @@ This is especially designed to work for Home Assistant (HA), and the [Nord Pool
|
|
|
125
125
|
|
|
126
126
|
Data can be sent from both the `current state` node or the `events: state` node.
|
|
127
127
|
|
|
128
|
+
::: tip Output properties
|
|
129
|
+
When using the `current state` node, configure the output properties to set `msg.payload` to `entity`.
|
|
130
|
+
:::
|
|
131
|
+
|
|
128
132
|
[See example with Nord Pool and `current state` node](doc/example-nordpool-current-state.md)
|
|
129
133
|
|
|
130
134
|
[See example with Nord Pool and `events: state` node](doc/example-nordpool-events-state.md)
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "node-red-contrib-power-saver",
|
|
3
|
-
"version": "3.0.
|
|
3
|
+
"version": "3.0.9",
|
|
4
4
|
"description": "A module for Node-RED that you can use to turn on and off a switch based on power prices",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"scripts": {
|
|
@@ -28,8 +28,7 @@
|
|
|
28
28
|
"ps-elvia-config": "src/elvia/elvia-config.js",
|
|
29
29
|
"ps-elvia-tariff-types": "src/elvia/elvia-tariff-types.js",
|
|
30
30
|
"ps-elvia-tariff": "src/elvia/elvia-tariff.js",
|
|
31
|
-
"ps-elvia-add-tariff": "src/elvia/elvia-add-tariff.js"
|
|
32
|
-
"ps-general-add-tariff": "src/general-add-tariff.js"
|
|
31
|
+
"ps-elvia-add-tariff": "src/elvia/elvia-add-tariff.js"
|
|
33
32
|
}
|
|
34
33
|
},
|
|
35
34
|
"prettier": {
|
package/src/handle-input.js
CHANGED
|
@@ -1,5 +1,4 @@
|
|
|
1
|
-
const { getEffectiveConfig } = require("./utils");
|
|
2
|
-
const { extractPlanForDate } = require("./utils");
|
|
1
|
+
const { extractPlanForDate, getEffectiveConfig, validationFailure } = require("./utils");
|
|
3
2
|
const { DateTime } = require("luxon");
|
|
4
3
|
|
|
5
4
|
function handleStrategyInput(node, msg, doPlanning) {
|
|
@@ -152,11 +151,6 @@ function validateInput(node, msg) {
|
|
|
152
151
|
return true;
|
|
153
152
|
}
|
|
154
153
|
|
|
155
|
-
function validationFailure(node, message, status = null) {
|
|
156
|
-
node.status({ fill: "red", shape: "ring", text: status ?? message });
|
|
157
|
-
node.warn(message);
|
|
158
|
-
}
|
|
159
|
-
|
|
160
154
|
module.exports = {
|
|
161
155
|
handleStrategyInput,
|
|
162
156
|
};
|
package/src/power-saver.js
CHANGED
|
@@ -8,6 +8,7 @@ const {
|
|
|
8
8
|
getStartAtIndex,
|
|
9
9
|
loadDayData,
|
|
10
10
|
makeSchedule,
|
|
11
|
+
validationFailure,
|
|
11
12
|
} = require("./utils");
|
|
12
13
|
const { convertMsg } = require("./receive-price-functions");
|
|
13
14
|
const { calculate } = require("./strategy-best-save-functions");
|
|
@@ -189,11 +190,6 @@ function makePlan(node, values, startTimes, onOffBefore, firstValueNextDay) {
|
|
|
189
190
|
};
|
|
190
191
|
}
|
|
191
192
|
|
|
192
|
-
function validationFailure(node, message) {
|
|
193
|
-
node.status({ fill: "red", shape: "ring", text: message });
|
|
194
|
-
node.warn(message);
|
|
195
|
-
}
|
|
196
|
-
|
|
197
193
|
function validateMsg(node, msg) {
|
|
198
194
|
if (!msg.payload && !msg.data?.new_state?.attributes) {
|
|
199
195
|
validationFailure(node, "Payload missing");
|
|
@@ -6,9 +6,6 @@ function getPriceData(node, msg) {
|
|
|
6
6
|
return node.context().get("lastPriceData");
|
|
7
7
|
}
|
|
8
8
|
|
|
9
|
-
if (!validateMsg(node, msg)) {
|
|
10
|
-
return null;
|
|
11
|
-
}
|
|
12
9
|
const input = convertMsg(msg);
|
|
13
10
|
if (!validateInput(node, input)) {
|
|
14
11
|
return null;
|
|
@@ -20,19 +17,6 @@ function getPriceData(node, msg) {
|
|
|
20
17
|
return { priceData, source };
|
|
21
18
|
}
|
|
22
19
|
|
|
23
|
-
function validateMsg(node, msg) {
|
|
24
|
-
if (!msg.payload && !msg.data?.new_state?.attributes) {
|
|
25
|
-
validationFailure(node, "Payload missing");
|
|
26
|
-
return false;
|
|
27
|
-
}
|
|
28
|
-
const payload = msg.data?.new_state?.attributes ?? msg.data?.attributes ?? msg.payload;
|
|
29
|
-
if (typeof payload !== "object") {
|
|
30
|
-
validationFailure(node, "Payload must be an object");
|
|
31
|
-
return false;
|
|
32
|
-
}
|
|
33
|
-
return true;
|
|
34
|
-
}
|
|
35
|
-
|
|
36
20
|
function validateInput(node, input) {
|
|
37
21
|
["today", "tomorrow"].forEach((arr) => {
|
|
38
22
|
if (
|
|
@@ -80,7 +64,15 @@ function convertMsg(msg) {
|
|
|
80
64
|
value: v.value,
|
|
81
65
|
start: v.start,
|
|
82
66
|
}));
|
|
83
|
-
} else if (msg.
|
|
67
|
+
} else if (msg.data?.attributes && msg.data?.attributes["raw_" + day]) {
|
|
68
|
+
result.source = "Nordpool";
|
|
69
|
+
result[day] = msg.data.attributes["raw_" + day]
|
|
70
|
+
.filter((v) => v.value)
|
|
71
|
+
.map((v) => ({
|
|
72
|
+
value: v.value,
|
|
73
|
+
start: v.start,
|
|
74
|
+
}));
|
|
75
|
+
} else if (msg.payload?.attributes && msg.payload.attributes["raw_" + day]) {
|
|
84
76
|
result.source = "Nordpool";
|
|
85
77
|
result[day] = msg.payload.attributes["raw_" + day]
|
|
86
78
|
.filter((v) => v.value)
|
|
@@ -48,7 +48,7 @@ function doPlanning(node, _, priceData, _, dateDayBefore, _) {
|
|
|
48
48
|
if (hour === to && to === from && currentStatus === "Inside") {
|
|
49
49
|
endIndexes.push(i - 1);
|
|
50
50
|
}
|
|
51
|
-
if (hour === to && to !== from && i > 0) {
|
|
51
|
+
if (hour === to && to !== from && i > 0 && currentStatus !== "StartMissing") {
|
|
52
52
|
currentStatus = "Outside";
|
|
53
53
|
endIndexes.push(i - 1);
|
|
54
54
|
}
|
|
@@ -66,6 +66,7 @@ function doPlanning(node, _, priceData, _, dateDayBefore, _) {
|
|
|
66
66
|
hour = DateTime.fromISO(priceData[i].start).hour;
|
|
67
67
|
i--;
|
|
68
68
|
} while (periodStatus[i] === "Inside" && hour !== from);
|
|
69
|
+
startIndexes.splice(startIndexes.length - 1, 1);
|
|
69
70
|
}
|
|
70
71
|
if (hour === (to === 0 ? 23 : to - 1)) {
|
|
71
72
|
endIndexes.push(priceData.length - 1);
|
package/src/utils.js
CHANGED
|
@@ -175,6 +175,11 @@ function getStartAtIndex(effectiveConfig, priceData, time) {
|
|
|
175
175
|
}
|
|
176
176
|
}
|
|
177
177
|
|
|
178
|
+
function validationFailure(node, message, status = null) {
|
|
179
|
+
node.status({ fill: "red", shape: "ring", text: status ?? message });
|
|
180
|
+
node.warn(message);
|
|
181
|
+
}
|
|
182
|
+
|
|
178
183
|
module.exports = {
|
|
179
184
|
booleanConfig,
|
|
180
185
|
countAtEnd,
|
|
@@ -191,4 +196,5 @@ module.exports = {
|
|
|
191
196
|
makeSchedule,
|
|
192
197
|
roundPrice,
|
|
193
198
|
sortedIndex,
|
|
199
|
+
validationFailure,
|
|
194
200
|
};
|