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 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
- [![Donation](./donation.png)](https://powersaver.no/contribute/#donate)
9
+ [![Donation](./donation.png)](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.5",
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
- // Set end on last period
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 = [
@@ -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 = null;
177
+ let data;
179
178
  let count = 0;
180
179
  do {
181
180
  date = date.plus({ days: -1 });
@@ -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
- debugLog(config, node, "Motion detected at startup, turning lights on");
721
- const level = findCurrentLevel(config, node, clock);
722
- if (level !== null && isBrightnessAllowingLights(config)) {
723
- controlLights(config, config.lights, level, node, homeAssistant);
724
- debugLog(config, node, `Lights turned on to ${level}% at startup (motion detected)`);
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