Skip to content

Commit

Permalink
Add min/max price sensor to Nord Pool (#133534)
Browse files Browse the repository at this point in the history
* Add min/max price sensor to Nord Pool

* Last fixes

* Make link in strings

* Replace func
  • Loading branch information
gjohansson-ST authored Dec 20, 2024
1 parent 2621279 commit ad34bc8
Show file tree
Hide file tree
Showing 3 changed files with 298 additions and 7 deletions.
75 changes: 68 additions & 7 deletions homeassistant/components/nordpool/sensor.py
Original file line number Diff line number Diff line change
Expand Up @@ -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]]:
Expand Down Expand Up @@ -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]]]:
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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,
),
)
Expand Down Expand Up @@ -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):
Expand Down
22 changes: 22 additions & 0 deletions homeassistant/components/nordpool/strings.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
},
Expand Down
208 changes: 208 additions & 0 deletions tests/components/nordpool/snapshots/test_sensor.ambr
Original file line number Diff line number Diff line change
Expand Up @@ -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': <ANY>,
'device_class': None,
'device_id': <ANY>,
'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': <ANY>,
'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': <ANY>,
'entity_id': 'sensor.nord_pool_se3_highest_price',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': '2.51265',
})
# ---
# name: test_sensor[sensor.nord_pool_se3_last_updated-entry]
EntityRegistryEntrySnapshot({
'aliases': set({
Expand Down Expand Up @@ -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': <ANY>,
'device_class': None,
'device_id': <ANY>,
'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': <ANY>,
'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': <ANY>,
'entity_id': 'sensor.nord_pool_se3_lowest_price',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': '0.06169',
})
# ---
# name: test_sensor[sensor.nord_pool_se3_next_price-entry]
EntityRegistryEntrySnapshot({
'aliases': set({
Expand Down Expand Up @@ -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': <ANY>,
'device_class': None,
'device_id': <ANY>,
'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': <ANY>,
'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': <ANY>,
'entity_id': 'sensor.nord_pool_se4_highest_price',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': '3.53303',
})
# ---
# name: test_sensor[sensor.nord_pool_se4_last_updated-entry]
EntityRegistryEntrySnapshot({
'aliases': set({
Expand Down Expand Up @@ -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': <ANY>,
'device_class': None,
'device_id': <ANY>,
'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': <ANY>,
'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': <ANY>,
'entity_id': 'sensor.nord_pool_se4_lowest_price',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': '0.06519',
})
# ---
# name: test_sensor[sensor.nord_pool_se4_next_price-entry]
EntityRegistryEntrySnapshot({
'aliases': set({
Expand Down

0 comments on commit ad34bc8

Please sign in to comment.