Home Assistant Unofficial Reference 2024.12.1
sensor.py
Go to the documentation of this file.
1 """Support for Rejseplanen information from rejseplanen.dk.
2 
3 For more info on the API see:
4 https://help.rejseplanen.dk/hc/en-us/articles/214174465-Rejseplanen-s-API
5 """
6 
7 from __future__ import annotations
8 
9 from contextlib import suppress
10 from datetime import datetime, timedelta
11 import logging
12 from operator import itemgetter
13 
14 import rjpl
15 import voluptuous as vol
16 
18  PLATFORM_SCHEMA as SENSOR_PLATFORM_SCHEMA,
19  SensorEntity,
20 )
21 from homeassistant.const import CONF_NAME, UnitOfTime
22 from homeassistant.core import HomeAssistant
24 from homeassistant.helpers.entity_platform import AddEntitiesCallback
25 from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType
26 import homeassistant.util.dt as dt_util
27 
28 _LOGGER = logging.getLogger(__name__)
29 
30 ATTR_STOP_ID = "stop_id"
31 ATTR_STOP_NAME = "stop"
32 ATTR_ROUTE = "route"
33 ATTR_TYPE = "type"
34 ATTR_DIRECTION = "direction"
35 ATTR_FINAL_STOP = "final_stop"
36 ATTR_DUE_IN = "due_in"
37 ATTR_DUE_AT = "due_at"
38 ATTR_SCHEDULED_AT = "scheduled_at"
39 ATTR_REAL_TIME_AT = "real_time_at"
40 ATTR_TRACK = "track"
41 ATTR_NEXT_UP = "next_departures"
42 
43 CONF_STOP_ID = "stop_id"
44 CONF_ROUTE = "route"
45 CONF_DIRECTION = "direction"
46 CONF_DEPARTURE_TYPE = "departure_type"
47 
48 DEFAULT_NAME = "Next departure"
49 
50 
51 SCAN_INTERVAL = timedelta(minutes=1)
52 
53 BUS_TYPES = ["BUS", "EXB", "TB"]
54 TRAIN_TYPES = ["LET", "S", "REG", "IC", "LYN", "TOG"]
55 METRO_TYPES = ["M"]
56 
57 PLATFORM_SCHEMA = SENSOR_PLATFORM_SCHEMA.extend(
58  {
59  vol.Required(CONF_STOP_ID): cv.string,
60  vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
61  vol.Optional(CONF_ROUTE, default=[]): vol.All(cv.ensure_list, [cv.string]),
62  vol.Optional(CONF_DIRECTION, default=[]): vol.All(cv.ensure_list, [cv.string]),
63  vol.Optional(CONF_DEPARTURE_TYPE, default=[]): vol.All(
64  cv.ensure_list, [vol.In([*BUS_TYPES, *TRAIN_TYPES, *METRO_TYPES])]
65  ),
66  }
67 )
68 
69 
70 def due_in_minutes(timestamp):
71  """Get the time in minutes from a timestamp.
72 
73  The timestamp should be in the format day.month.year hour:minute
74  """
75  diff = datetime.strptime(timestamp, "%d.%m.%y %H:%M") - dt_util.now().replace(
76  tzinfo=None
77  )
78 
79  return int(diff.total_seconds() // 60)
80 
81 
83  hass: HomeAssistant,
84  config: ConfigType,
85  add_devices: AddEntitiesCallback,
86  discovery_info: DiscoveryInfoType | None = None,
87 ) -> None:
88  """Set up the Rejseplanen transport sensor."""
89  name = config[CONF_NAME]
90  stop_id = config[CONF_STOP_ID]
91  route = config.get(CONF_ROUTE)
92  direction = config[CONF_DIRECTION]
93  departure_type = config[CONF_DEPARTURE_TYPE]
94 
95  data = PublicTransportData(stop_id, route, direction, departure_type)
96  add_devices(
97  [RejseplanenTransportSensor(data, stop_id, route, direction, name)], True
98  )
99 
100 
102  """Implementation of Rejseplanen transport sensor."""
103 
104  _attr_attribution = "Data provided by rejseplanen.dk"
105  _attr_icon = "mdi:bus"
106 
107  def __init__(self, data, stop_id, route, direction, name):
108  """Initialize the sensor."""
109  self.datadata = data
110  self._name_name = name
111  self._stop_id_stop_id = stop_id
112  self._route_route = route
113  self._direction_direction = direction
114  self._times_times = self._state_state = None
115 
116  @property
117  def name(self):
118  """Return the name of the sensor."""
119  return self._name_name
120 
121  @property
122  def native_value(self):
123  """Return the state of the sensor."""
124  return self._state_state
125 
126  @property
128  """Return the state attributes."""
129  if not self._times_times:
130  return {ATTR_STOP_ID: self._stop_id_stop_id}
131 
132  next_up = []
133  if len(self._times_times) > 1:
134  next_up = self._times_times[1:]
135 
136  attributes = {
137  ATTR_NEXT_UP: next_up,
138  ATTR_STOP_ID: self._stop_id_stop_id,
139  }
140 
141  if self._times_times[0] is not None:
142  attributes.update(self._times_times[0])
143 
144  return attributes
145 
146  @property
148  """Return the unit this state is expressed in."""
149  return UnitOfTime.MINUTES
150 
151  def update(self) -> None:
152  """Get the latest data from rejseplanen.dk and update the states."""
153  self.datadata.update()
154  self._times_times = self.datadata.info
155 
156  if not self._times_times:
157  self._state_state = None
158  else:
159  with suppress(TypeError):
160  self._state_state = self._times_times[0][ATTR_DUE_IN]
161 
162 
164  """The Class for handling the data retrieval."""
165 
166  def __init__(self, stop_id, route, direction, departure_type):
167  """Initialize the data object."""
168  self.stop_idstop_id = stop_id
169  self.routeroute = route
170  self.directiondirection = direction
171  self.departure_typedeparture_type = departure_type
172  self.infoinfo = []
173 
174  def update(self):
175  """Get the latest data from rejseplanen."""
176  self.infoinfo = []
177 
178  def intersection(lst1, lst2):
179  """Return items contained in both lists."""
180  return list(set(lst1) & set(lst2))
181 
182  # Limit search to selected types, to get more results
183  all_types = not bool(self.departure_typedeparture_type)
184  use_train = all_types or bool(intersection(TRAIN_TYPES, self.departure_typedeparture_type))
185  use_bus = all_types or bool(intersection(BUS_TYPES, self.departure_typedeparture_type))
186  use_metro = all_types or bool(intersection(METRO_TYPES, self.departure_typedeparture_type))
187 
188  try:
189  results = rjpl.departureBoard(
190  int(self.stop_idstop_id),
191  timeout=5,
192  useTrain=use_train,
193  useBus=use_bus,
194  useMetro=use_metro,
195  )
196  except rjpl.rjplAPIError as error:
197  _LOGGER.debug("API returned error: %s", error)
198  return
199  except (rjpl.rjplConnectionError, rjpl.rjplHTTPError):
200  _LOGGER.debug("Error occurred while connecting to the API")
201  return
202 
203  # Filter result
204  results = [d for d in results if "cancelled" not in d]
205  if self.routeroute:
206  results = [d for d in results if d["name"] in self.routeroute]
207  if self.directiondirection:
208  results = [d for d in results if d["direction"] in self.directiondirection]
209  if self.departure_typedeparture_type:
210  results = [d for d in results if d["type"] in self.departure_typedeparture_type]
211 
212  for item in results:
213  route = item.get("name")
214 
215  scheduled_date = item.get("date")
216  scheduled_time = item.get("time")
217  real_time_date = due_at_date = item.get("rtDate")
218  real_time_time = due_at_time = item.get("rtTime")
219 
220  if due_at_date is None:
221  due_at_date = scheduled_date
222  if due_at_time is None:
223  due_at_time = scheduled_time
224 
225  if (
226  due_at_date is not None
227  and due_at_time is not None
228  and route is not None
229  ):
230  due_at = f"{due_at_date} {due_at_time}"
231  scheduled_at = f"{scheduled_date} {scheduled_time}"
232 
233  departure_data = {
234  ATTR_DIRECTION: item.get("direction"),
235  ATTR_DUE_IN: due_in_minutes(due_at),
236  ATTR_DUE_AT: due_at,
237  ATTR_FINAL_STOP: item.get("finalStop"),
238  ATTR_ROUTE: route,
239  ATTR_SCHEDULED_AT: scheduled_at,
240  ATTR_STOP_NAME: item.get("stop"),
241  ATTR_TYPE: item.get("type"),
242  }
243 
244  if real_time_date is not None and real_time_time is not None:
245  departure_data[ATTR_REAL_TIME_AT] = (
246  f"{real_time_date} {real_time_time}"
247  )
248  if item.get("rtTrack") is not None:
249  departure_data[ATTR_TRACK] = item.get("rtTrack")
250 
251  self.infoinfo.append(departure_data)
252 
253  if not self.infoinfo:
254  _LOGGER.debug("No departures with given parameters")
255 
256  # Sort the data by time
257  self.infoinfo = sorted(self.infoinfo, key=itemgetter(ATTR_DUE_IN))
def __init__(self, stop_id, route, direction, departure_type)
Definition: sensor.py:166
def __init__(self, data, stop_id, route, direction, name)
Definition: sensor.py:107
None setup_platform(HomeAssistant hass, ConfigType config, AddEntitiesCallback add_devices, DiscoveryInfoType|None discovery_info=None)
Definition: sensor.py:87