node-red-contrib-power-saver 5.1.5 → 5.2.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 +1 -2
- package/eslint.config.cjs +16 -0
- package/examples/price-filter.json +139 -0
- package/package.json +6 -2
- package/src/elvia/elvia-add-tariff.js +1 -1
- package/src/general-add-tariff-functions.js +3 -4
- package/src/general-add-tariff.html +2 -2
- package/src/handle-input.js +2 -3
- package/src/handle-output.js +0 -4
- package/src/light-saver-functions.js +9 -8
- package/src/light-saver.html +697 -646
- package/src/light-saver.js +3 -3
- package/src/price-filter.html +72 -0
- package/src/price-filter.js +100 -0
- package/src/receive-price-functions.js +1 -2
- package/src/schedule-merger-functions.js +1 -1
- package/src/schedule-merger.js +2 -2
- package/src/strategy-best-save-functions.js +21 -6
- package/src/strategy-best-save.js +1 -1
- package/src/strategy-fixed-schedule.html +2 -2
- package/src/strategy-fixed-schedule.js +0 -1
- package/src/strategy-functions.js +1 -1
- package/src/strategy-heat-capacitor-functions.js +5 -5
- package/src/strategy-heat-capacitor.js +21 -21
- package/src/strategy-lowest-price.html +1 -2
- package/src/strategy-lowest-price.js +3 -4
- package/src/utils.js +6 -7
package/README.md
CHANGED
|
@@ -6,5 +6,4 @@ A Node-RED node to save money when power prices are changing by the hour.
|
|
|
6
6
|
|
|
7
7
|
## Please read more in the [documentation](https://powersaver.no/).
|
|
8
8
|
|
|
9
|
-
|
|
10
|
-
[](https://powersaver.no/contribute/#donate)
|
|
9
|
+
[](https://powersaver.no/contribute/#donate)
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
const js = require("@eslint/js");
|
|
2
|
+
const globals = require("globals");
|
|
3
|
+
|
|
4
|
+
module.exports = [
|
|
5
|
+
js.configs.recommended,
|
|
6
|
+
{
|
|
7
|
+
languageOptions: {
|
|
8
|
+
ecmaVersion: "latest",
|
|
9
|
+
globals: {
|
|
10
|
+
...globals.browser,
|
|
11
|
+
...globals.commonjs,
|
|
12
|
+
...globals.es2021,
|
|
13
|
+
},
|
|
14
|
+
},
|
|
15
|
+
},
|
|
16
|
+
];
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
[
|
|
2
|
+
{
|
|
3
|
+
"id": "pf01a2b3c4d5e6f7",
|
|
4
|
+
"type": "inject",
|
|
5
|
+
"z": "pf01tab01tab01t1",
|
|
6
|
+
"name": "Refresh prices (every hour)",
|
|
7
|
+
"props": [
|
|
8
|
+
{
|
|
9
|
+
"p": "payload"
|
|
10
|
+
}
|
|
11
|
+
],
|
|
12
|
+
"repeat": "3600",
|
|
13
|
+
"crontab": "",
|
|
14
|
+
"once": true,
|
|
15
|
+
"onceDelay": "1",
|
|
16
|
+
"topic": "",
|
|
17
|
+
"payload": "{viewer{homes{currentSubscription{priceInfo(resolution:QUARTER_HOURLY){today{totalstartsAt}tomorrow{totalstartsAt}}}}}}",
|
|
18
|
+
"payloadType": "str",
|
|
19
|
+
"x": 180,
|
|
20
|
+
"y": 80,
|
|
21
|
+
"wires": [["pf02a2b3c4d5e6f7"]]
|
|
22
|
+
},
|
|
23
|
+
{
|
|
24
|
+
"id": "pf02a2b3c4d5e6f7",
|
|
25
|
+
"type": "tibber-query",
|
|
26
|
+
"z": "pf01tab01tab01t1",
|
|
27
|
+
"name": "Get Tibber prices",
|
|
28
|
+
"active": true,
|
|
29
|
+
"apiEndpointRef": "pf0btibberapi01",
|
|
30
|
+
"x": 430,
|
|
31
|
+
"y": 80,
|
|
32
|
+
"wires": [["pf03a2b3c4d5e6f7"]]
|
|
33
|
+
},
|
|
34
|
+
{
|
|
35
|
+
"id": "pf03a2b3c4d5e6f7",
|
|
36
|
+
"type": "ps-receive-price",
|
|
37
|
+
"z": "pf01tab01tab01t1",
|
|
38
|
+
"name": "Price Receiver",
|
|
39
|
+
"x": 170,
|
|
40
|
+
"y": 180,
|
|
41
|
+
"wires": [["pf04a2b3c4d5e6f7"]]
|
|
42
|
+
},
|
|
43
|
+
{
|
|
44
|
+
"id": "pf04a2b3c4d5e6f7",
|
|
45
|
+
"type": "ps-strategy-lowest-price",
|
|
46
|
+
"z": "pf01tab01tab01t1",
|
|
47
|
+
"name": "4 Cheapest Hours",
|
|
48
|
+
"fromHour": "00",
|
|
49
|
+
"fromMinute": "00",
|
|
50
|
+
"toHour": "00",
|
|
51
|
+
"toMinute": "00",
|
|
52
|
+
"minutesOn": "240",
|
|
53
|
+
"maxPrice": "",
|
|
54
|
+
"doNotSplit": false,
|
|
55
|
+
"sendCurrentValueWhenRescheduling": true,
|
|
56
|
+
"outputValueForOn": "true",
|
|
57
|
+
"outputValueForOff": "false",
|
|
58
|
+
"outputValueForOntype": "bool",
|
|
59
|
+
"outputValueForOfftype": "bool",
|
|
60
|
+
"outputIfNoSchedule": "false",
|
|
61
|
+
"outputOutsidePeriod": "false",
|
|
62
|
+
"contextStorage": "default",
|
|
63
|
+
"x": 390,
|
|
64
|
+
"y": 180,
|
|
65
|
+
"wires": [[], [], ["pf05a2b3c4d5e6f7"]]
|
|
66
|
+
},
|
|
67
|
+
{
|
|
68
|
+
"id": "pf05a2b3c4d5e6f7",
|
|
69
|
+
"type": "ps-price-filter",
|
|
70
|
+
"z": "pf01tab01tab01t1",
|
|
71
|
+
"name": "No heating above 1.00",
|
|
72
|
+
"turn": "off",
|
|
73
|
+
"condition": "over",
|
|
74
|
+
"limit": 1,
|
|
75
|
+
"x": 630,
|
|
76
|
+
"y": 180,
|
|
77
|
+
"wires": [["pf06a2b3c4d5e6f7"], ["pf07a2b3c4d5e6f7"], ["pf08a2b3c4d5e6f7"]]
|
|
78
|
+
},
|
|
79
|
+
{
|
|
80
|
+
"id": "pf06a2b3c4d5e6f7",
|
|
81
|
+
"type": "debug",
|
|
82
|
+
"z": "pf01tab01tab01t1",
|
|
83
|
+
"name": "Turn ON",
|
|
84
|
+
"active": true,
|
|
85
|
+
"tosidebar": true,
|
|
86
|
+
"console": false,
|
|
87
|
+
"tostatus": false,
|
|
88
|
+
"complete": "payload",
|
|
89
|
+
"targetType": "msg",
|
|
90
|
+
"x": 860,
|
|
91
|
+
"y": 140,
|
|
92
|
+
"wires": []
|
|
93
|
+
},
|
|
94
|
+
{
|
|
95
|
+
"id": "pf07a2b3c4d5e6f7",
|
|
96
|
+
"type": "debug",
|
|
97
|
+
"z": "pf01tab01tab01t1",
|
|
98
|
+
"name": "Turn OFF",
|
|
99
|
+
"active": true,
|
|
100
|
+
"tosidebar": true,
|
|
101
|
+
"console": false,
|
|
102
|
+
"tostatus": false,
|
|
103
|
+
"complete": "payload",
|
|
104
|
+
"targetType": "msg",
|
|
105
|
+
"x": 860,
|
|
106
|
+
"y": 180,
|
|
107
|
+
"wires": []
|
|
108
|
+
},
|
|
109
|
+
{
|
|
110
|
+
"id": "pf08a2b3c4d5e6f7",
|
|
111
|
+
"type": "debug",
|
|
112
|
+
"z": "pf01tab01tab01t1",
|
|
113
|
+
"name": "Filtered Schedule",
|
|
114
|
+
"active": true,
|
|
115
|
+
"tosidebar": true,
|
|
116
|
+
"console": false,
|
|
117
|
+
"tostatus": false,
|
|
118
|
+
"complete": "payload",
|
|
119
|
+
"targetType": "msg",
|
|
120
|
+
"x": 870,
|
|
121
|
+
"y": 220,
|
|
122
|
+
"wires": []
|
|
123
|
+
},
|
|
124
|
+
{
|
|
125
|
+
"id": "pf0btibberapi01",
|
|
126
|
+
"type": "tibber-api-endpoint",
|
|
127
|
+
"queryUrl": "https://api.tibber.com/v1-beta/gql",
|
|
128
|
+
"name": "Tibber API"
|
|
129
|
+
},
|
|
130
|
+
{
|
|
131
|
+
"id": "cd9babd81a9c291a",
|
|
132
|
+
"type": "global-config",
|
|
133
|
+
"env": [],
|
|
134
|
+
"modules": {
|
|
135
|
+
"node-red-contrib-tibber-api": "6.4.1",
|
|
136
|
+
"node-red-contrib-power-saver": "5.1.5"
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
]
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "node-red-contrib-power-saver",
|
|
3
|
-
"version": "5.1
|
|
3
|
+
"version": "5.2.1",
|
|
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": {
|
|
@@ -32,7 +32,8 @@
|
|
|
32
32
|
"ps-elvia-tariff-types": "src/elvia/elvia-tariff-types.js",
|
|
33
33
|
"ps-elvia-tariff": "src/elvia/elvia-tariff.js",
|
|
34
34
|
"ps-elvia-add-tariff": "src/elvia/elvia-add-tariff.js",
|
|
35
|
-
"ps-general-add-tariff": "src/general-add-tariff.js"
|
|
35
|
+
"ps-general-add-tariff": "src/general-add-tariff.js",
|
|
36
|
+
"ps-price-filter": "src/price-filter.js"
|
|
36
37
|
},
|
|
37
38
|
"version": ">=2.2.0"
|
|
38
39
|
},
|
|
@@ -47,6 +48,7 @@
|
|
|
47
48
|
"url": "https://github.com/ottopaulsen/node-red-contrib-power-saver.git"
|
|
48
49
|
},
|
|
49
50
|
"devDependencies": {
|
|
51
|
+
"@eslint/js": "^10.0.1",
|
|
50
52
|
"@vuepress/bundler-vite": "2.0.0-rc.26",
|
|
51
53
|
"@vuepress/plugin-google-analytics": "2.0.0-rc.123",
|
|
52
54
|
"@vuepress/plugin-register-components": "2.0.0-rc.123",
|
|
@@ -56,9 +58,11 @@
|
|
|
56
58
|
"chai": "6.2.2",
|
|
57
59
|
"eslint": "10.0.0",
|
|
58
60
|
"expect": "30.2.0",
|
|
61
|
+
"globals": "^17.4.0",
|
|
59
62
|
"mocha": "^11.7.5",
|
|
60
63
|
"node-red": "^4.1.5",
|
|
61
64
|
"node-red-node-test-helper": "0.3.6",
|
|
65
|
+
"prettier": "^3.8.1",
|
|
62
66
|
"sass-embedded": "^1.97.3",
|
|
63
67
|
"sass-loader": "^16.0.7",
|
|
64
68
|
"vuepress": "2.0.0-rc.26"
|
|
@@ -15,7 +15,7 @@ module.exports = function (RED) {
|
|
|
15
15
|
const prices = msg.payload.priceData;
|
|
16
16
|
if (!prices) {
|
|
17
17
|
node.warn(
|
|
18
|
-
"No price data received on input. Did you use the ps-receive-price node or convert to correct format otherwise?"
|
|
18
|
+
"No price data received on input. Did you use the ps-receive-price node or convert to correct format otherwise?",
|
|
19
19
|
);
|
|
20
20
|
return;
|
|
21
21
|
}
|
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
const { DateTime } = require("luxon");
|
|
2
1
|
const { roundPrice } = require("./utils");
|
|
3
2
|
|
|
4
3
|
// Build all periods that are different.
|
|
@@ -68,13 +67,13 @@ function addTariffToPrices(node, config, prices) {
|
|
|
68
67
|
if (p.start.substring(0, 10) >= validFrom && p.start.substring(0, 10) <= validTo && config.days[day]) {
|
|
69
68
|
p.sum = roundPrice(p.value + p.add);
|
|
70
69
|
} else {
|
|
71
|
-
p.sum = p.value
|
|
70
|
+
p.sum = p.value;
|
|
72
71
|
}
|
|
73
72
|
});
|
|
74
73
|
|
|
75
|
-
const result = sortedPeriods.map(p => ({start: p.start, value: p.sum}))
|
|
74
|
+
const result = sortedPeriods.map((p) => ({ start: p.start, value: p.sum }));
|
|
76
75
|
|
|
77
|
-
|
|
76
|
+
// Set end on last period
|
|
78
77
|
result[result.length - 1].end = lastEnd;
|
|
79
78
|
|
|
80
79
|
return result;
|
|
@@ -80,7 +80,7 @@
|
|
|
80
80
|
["for", id],
|
|
81
81
|
["style", "margin-right: 10px;"],
|
|
82
82
|
],
|
|
83
|
-
[]
|
|
83
|
+
[],
|
|
84
84
|
);
|
|
85
85
|
label.innerHTML = text;
|
|
86
86
|
const inp = createElement("input", [
|
|
@@ -160,7 +160,7 @@
|
|
|
160
160
|
["for", id],
|
|
161
161
|
["style", " margin: 4px 10px 0px 2px;width: 30px; text-align: left;"],
|
|
162
162
|
],
|
|
163
|
-
[]
|
|
163
|
+
[],
|
|
164
164
|
);
|
|
165
165
|
label.innerHTML = day;
|
|
166
166
|
const attrs = [
|
package/src/handle-input.js
CHANGED
|
@@ -74,7 +74,6 @@ function makePlanFromPriceData(node, msg, config, doPlanning, calcSavings) {
|
|
|
74
74
|
const startTimes = priceDatePerMinute.map((d) => d.start);
|
|
75
75
|
const prices = priceDatePerMinute.map((d) => d.value);
|
|
76
76
|
|
|
77
|
-
|
|
78
77
|
const onOff = doPlanning(node, priceDatePerMinute);
|
|
79
78
|
const savings = calcSavings(prices, onOff);
|
|
80
79
|
const minutes = startTimes.map((v, i) => ({
|
|
@@ -87,7 +86,7 @@ function makePlanFromPriceData(node, msg, config, doPlanning, calcSavings) {
|
|
|
87
86
|
const schedule = trimScheduleToStart(fullSchedule, priceData[0].start);
|
|
88
87
|
addLastSwitchIfNoSchedule(schedule, minutes, config);
|
|
89
88
|
|
|
90
|
-
plan = {
|
|
89
|
+
const plan = {
|
|
91
90
|
minutes,
|
|
92
91
|
schedule,
|
|
93
92
|
source,
|
|
@@ -175,7 +174,7 @@ function trimScheduleToStart(schedule, startTime) {
|
|
|
175
174
|
|
|
176
175
|
function deleteSavedScheduleBefore(node, day, checkDays = 0) {
|
|
177
176
|
let date = day;
|
|
178
|
-
let data
|
|
177
|
+
let data;
|
|
179
178
|
let count = 0;
|
|
180
179
|
do {
|
|
181
180
|
date = date.plus({ days: -1 });
|
package/src/handle-output.js
CHANGED
|
@@ -73,7 +73,6 @@ function runSchedule(node, schedule, time, currentSent = false) {
|
|
|
73
73
|
const entry = remainingSchedule[0];
|
|
74
74
|
const nextTime = DateTime.fromISO(entry.time);
|
|
75
75
|
const wait = nextTime - time;
|
|
76
|
-
const onOff = entry.value ? "on" : "off";
|
|
77
76
|
const statusMessage = `${remainingSchedule.length} changes - ${
|
|
78
77
|
remainingSchedule[0].value ? "on" : "off"
|
|
79
78
|
} at ${nextTime.toFormat("HH:mm")}`;
|
|
@@ -114,9 +113,6 @@ function collapseMinutes(minutes) {
|
|
|
114
113
|
return a.price === b.price && a.onOff === b.onOff && a.saving === b.saving;
|
|
115
114
|
}
|
|
116
115
|
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
116
|
if (!Array.isArray(minutes) || minutes.length === 0) {
|
|
121
117
|
return [];
|
|
122
118
|
}
|
|
@@ -249,7 +249,6 @@ function handleStateChange(event, config, state, node, homeAssistant, clock = nu
|
|
|
249
249
|
// Check if it's the brightness sensor
|
|
250
250
|
if (config.brightnessSensor && config.brightnessSensor.entity_id === entityId) {
|
|
251
251
|
const wasBrightnessAllowing = isBrightnessAllowingLights(config); // Check before update
|
|
252
|
-
const oldBrightness = config.brightnessSensor.state;
|
|
253
252
|
config.brightnessSensor.lastChanged = timestamp;
|
|
254
253
|
config.brightnessSensor.state = newState.state;
|
|
255
254
|
|
|
@@ -715,14 +714,16 @@ function fetchMissingStates(config, state, node, homeAssistant, clock = null) {
|
|
|
715
714
|
`Initial timedOut set to ${state.timedOut} (all triggers actually timed out: ${allTimedOut})`,
|
|
716
715
|
);
|
|
717
716
|
|
|
718
|
-
// If motion is detected at startup (timedOut is false), turn lights on
|
|
719
717
|
if (!allTimedOut) {
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
718
|
+
const levelConfig = findLevelConfig(config, clock);
|
|
719
|
+
state.lastImmediateTime = levelConfig && levelConfig.immediate === true ? levelConfig.fromTime : null;
|
|
720
|
+
debugLog(
|
|
721
|
+
config,
|
|
722
|
+
node,
|
|
723
|
+
`Startup preserved current light state while motion is still active${state.lastImmediateTime ? ` (immediate period ${state.lastImmediateTime} already active)` : ""}`,
|
|
724
|
+
);
|
|
725
|
+
} else {
|
|
726
|
+
state.lastImmediateTime = null;
|
|
726
727
|
}
|
|
727
728
|
|
|
728
729
|
return true; // Indicates that initial timedOut was set
|