Skip to content

Commit db5f595

Browse files
authored
fix: change peak hours bahavior (#57)
* fix state * use energy compass name * fix attributes
1 parent 2b34d44 commit db5f595

5 files changed

Lines changed: 45 additions & 150 deletions

File tree

custom_components/rce_pse/sensors/peak_hours.py

Lines changed: 15 additions & 55 deletions
Original file line numberDiff line numberDiff line change
@@ -38,39 +38,6 @@ def _load_state_display_names(lang: str, translation_key: str) -> dict[str, str]
3838
return result
3939

4040

41-
def _pdgsz_records_to_ranges(records: list[dict]) -> dict[str, list[str]]:
42-
attr_keys = set(PDGSZ_USAGE_FCST_TO_ATTR.values())
43-
result: dict[str, list[str]] = {k: [] for k in attr_keys}
44-
if not records:
45-
return result
46-
records_sorted = sorted(records, key=lambda r: r.get("dtime", ""))
47-
i = 0
48-
while i < len(records_sorted):
49-
rec = records_sorted[i]
50-
fcst = rec.get("usage_fcst", 1)
51-
attr_key = PDGSZ_USAGE_FCST_TO_ATTR.get(fcst, "normal_usage")
52-
dtime_str = rec.get("dtime", "")
53-
if " " not in dtime_str:
54-
i += 1
55-
continue
56-
start_part = dtime_str.split(" ", 1)[1]
57-
try:
58-
start_hour = int(start_part.split(":")[0])
59-
except (ValueError, IndexError):
60-
i += 1
61-
continue
62-
j = i + 1
63-
while j < len(records_sorted) and records_sorted[j].get("usage_fcst") == fcst:
64-
j += 1
65-
end_hour = start_hour + (j - i)
66-
if end_hour > 24:
67-
end_hour = 24
68-
end_part = f"{end_hour:02d}:00" if end_hour < 24 else "24:00"
69-
result[attr_key].append(f"{start_part}{end_part}")
70-
i = j
71-
return result
72-
73-
7441
def _pdgsz_records_to_hourly_state(records: list[dict]) -> dict[int, str]:
7542
result: dict[int, str] = {}
7643
for rec in records:
@@ -87,18 +54,22 @@ def _pdgsz_records_to_hourly_state(records: list[dict]) -> dict[int, str]:
8754
return result
8855

8956

90-
def _hourly_states_attributes(
91-
hourly: dict[int, str],
57+
def _records_to_values(
58+
records: list[dict],
9259
display_names: dict[str, str],
9360
) -> list[dict[str, Any]]:
94-
return [
95-
{
96-
"hour": f"{h:02d}:00",
97-
"state": hourly.get(h),
98-
"state_display": display_names.get(hourly.get(h, ""), ""),
99-
}
100-
for h in range(24)
101-
]
61+
result: list[dict[str, Any]] = []
62+
for rec in records:
63+
usage_fcst = rec.get("usage_fcst", 1)
64+
state = PDGSZ_USAGE_FCST_TO_ATTR.get(usage_fcst, "normal_usage")
65+
result.append({
66+
"dtime": rec.get("dtime"),
67+
"usage_fcst": usage_fcst,
68+
"business_date": rec.get("business_date"),
69+
"state": state,
70+
"display_state": display_names.get(state, ""),
71+
})
72+
return result
10273

10374

10475
class RCEPeakHoursSensorBase(RCEBaseSensor):
@@ -137,7 +108,6 @@ def native_value(self) -> str | None:
137108
@property
138109
def extra_state_attributes(self) -> dict[str, Any]:
139110
records = self._get_pdgsz_records()
140-
hourly = _pdgsz_records_to_hourly_state(records)
141111
try:
142112
lang = self.hass.config.language
143113
if lang not in ("pl", "en"):
@@ -146,8 +116,7 @@ def extra_state_attributes(self) -> dict[str, Any]:
146116
except (AttributeError, KeyError):
147117
display_names = {}
148118
return {
149-
"records": records,
150-
"hourly_states": _hourly_states_attributes(hourly, display_names),
119+
"values": _records_to_values(records, display_names),
151120
}
152121

153122
@property
@@ -171,12 +140,3 @@ def __init__(self, coordinator: RCEPSEDataUpdateCoordinator) -> None:
171140

172141
def _get_pdgsz_records(self) -> list[dict]:
173142
return self.get_tomorrow_pdgsz_data()
174-
175-
def _get_current_hour_for_state(self) -> int:
176-
return 0
177-
178-
@property
179-
def available(self) -> bool:
180-
if not self.is_tomorrow_data_available():
181-
return False
182-
return super().available

custom_components/rce_pse/translations/en.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -172,7 +172,7 @@
172172
"name": "Lowest Price End Tomorrow"
173173
},
174174
"rce_pse_today_peak_hours": {
175-
"name": "Peak Hours Today",
175+
"name": "Energy Compass Today",
176176
"state_attributes": {
177177
"recommended_usage": {
178178
"name": "Recommended usage"
@@ -189,7 +189,7 @@
189189
}
190190
},
191191
"rce_pse_tomorrow_peak_hours": {
192-
"name": "Peak Hours Tomorrow",
192+
"name": "Energy Compass Tomorrow",
193193
"state_attributes": {
194194
"recommended_usage": {
195195
"name": "Recommended usage"

custom_components/rce_pse/translations/pl.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -172,7 +172,7 @@
172172
"name": "Koniec Najniższej Ceny Jutro"
173173
},
174174
"rce_pse_today_peak_hours": {
175-
"name": "Godziny Szczytu Dzisiaj",
175+
"name": "Kompas Energetyczny Dzisiaj",
176176
"state_attributes": {
177177
"recommended_usage": {
178178
"name": "Zalecane użytkowanie"
@@ -189,7 +189,7 @@
189189
}
190190
},
191191
"rce_pse_tomorrow_peak_hours": {
192-
"name": "Godziny Szczytu Jutro",
192+
"name": "Kompas Energetyczny Jutro",
193193
"state_attributes": {
194194
"recommended_usage": {
195195
"name": "Zalecane użytkowanie"

docs/SENSORY.md

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -32,12 +32,12 @@
3232

3333
Wszystkie zwracają **timestamp** (datetime). Aby pokazać tylko godzinę (np. HH:MM), użyj szablonu: `as_timestamp(...) | timestamp_custom('%H:%M')`. Szczegóły: [Migracja do v2.0.0](MIGRACJA-V2.md).
3434

35-
## Godziny Szczytu (PDGSZ)
35+
## Kompas Energetyczny (PDGSZ)
3636

3737
Dane z raportu PSE „Godziny Szczytu” (API PDGSZ) – kiedy zalecane jest użytkowanie energii, a kiedy oszczędzanie.
3838

39-
- **Godziny Szczytu Dzisiaj** – stan: wartość tekstowa dla bieżącej godziny (np. „Zalecane użytkowanie”) lub brak danych jak przy sensorach „Jutro” (np. okna najniższej ceny) – wtedy stan „unknown”. Atrybuty: **records** – surowa odpowiedź API z dnia (tylko wpisy aktywne, is_active); **hourly_states** – lista 24 wpisów `{ "hour": "HH:00", "state": "klucz", "state_display": "tekst np. Zalecane użytkowanie" }` dla każdej godziny dnia.
40-
- **Godziny Szczytu Jutro** – to samo dla następnego dnia (dostępne po 14:00 CET); stan odnosi się do godziny 00:00 jutro.
39+
- **Kompas Energetyczny Dzisiaj** – stan: wartość tekstowa dla bieżącej godziny (np. „Zalecane użytkowanie”) lub brak danych jak przy sensorach „Jutro” (np. okna najniższej ceny) – wtedy stan „unknown”. Atrybut **values**: lista wpisów z API (tylko `dtime`, `usage_fcst`, `business_date`) z dodanymi `state` i `display_state` (klucz i tekst w języku interfejsu).
40+
- **Kompas Energetyczny Jutro** – to samo dla następnego dnia (dostępne po 14:00 CET); stan dla aktualnej godziny, ale dnia jutrzejszego (jak sensor „Cena Jutro”).
4141

4242
Możliwe stany (wyświetlane jako tekst w języku interfejsu):
4343

tests/test_peak_hours_sensors.py

Lines changed: 23 additions & 88 deletions
Original file line numberDiff line numberDiff line change
@@ -3,83 +3,11 @@
33
from unittest.mock import patch
44

55
from custom_components.rce_pse.sensors.peak_hours import (
6-
_pdgsz_records_to_hourly_state,
7-
_pdgsz_records_to_ranges,
86
RCETodayPeakHoursSensor,
97
RCETomorrowPeakHoursSensor,
108
)
119

1210

13-
class TestPdgszRecordsToRanges:
14-
15-
def test_empty_records_returns_all_keys_empty(self):
16-
result = _pdgsz_records_to_ranges([])
17-
assert result["recommended_usage"] == []
18-
assert result["normal_usage"] == []
19-
assert result["recommended_saving"] == []
20-
assert result["required_restriction"] == []
21-
22-
def test_single_hour_single_category(self):
23-
records = [
24-
{"dtime": "2025-05-29 07:00", "business_date": "2025-05-29", "usage_fcst": 0},
25-
]
26-
result = _pdgsz_records_to_ranges(records)
27-
assert result["recommended_usage"] == ["07:00–08:00"]
28-
assert result["normal_usage"] == []
29-
assert result["recommended_saving"] == []
30-
assert result["required_restriction"] == []
31-
32-
def test_consecutive_same_fcst_merged(self):
33-
records = [
34-
{"dtime": "2025-05-29 07:00", "business_date": "2025-05-29", "usage_fcst": 0},
35-
{"dtime": "2025-05-29 08:00", "business_date": "2025-05-29", "usage_fcst": 0},
36-
{"dtime": "2025-05-29 09:00", "business_date": "2025-05-29", "usage_fcst": 0},
37-
]
38-
result = _pdgsz_records_to_ranges(records)
39-
assert result["recommended_usage"] == ["07:00–10:00"]
40-
assert result["normal_usage"] == []
41-
42-
def test_two_categories_two_ranges(self):
43-
records = [
44-
{"dtime": "2025-05-29 06:00", "business_date": "2025-05-29", "usage_fcst": 1},
45-
{"dtime": "2025-05-29 07:00", "business_date": "2025-05-29", "usage_fcst": 1},
46-
{"dtime": "2025-05-29 08:00", "business_date": "2025-05-29", "usage_fcst": 2},
47-
{"dtime": "2025-05-29 09:00", "business_date": "2025-05-29", "usage_fcst": 2},
48-
]
49-
result = _pdgsz_records_to_ranges(records)
50-
assert result["normal_usage"] == ["06:00–08:00"]
51-
assert result["recommended_saving"] == ["08:00–10:00"]
52-
53-
def test_usage_fcst_3_maps_to_required_restriction(self):
54-
records = [
55-
{"dtime": "2025-05-29 18:00", "business_date": "2025-05-29", "usage_fcst": 3},
56-
]
57-
result = _pdgsz_records_to_ranges(records)
58-
assert result["required_restriction"] == ["18:00–19:00"]
59-
assert result["recommended_usage"] == []
60-
61-
62-
class TestPdgszRecordsToHourlyState:
63-
64-
def test_empty_returns_empty_dict(self):
65-
assert _pdgsz_records_to_hourly_state([]) == {}
66-
67-
def test_maps_hour_to_attr_key(self):
68-
records = [
69-
{"dtime": "2025-05-29 07:00", "business_date": "2025-05-29", "usage_fcst": 0},
70-
{"dtime": "2025-05-29 08:00", "business_date": "2025-05-29", "usage_fcst": 2},
71-
]
72-
result = _pdgsz_records_to_hourly_state(records)
73-
assert result[7] == "recommended_usage"
74-
assert result[8] == "recommended_saving"
75-
76-
def test_skips_invalid_dtime(self):
77-
records = [
78-
{"dtime": "no-space", "usage_fcst": 0},
79-
]
80-
assert _pdgsz_records_to_hourly_state(records) == {}
81-
82-
8311
class TestTodayPeakHoursSensor:
8412

8513
def test_initialization(self, mock_coordinator):
@@ -107,7 +35,7 @@ def test_native_value_empty_data_returns_none(self, mock_coordinator):
10735
with patch.object(sensor, 'get_today_pdgsz_data', return_value=[]):
10836
assert sensor.native_value is None
10937

110-
def test_extra_state_attributes_records_and_hourly_states(self, mock_coordinator):
38+
def test_extra_state_attributes_values_filtered_with_state_and_display_state(self, mock_coordinator):
11139
today = "2025-05-29"
11240
records = [
11341
{"dtime": f"{today} 07:00", "business_date": today, "usage_fcst": 0},
@@ -119,15 +47,23 @@ def test_extra_state_attributes_records_and_hourly_states(self, mock_coordinator
11947
with patch.object(sensor, 'get_today_pdgsz_data') as mock_get:
12048
mock_get.return_value = records
12149
attrs = sensor.extra_state_attributes
122-
assert "records" in attrs
123-
assert attrs["records"] == records
124-
assert "hourly_states" in attrs
125-
hourly = attrs["hourly_states"]
126-
assert len(hourly) == 24
127-
assert hourly[7] == {"hour": "07:00", "state": "recommended_usage", "state_display": "Recommended usage"}
128-
assert hourly[8] == {"hour": "08:00", "state": "recommended_saving", "state_display": "Recommended saving"}
129-
assert hourly[0]["state"] is None
130-
assert hourly[0]["state_display"] == ""
50+
assert "values" in attrs
51+
values = attrs["values"]
52+
assert len(values) == 2
53+
assert values[0] == {
54+
"dtime": f"{today} 07:00",
55+
"usage_fcst": 0,
56+
"business_date": today,
57+
"state": "recommended_usage",
58+
"display_state": "Recommended usage",
59+
}
60+
assert values[1] == {
61+
"dtime": f"{today} 08:00",
62+
"usage_fcst": 2,
63+
"business_date": today,
64+
"state": "recommended_saving",
65+
"display_state": "Recommended saving",
66+
}
13167

13268
def test_available_when_coordinator_has_data(self, mock_coordinator):
13369
mock_coordinator.data["pdgsz_data"] = []
@@ -149,14 +85,13 @@ def test_initialization(self, mock_coordinator):
14985
sensor = RCETomorrowPeakHoursSensor(mock_coordinator)
15086
assert sensor._attr_unique_id == "rce_pse_tomorrow_peak_hours"
15187

152-
def test_not_available_before_14(self, mock_coordinator):
88+
def test_available_when_coordinator_has_data_like_tomorrow_price(self, mock_coordinator):
15389
mock_coordinator.data["pdgsz_data"] = []
15490
sensor = RCETomorrowPeakHoursSensor(mock_coordinator)
155-
with patch.object(sensor, 'is_tomorrow_data_available', return_value=False):
156-
assert sensor.available is False
91+
assert sensor.available is True
15792

158-
def test_available_after_14_when_pdgsz_present(self, mock_coordinator):
93+
def test_native_value_none_when_no_tomorrow_data_shows_unknown(self, mock_coordinator):
15994
mock_coordinator.data["pdgsz_data"] = []
16095
sensor = RCETomorrowPeakHoursSensor(mock_coordinator)
161-
with patch.object(sensor, 'is_tomorrow_data_available', return_value=True):
162-
assert sensor.available is True
96+
with patch.object(sensor, 'get_tomorrow_pdgsz_data', return_value=[]):
97+
assert sensor.native_value is None

0 commit comments

Comments
 (0)