1 """Support for UK public transport data provided by transportapi.com."""
3 from __future__
import annotations
5 from datetime
import datetime, timedelta
6 from http
import HTTPStatus
12 import voluptuous
as vol
15 PLATFORM_SCHEMA
as SENSOR_PLATFORM_SCHEMA,
26 _LOGGER = logging.getLogger(__name__)
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"
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"
43 _QUERY_SCHEME = vol.Schema(
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,
51 PLATFORM_SCHEMA = SENSOR_PLATFORM_SCHEMA.extend(
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],
63 add_entities: AddEntitiesCallback,
64 discovery_info: DiscoveryInfoType |
None =
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)
71 api_app_id = config[CONF_API_APP_ID]
72 api_app_key = config[CONF_API_APP_KEY]
75 if "bus" in query.get(CONF_MODE):
76 stop_atcocode = query.get(CONF_ORIGIN)
77 bus_direction = query.get(CONF_DESTINATION)
88 elif "train" in query.get(CONF_MODE):
89 station_code = query.get(CONF_ORIGIN)
90 calling_at = query.get(CONF_DESTINATION)
105 """Sensor that reads the UK transport web API.
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.
112 TRANSPORT_API_URL_BASE =
"https://transportapi.com/v3/uk/"
113 _attr_icon =
"mdi:train"
114 _attr_native_unit_of_measurement = UnitOfTime.MINUTES
116 def __init__(self, name, api_app_id, api_app_key, url):
117 """Initialize the sensor."""
127 """Return the name of the sensor."""
128 return self.
_name_name
132 """Return the state of the sensor."""
136 """Perform an API request."""
137 request_params =
dict(
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"
150 self.
_data_data = response.json()
154 """Live bus time sensor from UK transportapi.com."""
156 _attr_icon =
"mdi:bus"
158 def __init__(self, api_app_id, api_app_key, stop_atcocode, bus_direction, interval):
159 """Construct a live bus time sensor."""
165 sensor_name = f
"Next bus to {bus_direction}"
166 stop_url = f
"bus/stop/{stop_atcocode}/live.json"
168 UkTransportSensor.__init__(self, sensor_name, api_app_id, api_app_key, stop_url)
172 """Get the latest live departure data for the specified stop."""
173 params = {
"group":
"route",
"nextbuses":
"no"}
177 if self.
_data_data != {}:
180 for route, departures
in self.
_data_data[
"departures"].items():
181 for departure
in departures:
186 "direction": departure[
"direction"],
187 "scheduled": departure[
"aimed_departure_time"],
188 "estimated": departure[
"best_departure_estimate"],
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}
210 attrs[key] = self.
_data_data.
get(key)
216 """Live train time sensor from UK transportapi.com."""
218 _attr_icon =
"mdi:train"
220 def __init__(self, api_app_id, api_app_key, station_code, calling_at, interval):
221 """Construct a live bus time sensor."""
226 sensor_name = f
"Next train to {calling_at}"
227 query_url = f
"train/station/{station_code}/live.json"
229 UkTransportSensor.__init__(
230 self, sensor_name, api_app_id, api_app_key, query_url
235 """Get the latest live departure data for the specified stop."""
239 "train_status":
"passenger",
245 if self.
_data_data != {}:
246 if self.
_data_data[
"departures"][
"all"] == []:
249 for departure
in self.
_data_data[
"departures"][
"all"]:
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"],
271 """Return other details about the sensor state."""
272 if self.
_data_data
is not None:
284 """Calculate time delta in minutes to a time in hh:mm format."""
286 hhmm_time = datetime.strptime(hhmm_time_str,
"%H:%M")
288 hhmm_datetime = now.replace(hour=hhmm_time.hour, minute=hhmm_time.minute)
290 if hhmm_datetime < now:
293 return (hhmm_datetime - now).total_seconds() // 60
dict[str, Any]|None extra_state_attributes(self)
def __init__(self, api_app_id, api_app_key, stop_atcocode, bus_direction, interval)
dict[str, Any]|None extra_state_attributes(self)
def __init__(self, api_app_id, api_app_key, station_code, calling_at, interval)
string TRANSPORT_API_URL_BASE
def __init__(self, name, api_app_id, api_app_key, url)
def _do_api_request(self, params)
web.Response get(self, web.Request request, str config_key)
def add_entities(account, async_add_entities, tracked)
None setup_platform(HomeAssistant hass, ConfigType config, AddEntitiesCallback add_entities, DiscoveryInfoType|None discovery_info=None)
def _delta_mins(hhmm_time_str)