node-red-contrib-power-saver 3.5.3 → 3.5.6
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 -0
- package/docs/changelog/README.md +13 -0
- package/docs/examples/README.md +2 -0
- package/docs/examples/example-visualize-on-off/example-visualize-on-off.md +41 -0
- package/docs/examples/example-visualize-on-off/lovelace.jpg +0 -0
- package/docs/examples/example-visualize-on-off/lovelace.yaml +89 -0
- package/docs/examples/example-visualize-on-off/nodes.json +88 -0
- package/docs/examples/example-visualize-on-off/nodes.png +0 -0
- package/package.json +1 -1
- package/src/elvia/elvia-add-tariff.js +2 -2
- package/src/elvia/elvia-api.js +6 -5
- package/src/handle-input.js +2 -3
- package/src/strategy-best-save.js +1 -3
- package/src/strategy-lowest-price.js +3 -1
- package/src/utils.js +6 -2
package/docs/.vuepress/config.js
CHANGED
package/docs/changelog/README.md
CHANGED
|
@@ -6,6 +6,19 @@ sidebar: "auto"
|
|
|
6
6
|
|
|
7
7
|
List the most significant changes, starting in version 1.0.9.
|
|
8
8
|
|
|
9
|
+
## 3.5.6
|
|
10
|
+
|
|
11
|
+
- Update Elvia nodes so they use the new `digin` API. NB! There is no guarantee this is working right.
|
|
12
|
+
|
|
13
|
+
## 3.5.5
|
|
14
|
+
|
|
15
|
+
- Fix config storage for Best Save node
|
|
16
|
+
|
|
17
|
+
## 3.5.4
|
|
18
|
+
|
|
19
|
+
- Fix bug in context selection
|
|
20
|
+
- Add example for visualization in Lovelace
|
|
21
|
+
|
|
9
22
|
## 3.5.3
|
|
10
23
|
|
|
11
24
|
- Fix a couple of bugs in how context is used.
|
package/docs/examples/README.md
CHANGED
|
@@ -10,6 +10,8 @@
|
|
|
10
10
|
|
|
11
11
|
[Cascade temperature control](./example-cascade-temperature-control)
|
|
12
12
|
|
|
13
|
+
[Visualize on/off, price and consumption in Lovelace](example-visualize-on-off/example-visualize-on-off)
|
|
14
|
+
|
|
13
15
|
## User provided examples
|
|
14
16
|
|
|
15
17
|
[Output schedule to a sensor entity](./example-next-schedule-entity.md) (by Stefan)
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
# Lovelace Visualization
|
|
2
|
+
|
|
3
|
+
The source of this example is made by Kim Storøy, and Otto Paulsen has written the documentation and made some changes to the files.
|
|
4
|
+
|
|
5
|
+

|
|
6
|
+
|
|
7
|
+
## Introduction
|
|
8
|
+
|
|
9
|
+
With this example you can visualize the schedule together with the price and the consumption in Lovelace, as shown above.
|
|
10
|
+
|
|
11
|
+
## Installation
|
|
12
|
+
|
|
13
|
+
You need the following installed before you can use this example:
|
|
14
|
+
|
|
15
|
+
- The [Node-RED Companion Integration](https://github.com/zachowj/hass-node-red).
|
|
16
|
+
- The [apexcharts-card](https://github.com/RomRider/apexcharts-card)
|
|
17
|
+
- The Tibber integration (Configuration -> Integrations)
|
|
18
|
+
|
|
19
|
+
If you don't already have them, install them first.
|
|
20
|
+
|
|
21
|
+
In Node-RED, select `Import` from the menu, and paste the code for nodes below. Connect them like this:
|
|
22
|
+
|
|
23
|
+

|
|
24
|
+
|
|
25
|
+
In Lovelace, use the Lovelace code below for the new card.
|
|
26
|
+
Replace the `sensor.accumulated_consumption_current_hour_your_address`
|
|
27
|
+
with the correct name for your corresponding sensor in your Tibber integration.
|
|
28
|
+
|
|
29
|
+
### Code
|
|
30
|
+
|
|
31
|
+
::: details Nodes
|
|
32
|
+
|
|
33
|
+
@[code](./nodes.json)
|
|
34
|
+
|
|
35
|
+
:::
|
|
36
|
+
|
|
37
|
+
::: details Lovelace
|
|
38
|
+
|
|
39
|
+
@[code](./lovelace.yaml)
|
|
40
|
+
|
|
41
|
+
:::
|
|
Binary file
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
type: custom:apexcharts-card
|
|
2
|
+
header:
|
|
3
|
+
show: true
|
|
4
|
+
title: Pris 48t + Forbruk + Powersaver
|
|
5
|
+
now:
|
|
6
|
+
show: true
|
|
7
|
+
label: Nå
|
|
8
|
+
graph_span: 2d
|
|
9
|
+
span:
|
|
10
|
+
start: day
|
|
11
|
+
apex_config:
|
|
12
|
+
stroke:
|
|
13
|
+
width: 2
|
|
14
|
+
dataLabels:
|
|
15
|
+
enabled: true
|
|
16
|
+
fill:
|
|
17
|
+
type: gradient
|
|
18
|
+
gradient:
|
|
19
|
+
shadeIntensity: 1
|
|
20
|
+
inverseColors: false
|
|
21
|
+
opacityFrom: 0.45
|
|
22
|
+
opacityTo: 0.05
|
|
23
|
+
stops:
|
|
24
|
+
- 10
|
|
25
|
+
- 50
|
|
26
|
+
- 75
|
|
27
|
+
- 1000
|
|
28
|
+
legend:
|
|
29
|
+
show: false
|
|
30
|
+
yaxis:
|
|
31
|
+
- id: price
|
|
32
|
+
show: true
|
|
33
|
+
decimalsInFloat: 1
|
|
34
|
+
floating: false
|
|
35
|
+
forceNiceScale: true
|
|
36
|
+
extend_to: end
|
|
37
|
+
- id: usage
|
|
38
|
+
show: true
|
|
39
|
+
opposite: true
|
|
40
|
+
decimalsInFloat: 1
|
|
41
|
+
floating: false
|
|
42
|
+
forceNiceScale: true
|
|
43
|
+
extend_to: end
|
|
44
|
+
- id: powersaver
|
|
45
|
+
show: false
|
|
46
|
+
decimalsInFloat: 0
|
|
47
|
+
floating: false
|
|
48
|
+
extend_to: now
|
|
49
|
+
series:
|
|
50
|
+
- entity: sensor.powersaver
|
|
51
|
+
yaxis_id: price
|
|
52
|
+
extend_to: now
|
|
53
|
+
name: Pris
|
|
54
|
+
type: area
|
|
55
|
+
curve: smooth
|
|
56
|
+
color: tomato
|
|
57
|
+
show:
|
|
58
|
+
legend_value: false
|
|
59
|
+
data_generator: |
|
|
60
|
+
return entity.attributes.hours.map((entry) => {
|
|
61
|
+
return [new Date(entry.start), entry.price];
|
|
62
|
+
});
|
|
63
|
+
- entity: sensor.accumulated_consumption_current_hour_xxxx
|
|
64
|
+
yaxis_id: usage
|
|
65
|
+
type: column
|
|
66
|
+
name: Forbruk
|
|
67
|
+
group_by:
|
|
68
|
+
func: max
|
|
69
|
+
show:
|
|
70
|
+
legend_value: false
|
|
71
|
+
- entity: sensor.powersaver
|
|
72
|
+
data_generator: |
|
|
73
|
+
return entity.attributes.hours.map((entry) => {
|
|
74
|
+
return [new Date(entry.start), entry.onOff];
|
|
75
|
+
});
|
|
76
|
+
yaxis_id: powersaver
|
|
77
|
+
name: ' '
|
|
78
|
+
type: area
|
|
79
|
+
color: rgb(0, 255, 0)
|
|
80
|
+
opacity: 0.2
|
|
81
|
+
stroke_width: 0
|
|
82
|
+
curve: stepline
|
|
83
|
+
group_by:
|
|
84
|
+
func: min
|
|
85
|
+
show:
|
|
86
|
+
legend_value: false
|
|
87
|
+
in_header: false
|
|
88
|
+
name_in_header: false
|
|
89
|
+
datalabels: false
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
[
|
|
2
|
+
{
|
|
3
|
+
"id": "eab799518168f5a3",
|
|
4
|
+
"type": "ha-entity",
|
|
5
|
+
"z": "d938c47f.3398f8",
|
|
6
|
+
"name": "Info fra PS til HA",
|
|
7
|
+
"server": "ec4a12a1.b2be9",
|
|
8
|
+
"version": 2,
|
|
9
|
+
"debugenabled": false,
|
|
10
|
+
"outputs": 1,
|
|
11
|
+
"entityType": "sensor",
|
|
12
|
+
"config": [
|
|
13
|
+
{
|
|
14
|
+
"property": "name",
|
|
15
|
+
"value": "Powersaver"
|
|
16
|
+
},
|
|
17
|
+
{
|
|
18
|
+
"property": "device_class",
|
|
19
|
+
"value": ""
|
|
20
|
+
},
|
|
21
|
+
{
|
|
22
|
+
"property": "icon",
|
|
23
|
+
"value": ""
|
|
24
|
+
},
|
|
25
|
+
{
|
|
26
|
+
"property": "unit_of_measurement",
|
|
27
|
+
"value": ""
|
|
28
|
+
},
|
|
29
|
+
{
|
|
30
|
+
"property": "state_class",
|
|
31
|
+
"value": ""
|
|
32
|
+
},
|
|
33
|
+
{
|
|
34
|
+
"property": "last_reset",
|
|
35
|
+
"value": ""
|
|
36
|
+
}
|
|
37
|
+
],
|
|
38
|
+
"state": "payload",
|
|
39
|
+
"stateType": "str",
|
|
40
|
+
"attributes": [
|
|
41
|
+
{
|
|
42
|
+
"property": "Schedule",
|
|
43
|
+
"value": "payload.schedule",
|
|
44
|
+
"valueType": "msg"
|
|
45
|
+
},
|
|
46
|
+
{
|
|
47
|
+
"property": "Hours",
|
|
48
|
+
"value": "payload.hours",
|
|
49
|
+
"valueType": "msg"
|
|
50
|
+
},
|
|
51
|
+
{
|
|
52
|
+
"property": "Control",
|
|
53
|
+
"value": "payload.hours[0].onOff",
|
|
54
|
+
"valueType": "str"
|
|
55
|
+
},
|
|
56
|
+
{
|
|
57
|
+
"property": "Current",
|
|
58
|
+
"value": "payload.current",
|
|
59
|
+
"valueType": "str"
|
|
60
|
+
}
|
|
61
|
+
],
|
|
62
|
+
"resend": true,
|
|
63
|
+
"outputLocation": "payload",
|
|
64
|
+
"outputLocationType": "none",
|
|
65
|
+
"inputOverride": "allow",
|
|
66
|
+
"outputOnStateChange": false,
|
|
67
|
+
"outputPayload": "",
|
|
68
|
+
"outputPayloadType": "str",
|
|
69
|
+
"x": 830,
|
|
70
|
+
"y": 630,
|
|
71
|
+
"wires": [[]]
|
|
72
|
+
},
|
|
73
|
+
{
|
|
74
|
+
"id": "cad33a63f66ef72e",
|
|
75
|
+
"type": "function",
|
|
76
|
+
"z": "d938c47f.3398f8",
|
|
77
|
+
"name": "Convert true/false to 1/0",
|
|
78
|
+
"func": "msg.payload.hours.forEach(h => h.onOff = h.onOff ? \"1\" : \"0\")\nreturn msg;",
|
|
79
|
+
"outputs": 1,
|
|
80
|
+
"noerr": 0,
|
|
81
|
+
"initialize": "",
|
|
82
|
+
"finalize": "",
|
|
83
|
+
"libs": [],
|
|
84
|
+
"x": 550,
|
|
85
|
+
"y": 630,
|
|
86
|
+
"wires": [["eab799518168f5a3", "37a23d88cfc668f2"]]
|
|
87
|
+
}
|
|
88
|
+
]
|
|
Binary file
|
package/package.json
CHANGED
|
@@ -28,14 +28,14 @@ module.exports = function (RED) {
|
|
|
28
28
|
|
|
29
29
|
getTariffForPeriod(node, key, node.tariffKey, fromTime, toTime).then((json) => {
|
|
30
30
|
const tariff = json;
|
|
31
|
-
const priceInfo = tariff.gridTariff.tariffPrice.
|
|
31
|
+
const priceInfo = tariff.gridTariff.tariffPrice.hours;
|
|
32
32
|
if (priceInfo.length !== prices.length) {
|
|
33
33
|
node.warn(`Elvia tariff count mismatch. Expected ${prices.length} items, but got ${priceInfo.length}`);
|
|
34
34
|
node.status({ fill: "red", shape: "dot", text: "Tariff error" });
|
|
35
35
|
} else {
|
|
36
36
|
prices.forEach((p, i) => {
|
|
37
37
|
p.powerPrice = p.value;
|
|
38
|
-
p.gridTariffVariable = priceInfo[i].
|
|
38
|
+
p.gridTariffVariable = priceInfo[i].energyPrice.total;
|
|
39
39
|
p.value = roundPrice(p.powerPrice + p.gridTariffVariable);
|
|
40
40
|
});
|
|
41
41
|
}
|
package/src/elvia/elvia-api.js
CHANGED
|
@@ -2,7 +2,7 @@ const fetch = require("node-fetch");
|
|
|
2
2
|
|
|
3
3
|
function ping(node, subscriptionKey, setResultStatus = true) {
|
|
4
4
|
const url = "https://elvia.azure-api.net/grid-tariff/Ping";
|
|
5
|
-
const headers = { "
|
|
5
|
+
const headers = { "X-API-Key": subscriptionKey };
|
|
6
6
|
fetch(url, { headers }).then((res) => {
|
|
7
7
|
if (setResultStatus) {
|
|
8
8
|
setNodeStatus(node, res.status);
|
|
@@ -11,13 +11,14 @@ function ping(node, subscriptionKey, setResultStatus = true) {
|
|
|
11
11
|
}
|
|
12
12
|
|
|
13
13
|
function getTariff(node, subscriptionKey, tariffKey, range = "today", setResultStatus = true) {
|
|
14
|
-
const url =
|
|
14
|
+
const url =
|
|
15
|
+
"https://elvia.azure-api.net/grid-tariff/digin/api/1.0/tariffquery?TariffKey=" + tariffKey + "&Range=" + range;
|
|
15
16
|
return get(node, subscriptionKey, url, setResultStatus);
|
|
16
17
|
}
|
|
17
18
|
|
|
18
19
|
function getTariffForPeriod(node, subscriptionKey, tariffKey, startTime, endTime, setResultStatus = true) {
|
|
19
20
|
const url =
|
|
20
|
-
"https://elvia.azure-api.net/grid-tariff/api/1/tariffquery?TariffKey=" +
|
|
21
|
+
"https://elvia.azure-api.net/grid-tariff/digin/api/1.0/tariffquery?TariffKey=" +
|
|
21
22
|
tariffKey +
|
|
22
23
|
"&StartTime=" +
|
|
23
24
|
startTime +
|
|
@@ -27,12 +28,12 @@ function getTariffForPeriod(node, subscriptionKey, tariffKey, startTime, endTime
|
|
|
27
28
|
}
|
|
28
29
|
|
|
29
30
|
function getTariffTypes(node, subscriptionKey, setResultStatus = true) {
|
|
30
|
-
const url = "https://elvia.azure-api.net/grid-tariff/api/1/tarifftype";
|
|
31
|
+
const url = "https://elvia.azure-api.net/grid-tariff/digin/api/1.0/tarifftype";
|
|
31
32
|
return get(node, subscriptionKey, url, setResultStatus);
|
|
32
33
|
}
|
|
33
34
|
|
|
34
35
|
function get(node, subscriptionKey, url, setResultStatus) {
|
|
35
|
-
const headers = { "
|
|
36
|
+
const headers = { "X-API-Key": subscriptionKey };
|
|
36
37
|
return fetch(url, { headers }).then((res) => {
|
|
37
38
|
if (setResultStatus && node) {
|
|
38
39
|
setNodeStatus(node, res.status);
|
package/src/handle-input.js
CHANGED
|
@@ -4,6 +4,8 @@ const { version } = require("../package.json");
|
|
|
4
4
|
|
|
5
5
|
function handleStrategyInput(node, msg, doPlanning) {
|
|
6
6
|
const effectiveConfig = getEffectiveConfig(node, msg);
|
|
7
|
+
// Store config variables in node
|
|
8
|
+
Object.keys(effectiveConfig).forEach((key) => (node[key] = effectiveConfig[key]));
|
|
7
9
|
|
|
8
10
|
if (!validateInput(node, msg)) {
|
|
9
11
|
return;
|
|
@@ -23,9 +25,6 @@ function handleStrategyInput(node, msg, doPlanning) {
|
|
|
23
25
|
deleteSavedScheduleBefore(node, DateTime.now().plus({ days: 2 }), 100);
|
|
24
26
|
}
|
|
25
27
|
|
|
26
|
-
// Store config variables in node
|
|
27
|
-
Object.keys(effectiveConfig).forEach((key) => (node[key] = effectiveConfig[key]));
|
|
28
|
-
|
|
29
28
|
let { priceData, source } = getPriceData(node, msg);
|
|
30
29
|
if (!priceData) {
|
|
31
30
|
// Use last saved price data
|
|
@@ -1,14 +1,12 @@
|
|
|
1
1
|
const { countAtEnd, makeSchedule, getSavings, getDiff } = require("./utils");
|
|
2
2
|
const { handleStrategyInput } = require("./handle-input");
|
|
3
3
|
const { loadDayData } = require("./utils");
|
|
4
|
-
|
|
5
4
|
const mostSavedStrategy = require("./strategy-best-save-functions");
|
|
6
5
|
|
|
7
6
|
module.exports = function (RED) {
|
|
8
7
|
function StrategyBestSaveNode(config) {
|
|
9
8
|
RED.nodes.createNode(this, config);
|
|
10
9
|
const node = this;
|
|
11
|
-
|
|
12
10
|
node.status({});
|
|
13
11
|
|
|
14
12
|
const originalConfig = {
|
|
@@ -19,7 +17,7 @@ module.exports = function (RED) {
|
|
|
19
17
|
outputIfNoSchedule: config.outputIfNoSchedule === "true",
|
|
20
18
|
contextStorage: config.contextStorage || "default",
|
|
21
19
|
};
|
|
22
|
-
node.context().set("config", originalConfig
|
|
20
|
+
node.context().set("config", originalConfig);
|
|
23
21
|
node.contextStorage = originalConfig.contextStorage;
|
|
24
22
|
|
|
25
23
|
node.on("close", function () {
|
|
@@ -19,7 +19,9 @@ module.exports = function (RED) {
|
|
|
19
19
|
outputOutsidePeriod: booleanConfig(config.outputOutsidePeriod),
|
|
20
20
|
contextStorage: config.contextStorage || "default",
|
|
21
21
|
};
|
|
22
|
-
node.context().set("config", originalConfig
|
|
22
|
+
node.context().set("config", originalConfig);
|
|
23
|
+
node.contextStorage = originalConfig.contextStorage;
|
|
24
|
+
|
|
23
25
|
|
|
24
26
|
node.on("close", function () {
|
|
25
27
|
clearTimeout(node.schedulingTimeout);
|
package/src/utils.js
CHANGED
|
@@ -57,14 +57,18 @@ function getDiff(large, small) {
|
|
|
57
57
|
}
|
|
58
58
|
|
|
59
59
|
function getEffectiveConfig(node, msg) {
|
|
60
|
-
const res = node.context().get("config"
|
|
60
|
+
const res = node.context().get("config");
|
|
61
|
+
if (!res) {
|
|
62
|
+
node.error("Node has no config");
|
|
63
|
+
return {};
|
|
64
|
+
}
|
|
61
65
|
const isConfigMsg = !!msg?.payload?.config;
|
|
62
66
|
if (isConfigMsg) {
|
|
63
67
|
const inputConfig = msg.payload.config;
|
|
64
68
|
Object.keys(inputConfig).forEach((key) => {
|
|
65
69
|
res[key] = inputConfig[key];
|
|
66
70
|
});
|
|
67
|
-
node.context().set("config", res
|
|
71
|
+
node.context().set("config", res);
|
|
68
72
|
}
|
|
69
73
|
return res;
|
|
70
74
|
}
|