From ad34bc89101f16a3a8b5ebf55ad45fa133548456 Mon Sep 17 00:00:00 2001 From: G Johansson Date: Fri, 20 Dec 2024 08:26:36 +0100 Subject: [PATCH] Add min/max price sensor to Nord Pool (#133534) * Add min/max price sensor to Nord Pool * Last fixes * Make link in strings * Replace func --- homeassistant/components/nordpool/sensor.py | 75 ++++++- .../components/nordpool/strings.json | 22 ++ .../nordpool/snapshots/test_sensor.ambr | 208 ++++++++++++++++++ 3 files changed, 298 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/nordpool/sensor.py b/homeassistant/components/nordpool/sensor.py index 47617cc8e42cd1..fe966e9916801d 100644 --- a/homeassistant/components/nordpool/sensor.py +++ b/homeassistant/components/nordpool/sensor.py @@ -27,6 +27,20 @@ PARALLEL_UPDATES = 0 +def validate_prices( + func: Callable[ + [DeliveryPeriodData], dict[str, tuple[float | None, float, float | None]] + ], + data: DeliveryPeriodData, + area: str, + index: int, +) -> float | None: + """Validate and return.""" + if result := func(data)[area][index]: + return result / 1000 + return None + + def get_prices( data: DeliveryPeriodData, ) -> dict[str, tuple[float | None, float, float | None]]: @@ -67,6 +81,26 @@ def get_prices( return result +def get_min_max_price( + data: DeliveryPeriodData, + area: str, + func: Callable[[float, float], float], +) -> tuple[float, datetime, datetime]: + """Get the lowest price from the data.""" + price_data = data.entries + price: float = price_data[0].entry[area] + start: datetime = price_data[0].start + end: datetime = price_data[0].end + for entry in price_data: + for _area, _price in entry.entry.items(): + if _area == area and _price == func(price, _price): + price = _price + start = entry.start + end = entry.end + + return (price, start, end) + + def get_blockprices( data: DeliveryPeriodData, ) -> dict[str, dict[str, tuple[datetime, datetime, float, float, float]]]: @@ -103,7 +137,8 @@ class NordpoolDefaultSensorEntityDescription(SensorEntityDescription): class NordpoolPricesSensorEntityDescription(SensorEntityDescription): """Describes Nord Pool prices sensor entity.""" - value_fn: Callable[[tuple[float | None, float, float | None]], float | None] + value_fn: Callable[[DeliveryPeriodData, str], float | None] + extra_fn: Callable[[DeliveryPeriodData, str], dict[str, str] | None] @dataclass(frozen=True, kw_only=True) @@ -142,20 +177,43 @@ class NordpoolBlockPricesSensorEntityDescription(SensorEntityDescription): NordpoolPricesSensorEntityDescription( key="current_price", translation_key="current_price", - value_fn=lambda data: data[1] / 1000, + value_fn=lambda data, area: validate_prices(get_prices, data, area, 1), + extra_fn=lambda data, area: None, state_class=SensorStateClass.MEASUREMENT, suggested_display_precision=2, ), NordpoolPricesSensorEntityDescription( key="last_price", translation_key="last_price", - value_fn=lambda data: data[0] / 1000 if data[0] else None, + value_fn=lambda data, area: validate_prices(get_prices, data, area, 0), + extra_fn=lambda data, area: None, suggested_display_precision=2, ), NordpoolPricesSensorEntityDescription( key="next_price", translation_key="next_price", - value_fn=lambda data: data[2] / 1000 if data[2] else None, + value_fn=lambda data, area: validate_prices(get_prices, data, area, 2), + extra_fn=lambda data, area: None, + suggested_display_precision=2, + ), + NordpoolPricesSensorEntityDescription( + key="lowest_price", + translation_key="lowest_price", + value_fn=lambda data, area: get_min_max_price(data, area, min)[0] / 1000, + extra_fn=lambda data, area: { + "start": get_min_max_price(data, area, min)[1].isoformat(), + "end": get_min_max_price(data, area, min)[2].isoformat(), + }, + suggested_display_precision=2, + ), + NordpoolPricesSensorEntityDescription( + key="highest_price", + translation_key="highest_price", + value_fn=lambda data, area: get_min_max_price(data, area, max)[0] / 1000, + extra_fn=lambda data, area: { + "start": get_min_max_price(data, area, max)[1].isoformat(), + "end": get_min_max_price(data, area, max)[2].isoformat(), + }, suggested_display_precision=2, ), ) @@ -285,9 +343,12 @@ def __init__( @property def native_value(self) -> float | None: """Return value of sensor.""" - return self.entity_description.value_fn( - get_prices(self.coordinator.data)[self.area] - ) + return self.entity_description.value_fn(self.coordinator.data, self.area) + + @property + def extra_state_attributes(self) -> dict[str, str] | None: + """Return the extra state attributes.""" + return self.entity_description.extra_fn(self.coordinator.data, self.area) class NordpoolBlockPriceSensor(NordpoolBaseEntity, SensorEntity): diff --git a/homeassistant/components/nordpool/strings.json b/homeassistant/components/nordpool/strings.json index d30898730b933b..cc10a1a064008d 100644 --- a/homeassistant/components/nordpool/strings.json +++ b/homeassistant/components/nordpool/strings.json @@ -50,6 +50,28 @@ "next_price": { "name": "Next price" }, + "lowest_price": { + "name": "Lowest price", + "state_attributes": { + "start": { + "name": "Start time" + }, + "end": { + "name": "End time" + } + } + }, + "highest_price": { + "name": "Highest price", + "state_attributes": { + "start": { + "name": "[%key:component::nordpool::entity::sensor::lowest_price::state_attributes::start::name%]" + }, + "end": { + "name": "[%key:component::nordpool::entity::sensor::lowest_price::state_attributes::end::name%]" + } + } + }, "block_average": { "name": "{block} average" }, diff --git a/tests/components/nordpool/snapshots/test_sensor.ambr b/tests/components/nordpool/snapshots/test_sensor.ambr index 01600352861419..9b328c3a71d951 100644 --- a/tests/components/nordpool/snapshots/test_sensor.ambr +++ b/tests/components/nordpool/snapshots/test_sensor.ambr @@ -200,6 +200,58 @@ 'state': '11.6402', }) # --- +# name: test_sensor[sensor.nord_pool_se3_highest_price-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.nord_pool_se3_highest_price', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 2, + }), + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Highest price', + 'platform': 'nordpool', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'highest_price', + 'unique_id': 'SE3-highest_price', + 'unit_of_measurement': 'SEK/kWh', + }) +# --- +# name: test_sensor[sensor.nord_pool_se3_highest_price-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'end': '2024-11-05T17:00:00+00:00', + 'friendly_name': 'Nord Pool SE3 Highest price', + 'start': '2024-11-05T16:00:00+00:00', + 'unit_of_measurement': 'SEK/kWh', + }), + 'context': , + 'entity_id': 'sensor.nord_pool_se3_highest_price', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '2.51265', + }) +# --- # name: test_sensor[sensor.nord_pool_se3_last_updated-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ @@ -247,6 +299,58 @@ 'state': '2024-11-04T12:15:03+00:00', }) # --- +# name: test_sensor[sensor.nord_pool_se3_lowest_price-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.nord_pool_se3_lowest_price', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 2, + }), + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Lowest price', + 'platform': 'nordpool', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'lowest_price', + 'unique_id': 'SE3-lowest_price', + 'unit_of_measurement': 'SEK/kWh', + }) +# --- +# name: test_sensor[sensor.nord_pool_se3_lowest_price-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'end': '2024-11-05T03:00:00+00:00', + 'friendly_name': 'Nord Pool SE3 Lowest price', + 'start': '2024-11-05T02:00:00+00:00', + 'unit_of_measurement': 'SEK/kWh', + }), + 'context': , + 'entity_id': 'sensor.nord_pool_se3_lowest_price', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '0.06169', + }) +# --- # name: test_sensor[sensor.nord_pool_se3_next_price-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ @@ -1307,6 +1411,58 @@ 'state': '11.6402', }) # --- +# name: test_sensor[sensor.nord_pool_se4_highest_price-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.nord_pool_se4_highest_price', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 2, + }), + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Highest price', + 'platform': 'nordpool', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'highest_price', + 'unique_id': 'SE4-highest_price', + 'unit_of_measurement': 'SEK/kWh', + }) +# --- +# name: test_sensor[sensor.nord_pool_se4_highest_price-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'end': '2024-11-05T17:00:00+00:00', + 'friendly_name': 'Nord Pool SE4 Highest price', + 'start': '2024-11-05T16:00:00+00:00', + 'unit_of_measurement': 'SEK/kWh', + }), + 'context': , + 'entity_id': 'sensor.nord_pool_se4_highest_price', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '3.53303', + }) +# --- # name: test_sensor[sensor.nord_pool_se4_last_updated-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ @@ -1354,6 +1510,58 @@ 'state': '2024-11-04T12:15:03+00:00', }) # --- +# name: test_sensor[sensor.nord_pool_se4_lowest_price-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.nord_pool_se4_lowest_price', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 2, + }), + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Lowest price', + 'platform': 'nordpool', + 'previous_unique_id': None, + 'supported_features': 0, + 'translation_key': 'lowest_price', + 'unique_id': 'SE4-lowest_price', + 'unit_of_measurement': 'SEK/kWh', + }) +# --- +# name: test_sensor[sensor.nord_pool_se4_lowest_price-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'end': '2024-11-05T03:00:00+00:00', + 'friendly_name': 'Nord Pool SE4 Lowest price', + 'start': '2024-11-05T02:00:00+00:00', + 'unit_of_measurement': 'SEK/kWh', + }), + 'context': , + 'entity_id': 'sensor.nord_pool_se4_lowest_price', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '0.06519', + }) +# --- # name: test_sensor[sensor.nord_pool_se4_next_price-entry] EntityRegistryEntrySnapshot({ 'aliases': set({