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.
Files changed (105) hide show
  1. package/.eslintrc.js +15 -0
  2. package/docs/.vuepress/components/DonateButtons.vue +26 -3
  3. package/docs/.vuepress/components/VippsPlakat.vue +20 -0
  4. package/docs/.vuepress/config.js +17 -10
  5. package/docs/.vuepress/public/ads.txt +1 -0
  6. package/docs/README.md +4 -4
  7. package/docs/changelog/README.md +55 -1
  8. package/docs/contribute/README.md +8 -3
  9. package/docs/examples/example-grid-tariff-capacity-flow.json +23 -7
  10. package/docs/examples/example-grid-tariff-capacity-part.md +657 -22
  11. package/docs/faq/README.md +1 -1
  12. package/docs/faq/best-save-viewer.md +1 -1
  13. package/docs/guide/README.md +20 -5
  14. package/docs/images/best-save-config.png +0 -0
  15. package/docs/images/combine-two-lowest-price.png +0 -0
  16. package/docs/images/fixed-schedule-config.png +0 -0
  17. package/docs/images/global-context-window.png +0 -0
  18. package/docs/images/lowest-price-config.png +0 -0
  19. package/docs/images/node-ps-schedule-merger.png +0 -0
  20. package/docs/images/node-ps-strategy-fixed-schedule.png +0 -0
  21. package/docs/images/ps-strategy-fixed-schedule-example.png +0 -0
  22. package/docs/images/schedule-merger-config.png +0 -0
  23. package/docs/images/schedule-merger-example-1.png +0 -0
  24. package/docs/images/vipps-plakat.png +0 -0
  25. package/docs/images/vipps-qr.png +0 -0
  26. package/docs/images/vipps-smiling-rgb-orange-pos.png +0 -0
  27. package/docs/nodes/README.md +12 -6
  28. package/docs/nodes/dynamic-commands.md +79 -0
  29. package/docs/nodes/dynamic-config.md +76 -0
  30. package/docs/nodes/ps-elvia-add-tariff.md +4 -0
  31. package/docs/nodes/ps-general-add-tariff.md +10 -0
  32. package/docs/nodes/ps-receive-price.md +2 -1
  33. package/docs/nodes/ps-schedule-merger.md +227 -0
  34. package/docs/nodes/ps-strategy-best-save.md +46 -110
  35. package/docs/nodes/ps-strategy-fixed-schedule.md +101 -0
  36. package/docs/nodes/ps-strategy-heat-capacitor.md +6 -1
  37. package/docs/nodes/ps-strategy-lowest-price.md +51 -112
  38. package/package.json +5 -2
  39. package/src/elvia/elvia-add-tariff.html +1 -2
  40. package/src/elvia/elvia-add-tariff.js +0 -1
  41. package/src/elvia/elvia-api.js +6 -0
  42. package/src/elvia/elvia-tariff.html +1 -1
  43. package/src/general-add-tariff.html +14 -8
  44. package/src/general-add-tariff.js +0 -1
  45. package/src/handle-input.js +94 -106
  46. package/src/handle-output.js +109 -0
  47. package/src/receive-price-functions.js +3 -3
  48. package/src/schedule-merger-functions.js +98 -0
  49. package/src/schedule-merger.html +135 -0
  50. package/src/schedule-merger.js +108 -0
  51. package/src/strategy-best-save.html +38 -1
  52. package/src/strategy-best-save.js +17 -63
  53. package/src/strategy-fixed-schedule.html +339 -0
  54. package/src/strategy-fixed-schedule.js +84 -0
  55. package/src/strategy-functions.js +35 -0
  56. package/src/strategy-lowest-price.html +76 -38
  57. package/src/strategy-lowest-price.js +16 -35
  58. package/src/utils.js +75 -2
  59. package/test/commands-input-best-save.test.js +142 -0
  60. package/test/commands-input-lowest-price.test.js +149 -0
  61. package/test/commands-input-schedule-merger.test.js +128 -0
  62. package/test/data/best-save-overlap-result.json +5 -1
  63. package/test/data/best-save-result.json +4 -0
  64. package/test/data/commands-result-best-save.json +383 -0
  65. package/test/data/commands-result-lowest-price.json +340 -0
  66. package/test/data/fixed-schedule-result.json +353 -0
  67. package/test/data/lowest-price-result-cont-max-fail.json +5 -1
  68. package/test/data/lowest-price-result-cont-max.json +3 -1
  69. package/test/data/lowest-price-result-cont.json +8 -1
  70. package/test/data/lowest-price-result-missing-end.json +8 -3
  71. package/test/data/lowest-price-result-neg-cont.json +27 -0
  72. package/test/data/lowest-price-result-neg-split.json +23 -0
  73. package/test/data/lowest-price-result-split-allday.json +3 -1
  74. package/test/data/lowest-price-result-split-allday10.json +1 -0
  75. package/test/data/lowest-price-result-split-max.json +3 -1
  76. package/test/data/lowest-price-result-split.json +3 -1
  77. package/test/data/merge-schedule-data.js +238 -0
  78. package/test/data/negative-prices.json +197 -0
  79. package/test/data/nordpool-event-prices.json +96 -480
  80. package/test/data/nordpool-zero-prices.json +90 -0
  81. package/test/data/reconfigResult.js +1 -0
  82. package/test/data/result.js +1 -0
  83. package/test/data/tibber-result-end-0-24h.json +12 -2
  84. package/test/data/tibber-result-end-0.json +12 -2
  85. package/test/data/tibber-result.json +1 -0
  86. package/test/receive-price.test.js +22 -0
  87. package/test/schedule-merger-functions.test.js +101 -0
  88. package/test/schedule-merger-test-utils.js +27 -0
  89. package/test/schedule-merger.test.js +130 -0
  90. package/test/send-config-input.test.js +45 -2
  91. package/test/strategy-best-save-test-utils.js +1 -1
  92. package/test/strategy-best-save.test.js +45 -0
  93. package/test/strategy-fixed-schedule.test.js +117 -0
  94. package/test/strategy-heat-capacitor.test.js +1 -1
  95. package/test/strategy-lowest-price-functions.test.js +1 -1
  96. package/test/strategy-lowest-price-test-utils.js +31 -0
  97. package/test/strategy-lowest-price.test.js +55 -45
  98. package/test/test-utils.js +43 -36
  99. package/test/utils.test.js +13 -0
  100. package/docs/images/node-power-saver.png +0 -0
  101. package/docs/nodes/power-saver.md +0 -23
  102. package/src/power-saver.html +0 -116
  103. package/src/power-saver.js +0 -260
  104. package/test/commands-input.test.js +0 -47
  105. 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
+ ![Global context window](../images/global-context-window.png)
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
- | sensor | The entity_id of a sensor that gives the consumption that will be reduced by taking the action. |
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
- sensor: "sensor.varmtvannsbereder_electric_consumption_w",
886
+ consumption: "sensor.varmtvannsbereder_electric_consumption_w",
430
887
  name: "Varmtvannsbereder",
431
888
  id: "vvb",
432
889
  minAlarmLevel: 3,
433
- reduceWhenRecommended: false,
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
- sensor: "sensor.varme_gulv_bad_electric_consumption_w",
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.varme_gulv_bad"],
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.varme_gulv_bad"],
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
- ::: danger On Start code
476
- Please note that there is a small piece of code after the `actions` array
477
- in the `On Start` tab. Make sure you do not delete that code.
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).