Home Assistant Unofficial Reference 2024.12.1
coordinator.py
Go to the documentation of this file.
1 """The HERE Travel Time integration."""
2 
3 from __future__ import annotations
4 
5 from datetime import datetime, time, timedelta
6 import logging
7 from typing import Any
8 
9 import here_routing
10 from here_routing import (
11  HERERoutingApi,
12  HERERoutingTooManyRequestsError,
13  Return,
14  RoutingMode,
15  Spans,
16  TransportMode,
17 )
18 import here_transit
19 from here_transit import (
20  HERETransitApi,
21  HERETransitConnectionError,
22  HERETransitDepartureArrivalTooCloseError,
23  HERETransitNoRouteFoundError,
24  HERETransitTooManyRequestsError,
25 )
26 import voluptuous as vol
27 
28 from homeassistant.const import UnitOfLength
29 from homeassistant.core import HomeAssistant
31 from homeassistant.helpers.location import find_coordinates
32 from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
33 from homeassistant.util import dt as dt_util
34 from homeassistant.util.unit_conversion import DistanceConverter
35 
36 from .const import DEFAULT_SCAN_INTERVAL, DOMAIN, ROUTE_MODE_FASTEST
37 from .model import HERETravelTimeConfig, HERETravelTimeData
38 
39 BACKOFF_MULTIPLIER = 1.1
40 
41 _LOGGER = logging.getLogger(__name__)
42 
43 
45  """here_routing DataUpdateCoordinator."""
46 
47  def __init__(
48  self,
49  hass: HomeAssistant,
50  api_key: str,
51  config: HERETravelTimeConfig,
52  ) -> None:
53  """Initialize."""
54  super().__init__(
55  hass,
56  _LOGGER,
57  name=DOMAIN,
58  update_interval=timedelta(seconds=DEFAULT_SCAN_INTERVAL),
59  )
60  self._api_api = HERERoutingApi(api_key)
61  self.configconfig = config
62 
63  async def _async_update_data(self) -> HERETravelTimeData:
64  """Get the latest data from the HERE Routing API."""
65  origin, destination, arrival, departure = prepare_parameters(
66  self.hasshass, self.configconfig
67  )
68 
69  route_mode = (
70  RoutingMode.FAST
71  if self.configconfig.route_mode == ROUTE_MODE_FASTEST
72  else RoutingMode.SHORT
73  )
74 
75  _LOGGER.debug(
76  (
77  "Requesting route for origin: %s, destination: %s, route_mode: %s,"
78  " mode: %s, arrival: %s, departure: %s"
79  ),
80  origin,
81  destination,
82  route_mode,
83  TransportMode(self.configconfig.travel_mode),
84  arrival,
85  departure,
86  )
87 
88  try:
89  response = await self._api_api.route(
90  transport_mode=TransportMode(self.configconfig.travel_mode),
91  origin=here_routing.Place(origin[0], origin[1]),
92  destination=here_routing.Place(destination[0], destination[1]),
93  routing_mode=route_mode,
94  arrival_time=arrival,
95  departure_time=departure,
96  return_values=[Return.POLYINE, Return.SUMMARY],
97  spans=[Spans.NAMES],
98  )
99  except HERERoutingTooManyRequestsError as error:
100  assert self.update_intervalupdate_intervalupdate_intervalupdate_intervalupdate_interval is not None
101  _LOGGER.debug(
102  "Rate limit has been reached. Increasing update interval to %s",
103  self.update_intervalupdate_intervalupdate_intervalupdate_intervalupdate_interval.total_seconds() * BACKOFF_MULTIPLIER,
104  )
106  seconds=self.update_intervalupdate_intervalupdate_intervalupdate_intervalupdate_interval.total_seconds() * BACKOFF_MULTIPLIER
107  )
108  raise UpdateFailed("Rate limit has been reached") from error
109  _LOGGER.debug("Raw response is: %s", response)
110 
111  if self.update_intervalupdate_intervalupdate_intervalupdate_intervalupdate_interval != timedelta(seconds=DEFAULT_SCAN_INTERVAL):
112  _LOGGER.debug(
113  "Resetting update interval to %s",
114  DEFAULT_SCAN_INTERVAL,
115  )
116  self.update_intervalupdate_intervalupdate_intervalupdate_intervalupdate_interval = timedelta(seconds=DEFAULT_SCAN_INTERVAL)
117  return self._parse_routing_response_parse_routing_response(response)
118 
119  def _parse_routing_response(self, response: dict[str, Any]) -> HERETravelTimeData:
120  """Parse the routing response dict to a HERETravelTimeData."""
121  distance: float = 0.0
122  duration: float = 0.0
123  duration_in_traffic: float = 0.0
124 
125  for section in response["routes"][0]["sections"]:
126  distance += DistanceConverter.convert(
127  section["summary"]["length"],
128  UnitOfLength.METERS,
129  UnitOfLength.KILOMETERS,
130  )
131  duration += section["summary"]["baseDuration"]
132  duration_in_traffic += section["summary"]["duration"]
133 
134  first_section = response["routes"][0]["sections"][0]
135  last_section = response["routes"][0]["sections"][-1]
136  mapped_origin_lat: float = first_section["departure"]["place"]["location"][
137  "lat"
138  ]
139  mapped_origin_lon: float = first_section["departure"]["place"]["location"][
140  "lng"
141  ]
142  mapped_destination_lat: float = last_section["arrival"]["place"]["location"][
143  "lat"
144  ]
145  mapped_destination_lon: float = last_section["arrival"]["place"]["location"][
146  "lng"
147  ]
148  origin_name: str | None = None
149  if (names := first_section["spans"][0].get("names")) is not None:
150  origin_name = names[0]["value"]
151  destination_name: str | None = None
152  if (names := last_section["spans"][-1].get("names")) is not None:
153  destination_name = names[0]["value"]
154  return HERETravelTimeData(
155  attribution=None,
156  duration=round(duration / 60),
157  duration_in_traffic=round(duration_in_traffic / 60),
158  distance=distance,
159  origin=f"{mapped_origin_lat},{mapped_origin_lon}",
160  destination=f"{mapped_destination_lat},{mapped_destination_lon}",
161  origin_name=origin_name,
162  destination_name=destination_name,
163  )
164 
165 
167  DataUpdateCoordinator[HERETravelTimeData | None]
168 ):
169  """HERETravelTime DataUpdateCoordinator."""
170 
171  def __init__(
172  self,
173  hass: HomeAssistant,
174  api_key: str,
175  config: HERETravelTimeConfig,
176  ) -> None:
177  """Initialize."""
178  super().__init__(
179  hass,
180  _LOGGER,
181  name=DOMAIN,
182  update_interval=timedelta(seconds=DEFAULT_SCAN_INTERVAL),
183  )
184  self._api_api = HERETransitApi(api_key)
185  self.configconfig = config
186 
187  async def _async_update_data(self) -> HERETravelTimeData | None:
188  """Get the latest data from the HERE Routing API."""
189  origin, destination, arrival, departure = prepare_parameters(
190  self.hasshass, self.configconfig
191  )
192 
193  _LOGGER.debug(
194  (
195  "Requesting transit route for origin: %s, destination: %s, arrival: %s,"
196  " departure: %s"
197  ),
198  origin,
199  destination,
200  arrival,
201  departure,
202  )
203  try:
204  response = await self._api_api.route(
205  origin=here_transit.Place(latitude=origin[0], longitude=origin[1]),
206  destination=here_transit.Place(
207  latitude=destination[0], longitude=destination[1]
208  ),
209  arrival_time=arrival,
210  departure_time=departure,
211  return_values=[
212  here_transit.Return.POLYLINE,
213  here_transit.Return.TRAVEL_SUMMARY,
214  ],
215  )
216  except HERETransitTooManyRequestsError as error:
217  assert self.update_intervalupdate_intervalupdate_intervalupdate_intervalupdate_interval is not None
218  _LOGGER.debug(
219  "Rate limit has been reached. Increasing update interval to %s",
220  self.update_intervalupdate_intervalupdate_intervalupdate_intervalupdate_interval.total_seconds() * BACKOFF_MULTIPLIER,
221  )
223  seconds=self.update_intervalupdate_intervalupdate_intervalupdate_intervalupdate_interval.total_seconds() * BACKOFF_MULTIPLIER
224  )
225  raise UpdateFailed("Rate limit has been reached") from error
226  except HERETransitDepartureArrivalTooCloseError:
227  _LOGGER.debug("Ignoring HERETransitDepartureArrivalTooCloseError")
228  return None
229  except (HERETransitConnectionError, HERETransitNoRouteFoundError) as error:
230  raise UpdateFailed from error
231 
232  _LOGGER.debug("Raw response is: %s", response)
233  if self.update_intervalupdate_intervalupdate_intervalupdate_intervalupdate_interval != timedelta(seconds=DEFAULT_SCAN_INTERVAL):
234  _LOGGER.debug(
235  "Resetting update interval to %s",
236  DEFAULT_SCAN_INTERVAL,
237  )
238  self.update_intervalupdate_intervalupdate_intervalupdate_intervalupdate_interval = timedelta(seconds=DEFAULT_SCAN_INTERVAL)
239  return self._parse_transit_response_parse_transit_response(response)
240 
241  def _parse_transit_response(self, response: dict[str, Any]) -> HERETravelTimeData:
242  """Parse the transit response dict to a HERETravelTimeData."""
243  sections: list[dict[str, Any]] = response["routes"][0]["sections"]
244  attribution: str | None = build_hass_attribution(sections)
245  mapped_origin_lat: float = sections[0]["departure"]["place"]["location"]["lat"]
246  mapped_origin_lon: float = sections[0]["departure"]["place"]["location"]["lng"]
247  mapped_destination_lat: float = sections[-1]["arrival"]["place"]["location"][
248  "lat"
249  ]
250  mapped_destination_lon: float = sections[-1]["arrival"]["place"]["location"][
251  "lng"
252  ]
253  distance: float = DistanceConverter.convert(
254  sum(section["travelSummary"]["length"] for section in sections),
255  UnitOfLength.METERS,
256  UnitOfLength.KILOMETERS,
257  )
258  duration: float = sum(
259  section["travelSummary"]["duration"] for section in sections
260  )
261  return HERETravelTimeData(
262  attribution=attribution,
263  duration=round(duration / 60),
264  duration_in_traffic=round(duration / 60),
265  distance=distance,
266  origin=f"{mapped_origin_lat},{mapped_origin_lon}",
267  destination=f"{mapped_destination_lat},{mapped_destination_lon}",
268  origin_name=sections[0]["departure"]["place"].get("name"),
269  destination_name=sections[-1]["arrival"]["place"].get("name"),
270  )
271 
272 
274  hass: HomeAssistant,
275  config: HERETravelTimeConfig,
276 ) -> tuple[list[str], list[str], str | None, str | None]:
277  """Prepare parameters for the HERE api."""
278 
279  def _from_entity_id(entity_id: str) -> list[str]:
280  coordinates = find_coordinates(hass, entity_id)
281  if coordinates is None:
282  raise UpdateFailed(f"No coordinates found for {entity_id}")
283  if coordinates is entity_id:
284  raise UpdateFailed(f"Could not find entity {entity_id}")
285  try:
286  formatted_coordinates = coordinates.split(",")
287  vol.Schema(cv.gps(formatted_coordinates))
288  except (AttributeError, vol.ExactSequenceInvalid) as ex:
289  raise UpdateFailed(
290  f"{entity_id} does not have valid coordinates: {coordinates}"
291  ) from ex
292  return formatted_coordinates
293 
294  # Destination
295  if config.destination_entity_id is not None:
296  destination = _from_entity_id(config.destination_entity_id)
297  else:
298  destination = [
299  str(config.destination_latitude),
300  str(config.destination_longitude),
301  ]
302 
303  # Origin
304  if config.origin_entity_id is not None:
305  origin = _from_entity_id(config.origin_entity_id)
306  else:
307  origin = [
308  str(config.origin_latitude),
309  str(config.origin_longitude),
310  ]
311 
312  # Arrival/Departure
313  arrival: str | None = None
314  departure: str | None = None
315  if config.arrival is not None:
316  arrival = next_datetime(config.arrival).isoformat()
317  if config.departure is not None:
318  departure = next_datetime(config.departure).isoformat()
319 
320  return (origin, destination, arrival, departure)
321 
322 
323 def build_hass_attribution(sections: list[dict[str, Any]]) -> str | None:
324  """Build a hass frontend ready string out of the attributions."""
325  relevant_attributions = []
326  for section in sections:
327  if (attributions := section.get("attributions")) is not None:
328  for attribution in attributions:
329  if (href := attribution.get("href")) is not None:
330  relevant_attributions.append(f"{href}")
331  if (text := attribution.get("text")) is not None:
332  relevant_attributions.append(text)
333  if len(relevant_attributions) > 0:
334  return ",".join(relevant_attributions)
335  return None
336 
337 
338 def next_datetime(simple_time: time) -> datetime:
339  """Take a time like 08:00:00 and combine it with the current date."""
340  combined = datetime.combine(dt_util.start_of_local_day(), simple_time)
341  if combined < datetime.now():
342  combined = combined + timedelta(days=1)
343  return combined
None __init__(self, HomeAssistant hass, str api_key, HERETravelTimeConfig config)
Definition: coordinator.py:52
None __init__(self, HomeAssistant hass, str api_key, HERETravelTimeConfig config)
Definition: coordinator.py:176
web.Response get(self, web.Request request, str config_key)
Definition: view.py:88
tuple[list[str], list[str], str|None, str|None] prepare_parameters(HomeAssistant hass, HERETravelTimeConfig config)
Definition: coordinator.py:276
str|None build_hass_attribution(list[dict[str, Any]] sections)
Definition: coordinator.py:323
str|None find_coordinates(HomeAssistant hass, str name, list|None recursion_history=None)
Definition: location.py:51