From ea088f7b1cb0b5e9726fe3f5d7f96b7ffce66df4 Mon Sep 17 00:00:00 2001 From: Lewa Reka Date: Sun, 15 Mar 2026 09:00:03 +0100 Subject: [PATCH 1/3] fix state --- .../rce_pse/sensors/peak_hours.py | 43 ------------------- docs/SENSORY.md | 2 +- tests/test_peak_hours_sensors.py | 11 +++-- 3 files changed, 6 insertions(+), 50 deletions(-) diff --git a/custom_components/rce_pse/sensors/peak_hours.py b/custom_components/rce_pse/sensors/peak_hours.py index 27b4e6b..272f7c4 100644 --- a/custom_components/rce_pse/sensors/peak_hours.py +++ b/custom_components/rce_pse/sensors/peak_hours.py @@ -37,40 +37,6 @@ def _load_state_display_names(lang: str, translation_key: str) -> dict[str, str] _STATE_DISPLAY_CACHE[cache_key] = result return result - -def _pdgsz_records_to_ranges(records: list[dict]) -> dict[str, list[str]]: - attr_keys = set(PDGSZ_USAGE_FCST_TO_ATTR.values()) - result: dict[str, list[str]] = {k: [] for k in attr_keys} - if not records: - return result - records_sorted = sorted(records, key=lambda r: r.get("dtime", "")) - i = 0 - while i < len(records_sorted): - rec = records_sorted[i] - fcst = rec.get("usage_fcst", 1) - attr_key = PDGSZ_USAGE_FCST_TO_ATTR.get(fcst, "normal_usage") - dtime_str = rec.get("dtime", "") - if " " not in dtime_str: - i += 1 - continue - start_part = dtime_str.split(" ", 1)[1] - try: - start_hour = int(start_part.split(":")[0]) - except (ValueError, IndexError): - i += 1 - continue - j = i + 1 - while j < len(records_sorted) and records_sorted[j].get("usage_fcst") == fcst: - j += 1 - end_hour = start_hour + (j - i) - if end_hour > 24: - end_hour = 24 - end_part = f"{end_hour:02d}:00" if end_hour < 24 else "24:00" - result[attr_key].append(f"{start_part}–{end_part}") - i = j - return result - - def _pdgsz_records_to_hourly_state(records: list[dict]) -> dict[int, str]: result: dict[int, str] = {} for rec in records: @@ -171,12 +137,3 @@ def __init__(self, coordinator: RCEPSEDataUpdateCoordinator) -> None: def _get_pdgsz_records(self) -> list[dict]: return self.get_tomorrow_pdgsz_data() - - def _get_current_hour_for_state(self) -> int: - return 0 - - @property - def available(self) -> bool: - if not self.is_tomorrow_data_available(): - return False - return super().available diff --git a/docs/SENSORY.md b/docs/SENSORY.md index cd06a4d..b2c97b1 100644 --- a/docs/SENSORY.md +++ b/docs/SENSORY.md @@ -37,7 +37,7 @@ Wszystkie zwracają **timestamp** (datetime). Aby pokazać tylko godzinę (np. H Dane z raportu PSE „Godziny Szczytu” (API PDGSZ) – kiedy zalecane jest użytkowanie energii, a kiedy oszczędzanie. - **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. -- **Godziny Szczytu Jutro** – to samo dla następnego dnia (dostępne po 14:00 CET); stan odnosi się do godziny 00:00 jutro. +- **Godziny Szczytu Jutro** – to samo dla następnego dnia (dostępne po 14:00 CET); stan dla aktualnej godziny, ale dnia jutrzejszego (jak sensor „Cena Jutro”). Możliwe stany (wyświetlane jako tekst w języku interfejsu): diff --git a/tests/test_peak_hours_sensors.py b/tests/test_peak_hours_sensors.py index a48b0bc..e6a9d8d 100644 --- a/tests/test_peak_hours_sensors.py +++ b/tests/test_peak_hours_sensors.py @@ -149,14 +149,13 @@ def test_initialization(self, mock_coordinator): sensor = RCETomorrowPeakHoursSensor(mock_coordinator) assert sensor._attr_unique_id == "rce_pse_tomorrow_peak_hours" - def test_not_available_before_14(self, mock_coordinator): + def test_available_when_coordinator_has_data_like_tomorrow_price(self, mock_coordinator): mock_coordinator.data["pdgsz_data"] = [] sensor = RCETomorrowPeakHoursSensor(mock_coordinator) - with patch.object(sensor, 'is_tomorrow_data_available', return_value=False): - assert sensor.available is False + assert sensor.available is True - def test_available_after_14_when_pdgsz_present(self, mock_coordinator): + def test_native_value_none_when_no_tomorrow_data_shows_unknown(self, mock_coordinator): mock_coordinator.data["pdgsz_data"] = [] sensor = RCETomorrowPeakHoursSensor(mock_coordinator) - with patch.object(sensor, 'is_tomorrow_data_available', return_value=True): - assert sensor.available is True + with patch.object(sensor, 'get_tomorrow_pdgsz_data', return_value=[]): + assert sensor.native_value is None From 7c0214c073021cf13fbbb2e587c12fae968c410e Mon Sep 17 00:00:00 2001 From: Lewa Reka Date: Sun, 15 Mar 2026 09:08:04 +0100 Subject: [PATCH 2/3] use energy compass name --- custom_components/rce_pse/translations/en.json | 4 ++-- custom_components/rce_pse/translations/pl.json | 4 ++-- docs/SENSORY.md | 6 +++--- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/custom_components/rce_pse/translations/en.json b/custom_components/rce_pse/translations/en.json index 92c84f4..fbfd00f 100644 --- a/custom_components/rce_pse/translations/en.json +++ b/custom_components/rce_pse/translations/en.json @@ -172,7 +172,7 @@ "name": "Lowest Price End Tomorrow" }, "rce_pse_today_peak_hours": { - "name": "Peak Hours Today", + "name": "Energy Compass Today", "state_attributes": { "recommended_usage": { "name": "Recommended usage" @@ -189,7 +189,7 @@ } }, "rce_pse_tomorrow_peak_hours": { - "name": "Peak Hours Tomorrow", + "name": "Energy Compass Tomorrow", "state_attributes": { "recommended_usage": { "name": "Recommended usage" diff --git a/custom_components/rce_pse/translations/pl.json b/custom_components/rce_pse/translations/pl.json index fb88716..bf3adee 100644 --- a/custom_components/rce_pse/translations/pl.json +++ b/custom_components/rce_pse/translations/pl.json @@ -172,7 +172,7 @@ "name": "Koniec Najniższej Ceny Jutro" }, "rce_pse_today_peak_hours": { - "name": "Godziny Szczytu Dzisiaj", + "name": "Kompas Energetyczny Dzisiaj", "state_attributes": { "recommended_usage": { "name": "Zalecane użytkowanie" @@ -189,7 +189,7 @@ } }, "rce_pse_tomorrow_peak_hours": { - "name": "Godziny Szczytu Jutro", + "name": "Kompas Energetyczny Jutro", "state_attributes": { "recommended_usage": { "name": "Zalecane użytkowanie" diff --git a/docs/SENSORY.md b/docs/SENSORY.md index b2c97b1..e6d8736 100644 --- a/docs/SENSORY.md +++ b/docs/SENSORY.md @@ -32,12 +32,12 @@ 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). -## Godziny Szczytu (PDGSZ) +## Kompas Energetyczny (PDGSZ) Dane z raportu PSE „Godziny Szczytu” (API PDGSZ) – kiedy zalecane jest użytkowanie energii, a kiedy oszczędzanie. -- **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. -- **Godziny Szczytu Jutro** – to samo dla następnego dnia (dostępne po 14:00 CET); stan dla aktualnej godziny, ale dnia jutrzejszego (jak sensor „Cena Jutro”). +- **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”. 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. +- **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”). Możliwe stany (wyświetlane jako tekst w języku interfejsu): From a678fb0b0bc8e46d14c20f70d81cc47ef80807ee Mon Sep 17 00:00:00 2001 From: Lewa Reka Date: Sun, 15 Mar 2026 09:18:50 +0100 Subject: [PATCH 3/3] fix attributes --- .../rce_pse/sensors/peak_hours.py | 29 ++--- docs/SENSORY.md | 2 +- tests/test_peak_hours_sensors.py | 100 ++++-------------- 3 files changed, 35 insertions(+), 96 deletions(-) diff --git a/custom_components/rce_pse/sensors/peak_hours.py b/custom_components/rce_pse/sensors/peak_hours.py index 272f7c4..2aa13b8 100644 --- a/custom_components/rce_pse/sensors/peak_hours.py +++ b/custom_components/rce_pse/sensors/peak_hours.py @@ -37,6 +37,7 @@ def _load_state_display_names(lang: str, translation_key: str) -> dict[str, str] _STATE_DISPLAY_CACHE[cache_key] = result return result + def _pdgsz_records_to_hourly_state(records: list[dict]) -> dict[int, str]: result: dict[int, str] = {} for rec in records: @@ -53,18 +54,22 @@ def _pdgsz_records_to_hourly_state(records: list[dict]) -> dict[int, str]: return result -def _hourly_states_attributes( - hourly: dict[int, str], +def _records_to_values( + records: list[dict], display_names: dict[str, str], ) -> list[dict[str, Any]]: - return [ - { - "hour": f"{h:02d}:00", - "state": hourly.get(h), - "state_display": display_names.get(hourly.get(h, ""), ""), - } - for h in range(24) - ] + result: list[dict[str, Any]] = [] + for rec in records: + usage_fcst = rec.get("usage_fcst", 1) + state = PDGSZ_USAGE_FCST_TO_ATTR.get(usage_fcst, "normal_usage") + result.append({ + "dtime": rec.get("dtime"), + "usage_fcst": usage_fcst, + "business_date": rec.get("business_date"), + "state": state, + "display_state": display_names.get(state, ""), + }) + return result class RCEPeakHoursSensorBase(RCEBaseSensor): @@ -103,7 +108,6 @@ def native_value(self) -> str | None: @property def extra_state_attributes(self) -> dict[str, Any]: records = self._get_pdgsz_records() - hourly = _pdgsz_records_to_hourly_state(records) try: lang = self.hass.config.language if lang not in ("pl", "en"): @@ -112,8 +116,7 @@ def extra_state_attributes(self) -> dict[str, Any]: except (AttributeError, KeyError): display_names = {} return { - "records": records, - "hourly_states": _hourly_states_attributes(hourly, display_names), + "values": _records_to_values(records, display_names), } @property diff --git a/docs/SENSORY.md b/docs/SENSORY.md index e6d8736..8d251ed 100644 --- a/docs/SENSORY.md +++ b/docs/SENSORY.md @@ -36,7 +36,7 @@ Wszystkie zwracają **timestamp** (datetime). Aby pokazać tylko godzinę (np. H Dane z raportu PSE „Godziny Szczytu” (API PDGSZ) – kiedy zalecane jest użytkowanie energii, a kiedy oszczędzanie. -- **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”. 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. +- **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). - **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”). Możliwe stany (wyświetlane jako tekst w języku interfejsu): diff --git a/tests/test_peak_hours_sensors.py b/tests/test_peak_hours_sensors.py index e6a9d8d..f09f0c9 100644 --- a/tests/test_peak_hours_sensors.py +++ b/tests/test_peak_hours_sensors.py @@ -3,83 +3,11 @@ from unittest.mock import patch from custom_components.rce_pse.sensors.peak_hours import ( - _pdgsz_records_to_hourly_state, - _pdgsz_records_to_ranges, RCETodayPeakHoursSensor, RCETomorrowPeakHoursSensor, ) -class TestPdgszRecordsToRanges: - - def test_empty_records_returns_all_keys_empty(self): - result = _pdgsz_records_to_ranges([]) - assert result["recommended_usage"] == [] - assert result["normal_usage"] == [] - assert result["recommended_saving"] == [] - assert result["required_restriction"] == [] - - def test_single_hour_single_category(self): - records = [ - {"dtime": "2025-05-29 07:00", "business_date": "2025-05-29", "usage_fcst": 0}, - ] - result = _pdgsz_records_to_ranges(records) - assert result["recommended_usage"] == ["07:00–08:00"] - assert result["normal_usage"] == [] - assert result["recommended_saving"] == [] - assert result["required_restriction"] == [] - - def test_consecutive_same_fcst_merged(self): - records = [ - {"dtime": "2025-05-29 07:00", "business_date": "2025-05-29", "usage_fcst": 0}, - {"dtime": "2025-05-29 08:00", "business_date": "2025-05-29", "usage_fcst": 0}, - {"dtime": "2025-05-29 09:00", "business_date": "2025-05-29", "usage_fcst": 0}, - ] - result = _pdgsz_records_to_ranges(records) - assert result["recommended_usage"] == ["07:00–10:00"] - assert result["normal_usage"] == [] - - def test_two_categories_two_ranges(self): - records = [ - {"dtime": "2025-05-29 06:00", "business_date": "2025-05-29", "usage_fcst": 1}, - {"dtime": "2025-05-29 07:00", "business_date": "2025-05-29", "usage_fcst": 1}, - {"dtime": "2025-05-29 08:00", "business_date": "2025-05-29", "usage_fcst": 2}, - {"dtime": "2025-05-29 09:00", "business_date": "2025-05-29", "usage_fcst": 2}, - ] - result = _pdgsz_records_to_ranges(records) - assert result["normal_usage"] == ["06:00–08:00"] - assert result["recommended_saving"] == ["08:00–10:00"] - - def test_usage_fcst_3_maps_to_required_restriction(self): - records = [ - {"dtime": "2025-05-29 18:00", "business_date": "2025-05-29", "usage_fcst": 3}, - ] - result = _pdgsz_records_to_ranges(records) - assert result["required_restriction"] == ["18:00–19:00"] - assert result["recommended_usage"] == [] - - -class TestPdgszRecordsToHourlyState: - - def test_empty_returns_empty_dict(self): - assert _pdgsz_records_to_hourly_state([]) == {} - - def test_maps_hour_to_attr_key(self): - records = [ - {"dtime": "2025-05-29 07:00", "business_date": "2025-05-29", "usage_fcst": 0}, - {"dtime": "2025-05-29 08:00", "business_date": "2025-05-29", "usage_fcst": 2}, - ] - result = _pdgsz_records_to_hourly_state(records) - assert result[7] == "recommended_usage" - assert result[8] == "recommended_saving" - - def test_skips_invalid_dtime(self): - records = [ - {"dtime": "no-space", "usage_fcst": 0}, - ] - assert _pdgsz_records_to_hourly_state(records) == {} - - class TestTodayPeakHoursSensor: def test_initialization(self, mock_coordinator): @@ -107,7 +35,7 @@ def test_native_value_empty_data_returns_none(self, mock_coordinator): with patch.object(sensor, 'get_today_pdgsz_data', return_value=[]): assert sensor.native_value is None - def test_extra_state_attributes_records_and_hourly_states(self, mock_coordinator): + def test_extra_state_attributes_values_filtered_with_state_and_display_state(self, mock_coordinator): today = "2025-05-29" records = [ {"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 with patch.object(sensor, 'get_today_pdgsz_data') as mock_get: mock_get.return_value = records attrs = sensor.extra_state_attributes - assert "records" in attrs - assert attrs["records"] == records - assert "hourly_states" in attrs - hourly = attrs["hourly_states"] - assert len(hourly) == 24 - assert hourly[7] == {"hour": "07:00", "state": "recommended_usage", "state_display": "Recommended usage"} - assert hourly[8] == {"hour": "08:00", "state": "recommended_saving", "state_display": "Recommended saving"} - assert hourly[0]["state"] is None - assert hourly[0]["state_display"] == "" + assert "values" in attrs + values = attrs["values"] + assert len(values) == 2 + assert values[0] == { + "dtime": f"{today} 07:00", + "usage_fcst": 0, + "business_date": today, + "state": "recommended_usage", + "display_state": "Recommended usage", + } + assert values[1] == { + "dtime": f"{today} 08:00", + "usage_fcst": 2, + "business_date": today, + "state": "recommended_saving", + "display_state": "Recommended saving", + } def test_available_when_coordinator_has_data(self, mock_coordinator): mock_coordinator.data["pdgsz_data"] = []