Home Assistant Unofficial Reference 2024.12.1
sensor.py
Go to the documentation of this file.
1 """Sensor platform for Nord Pool integration."""
2 
3 from __future__ import annotations
4 
5 from collections.abc import Callable
6 from dataclasses import dataclass
7 from datetime import datetime, timedelta
8 
9 from pynordpool import DeliveryPeriodData
10 
12  EntityCategory,
13  SensorDeviceClass,
14  SensorEntity,
15  SensorEntityDescription,
16  SensorStateClass,
17 )
18 from homeassistant.core import HomeAssistant
19 from homeassistant.helpers.entity_platform import AddEntitiesCallback
20 from homeassistant.util import dt as dt_util, slugify
21 
22 from . import NordPoolConfigEntry
23 from .const import LOGGER
24 from .coordinator import NordPoolDataUpdateCoordinator
25 from .entity import NordpoolBaseEntity
26 
27 PARALLEL_UPDATES = 0
28 
29 
31  data: DeliveryPeriodData,
32 ) -> dict[str, tuple[float | None, float, float | None]]:
33  """Return previous, current and next prices.
34 
35  Output: {"SE3": (10.0, 10.5, 12.1)}
36  """
37  last_price_entries: dict[str, float] = {}
38  current_price_entries: dict[str, float] = {}
39  next_price_entries: dict[str, float] = {}
40  current_time = dt_util.utcnow()
41  previous_time = current_time - timedelta(hours=1)
42  next_time = current_time + timedelta(hours=1)
43  price_data = data.entries
44  LOGGER.debug("Price data: %s", price_data)
45  for entry in price_data:
46  if entry.start <= current_time <= entry.end:
47  current_price_entries = entry.entry
48  if entry.start <= previous_time <= entry.end:
49  last_price_entries = entry.entry
50  if entry.start <= next_time <= entry.end:
51  next_price_entries = entry.entry
52  LOGGER.debug(
53  "Last price %s, current price %s, next price %s",
54  last_price_entries,
55  current_price_entries,
56  next_price_entries,
57  )
58 
59  result = {}
60  for area, price in current_price_entries.items():
61  result[area] = (
62  last_price_entries.get(area),
63  price,
64  next_price_entries.get(area),
65  )
66  LOGGER.debug("Prices: %s", result)
67  return result
68 
69 
71  data: DeliveryPeriodData,
72 ) -> dict[str, dict[str, tuple[datetime, datetime, float, float, float]]]:
73  """Return average, min and max for block prices.
74 
75  Output: {"SE3": {"Off-peak 1": (_datetime_, _datetime_, 9.3, 10.5, 12.1)}}
76  """
77  result: dict[str, dict[str, tuple[datetime, datetime, float, float, float]]] = {}
78  block_prices = data.block_prices
79  for entry in block_prices:
80  for _area in entry.average:
81  if _area not in result:
82  result[_area] = {}
83  result[_area][entry.name] = (
84  entry.start,
85  entry.end,
86  entry.average[_area]["average"],
87  entry.average[_area]["min"],
88  entry.average[_area]["max"],
89  )
90 
91  LOGGER.debug("Block prices: %s", result)
92  return result
93 
94 
95 @dataclass(frozen=True, kw_only=True)
97  """Describes Nord Pool default sensor entity."""
98 
99  value_fn: Callable[[DeliveryPeriodData], str | float | datetime | None]
100 
101 
102 @dataclass(frozen=True, kw_only=True)
104  """Describes Nord Pool prices sensor entity."""
105 
106  value_fn: Callable[[tuple[float | None, float, float | None]], float | None]
107 
108 
109 @dataclass(frozen=True, kw_only=True)
111  """Describes Nord Pool block prices sensor entity."""
112 
113  value_fn: Callable[
114  [tuple[datetime, datetime, float, float, float]], float | datetime | None
115  ]
116 
117 
118 DEFAULT_SENSOR_TYPES: tuple[NordpoolDefaultSensorEntityDescription, ...] = (
120  key="updated_at",
121  translation_key="updated_at",
122  device_class=SensorDeviceClass.TIMESTAMP,
123  value_fn=lambda data: data.updated_at,
124  entity_category=EntityCategory.DIAGNOSTIC,
125  ),
127  key="currency",
128  translation_key="currency",
129  value_fn=lambda data: data.currency,
130  entity_category=EntityCategory.DIAGNOSTIC,
131  ),
133  key="exchange_rate",
134  translation_key="exchange_rate",
135  value_fn=lambda data: data.exchange_rate,
136  state_class=SensorStateClass.MEASUREMENT,
137  entity_registry_enabled_default=False,
138  entity_category=EntityCategory.DIAGNOSTIC,
139  ),
140 )
141 PRICES_SENSOR_TYPES: tuple[NordpoolPricesSensorEntityDescription, ...] = (
143  key="current_price",
144  translation_key="current_price",
145  value_fn=lambda data: data[1] / 1000,
146  state_class=SensorStateClass.MEASUREMENT,
147  suggested_display_precision=2,
148  ),
150  key="last_price",
151  translation_key="last_price",
152  value_fn=lambda data: data[0] / 1000 if data[0] else None,
153  suggested_display_precision=2,
154  ),
156  key="next_price",
157  translation_key="next_price",
158  value_fn=lambda data: data[2] / 1000 if data[2] else None,
159  suggested_display_precision=2,
160  ),
161 )
162 BLOCK_PRICES_SENSOR_TYPES: tuple[NordpoolBlockPricesSensorEntityDescription, ...] = (
164  key="block_average",
165  translation_key="block_average",
166  value_fn=lambda data: data[2] / 1000,
167  state_class=SensorStateClass.MEASUREMENT,
168  suggested_display_precision=2,
169  entity_registry_enabled_default=False,
170  ),
172  key="block_min",
173  translation_key="block_min",
174  value_fn=lambda data: data[3] / 1000,
175  state_class=SensorStateClass.MEASUREMENT,
176  suggested_display_precision=2,
177  entity_registry_enabled_default=False,
178  ),
180  key="block_max",
181  translation_key="block_max",
182  value_fn=lambda data: data[4] / 1000,
183  state_class=SensorStateClass.MEASUREMENT,
184  suggested_display_precision=2,
185  entity_registry_enabled_default=False,
186  ),
188  key="block_start_time",
189  translation_key="block_start_time",
190  value_fn=lambda data: data[0],
191  device_class=SensorDeviceClass.TIMESTAMP,
192  entity_registry_enabled_default=False,
193  ),
195  key="block_end_time",
196  translation_key="block_end_time",
197  value_fn=lambda data: data[1],
198  device_class=SensorDeviceClass.TIMESTAMP,
199  entity_registry_enabled_default=False,
200  ),
201 )
202 DAILY_AVERAGE_PRICES_SENSOR_TYPES: tuple[SensorEntityDescription, ...] = (
204  key="daily_average",
205  translation_key="daily_average",
206  state_class=SensorStateClass.MEASUREMENT,
207  suggested_display_precision=2,
208  entity_registry_enabled_default=False,
209  ),
210 )
211 
212 
214  hass: HomeAssistant,
215  entry: NordPoolConfigEntry,
216  async_add_entities: AddEntitiesCallback,
217 ) -> None:
218  """Set up Nord Pool sensor platform."""
219 
220  coordinator = entry.runtime_data
221 
222  entities: list[NordpoolBaseEntity] = []
223  currency = entry.runtime_data.data.currency
224 
225  for area in get_prices(entry.runtime_data.data):
226  LOGGER.debug("Setting up base sensors for area %s", area)
227  entities.extend(
228  NordpoolSensor(coordinator, description, area)
229  for description in DEFAULT_SENSOR_TYPES
230  )
231  LOGGER.debug(
232  "Setting up price sensors for area %s with currency %s", area, currency
233  )
234  entities.extend(
235  NordpoolPriceSensor(coordinator, description, area, currency)
236  for description in PRICES_SENSOR_TYPES
237  )
238  entities.extend(
239  NordpoolDailyAveragePriceSensor(coordinator, description, area, currency)
240  for description in DAILY_AVERAGE_PRICES_SENSOR_TYPES
241  )
242  for block_name in get_blockprices(coordinator.data)[area]:
243  LOGGER.debug(
244  "Setting up block price sensors for area %s with currency %s in block %s",
245  area,
246  currency,
247  block_name,
248  )
249  entities.extend(
251  coordinator, description, area, currency, block_name
252  )
253  for description in BLOCK_PRICES_SENSOR_TYPES
254  )
255  async_add_entities(entities)
256 
257 
259  """Representation of a Nord Pool sensor."""
260 
261  entity_description: NordpoolDefaultSensorEntityDescription
262 
263  @property
264  def native_value(self) -> str | float | datetime | None:
265  """Return value of sensor."""
266  return self.entity_descriptionentity_description.value_fn(self.coordinator.data)
267 
268 
270  """Representation of a Nord Pool price sensor."""
271 
272  entity_description: NordpoolPricesSensorEntityDescription
273 
274  def __init__(
275  self,
276  coordinator: NordPoolDataUpdateCoordinator,
277  entity_description: NordpoolPricesSensorEntityDescription,
278  area: str,
279  currency: str,
280  ) -> None:
281  """Initiate Nord Pool sensor."""
282  super().__init__(coordinator, entity_description, area)
283  self._attr_native_unit_of_measurement_attr_native_unit_of_measurement = f"{currency}/kWh"
284 
285  @property
286  def native_value(self) -> float | None:
287  """Return value of sensor."""
288  return self.entity_descriptionentity_description.value_fn(
289  get_prices(self.coordinator.data)[self.areaarea]
290  )
291 
292 
294  """Representation of a Nord Pool block price sensor."""
295 
296  entity_description: NordpoolBlockPricesSensorEntityDescription
297 
298  def __init__(
299  self,
300  coordinator: NordPoolDataUpdateCoordinator,
301  entity_description: NordpoolBlockPricesSensorEntityDescription,
302  area: str,
303  currency: str,
304  block_name: str,
305  ) -> None:
306  """Initiate Nord Pool sensor."""
307  super().__init__(coordinator, entity_description, area)
308  if entity_description.device_class is not SensorDeviceClass.TIMESTAMP:
309  self._attr_native_unit_of_measurement_attr_native_unit_of_measurement = f"{currency}/kWh"
310  self._attr_unique_id_attr_unique_id_attr_unique_id = f"{slugify(block_name)}-{area}-{entity_description.key}"
311  self.block_nameblock_name = block_name
312  self._attr_translation_placeholders_attr_translation_placeholders = {"block": block_name}
313 
314  @property
315  def native_value(self) -> float | datetime | None:
316  """Return value of sensor."""
317  return self.entity_descriptionentity_description.value_fn(
318  get_blockprices(self.coordinator.data)[self.areaarea][self.block_nameblock_name]
319  )
320 
321 
323  """Representation of a Nord Pool daily average price sensor."""
324 
325  entity_description: SensorEntityDescription
326 
327  def __init__(
328  self,
329  coordinator: NordPoolDataUpdateCoordinator,
330  entity_description: SensorEntityDescription,
331  area: str,
332  currency: str,
333  ) -> None:
334  """Initiate Nord Pool sensor."""
335  super().__init__(coordinator, entity_description, area)
336  self._attr_native_unit_of_measurement_attr_native_unit_of_measurement = f"{currency}/kWh"
337 
338  @property
339  def native_value(self) -> float | None:
340  """Return value of sensor."""
341  return self.coordinator.data.area_average[self.areaarea] / 1000
None __init__(self, NordPoolDataUpdateCoordinator coordinator, NordpoolBlockPricesSensorEntityDescription entity_description, str area, str currency, str block_name)
Definition: sensor.py:305
None __init__(self, NordPoolDataUpdateCoordinator coordinator, SensorEntityDescription entity_description, str area, str currency)
Definition: sensor.py:333
None __init__(self, NordPoolDataUpdateCoordinator coordinator, NordpoolPricesSensorEntityDescription entity_description, str area, str currency)
Definition: sensor.py:280
str|float|datetime|None native_value(self)
Definition: sensor.py:264
dict[str, tuple[float|None, float, float|None]] get_prices(DeliveryPeriodData data)
Definition: sensor.py:32
None async_setup_entry(HomeAssistant hass, NordPoolConfigEntry entry, AddEntitiesCallback async_add_entities)
Definition: sensor.py:217
dict[str, dict[str, tuple[datetime, datetime, float, float, float]]] get_blockprices(DeliveryPeriodData data)
Definition: sensor.py:72