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.
@@ -49,6 +49,7 @@ module.exports = {
49
49
  "/examples/example-tibber-mqtt.md",
50
50
  "/examples/example-heat-capacitor.md",
51
51
  "/examples/example-cascade-temperature-control.md",
52
+ "/examples/example-visualize-on-off/example-visualize-on-off.md",
52
53
  ],
53
54
  },
54
55
  ],
@@ -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.
@@ -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
+ ![Lovelace image](./lovelace.jpg)
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
+ ![nodes](./nodes.png)
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
+ :::
@@ -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
+ ]
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "node-red-contrib-power-saver",
3
- "version": "3.5.3",
3
+ "version": "3.5.6",
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,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.priceInfo;
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].variablePrice.total;
38
+ p.gridTariffVariable = priceInfo[i].energyPrice.total;
39
39
  p.value = roundPrice(p.powerPrice + p.gridTariffVariable);
40
40
  });
41
41
  }
@@ -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 = { "Ocp-Apim-Subscription-Key": subscriptionKey };
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 = "https://elvia.azure-api.net/grid-tariff/api/1/tariffquery?TariffKey=" + tariffKey + "&Range=" + range;
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 = { "Ocp-Apim-Subscription-Key": subscriptionKey };
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);
@@ -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, originalConfig.contextStorage);
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, originalConfig.contextStorage);
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", node.contextStorage);
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, node.contextStorage);
71
+ node.context().set("config", res);
68
72
  }
69
73
  return res;
70
74
  }