Home Assistant Unofficial Reference 2024.12.1
coordinator.py
Go to the documentation of this file.
1 """Amber Electric Coordinator."""
2 
3 from __future__ import annotations
4 
5 from datetime import timedelta
6 from typing import Any
7 
8 import amberelectric
9 from amberelectric.models.actual_interval import ActualInterval
10 from amberelectric.models.channel import ChannelType
11 from amberelectric.models.current_interval import CurrentInterval
12 from amberelectric.models.forecast_interval import ForecastInterval
13 from amberelectric.models.price_descriptor import PriceDescriptor
14 from amberelectric.rest import ApiException
15 
16 from homeassistant.core import HomeAssistant
17 from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
18 
19 from .const import LOGGER
20 
21 
22 def is_current(interval: ActualInterval | CurrentInterval | ForecastInterval) -> bool:
23  """Return true if the supplied interval is a CurrentInterval."""
24  return isinstance(interval, CurrentInterval)
25 
26 
27 def is_forecast(interval: ActualInterval | CurrentInterval | ForecastInterval) -> bool:
28  """Return true if the supplied interval is a ForecastInterval."""
29  return isinstance(interval, ForecastInterval)
30 
31 
32 def is_general(interval: ActualInterval | CurrentInterval | ForecastInterval) -> bool:
33  """Return true if the supplied interval is on the general channel."""
34  return interval.channel_type == ChannelType.GENERAL
35 
36 
38  interval: ActualInterval | CurrentInterval | ForecastInterval,
39 ) -> bool:
40  """Return true if the supplied interval is on the controlled load channel."""
41  return interval.channel_type == ChannelType.CONTROLLEDLOAD
42 
43 
44 def is_feed_in(interval: ActualInterval | CurrentInterval | ForecastInterval) -> bool:
45  """Return true if the supplied interval is on the feed in channel."""
46  return interval.channel_type == ChannelType.FEEDIN
47 
48 
49 def normalize_descriptor(descriptor: PriceDescriptor | None) -> str | None:
50  """Return the snake case versions of descriptor names. Returns None if the name is not recognized."""
51  if descriptor is None:
52  return None
53  if descriptor.value == "spike":
54  return "spike"
55  if descriptor.value == "high":
56  return "high"
57  if descriptor.value == "neutral":
58  return "neutral"
59  if descriptor.value == "low":
60  return "low"
61  if descriptor.value == "veryLow":
62  return "very_low"
63  if descriptor.value == "extremelyLow":
64  return "extremely_low"
65  if descriptor.value == "negative":
66  return "negative"
67  return None
68 
69 
71  """AmberUpdateCoordinator - In charge of downloading the data for a site, which all the sensors read."""
72 
73  def __init__(
74  self, hass: HomeAssistant, api: amberelectric.AmberApi, site_id: str
75  ) -> None:
76  """Initialise the data service."""
77  super().__init__(
78  hass,
79  LOGGER,
80  name="amberelectric",
81  update_interval=timedelta(minutes=1),
82  )
83  self._api_api = api
84  self.site_idsite_id = site_id
85 
86  def update_price_data(self) -> dict[str, dict[str, Any]]:
87  """Update callback."""
88 
89  result: dict[str, dict[str, Any]] = {
90  "current": {},
91  "descriptors": {},
92  "forecasts": {},
93  "grid": {},
94  }
95  try:
96  data = self._api_api.get_current_prices(self.site_idsite_id, next=48)
97  intervals = [interval.actual_instance for interval in data]
98  except ApiException as api_exception:
99  raise UpdateFailed("Missing price data, skipping update") from api_exception
100 
101  current = [interval for interval in intervals if is_current(interval)]
102  forecasts = [interval for interval in intervals if is_forecast(interval)]
103  general = [interval for interval in current if is_general(interval)]
104 
105  if len(general) == 0:
106  raise UpdateFailed("No general channel configured")
107 
108  result["current"]["general"] = general[0]
109  result["descriptors"]["general"] = normalize_descriptor(general[0].descriptor)
110  result["forecasts"]["general"] = [
111  interval for interval in forecasts if is_general(interval)
112  ]
113  result["grid"]["renewables"] = round(general[0].renewables)
114  result["grid"]["price_spike"] = general[0].spike_status.value
115  tariff_information = general[0].tariff_information
116  if tariff_information:
117  result["grid"]["demand_window"] = tariff_information.demand_window
118 
119  controlled_load = [
120  interval for interval in current if is_controlled_load(interval)
121  ]
122  if controlled_load:
123  result["current"]["controlled_load"] = controlled_load[0]
124  result["descriptors"]["controlled_load"] = normalize_descriptor(
125  controlled_load[0].descriptor
126  )
127  result["forecasts"]["controlled_load"] = [
128  interval for interval in forecasts if is_controlled_load(interval)
129  ]
130 
131  feed_in = [interval for interval in current if is_feed_in(interval)]
132  if feed_in:
133  result["current"]["feed_in"] = feed_in[0]
134  result["descriptors"]["feed_in"] = normalize_descriptor(
135  feed_in[0].descriptor
136  )
137  result["forecasts"]["feed_in"] = [
138  interval for interval in forecasts if is_feed_in(interval)
139  ]
140 
141  LOGGER.debug("Fetched new Amber data: %s", intervals)
142  return result
143 
144  async def _async_update_data(self) -> dict[str, Any]:
145  """Async update wrapper."""
146  return await self.hasshass.async_add_executor_job(self.update_price_dataupdate_price_data)
None __init__(self, HomeAssistant hass, amberelectric.AmberApi api, str site_id)
Definition: coordinator.py:75
bool is_forecast(ActualInterval|CurrentInterval|ForecastInterval interval)
Definition: coordinator.py:27
str|None normalize_descriptor(PriceDescriptor|None descriptor)
Definition: coordinator.py:49
bool is_controlled_load(ActualInterval|CurrentInterval|ForecastInterval interval)
Definition: coordinator.py:39
bool is_feed_in(ActualInterval|CurrentInterval|ForecastInterval interval)
Definition: coordinator.py:44
bool is_current(ActualInterval|CurrentInterval|ForecastInterval interval)
Definition: coordinator.py:22
bool is_general(ActualInterval|CurrentInterval|ForecastInterval interval)
Definition: coordinator.py:32