@energiok/node-red-contrib-pricecontrol-thermal 1.0.0 → 1.1.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/package.json
CHANGED
|
@@ -523,41 +523,12 @@ function createSchedule(prices, outdoorTemp, config) {
|
|
|
523
523
|
}
|
|
524
524
|
}
|
|
525
525
|
} else {
|
|
526
|
-
//
|
|
527
|
-
//
|
|
528
|
-
|
|
529
|
-
//
|
|
530
|
-
|
|
531
|
-
|
|
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,
|