node-red-contrib-power-saver 3.6.2 → 4.0.0
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/.eslintrc.js +15 -0
- package/docs/.vuepress/components/DonateButtons.vue +26 -3
- package/docs/.vuepress/components/VippsPlakat.vue +20 -0
- package/docs/.vuepress/config.js +17 -10
- package/docs/.vuepress/public/ads.txt +1 -0
- package/docs/README.md +4 -4
- package/docs/changelog/README.md +55 -1
- package/docs/contribute/README.md +8 -3
- package/docs/examples/example-grid-tariff-capacity-flow.json +23 -7
- package/docs/examples/example-grid-tariff-capacity-part.md +657 -22
- package/docs/faq/README.md +1 -1
- package/docs/faq/best-save-viewer.md +1 -1
- package/docs/guide/README.md +20 -5
- package/docs/images/best-save-config.png +0 -0
- package/docs/images/combine-two-lowest-price.png +0 -0
- package/docs/images/fixed-schedule-config.png +0 -0
- package/docs/images/global-context-window.png +0 -0
- package/docs/images/lowest-price-config.png +0 -0
- package/docs/images/node-ps-schedule-merger.png +0 -0
- package/docs/images/node-ps-strategy-fixed-schedule.png +0 -0
- package/docs/images/ps-strategy-fixed-schedule-example.png +0 -0
- package/docs/images/schedule-merger-config.png +0 -0
- package/docs/images/schedule-merger-example-1.png +0 -0
- package/docs/images/vipps-plakat.png +0 -0
- package/docs/images/vipps-qr.png +0 -0
- package/docs/images/vipps-smiling-rgb-orange-pos.png +0 -0
- package/docs/nodes/README.md +12 -6
- package/docs/nodes/dynamic-commands.md +79 -0
- package/docs/nodes/dynamic-config.md +76 -0
- package/docs/nodes/ps-elvia-add-tariff.md +4 -0
- package/docs/nodes/ps-general-add-tariff.md +10 -0
- package/docs/nodes/ps-receive-price.md +2 -1
- package/docs/nodes/ps-schedule-merger.md +227 -0
- package/docs/nodes/ps-strategy-best-save.md +46 -110
- package/docs/nodes/ps-strategy-fixed-schedule.md +101 -0
- package/docs/nodes/ps-strategy-heat-capacitor.md +6 -1
- package/docs/nodes/ps-strategy-lowest-price.md +51 -112
- package/package.json +5 -2
- package/src/elvia/elvia-add-tariff.html +1 -2
- package/src/elvia/elvia-add-tariff.js +0 -1
- package/src/elvia/elvia-api.js +6 -0
- package/src/elvia/elvia-tariff.html +1 -1
- package/src/general-add-tariff.html +14 -8
- package/src/general-add-tariff.js +0 -1
- package/src/handle-input.js +94 -106
- package/src/handle-output.js +109 -0
- package/src/receive-price-functions.js +3 -3
- package/src/schedule-merger-functions.js +98 -0
- package/src/schedule-merger.html +135 -0
- package/src/schedule-merger.js +108 -0
- package/src/strategy-best-save.html +38 -1
- package/src/strategy-best-save.js +17 -63
- package/src/strategy-fixed-schedule.html +339 -0
- package/src/strategy-fixed-schedule.js +84 -0
- package/src/strategy-functions.js +35 -0
- package/src/strategy-lowest-price.html +76 -38
- package/src/strategy-lowest-price.js +16 -35
- package/src/utils.js +75 -2
- package/test/commands-input-best-save.test.js +142 -0
- package/test/commands-input-lowest-price.test.js +149 -0
- package/test/commands-input-schedule-merger.test.js +128 -0
- package/test/data/best-save-overlap-result.json +5 -1
- package/test/data/best-save-result.json +4 -0
- package/test/data/commands-result-best-save.json +383 -0
- package/test/data/commands-result-lowest-price.json +340 -0
- package/test/data/fixed-schedule-result.json +353 -0
- package/test/data/lowest-price-result-cont-max-fail.json +5 -1
- package/test/data/lowest-price-result-cont-max.json +3 -1
- package/test/data/lowest-price-result-cont.json +8 -1
- package/test/data/lowest-price-result-missing-end.json +8 -3
- package/test/data/lowest-price-result-neg-cont.json +27 -0
- package/test/data/lowest-price-result-neg-split.json +23 -0
- package/test/data/lowest-price-result-split-allday.json +3 -1
- package/test/data/lowest-price-result-split-allday10.json +1 -0
- package/test/data/lowest-price-result-split-max.json +3 -1
- package/test/data/lowest-price-result-split.json +3 -1
- package/test/data/merge-schedule-data.js +238 -0
- package/test/data/negative-prices.json +197 -0
- package/test/data/nordpool-event-prices.json +96 -480
- package/test/data/nordpool-zero-prices.json +90 -0
- package/test/data/reconfigResult.js +1 -0
- package/test/data/result.js +1 -0
- package/test/data/tibber-result-end-0-24h.json +12 -2
- package/test/data/tibber-result-end-0.json +12 -2
- package/test/data/tibber-result.json +1 -0
- package/test/receive-price.test.js +22 -0
- package/test/schedule-merger-functions.test.js +101 -0
- package/test/schedule-merger-test-utils.js +27 -0
- package/test/schedule-merger.test.js +130 -0
- package/test/send-config-input.test.js +45 -2
- package/test/strategy-best-save-test-utils.js +1 -1
- package/test/strategy-best-save.test.js +45 -0
- package/test/strategy-fixed-schedule.test.js +117 -0
- package/test/strategy-heat-capacitor.test.js +1 -1
- package/test/strategy-lowest-price-functions.test.js +1 -1
- package/test/strategy-lowest-price-test-utils.js +31 -0
- package/test/strategy-lowest-price.test.js +55 -45
- package/test/test-utils.js +43 -36
- package/test/utils.test.js +13 -0
- package/docs/images/node-power-saver.png +0 -0
- package/docs/nodes/power-saver.md +0 -23
- package/src/power-saver.html +0 -116
- package/src/power-saver.js +0 -260
- package/test/commands-input.test.js +0 -47
- package/test/power-saver.test.js +0 -189
|
@@ -1,5 +1,81 @@
|
|
|
1
1
|
# Capacity part of grid tariff
|
|
2
2
|
|
|
3
|
+
::: danger Bug-fix 12. September 2022
|
|
4
|
+
|
|
5
|
+
::: details A bug was found 12. sep 2022. Here is how to fix:
|
|
6
|
+
|
|
7
|
+
### 1. Node "Find highest per day":
|
|
8
|
+
|
|
9
|
+
Replace this line:
|
|
10
|
+
|
|
11
|
+
```js
|
|
12
|
+
const highestToday = days.get(new Date().getDate());
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
With this code:
|
|
16
|
+
|
|
17
|
+
```js
|
|
18
|
+
const highestToday = days.get(new Date().getDate()) ?? {
|
|
19
|
+
consumption: 0,
|
|
20
|
+
from: null,
|
|
21
|
+
};
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
This will set the `highestToday` to 0 during the first hour.
|
|
25
|
+
|
|
26
|
+
### 2. Node "Calculate values":
|
|
27
|
+
|
|
28
|
+
Above this line:
|
|
29
|
+
|
|
30
|
+
```js
|
|
31
|
+
function calculateLevel(hourEstimate, ...
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
Insert this code:
|
|
35
|
+
|
|
36
|
+
```js
|
|
37
|
+
function isNull(value) {
|
|
38
|
+
return value === null || value === undefined;
|
|
39
|
+
}
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
Further down the code you can find these 3 lines with 4 lines between:
|
|
43
|
+
|
|
44
|
+
```js
|
|
45
|
+
if (!highestPerDay) {
|
|
46
|
+
if (!highestToday) {
|
|
47
|
+
if (!hourEstimate) {
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
Change these to:
|
|
51
|
+
|
|
52
|
+
```js
|
|
53
|
+
if (isNull(highestPerDay)) {
|
|
54
|
+
if (isNull(highestToday)) {
|
|
55
|
+
if (isNull(hourEstimate)) {
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
Then these will not fail the first hour.
|
|
59
|
+
|
|
60
|
+
### 3. Node "Build query for consumption":
|
|
61
|
+
|
|
62
|
+
Find this line:
|
|
63
|
+
|
|
64
|
+
```js
|
|
65
|
+
const hour = time.getMinutes(); // NB Change to getMinutes()
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
Change it to:
|
|
69
|
+
|
|
70
|
+
```js
|
|
71
|
+
const hour = time.getHours();
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
The bug fixed on no. 3 does so data for hours are read every minute,
|
|
75
|
+
instead of every hour. This is not necessary.
|
|
76
|
+
However, it does not lead to any error.
|
|
77
|
+
:::
|
|
78
|
+
|
|
3
79
|
## Introduction
|
|
4
80
|
|
|
5
81
|
I Norway, there has been introduced a monthly fee for grid capacity.
|
|
@@ -277,6 +353,69 @@ This is a function node that is used to build a Tibber query. It runs for all th
|
|
|
277
353
|
This node needs the tibber home id, so you must find it in the [Tibber Developer Pages](https://developer.tibber.com/) and set the vale of `TIBBER_HOME_ID` in the beginning of the code.
|
|
278
354
|
:::
|
|
279
355
|
|
|
356
|
+
::: details Code
|
|
357
|
+
|
|
358
|
+
<CodeGroup>
|
|
359
|
+
<CodeGroupItem title="On Start">
|
|
360
|
+
|
|
361
|
+
```js
|
|
362
|
+
context.set("previousHour", undefined);
|
|
363
|
+
```
|
|
364
|
+
|
|
365
|
+
</CodeGroupItem>
|
|
366
|
+
|
|
367
|
+
<CodeGroupItem title="On Message" active>
|
|
368
|
+
|
|
369
|
+
```js
|
|
370
|
+
/*
|
|
371
|
+
Calculate number of hours to receive consumption for,
|
|
372
|
+
that is number of hours in the month until now.
|
|
373
|
+
Constructs a tibber query to get consumption per hour.
|
|
374
|
+
*/
|
|
375
|
+
|
|
376
|
+
const TIBBER_HOME_ID = "put your tibber ome id here";
|
|
377
|
+
|
|
378
|
+
const timestamp = msg.payload.timestamp;
|
|
379
|
+
|
|
380
|
+
// Stop if hour has not changed
|
|
381
|
+
const time = new Date(timestamp);
|
|
382
|
+
const hour = time.getHours();
|
|
383
|
+
const previousHour = context.get("previousHour");
|
|
384
|
+
if (previousHour !== undefined && hour === previousHour) {
|
|
385
|
+
return;
|
|
386
|
+
}
|
|
387
|
+
context.set("previousHour", hour);
|
|
388
|
+
|
|
389
|
+
// Calculate number of hours to query
|
|
390
|
+
const date = time.getDate() - 1;
|
|
391
|
+
const hour2 = time.getHours();
|
|
392
|
+
const count = date * 24 + hour2;
|
|
393
|
+
|
|
394
|
+
// Build query
|
|
395
|
+
const query = `
|
|
396
|
+
{
|
|
397
|
+
viewer {
|
|
398
|
+
home (id: "${TIBBER_HOME_ID}") {
|
|
399
|
+
consumption(resolution: HOURLY, last: ${count}) {
|
|
400
|
+
nodes {
|
|
401
|
+
from
|
|
402
|
+
consumption
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
`;
|
|
409
|
+
|
|
410
|
+
msg.payload = query;
|
|
411
|
+
return msg;
|
|
412
|
+
```
|
|
413
|
+
|
|
414
|
+
</CodeGroupItem>
|
|
415
|
+
</CodeGroup>
|
|
416
|
+
|
|
417
|
+
:::
|
|
418
|
+
|
|
280
419
|
### Get consumption
|
|
281
420
|
|
|
282
421
|
This is a `tibber-query` node used to get consumption per hour for passed hours. It takes a Tibber query as input, and sends the result as output. The query is built by the previous node.
|
|
@@ -346,6 +485,80 @@ As outputs it sends the following:
|
|
|
346
485
|
| `hourEstimate` | The estimated consumption for the total hour. |
|
|
347
486
|
| `currentHour` | The time of the current hour. |
|
|
348
487
|
|
|
488
|
+
::: details Code
|
|
489
|
+
|
|
490
|
+
<CodeGroup>
|
|
491
|
+
<CodeGroupItem title="On Start">
|
|
492
|
+
|
|
493
|
+
```js
|
|
494
|
+
context.set("buffer", []);
|
|
495
|
+
```
|
|
496
|
+
|
|
497
|
+
</CodeGroupItem>
|
|
498
|
+
|
|
499
|
+
<CodeGroupItem title="On Message" active>
|
|
500
|
+
|
|
501
|
+
```js
|
|
502
|
+
// Number of minutes used to calculate assumed consumption:
|
|
503
|
+
const ESTIMATION_TIME_MINUTES = 1;
|
|
504
|
+
|
|
505
|
+
const buffer = context.get("buffer") || [];
|
|
506
|
+
|
|
507
|
+
// Add new record to buffer
|
|
508
|
+
const time = new Date(msg.payload.timestamp);
|
|
509
|
+
const timeMs = time.getTime();
|
|
510
|
+
const accumulatedConsumption = msg.payload.accumulatedConsumption;
|
|
511
|
+
const accumulatedConsumptionLastHour = msg.payload.accumulatedConsumptionLastHour;
|
|
512
|
+
buffer.push({ timeMs, accumulatedConsumption });
|
|
513
|
+
|
|
514
|
+
const currentHour = new Date(msg.payload.timestamp);
|
|
515
|
+
currentHour.setMinutes(0);
|
|
516
|
+
currentHour.setSeconds(0);
|
|
517
|
+
|
|
518
|
+
// Remove too old records from buffer
|
|
519
|
+
const maxAgeMs = ESTIMATION_TIME_MINUTES * 60 * 1000;
|
|
520
|
+
let oldest = buffer[0];
|
|
521
|
+
while (timeMs - oldest.timeMs > maxAgeMs) {
|
|
522
|
+
buffer.splice(0, 1);
|
|
523
|
+
oldest = buffer[0];
|
|
524
|
+
}
|
|
525
|
+
context.set("buffer", buffer);
|
|
526
|
+
|
|
527
|
+
// Calculate buffer
|
|
528
|
+
const periodMs = buffer[buffer.length - 1].timeMs - buffer[0].timeMs;
|
|
529
|
+
const consumptionInPeriod = buffer[buffer.length - 1].accumulatedConsumption - buffer[0].accumulatedConsumption;
|
|
530
|
+
if (periodMs === 0) {
|
|
531
|
+
return; // First item in buffer
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
// Estimate remaining of current hour
|
|
535
|
+
const timeLeftMs = 60 * 60 * 1000 - (time.getMinutes() * 60000 + time.getSeconds() * 1000 + time.getMilliseconds());
|
|
536
|
+
const consumptionLeft = (consumptionInPeriod / periodMs) * timeLeftMs;
|
|
537
|
+
const averageConsumptionNow = (consumptionInPeriod / periodMs) * 60 * 60 * 1000;
|
|
538
|
+
|
|
539
|
+
// Estimate total hour
|
|
540
|
+
const hourEstimate = accumulatedConsumptionLastHour + consumptionLeft + 0; // Change for testing
|
|
541
|
+
|
|
542
|
+
msg.payload = {
|
|
543
|
+
accumulatedConsumption,
|
|
544
|
+
accumulatedConsumptionLastHour,
|
|
545
|
+
periodMs,
|
|
546
|
+
consumptionInPeriod,
|
|
547
|
+
averageConsumptionNow,
|
|
548
|
+
timeLeftMs,
|
|
549
|
+
consumptionLeft,
|
|
550
|
+
hourEstimate,
|
|
551
|
+
currentHour,
|
|
552
|
+
};
|
|
553
|
+
|
|
554
|
+
return msg;
|
|
555
|
+
```
|
|
556
|
+
|
|
557
|
+
</CodeGroupItem>
|
|
558
|
+
</CodeGroup>
|
|
559
|
+
|
|
560
|
+
:::
|
|
561
|
+
|
|
349
562
|
### Find highest per day
|
|
350
563
|
|
|
351
564
|
Based on the result from the tibber query, gives the following output:
|
|
@@ -359,6 +572,42 @@ Based on the result from the tibber query, gives the following output:
|
|
|
359
572
|
|
|
360
573
|
Output is sent when the query is run, that is on startup and when the hour changes.
|
|
361
574
|
|
|
575
|
+
::: details Code
|
|
576
|
+
|
|
577
|
+
<CodeGroup>
|
|
578
|
+
<CodeGroupItem title="On Message" active>
|
|
579
|
+
|
|
580
|
+
```js
|
|
581
|
+
const MAX_COUNTING = 3;
|
|
582
|
+
const hours = msg.payload.viewer.home.consumption.nodes;
|
|
583
|
+
const days = new Map();
|
|
584
|
+
hours.forEach((h) => {
|
|
585
|
+
const date = new Date(h.from).getDate();
|
|
586
|
+
if (!days.has(date) || h.consumption > days.get(date).consumption) {
|
|
587
|
+
days.set(date, { from: h.from, consumption: h.consumption });
|
|
588
|
+
}
|
|
589
|
+
});
|
|
590
|
+
const highestToday = days.get(new Date().getDate()) ?? 0;
|
|
591
|
+
const highestPerDay = [...days.values()].sort((a, b) => b.consumption - a.consumption);
|
|
592
|
+
const highestCounting = highestPerDay.slice(0, MAX_COUNTING);
|
|
593
|
+
const currentMonthlyMaxAverage =
|
|
594
|
+
highestCounting.length === 0
|
|
595
|
+
? 0
|
|
596
|
+
: highestCounting.reduce((prev, val) => prev + val.consumption, 0) / highestCounting.length;
|
|
597
|
+
msg.payload = {
|
|
598
|
+
highestPerDay,
|
|
599
|
+
highestCounting,
|
|
600
|
+
highestToday,
|
|
601
|
+
currentMonthlyMaxAverage,
|
|
602
|
+
};
|
|
603
|
+
return msg;
|
|
604
|
+
```
|
|
605
|
+
|
|
606
|
+
</CodeGroupItem>
|
|
607
|
+
</CodeGroup>
|
|
608
|
+
|
|
609
|
+
:::
|
|
610
|
+
|
|
362
611
|
### Calculate values
|
|
363
612
|
|
|
364
613
|
This is where calculation is done to produce all the output sensor values.
|
|
@@ -366,6 +615,7 @@ This is where calculation is done to produce all the output sensor values.
|
|
|
366
615
|
In the beginning of the script there are some constants you can configure:
|
|
367
616
|
|
|
368
617
|
```js
|
|
618
|
+
const HA_NAME = "homeAssistant"; // Your HA name
|
|
369
619
|
const STEPS = [2, 5, 10, 15, 20]; // Grid tariff steps in kWh
|
|
370
620
|
const MAX_COUNTING = 3; // Number of days to calculate month average of
|
|
371
621
|
const BUFFER = 0.5; // kWh - Closer to limit increases alarm level
|
|
@@ -373,8 +623,198 @@ const SAFE_SONE = 2; // kWh - Further from limit reduces level
|
|
|
373
623
|
const ALARM = 8; // Min level that causes status to be alarm
|
|
374
624
|
```
|
|
375
625
|
|
|
626
|
+
The `HA_NAME` must be set to the name you have given your Home Assistant. One place to find this is in Node-RED,
|
|
627
|
+
in the `Context Data` window (next to the `Debug` window), under `Global`, click the refresh button and see the `homeassistant` object.
|
|
628
|
+
Find the name used to the right.
|
|
629
|
+
In this example the value you are looking for is `homeAssistant`:
|
|
630
|
+
|
|
631
|
+

|
|
632
|
+
|
|
633
|
+
You must configure the `STEPS` array to contain steps relevant for you.
|
|
634
|
+
You should omit steps you do not plan to go under, to avoid non-necessary actions and warnings.
|
|
635
|
+
|
|
376
636
|
See [Calculated sensor values](#calculated-sensor-values) for description of the output.
|
|
377
637
|
|
|
638
|
+
::: details Code
|
|
639
|
+
|
|
640
|
+
<CodeGroup>
|
|
641
|
+
<CodeGroupItem title="On Message" active>
|
|
642
|
+
|
|
643
|
+
```js
|
|
644
|
+
const HA_NAME = "homeAssistant"; // Your HA name
|
|
645
|
+
const STEPS = [2, 5, 10, 15, 20];
|
|
646
|
+
const MAX_COUNTING = 3; // Number of days to calculate month
|
|
647
|
+
const BUFFER = 0.5; // Closer to limit increases level
|
|
648
|
+
const SAFE_ZONE = 2; // Further from limit reduces level
|
|
649
|
+
const ALARM = 8; // Min level that causes status to be alarm
|
|
650
|
+
|
|
651
|
+
const ha = global.get("homeassistant")[HA_NAME];
|
|
652
|
+
if (!ha.isConnected) {
|
|
653
|
+
return;
|
|
654
|
+
}
|
|
655
|
+
|
|
656
|
+
function isNull(value) {
|
|
657
|
+
return value === null || value === undefined;
|
|
658
|
+
}
|
|
659
|
+
|
|
660
|
+
function calculateLevel(hourEstimate, currentHourRanking, highestCountingAverageWithCurrent, nextStep) {
|
|
661
|
+
if (currentHourRanking === 0) {
|
|
662
|
+
return 0;
|
|
663
|
+
}
|
|
664
|
+
if (highestCountingAverageWithCurrent > nextStep) {
|
|
665
|
+
return 9;
|
|
666
|
+
}
|
|
667
|
+
if (highestCountingAverageWithCurrent > nextStep - BUFFER) {
|
|
668
|
+
return 8;
|
|
669
|
+
}
|
|
670
|
+
if (hourEstimate > nextStep) {
|
|
671
|
+
return 7;
|
|
672
|
+
}
|
|
673
|
+
if (hourEstimate > nextStep - BUFFER) {
|
|
674
|
+
return 6;
|
|
675
|
+
}
|
|
676
|
+
if (currentHourRanking === 1 && nextStep - hourEstimate < SAFE_ZONE) {
|
|
677
|
+
return 5;
|
|
678
|
+
}
|
|
679
|
+
if (currentHourRanking === 2 && nextStep - hourEstimate < SAFE_ZONE) {
|
|
680
|
+
return 4;
|
|
681
|
+
}
|
|
682
|
+
if (currentHourRanking === 3 && nextStep - hourEstimate < SAFE_ZONE) {
|
|
683
|
+
return 3;
|
|
684
|
+
}
|
|
685
|
+
if (currentHourRanking === 1) {
|
|
686
|
+
return 2;
|
|
687
|
+
}
|
|
688
|
+
if (currentHourRanking === 2) {
|
|
689
|
+
return 1;
|
|
690
|
+
}
|
|
691
|
+
return 0;
|
|
692
|
+
}
|
|
693
|
+
|
|
694
|
+
if (msg.payload.highestPerDay) {
|
|
695
|
+
context.set("highestPerDay", msg.payload.highestPerDay);
|
|
696
|
+
context.set("highestCounting", msg.payload.highestCounting);
|
|
697
|
+
context.set("highestToday", msg.payload.highestToday);
|
|
698
|
+
context.set("currentMonthlyMaxAverage", msg.payload.currentMonthlyMaxAverage);
|
|
699
|
+
node.status({ fill: "green", shape: "ring", text: "Got ranking" });
|
|
700
|
+
return;
|
|
701
|
+
}
|
|
702
|
+
|
|
703
|
+
const highestPerDay = context.get("highestPerDay");
|
|
704
|
+
const highestCounting = context.get("highestCounting");
|
|
705
|
+
const highestToday = context.get("highestToday");
|
|
706
|
+
const currentMonthlyMaxAverage = context.get("currentMonthlyMaxAverage");
|
|
707
|
+
const hourEstimate = msg.payload.hourEstimate;
|
|
708
|
+
const timeLeftMs = msg.payload.timeLeftMs;
|
|
709
|
+
const timeLeftSec = timeLeftMs / 1000;
|
|
710
|
+
const periodMs = msg.payload.periodMs;
|
|
711
|
+
const accumulatedConsumption = msg.payload.accumulatedConsumption;
|
|
712
|
+
const accumulatedConsumptionLastHour = msg.payload.accumulatedConsumptionLastHour;
|
|
713
|
+
const consumptionLeft = msg.payload.consumptionLeft;
|
|
714
|
+
const averageConsumptionNow = msg.payload.averageConsumptionNow;
|
|
715
|
+
const currentHour = msg.payload.currentHour;
|
|
716
|
+
|
|
717
|
+
if (timeLeftSec === 0) {
|
|
718
|
+
return null;
|
|
719
|
+
}
|
|
720
|
+
|
|
721
|
+
if (isNull(highestPerDay)) {
|
|
722
|
+
node.status({ fill: "red", shape: "dot", text: "No highest per day" });
|
|
723
|
+
return;
|
|
724
|
+
}
|
|
725
|
+
if (isNull(highestToday)) {
|
|
726
|
+
node.status({ fill: "red", shape: "dot", text: "No highest today" });
|
|
727
|
+
return;
|
|
728
|
+
}
|
|
729
|
+
if (isNull(hourEstimate)) {
|
|
730
|
+
node.status({ fill: "red", shape: "dot", text: "No estimate" });
|
|
731
|
+
return;
|
|
732
|
+
}
|
|
733
|
+
|
|
734
|
+
const currentStep = STEPS.reduceRight(
|
|
735
|
+
(prev, val) => (val > currentMonthlyMaxAverage ? val : prev),
|
|
736
|
+
STEPS[STEPS.length - 1]
|
|
737
|
+
);
|
|
738
|
+
|
|
739
|
+
// Set currentHourRanking
|
|
740
|
+
let currentHourRanking = MAX_COUNTING + 1;
|
|
741
|
+
for (let i = highestCounting.length - 1; i >= 0; i--) {
|
|
742
|
+
if (hourEstimate > highestCounting[i].consumption) {
|
|
743
|
+
currentHourRanking = i + 1;
|
|
744
|
+
}
|
|
745
|
+
}
|
|
746
|
+
if (hourEstimate < highestToday.consumption) {
|
|
747
|
+
currentHourRanking = 0;
|
|
748
|
+
}
|
|
749
|
+
|
|
750
|
+
const current = { from: currentHour, consumption: hourEstimate };
|
|
751
|
+
const highestCountingWithCurrent = [...highestCounting, current]
|
|
752
|
+
.sort((a, b) => b.consumption - a.consumption)
|
|
753
|
+
.slice(0, highestCounting.length);
|
|
754
|
+
const currentMonthlyEstimate =
|
|
755
|
+
highestCountingWithCurrent.length === 0
|
|
756
|
+
? 0
|
|
757
|
+
: highestCountingWithCurrent.reduce((prev, val) => prev + val.consumption, 0) / highestCountingWithCurrent.length;
|
|
758
|
+
|
|
759
|
+
// Set alarm level
|
|
760
|
+
const alarmLevel = calculateLevel(hourEstimate, currentHourRanking, currentMonthlyEstimate, currentStep);
|
|
761
|
+
|
|
762
|
+
// Evaluate status
|
|
763
|
+
const status = alarmLevel >= ALARM ? "Alarm" : alarmLevel > 0 ? "Warning" : "Ok";
|
|
764
|
+
|
|
765
|
+
// Calculate reduction
|
|
766
|
+
const reductionRequired =
|
|
767
|
+
alarmLevel < ALARM
|
|
768
|
+
? 0
|
|
769
|
+
: (Math.max((currentMonthlyEstimate - currentStep) * highestCounting.length, 0) * 3600) / timeLeftSec;
|
|
770
|
+
const reductionRecommended =
|
|
771
|
+
alarmLevel < 3 ? 0 : (Math.max(hourEstimate + SAFE_ZONE - currentStep, 0) * 3600) / timeLeftSec;
|
|
772
|
+
|
|
773
|
+
// Calculate increase possible
|
|
774
|
+
const increasePossible =
|
|
775
|
+
alarmLevel >= 3 ? 0 : (Math.max(currentStep - hourEstimate - SAFE_ZONE, 0) * 3600) / timeLeftSec;
|
|
776
|
+
|
|
777
|
+
// Create output
|
|
778
|
+
const fill = status === "Ok" ? "green" : status === "Alarm" ? "red" : "yellow";
|
|
779
|
+
node.status({ fill, shape: "dot", text: "Working" });
|
|
780
|
+
|
|
781
|
+
const RESOLUTION = 1000;
|
|
782
|
+
|
|
783
|
+
const payload = {
|
|
784
|
+
status, // Ok, Warning, Alarm
|
|
785
|
+
statusOk: status === "Ok",
|
|
786
|
+
statusWarning: status === "Warning",
|
|
787
|
+
statusAlarm: status === "Alarm",
|
|
788
|
+
alarmLevel,
|
|
789
|
+
highestPerDay,
|
|
790
|
+
highestCounting,
|
|
791
|
+
highestCountingWithCurrent,
|
|
792
|
+
highestToday,
|
|
793
|
+
currentMonthlyEstimate: Math.round(currentMonthlyEstimate * RESOLUTION) / RESOLUTION,
|
|
794
|
+
accumulatedConsumptionLastHour: Math.round(accumulatedConsumptionLastHour * RESOLUTION) / RESOLUTION,
|
|
795
|
+
consumptionLeft: Math.round(consumptionLeft * RESOLUTION) / RESOLUTION,
|
|
796
|
+
hourEstimate: Math.round(hourEstimate * RESOLUTION) / RESOLUTION,
|
|
797
|
+
averageConsumptionNow: Math.round(averageConsumptionNow * RESOLUTION) / RESOLUTION,
|
|
798
|
+
reductionRequired: Math.round(reductionRequired * RESOLUTION) / RESOLUTION,
|
|
799
|
+
reductionRecommended: Math.round(reductionRecommended * RESOLUTION) / RESOLUTION,
|
|
800
|
+
increasePossible: Math.round(increasePossible * RESOLUTION) / RESOLUTION,
|
|
801
|
+
currentStep,
|
|
802
|
+
currentHourRanking,
|
|
803
|
+
timeLeftSec,
|
|
804
|
+
periodMs,
|
|
805
|
+
accumulatedConsumption,
|
|
806
|
+
};
|
|
807
|
+
|
|
808
|
+
msg.payload = payload;
|
|
809
|
+
|
|
810
|
+
return msg;
|
|
811
|
+
```
|
|
812
|
+
|
|
813
|
+
</CodeGroupItem>
|
|
814
|
+
</CodeGroup>
|
|
815
|
+
|
|
816
|
+
:::
|
|
817
|
+
|
|
378
818
|
### Reduction Actions
|
|
379
819
|
|
|
380
820
|
This is where you set up actions to be taken in case reduction is required or recommended.
|
|
@@ -405,16 +845,16 @@ turn off. If this is not possible, the code must be changed in order to work.
|
|
|
405
845
|
|
|
406
846
|
Each item in the `actions` array contains the following data:
|
|
407
847
|
|
|
408
|
-
| Variable name | Description
|
|
409
|
-
| --------------------- |
|
|
410
|
-
|
|
|
411
|
-
| name | The name of the actions. Can be any thing.
|
|
412
|
-
| id | A unique id of the action.
|
|
413
|
-
| minAlarmLevel | The minimum alarm level that must be present to take this action.
|
|
414
|
-
| reduceWhenRecommended | If `true` the action will be taken when `Reduction Recommended` > 0. If `false` the action will be taken only when `Reduction Required` > 0
|
|
415
|
-
| minTimeOffSec | The action will not be reset until minimum this number of seconds has passed since the action was taken.
|
|
416
|
-
| payloadToTakeAction | The payload that shall be sent to the `call service` node to take the action (for example turn off a switch).
|
|
417
|
-
| payloadToResetAction | The payload that shall be sent to the `call service` node to reset the action (for example turn a switch back on again).
|
|
848
|
+
| Variable name | Description |
|
|
849
|
+
| --------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
850
|
+
| consumption | The consumption that will be reduced by taking the action, given as either a) (Recommended) The entity_id of a sensor that gives the consumption, or b) A number with the consumption in kWh, or c) a function returning the consumption. |
|
|
851
|
+
| name | The name of the actions. Can be any thing. |
|
|
852
|
+
| id | A unique id of the action. |
|
|
853
|
+
| minAlarmLevel | The minimum alarm level that must be present to take this action. |
|
|
854
|
+
| reduceWhenRecommended | If `true` the action will be taken when `Reduction Recommended` > 0. If `false` the action will be taken only when `Reduction Required` > 0 |
|
|
855
|
+
| minTimeOffSec | The action will not be reset until minimum this number of seconds has passed since the action was taken. |
|
|
856
|
+
| payloadToTakeAction | The payload that shall be sent to the `call service` node to take the action (for example turn off a switch). |
|
|
857
|
+
| payloadToResetAction | The payload that shall be sent to the `call service` node to reset the action (for example turn a switch back on again). |
|
|
418
858
|
|
|
419
859
|
::: tip Actions order
|
|
420
860
|
Actions to reduce consumption are taken in the order they appear in the `actions` array until enough reduction has been done,
|
|
@@ -423,14 +863,31 @@ so put first the actions you want to take first, and last those actions that you
|
|
|
423
863
|
|
|
424
864
|
Here is an example of an `actions` array with two items (a water heater and a heating cable):
|
|
425
865
|
|
|
866
|
+
::: danger On Start code
|
|
867
|
+
Please note that there is a small piece of code after the `actions` array
|
|
868
|
+
in the `On Start` tab. Make sure you do not delete that code.
|
|
869
|
+
:::
|
|
870
|
+
|
|
871
|
+
::: tip Sensors without actions
|
|
872
|
+
If you don't want the actions, or you want to control actions another way,
|
|
873
|
+
you can omit the action-related nodes and only use the nodes creating the sensors.
|
|
874
|
+
:::
|
|
875
|
+
|
|
876
|
+
::: details Code
|
|
877
|
+
|
|
878
|
+
<CodeGroup>
|
|
879
|
+
<CodeGroupItem title="On Start">
|
|
880
|
+
|
|
426
881
|
```js
|
|
882
|
+
// You MUST edit the actions array with your own actions.
|
|
883
|
+
|
|
427
884
|
const actions = [
|
|
428
885
|
{
|
|
429
|
-
|
|
886
|
+
consumption: "sensor.varmtvannsbereder_electric_consumption_w",
|
|
430
887
|
name: "Varmtvannsbereder",
|
|
431
888
|
id: "vvb",
|
|
432
889
|
minAlarmLevel: 3,
|
|
433
|
-
reduceWhenRecommended:
|
|
890
|
+
reduceWhenRecommended: true,
|
|
434
891
|
minTimeOffSec: 300,
|
|
435
892
|
payloadToTakeAction: {
|
|
436
893
|
domain: "switch",
|
|
@@ -448,7 +905,7 @@ const actions = [
|
|
|
448
905
|
},
|
|
449
906
|
},
|
|
450
907
|
{
|
|
451
|
-
|
|
908
|
+
consumption: "sensor.varme_gulv_bad_electric_consumption_w_2",
|
|
452
909
|
name: "Varme gulv bad 1. etg.",
|
|
453
910
|
id: "gulvbad",
|
|
454
911
|
minAlarmLevel: 3,
|
|
@@ -458,28 +915,160 @@ const actions = [
|
|
|
458
915
|
domain: "climate",
|
|
459
916
|
service: "turn_off",
|
|
460
917
|
target: {
|
|
461
|
-
entity_id: ["climate.
|
|
918
|
+
entity_id: ["climate.varme_gulv_bad_2"],
|
|
919
|
+
},
|
|
920
|
+
},
|
|
921
|
+
payloadToResetAction: {
|
|
922
|
+
domain: "climate",
|
|
923
|
+
service: "turn_on",
|
|
924
|
+
target: {
|
|
925
|
+
entity_id: ["climate.varme_gulv_bad_2"],
|
|
926
|
+
},
|
|
927
|
+
},
|
|
928
|
+
},
|
|
929
|
+
{
|
|
930
|
+
consumption: "sensor.varme_gulv_gang_electric_consumption_w",
|
|
931
|
+
name: "Varme gulv gang 1. etg.",
|
|
932
|
+
id: "gulvgang",
|
|
933
|
+
minAlarmLevel: 3,
|
|
934
|
+
reduceWhenRecommended: true,
|
|
935
|
+
minTimeOffSec: 300,
|
|
936
|
+
payloadToTakeAction: {
|
|
937
|
+
domain: "climate",
|
|
938
|
+
service: "turn_off",
|
|
939
|
+
target: {
|
|
940
|
+
entity_id: ["climate.varme_gulv_gang"],
|
|
462
941
|
},
|
|
463
942
|
},
|
|
464
943
|
payloadToResetAction: {
|
|
465
944
|
domain: "climate",
|
|
466
945
|
service: "turn_on",
|
|
467
946
|
target: {
|
|
468
|
-
entity_id: ["climate.
|
|
947
|
+
entity_id: ["climate.varme_gulv_gang"],
|
|
948
|
+
},
|
|
949
|
+
},
|
|
950
|
+
},
|
|
951
|
+
{
|
|
952
|
+
consumption: "sensor.varme_gulv_kjellerstue_electric_consumption_w",
|
|
953
|
+
name: "Varme gulv kjellerstue",
|
|
954
|
+
id: "gulvkjeller",
|
|
955
|
+
minAlarmLevel: 3,
|
|
956
|
+
reduceWhenRecommended: true,
|
|
957
|
+
minTimeOffSec: 300,
|
|
958
|
+
payloadToTakeAction: {
|
|
959
|
+
domain: "climate",
|
|
960
|
+
service: "turn_off",
|
|
961
|
+
target: {
|
|
962
|
+
entity_id: ["climate.varme_gulv_kjellerstue"],
|
|
963
|
+
},
|
|
964
|
+
},
|
|
965
|
+
payloadToResetAction: {
|
|
966
|
+
domain: "climate",
|
|
967
|
+
service: "turn_on",
|
|
968
|
+
target: {
|
|
969
|
+
entity_id: ["climate.varme_gulv_kjellerstue"],
|
|
970
|
+
},
|
|
971
|
+
},
|
|
972
|
+
},
|
|
973
|
+
{
|
|
974
|
+
consumption: 0.1,
|
|
975
|
+
name: "Test",
|
|
976
|
+
id: "test",
|
|
977
|
+
minAlarmLevel: 3,
|
|
978
|
+
reduceWhenRecommended: true,
|
|
979
|
+
minTimeOffSec: 30,
|
|
980
|
+
payloadToTakeAction: {
|
|
981
|
+
domain: "switch",
|
|
982
|
+
service: "turn_off",
|
|
983
|
+
target: {
|
|
984
|
+
entity_id: ["switch.lys_kjokkenskap_switch"],
|
|
985
|
+
},
|
|
986
|
+
},
|
|
987
|
+
payloadToResetAction: {
|
|
988
|
+
domain: "switch",
|
|
989
|
+
service: "turn_on",
|
|
990
|
+
target: {
|
|
991
|
+
entity_id: ["switch.lys_kjokkenskap_switch"],
|
|
469
992
|
},
|
|
470
993
|
},
|
|
471
994
|
},
|
|
472
995
|
];
|
|
996
|
+
// End of actions array
|
|
997
|
+
|
|
998
|
+
// DO NOT DELETE THE CODE BELOW
|
|
999
|
+
|
|
1000
|
+
// Set default values for all actions
|
|
1001
|
+
actions.forEach((a) => {
|
|
1002
|
+
a.actionTaken = false;
|
|
1003
|
+
a.savedConsumption = 0;
|
|
1004
|
+
});
|
|
1005
|
+
|
|
1006
|
+
flow.set("actions", actions);
|
|
473
1007
|
```
|
|
474
1008
|
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
1009
|
+
</CodeGroupItem>
|
|
1010
|
+
|
|
1011
|
+
<CodeGroupItem title="On Message" active>
|
|
1012
|
+
|
|
1013
|
+
```js
|
|
1014
|
+
const MIN_CONSUMPTION_TO_CARE = 0.05; // Do not reduce unless at least 50W
|
|
1015
|
+
|
|
1016
|
+
const actions = flow.get("actions");
|
|
1017
|
+
const ha = global.get("homeassistant").homeAssistant;
|
|
1018
|
+
|
|
1019
|
+
let reductionRequired = msg.payload.reductionRequired;
|
|
1020
|
+
let reductionRecommended = msg.payload.reductionRecommended;
|
|
1021
|
+
|
|
1022
|
+
if (reductionRecommended <= 0) {
|
|
1023
|
+
return null;
|
|
1024
|
+
}
|
|
1025
|
+
|
|
1026
|
+
function takeAction(action, consumption) {
|
|
1027
|
+
const info = {
|
|
1028
|
+
time: new Date().toISOString(),
|
|
1029
|
+
name: "Reduction action",
|
|
1030
|
+
data: msg.payload,
|
|
1031
|
+
action,
|
|
1032
|
+
};
|
|
1033
|
+
node.send([{ payload: action.payloadToTakeAction }, { payload: info }]);
|
|
1034
|
+
reductionRequired = Math.max(0, reductionRequired - consumption);
|
|
1035
|
+
reductionRecommended = Math.max(0, reductionRecommended - consumption);
|
|
1036
|
+
action.actionTaken = true;
|
|
1037
|
+
action.actionTime = Date.now();
|
|
1038
|
+
action.savedConsumption = consumption;
|
|
1039
|
+
flow.set("actions", actions);
|
|
1040
|
+
}
|
|
1041
|
+
|
|
1042
|
+
function getConsumption(consumption) {
|
|
1043
|
+
if (typeof consumption === "string") {
|
|
1044
|
+
const sensor = ha.states[consumption];
|
|
1045
|
+
return sensor.state;
|
|
1046
|
+
} else if (typeof consumption === "number") {
|
|
1047
|
+
return consumption;
|
|
1048
|
+
} else if (typeof consumption === "function") {
|
|
1049
|
+
return consumption();
|
|
1050
|
+
} else {
|
|
1051
|
+
node.warn("Config error: consumption has illegal type: " + typeof consumption);
|
|
1052
|
+
return 0;
|
|
1053
|
+
}
|
|
1054
|
+
}
|
|
1055
|
+
|
|
1056
|
+
actions
|
|
1057
|
+
.filter((a) => msg.payload.alarmLevel >= a.minAlarmLevel && !a.actionTaken)
|
|
1058
|
+
.forEach((a) => {
|
|
1059
|
+
const consumption = getConsumption(a.consumption);
|
|
1060
|
+
if (consumption < MIN_CONSUMPTION_TO_CARE) {
|
|
1061
|
+
return;
|
|
1062
|
+
}
|
|
1063
|
+
if (reductionRequired > 0 || (reductionRecommended > 0 && a.reduceWhenRecommended)) {
|
|
1064
|
+
takeAction(a, consumption);
|
|
1065
|
+
}
|
|
1066
|
+
});
|
|
1067
|
+
```
|
|
1068
|
+
|
|
1069
|
+
</CodeGroupItem>
|
|
1070
|
+
</CodeGroup>
|
|
479
1071
|
|
|
480
|
-
::: tip Sensors without actions
|
|
481
|
-
If you don't want the actions, or you want to control actions another way,
|
|
482
|
-
you can omit the action-related nodes and only use the nodes creating the sensors.
|
|
483
1072
|
:::
|
|
484
1073
|
|
|
485
1074
|
### Reset Actions
|
|
@@ -489,6 +1078,52 @@ This node will reset actions when there is enough capacity available, that is fo
|
|
|
489
1078
|
In the script, there is a `BUFFER_TO_RESET` constant used to set a buffer (in kW) so actions are not reset until there is
|
|
490
1079
|
some spare capacity. By default is it set to 1 kW.
|
|
491
1080
|
|
|
1081
|
+
::: details Code
|
|
1082
|
+
|
|
1083
|
+
<CodeGroup>
|
|
1084
|
+
<CodeGroupItem title="On Message" active>
|
|
1085
|
+
|
|
1086
|
+
```js
|
|
1087
|
+
const actions = flow.get("actions");
|
|
1088
|
+
const ha = global.get("homeassistant").homeAssistant;
|
|
1089
|
+
|
|
1090
|
+
const BUFFER_TO_RESET = 1; // Must have 1kW extra to perform reset
|
|
1091
|
+
|
|
1092
|
+
let increasePossible = msg.payload.increasePossible;
|
|
1093
|
+
|
|
1094
|
+
if (increasePossible <= 0) {
|
|
1095
|
+
return null;
|
|
1096
|
+
}
|
|
1097
|
+
|
|
1098
|
+
function resetAction(action) {
|
|
1099
|
+
const info = {
|
|
1100
|
+
time: new Date().toISOString(),
|
|
1101
|
+
name: "Reset action",
|
|
1102
|
+
data: msg.paylaod,
|
|
1103
|
+
action,
|
|
1104
|
+
};
|
|
1105
|
+
node.send([{ payload: action.payloadToResetAction }, { payload: info }]);
|
|
1106
|
+
increasePossible -= action.savedConsumption;
|
|
1107
|
+
action.actionTaken = false;
|
|
1108
|
+
action.savedConsumption = 0;
|
|
1109
|
+
flow.set("actions", actions);
|
|
1110
|
+
}
|
|
1111
|
+
|
|
1112
|
+
actions
|
|
1113
|
+
.filter(
|
|
1114
|
+
(a) =>
|
|
1115
|
+
a.actionTaken &&
|
|
1116
|
+
a.savedConsumption + BUFFER_TO_RESET <= increasePossible &&
|
|
1117
|
+
Date.now() - a.actionTime > a.minTimeOffSec * 1000
|
|
1118
|
+
)
|
|
1119
|
+
.forEach((a) => resetAction(a));
|
|
1120
|
+
```
|
|
1121
|
+
|
|
1122
|
+
</CodeGroupItem>
|
|
1123
|
+
</CodeGroup>
|
|
1124
|
+
|
|
1125
|
+
:::
|
|
1126
|
+
|
|
492
1127
|
### Perform action
|
|
493
1128
|
|
|
494
1129
|
This is a `call service` node used to perform the actions (both taking actions and resetting actions).
|