Home Assistant Unofficial Reference 2024.12.1
sensor.py
Go to the documentation of this file.
1 """Support for UK public transport data provided by transportapi.com."""
2 
3 from __future__ import annotations
4 
5 from datetime import datetime, timedelta
6 from http import HTTPStatus
7 import logging
8 import re
9 from typing import Any
10 
11 import requests
12 import voluptuous as vol
13 
15  PLATFORM_SCHEMA as SENSOR_PLATFORM_SCHEMA,
16  SensorEntity,
17 )
18 from homeassistant.const import CONF_MODE, UnitOfTime
19 from homeassistant.core import HomeAssistant
21 from homeassistant.helpers.entity_platform import AddEntitiesCallback
22 from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType
23 from homeassistant.util import Throttle
24 import homeassistant.util.dt as dt_util
25 
26 _LOGGER = logging.getLogger(__name__)
27 
28 ATTR_ATCOCODE = "atcocode"
29 ATTR_LOCALITY = "locality"
30 ATTR_STOP_NAME = "stop_name"
31 ATTR_REQUEST_TIME = "request_time"
32 ATTR_NEXT_BUSES = "next_buses"
33 ATTR_STATION_CODE = "station_code"
34 ATTR_CALLING_AT = "calling_at"
35 ATTR_NEXT_TRAINS = "next_trains"
36 
37 CONF_API_APP_KEY = "app_key"
38 CONF_API_APP_ID = "app_id"
39 CONF_QUERIES = "queries"
40 CONF_ORIGIN = "origin"
41 CONF_DESTINATION = "destination"
42 
43 _QUERY_SCHEME = vol.Schema(
44  {
45  vol.Required(CONF_MODE): vol.All(cv.ensure_list, [vol.In(["bus", "train"])]),
46  vol.Required(CONF_ORIGIN): cv.string,
47  vol.Required(CONF_DESTINATION): cv.string,
48  }
49 )
50 
51 PLATFORM_SCHEMA = SENSOR_PLATFORM_SCHEMA.extend(
52  {
53  vol.Required(CONF_API_APP_ID): cv.string,
54  vol.Required(CONF_API_APP_KEY): cv.string,
55  vol.Required(CONF_QUERIES): [_QUERY_SCHEME],
56  }
57 )
58 
59 
61  hass: HomeAssistant,
62  config: ConfigType,
63  add_entities: AddEntitiesCallback,
64  discovery_info: DiscoveryInfoType | None = None,
65 ) -> None:
66  """Get the uk_transport sensor."""
67  sensors: list[UkTransportSensor] = []
68  number_sensors = len(queries := config[CONF_QUERIES])
69  interval = timedelta(seconds=87 * number_sensors)
70 
71  api_app_id = config[CONF_API_APP_ID]
72  api_app_key = config[CONF_API_APP_KEY]
73 
74  for query in queries:
75  if "bus" in query.get(CONF_MODE):
76  stop_atcocode = query.get(CONF_ORIGIN)
77  bus_direction = query.get(CONF_DESTINATION)
78  sensors.append(
80  api_app_id,
81  api_app_key,
82  stop_atcocode,
83  bus_direction,
84  interval,
85  )
86  )
87 
88  elif "train" in query.get(CONF_MODE):
89  station_code = query.get(CONF_ORIGIN)
90  calling_at = query.get(CONF_DESTINATION)
91  sensors.append(
93  api_app_id,
94  api_app_key,
95  station_code,
96  calling_at,
97  interval,
98  )
99  )
100 
101  add_entities(sensors, True)
102 
103 
105  """Sensor that reads the UK transport web API.
106 
107  transportapi.com provides comprehensive transport data for UK train, tube
108  and bus travel across the UK via simple JSON API. Subclasses of this
109  base class can be used to access specific types of information.
110  """
111 
112  TRANSPORT_API_URL_BASE = "https://transportapi.com/v3/uk/"
113  _attr_icon = "mdi:train"
114  _attr_native_unit_of_measurement = UnitOfTime.MINUTES
115 
116  def __init__(self, name, api_app_id, api_app_key, url):
117  """Initialize the sensor."""
118  self._data_data = {}
119  self._api_app_id_api_app_id = api_app_id
120  self._api_app_key_api_app_key = api_app_key
121  self._url_url = self.TRANSPORT_API_URL_BASETRANSPORT_API_URL_BASE + url
122  self._name_name = name
123  self._state_state = None
124 
125  @property
126  def name(self):
127  """Return the name of the sensor."""
128  return self._name_name
129 
130  @property
131  def native_value(self):
132  """Return the state of the sensor."""
133  return self._state_state
134 
135  def _do_api_request(self, params):
136  """Perform an API request."""
137  request_params = dict(
138  {"app_id": self._api_app_id_api_app_id, "app_key": self._api_app_key_api_app_key}, **params
139  )
140 
141  response = requests.get(self._url_url, params=request_params, timeout=10)
142  if response.status_code != HTTPStatus.OK:
143  _LOGGER.warning("Invalid response from API")
144  elif "error" in response.json():
145  if "exceeded" in response.json()["error"]:
146  self._state_state = "Usage limits exceeded"
147  if "invalid" in response.json()["error"]:
148  self._state_state = "Credentials invalid"
149  else:
150  self._data_data = response.json()
151 
152 
154  """Live bus time sensor from UK transportapi.com."""
155 
156  _attr_icon = "mdi:bus"
157 
158  def __init__(self, api_app_id, api_app_key, stop_atcocode, bus_direction, interval):
159  """Construct a live bus time sensor."""
160  self._stop_atcocode_stop_atcocode = stop_atcocode
161  self._bus_direction_bus_direction = bus_direction
162  self._next_buses_next_buses = []
163  self._destination_re_destination_re = re.compile(f"{bus_direction}", re.IGNORECASE)
164 
165  sensor_name = f"Next bus to {bus_direction}"
166  stop_url = f"bus/stop/{stop_atcocode}/live.json"
167 
168  UkTransportSensor.__init__(self, sensor_name, api_app_id, api_app_key, stop_url)
169  self.updateupdate = Throttle(interval)(self._update_update)
170 
171  def _update(self):
172  """Get the latest live departure data for the specified stop."""
173  params = {"group": "route", "nextbuses": "no"}
174 
175  self._do_api_request_do_api_request(params)
176 
177  if self._data_data != {}:
178  self._next_buses_next_buses = []
179 
180  for route, departures in self._data_data["departures"].items():
181  for departure in departures:
182  if self._destination_re_destination_re.search(departure["direction"]):
183  self._next_buses_next_buses.append(
184  {
185  "route": route,
186  "direction": departure["direction"],
187  "scheduled": departure["aimed_departure_time"],
188  "estimated": departure["best_departure_estimate"],
189  }
190  )
191 
192  if self._next_buses_next_buses:
193  self._state_state_state = min(
194  _delta_mins(bus["scheduled"]) for bus in self._next_buses_next_buses
195  )
196  else:
197  self._state_state_state = None
198 
199  @property
200  def extra_state_attributes(self) -> dict[str, Any] | None:
201  """Return other details about the sensor state."""
202  if self._data_data is not None:
203  attrs = {ATTR_NEXT_BUSES: self._next_buses_next_buses}
204  for key in (
205  ATTR_ATCOCODE,
206  ATTR_LOCALITY,
207  ATTR_STOP_NAME,
208  ATTR_REQUEST_TIME,
209  ):
210  attrs[key] = self._data_data.get(key)
211  return attrs
212  return None
213 
214 
216  """Live train time sensor from UK transportapi.com."""
217 
218  _attr_icon = "mdi:train"
219 
220  def __init__(self, api_app_id, api_app_key, station_code, calling_at, interval):
221  """Construct a live bus time sensor."""
222  self._station_code_station_code = station_code
223  self._calling_at_calling_at = calling_at
224  self._next_trains_next_trains = []
225 
226  sensor_name = f"Next train to {calling_at}"
227  query_url = f"train/station/{station_code}/live.json"
228 
229  UkTransportSensor.__init__(
230  self, sensor_name, api_app_id, api_app_key, query_url
231  )
232  self.updateupdate = Throttle(interval)(self._update_update)
233 
234  def _update(self):
235  """Get the latest live departure data for the specified stop."""
236  params = {
237  "darwin": "false",
238  "calling_at": self._calling_at_calling_at,
239  "train_status": "passenger",
240  }
241 
242  self._do_api_request_do_api_request(params)
243  self._next_trains_next_trains = []
244 
245  if self._data_data != {}:
246  if self._data_data["departures"]["all"] == []:
247  self._state_state_state = "No departures"
248  else:
249  for departure in self._data_data["departures"]["all"]:
250  self._next_trains_next_trains.append(
251  {
252  "origin_name": departure["origin_name"],
253  "destination_name": departure["destination_name"],
254  "status": departure["status"],
255  "scheduled": departure["aimed_departure_time"],
256  "estimated": departure["expected_departure_time"],
257  "platform": departure["platform"],
258  "operator_name": departure["operator_name"],
259  }
260  )
261 
262  if self._next_trains_next_trains:
263  self._state_state_state = min(
264  _delta_mins(train["scheduled"]) for train in self._next_trains_next_trains
265  )
266  else:
267  self._state_state_state = None
268 
269  @property
270  def extra_state_attributes(self) -> dict[str, Any] | None:
271  """Return other details about the sensor state."""
272  if self._data_data is not None:
273  attrs = {
274  ATTR_STATION_CODE: self._station_code_station_code,
275  ATTR_CALLING_AT: self._calling_at_calling_at,
276  }
277  if self._next_trains_next_trains:
278  attrs[ATTR_NEXT_TRAINS] = self._next_trains_next_trains
279  return attrs
280  return None
281 
282 
283 def _delta_mins(hhmm_time_str):
284  """Calculate time delta in minutes to a time in hh:mm format."""
285  now = dt_util.now()
286  hhmm_time = datetime.strptime(hhmm_time_str, "%H:%M")
287 
288  hhmm_datetime = now.replace(hour=hhmm_time.hour, minute=hhmm_time.minute)
289 
290  if hhmm_datetime < now:
291  hhmm_datetime += timedelta(days=1)
292 
293  return (hhmm_datetime - now).total_seconds() // 60
def __init__(self, api_app_id, api_app_key, stop_atcocode, bus_direction, interval)
Definition: sensor.py:158
def __init__(self, api_app_id, api_app_key, station_code, calling_at, interval)
Definition: sensor.py:220
def __init__(self, name, api_app_id, api_app_key, url)
Definition: sensor.py:116
web.Response get(self, web.Request request, str config_key)
Definition: view.py:88
def add_entities(account, async_add_entities, tracked)
Definition: sensor.py:40
None setup_platform(HomeAssistant hass, ConfigType config, AddEntitiesCallback add_entities, DiscoveryInfoType|None discovery_info=None)
Definition: sensor.py:65