@energiok/node-red-contrib-pricecontrol-thermal 1.0.0 → 1.2.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/CHANGELOG.md CHANGED
@@ -1 +1,4 @@
1
- [The changelog has moved here](https://powersaver.no/changelog/#change-log)
1
+ # Changelog
2
+
3
+ ## 1.1.0
4
+ - Initial release of pricecontrol-thermal
package/README.md CHANGED
@@ -1,10 +1,5 @@
1
- # node-red-contrib-power-saver
1
+ # node-red-contrib-pricecontrol-thermal
2
2
 
3
3
  A Node-RED node to save money when power prices are changing by the hour.
4
4
 
5
- ![Logo](docs/.vuepress/public/logo.png)
6
-
7
- ## Please read more in the [documentation](https://powersaver.no/).
8
-
9
-
10
- [![Donation](./donation.png)](https://powersaver.no/contribute/#donate)
5
+ Smart thermal control for Node-RED - optimizes heat pump operation based on electricity prices and outdoor temperature.
package/nul ADDED
File without changes
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@energiok/node-red-contrib-pricecontrol-thermal",
3
- "version": "1.0.0",
3
+ "version": "1.2.0",
4
4
  "private": false,
5
5
  "publishConfig": {
6
6
  "access": "restricted"
@@ -523,41 +523,12 @@ function createSchedule(prices, outdoorTemp, config) {
523
523
  }
524
524
  }
525
525
  } else {
526
- // Apply thermal constraints PER DAY - thermal budget resets each day
527
- // because the house recovers during overnight boost periods
528
-
529
- // Group intervals by day (using output timezone for day boundaries)
530
- const dayGroups = new Map(); // Map<dayKey, Array<{index, price}>>
531
- for (let i = 0; i < n; i++) {
532
- if (modes[i] === "coast") {
533
- const utcTime = DateTime.fromISO(validPrices[i].time, { zone: "UTC" });
534
- const localTime = utcTime.setZone(outputTimezone);
535
- const dayKey = localTime.toFormat("yyyy-MM-dd");
536
-
537
- if (!dayGroups.has(dayKey)) {
538
- dayGroups.set(dayKey, []);
539
- }
540
- dayGroups.get(dayKey).push({ index: i, price: priceValues[i] });
541
- }
542
- }
543
-
544
- // For each day, apply thermal budget independently
545
- for (const [dayKey, coastIndices] of dayGroups) {
546
- // Sort by price (highest first) to prioritize most expensive periods
547
- coastIndices.sort((a, b) => b.price - a.price);
548
-
549
- // Keep only the most expensive coast periods within daily thermal budget
550
- let dailyCoastUsed = 0;
551
- for (const entry of coastIndices) {
552
- if (dailyCoastUsed < maxCoastIntervals) {
553
- dailyCoastUsed++;
554
- // Keep this coast interval
555
- } else {
556
- // Exceeded daily thermal budget - convert to normal
557
- modes[entry.index] = "normal";
558
- }
559
- }
560
- }
526
+ // STEP 2: Right-align coast and boost windows within their contiguous periods
527
+ // When thermal budget is shorter than a full cheap/expensive period,
528
+ // position the allowed intervals at the END (right side) of the period:
529
+ // - Coast at end of expensive period heat pump restarts when cheap prices begin
530
+ // - Boost at end of cheap period → heat pump stops right before expensive prices begin
531
+ applyRightAlignedWindows(modes, maxCoastIntervals, maxBoostLookahead);
561
532
  }
562
533
 
563
534
  // STEP 3: Extend coast periods using runway/lookahead
@@ -605,19 +576,19 @@ function createSchedule(prices, outdoorTemp, config) {
605
576
  avgPrice,
606
577
  minPrice: Math.min(...priceValues),
607
578
  maxPrice: Math.max(...priceValues),
608
- priceRange: Math.max(...priceValues) - Math.min(...priceValues),
609
- variationPercent: ((Math.max(...priceValues) - Math.min(...priceValues)) / avgPrice) * 100,
610
- minSavingsThreshold,
611
- minSavingsPercent, // Base configured value
612
- effectiveMinSavingsPercent, // Actual value after temperature scaling
613
- effectiveVariationThreshold,
614
- maxCoastMinutes,
615
- maxBoostRetention,
616
- maxCoastIntervals,
617
- maxBoostLookahead,
618
- extremeWeatherThreshold,
619
- isExtremeWeather,
620
- outdoorTemp,
579
+ priceRange: Math.max(...priceValues) - Math.min(...priceValues),
580
+ variationPercent: ((Math.max(...priceValues) - Math.min(...priceValues)) / avgPrice) * 100,
581
+ minSavingsThreshold,
582
+ minSavingsPercent, // Base configured value
583
+ effectiveMinSavingsPercent, // Actual value after temperature scaling
584
+ effectiveVariationThreshold,
585
+ maxCoastMinutes,
586
+ maxBoostRetention,
587
+ maxCoastIntervals,
588
+ maxBoostLookahead,
589
+ extremeWeatherThreshold,
590
+ isExtremeWeather,
591
+ outdoorTemp,
621
592
  intervalMinutes,
622
593
  minModeDuration,
623
594
  outputTimezone,
@@ -787,6 +758,72 @@ function enforceMinimumDurations(modes, intervalMinutes, minModeDuration) {
787
758
  return result;
788
759
  }
789
760
 
761
+ /**
762
+ * Right-align coast and boost windows within their contiguous periods.
763
+ *
764
+ * When thermal budget allows fewer intervals than a full cheap/expensive period,
765
+ * this positions the allowed intervals at the END of the period:
766
+ * - Coast at end of expensive period → heat pump restarts when cheap prices begin
767
+ * - Boost at end of cheap period → heat pump stops right before expensive prices begin
768
+ *
769
+ * Example:
770
+ * Prices: [EXPENSIVE][EXPENSIVE][EXPENSIVE][EXPENSIVE][cheap][cheap]
771
+ * Thermal budget: Can only coast 2 hours
772
+ * Before: [COAST ][COAST ][normal ][normal ]...
773
+ * After: [normal ][normal ][COAST ][COAST ]...
774
+ *
775
+ * @param {Array<string>} modes - Array of mode strings (modified in place)
776
+ * @param {number} maxCoastIntervals - Maximum consecutive coast intervals (thermal budget)
777
+ * @param {number} maxBoostIntervals - Maximum consecutive boost intervals (thermal budget)
778
+ */
779
+ function applyRightAlignedWindows(modes, maxCoastIntervals, maxBoostIntervals) {
780
+ const n = modes.length;
781
+
782
+ // Process COAST periods - keep rightmost intervals only
783
+ let i = 0;
784
+ while (i < n) {
785
+ if (modes[i] === "coast") {
786
+ const start = i;
787
+ while (i < n && modes[i] === "coast") {
788
+ i++;
789
+ }
790
+ const length = i - start;
791
+
792
+ if (length > maxCoastIntervals) {
793
+ // Convert leftmost excess to normal, keep rightmost
794
+ const keepStart = start + (length - maxCoastIntervals);
795
+ for (let j = start; j < keepStart; j++) {
796
+ modes[j] = "normal";
797
+ }
798
+ }
799
+ } else {
800
+ i++;
801
+ }
802
+ }
803
+
804
+ // Process BOOST periods - keep rightmost intervals only
805
+ i = 0;
806
+ while (i < n) {
807
+ if (modes[i] === "boost") {
808
+ const start = i;
809
+ while (i < n && modes[i] === "boost") {
810
+ i++;
811
+ }
812
+ const length = i - start;
813
+
814
+ if (length > maxBoostIntervals) {
815
+ // Convert leftmost excess to normal, keep rightmost
816
+ const keepStart = start + (length - maxBoostIntervals);
817
+ for (let j = start; j < keepStart; j++) {
818
+ modes[j] = "normal";
819
+ }
820
+ }
821
+ } else {
822
+ i++;
823
+ }
824
+ }
825
+ }
826
+
790
827
  // ============================================================================
791
828
  // Utility Functions
792
829
  // ============================================================================
@@ -1010,6 +1047,7 @@ module.exports = {
1010
1047
  enforceMinimumDurations,
1011
1048
  enforceMaxCoastDuration,
1012
1049
  extendCoastToRunway,
1050
+ applyRightAlignedWindows,
1013
1051
  alignToHourlyBoundaries,
1014
1052
  validateConfig,
1015
1053
  ckmeans,
@@ -1,6 +1,6 @@
1
1
  <script type="text/javascript">
2
2
  RED.nodes.registerType("ps-strategy-smart-thermal", {
3
- category: "Power Saver",
3
+ category: "Pricecontrol",
4
4
  color: "#FFCC66",
5
5
  defaults: {
6
6
  name: { value: "" },
@@ -241,5 +241,5 @@
241
241
  }</pre>
242
242
 
243
243
  <h3>References</h3>
244
- <p>See the <a href="https://powersaver.no/nodes/ps-strategy-smart-thermal" target="_blank">node documentation</a> for detailed examples and configuration guides.</p>
244
+ <p>See the node configuration above for detailed examples and configuration guides.</p>
245
245
  </script>