Skip to content

Commit 72e4e86

Browse files
committed
Add expensive-now price braking
1 parent e6ead57 commit 72e4e86

File tree

3 files changed

+83
-4
lines changed

3 files changed

+83
-4
lines changed

custom_components/pumpsteer/sensor/sensor.py

Lines changed: 33 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -128,7 +128,11 @@ def compute_block_window(
128128
block_start = update_time + timedelta(minutes=block.start_offset_minutes)
129129
block_end = update_time + timedelta(minutes=block.end_offset_minutes)
130130

131-
now_utc = dt_util.as_utc(update_time)
131+
now_utc = (
132+
dt_util.utcnow()
133+
if hasattr(dt_util, "utcnow")
134+
else dt_util.as_utc(dt_util.now())
135+
)
132136
block_start_utc = dt_util.as_utc(block_start)
133137
block_end_utc = dt_util.as_utc(block_end)
134138
in_price_block = block_start_utc <= now_utc < block_end_utc
@@ -274,6 +278,8 @@ def _compute_controls(
274278
current_slot_index: int,
275279
price_interval_minutes: int,
276280
config: Dict[str, Any],
281+
current_price: float,
282+
price_category: str,
277283
update_time: datetime,
278284
) -> Dict[str, Any]:
279285
"""Compute price brake and comfort push controls."""
@@ -310,9 +316,31 @@ def _compute_controls(
310316
desired_brake_level = price_brake["brake_level"]
311317
brake_blocked_reason = "allowed"
312318

313-
if desired_brake_level <= 0.0 and not in_price_block:
319+
category_label = price_category.split(" ")[0]
320+
is_very_expensive = category_label in {"very_expensive", "extreme"}
321+
is_expensive = category_label == "expensive"
322+
expensive_now = is_very_expensive or (
323+
is_expensive and current_price > price_brake["threshold"]
324+
)
325+
if desired_brake_level <= 0.0 and expensive_now:
326+
min_price = min(combined_prices) if combined_prices else current_price
327+
max_price = max(combined_prices) if combined_prices else current_price
328+
price_range = max_price - min_price
329+
if price_range > 0.0:
330+
price_factor = (current_price - min_price) / price_range
331+
else:
332+
price_factor = 0.0
333+
price_factor = max(0.0, min(price_factor, 1.0))
334+
aggressiveness_factor = min(
335+
1.0, max(0.0, sensor_data["aggressiveness"] / 5.0)
336+
)
337+
min_brake_level = 0.25 + 0.25 * (
338+
0.5 * (aggressiveness_factor + price_factor)
339+
)
340+
desired_brake_level = max(desired_brake_level, min_brake_level)
341+
if desired_brake_level <= 0.0 and not in_price_block and not expensive_now:
314342
brake_blocked_reason = "no_price_block"
315-
elif too_cold_to_brake:
343+
if too_cold_to_brake:
316344
desired_brake_level = 0.0
317345
brake_blocked_reason = "too_cold"
318346

@@ -767,6 +795,8 @@ async def async_update(self) -> None:
767795
current_slot_index,
768796
price_interval_minutes,
769797
config,
798+
current_price,
799+
price_category,
770800
update_time,
771801
)
772802

tests/test_block_window.py

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,12 @@
11
from datetime import datetime, timedelta
22

3+
from homeassistant.util import dt as dt_util
4+
35
from custom_components.pumpsteer.price_brake import PriceBlock
46
from custom_components.pumpsteer.sensor.sensor import compute_block_window
57

68

7-
def test_compute_block_window_active_now():
9+
def test_compute_block_window_active_now(monkeypatch):
810
update_time = datetime(2024, 1, 1, 12, 0, 0)
911
block = PriceBlock(
1012
start_index=0,
@@ -13,6 +15,10 @@ def test_compute_block_window_active_now():
1315
area=1.5,
1416
peak=2.5,
1517
)
18+
if hasattr(dt_util, "utcnow"):
19+
monkeypatch.setattr(dt_util, "utcnow", lambda: dt_util.as_utc(update_time))
20+
else:
21+
monkeypatch.setattr(dt_util, "now", lambda: update_time)
1622

1723
block_start, block_end, in_price_block, block_state = compute_block_window(
1824
update_time, block

tests/test_temperature_vs_price.py

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import sys
2+
from datetime import datetime
23
from pathlib import Path
34

45
from custom_components.pumpsteer.sensor import sensor
@@ -84,6 +85,48 @@ def test_extreme_price_brake_when_neutral():
8485
assert fake_temp == data["outdoor_temp"]
8586

8687

88+
def test_expensive_now_braking_outside_block(monkeypatch):
89+
s = create_sensor()
90+
data = base_sensor_data()
91+
combined_prices = [1.0, 3.28, 2.0]
92+
update_time = datetime(2024, 1, 1, 12, 0, 0)
93+
94+
def fake_compute_price_brake(**_kwargs):
95+
return {
96+
"brake_level": 0.0,
97+
"baseline": 0.0,
98+
"threshold": 2.54,
99+
"area": 0.0,
100+
"amplitude": 0.0,
101+
"block": None,
102+
}
103+
104+
monkeypatch.setattr(sensor, "compute_price_brake", fake_compute_price_brake)
105+
106+
pi_data = s._compute_controls(
107+
data,
108+
combined_prices,
109+
0,
110+
60,
111+
{},
112+
3.28,
113+
"expensive",
114+
update_time,
115+
)
116+
assert pi_data["price_brake_level"] > 0.0
117+
assert pi_data["brake_blocked_reason"] != "no_price_block"
118+
119+
fake_temp, mode = s._calculate_output_temperature(data, "expensive", 0)
120+
assert mode == "neutral"
121+
if (
122+
mode == "neutral"
123+
and pi_data["price_brake_level"] > 0.0
124+
and pi_data["brake_blocked_reason"] in {"allowed", "rate_limited"}
125+
):
126+
mode = "braking_by_price"
127+
assert mode == "braking_by_price"
128+
129+
87130
def test_very_cheap_price_overshoots_target():
88131
s = create_sensor()
89132
data = base_sensor_data()

0 commit comments

Comments
 (0)