Home Assistant Unofficial Reference 2024.12.1
sensor.py
Go to the documentation of this file.
1 """Real-time information about public transport departures in Norway."""
2 
3 from __future__ import annotations
4 
5 from datetime import datetime, timedelta
6 from random import randint
7 
8 from enturclient import EnturPublicTransportData
9 import voluptuous as vol
10 
12  PLATFORM_SCHEMA as SENSOR_PLATFORM_SCHEMA,
13  SensorEntity,
14 )
15 from homeassistant.const import (
16  CONF_LATITUDE,
17  CONF_LONGITUDE,
18  CONF_NAME,
19  CONF_SHOW_ON_MAP,
20  UnitOfTime,
21 )
22 from homeassistant.core import HomeAssistant
23 from homeassistant.helpers.aiohttp_client import async_get_clientsession
25 from homeassistant.helpers.entity_platform import AddEntitiesCallback
26 from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType
27 from homeassistant.util import Throttle
28 import homeassistant.util.dt as dt_util
29 
30 API_CLIENT_NAME = "homeassistant-{}"
31 
32 CONF_STOP_IDS = "stop_ids"
33 CONF_EXPAND_PLATFORMS = "expand_platforms"
34 CONF_WHITELIST_LINES = "line_whitelist"
35 CONF_OMIT_NON_BOARDING = "omit_non_boarding"
36 CONF_NUMBER_OF_DEPARTURES = "number_of_departures"
37 
38 DEFAULT_NAME = "Entur"
39 DEFAULT_ICON_KEY = "bus"
40 
41 ICONS = {
42  "air": "mdi:airplane",
43  "bus": "mdi:bus",
44  "metro": "mdi:subway",
45  "rail": "mdi:train",
46  "tram": "mdi:tram",
47  "water": "mdi:ferry",
48 }
49 
50 SCAN_INTERVAL = timedelta(seconds=45)
51 
52 PLATFORM_SCHEMA = SENSOR_PLATFORM_SCHEMA.extend(
53  {
54  vol.Required(CONF_STOP_IDS): vol.All(cv.ensure_list, [cv.string]),
55  vol.Optional(CONF_EXPAND_PLATFORMS, default=True): cv.boolean,
56  vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
57  vol.Optional(CONF_SHOW_ON_MAP, default=False): cv.boolean,
58  vol.Optional(CONF_WHITELIST_LINES, default=[]): cv.ensure_list,
59  vol.Optional(CONF_OMIT_NON_BOARDING, default=True): cv.boolean,
60  vol.Optional(CONF_NUMBER_OF_DEPARTURES, default=2): vol.All(
61  cv.positive_int, vol.Range(min=2, max=10)
62  ),
63  }
64 )
65 
66 
67 ATTR_STOP_ID = "stop_id"
68 
69 ATTR_ROUTE = "route"
70 ATTR_ROUTE_ID = "route_id"
71 ATTR_EXPECTED_AT = "due_at"
72 ATTR_DELAY = "delay"
73 ATTR_REALTIME = "real_time"
74 
75 ATTR_NEXT_UP_IN = "next_due_in"
76 ATTR_NEXT_UP_ROUTE = "next_route"
77 ATTR_NEXT_UP_ROUTE_ID = "next_route_id"
78 ATTR_NEXT_UP_AT = "next_due_at"
79 ATTR_NEXT_UP_DELAY = "next_delay"
80 ATTR_NEXT_UP_REALTIME = "next_real_time"
81 
82 ATTR_TRANSPORT_MODE = "transport_mode"
83 
84 
85 def due_in_minutes(timestamp: datetime) -> int:
86  """Get the time in minutes from a timestamp."""
87  if timestamp is None:
88  return None
89  diff = timestamp - dt_util.now()
90  return int(diff.total_seconds() / 60)
91 
92 
94  hass: HomeAssistant,
95  config: ConfigType,
96  async_add_entities: AddEntitiesCallback,
97  discovery_info: DiscoveryInfoType | None = None,
98 ) -> None:
99  """Set up the Entur public transport sensor."""
100 
101  expand = config[CONF_EXPAND_PLATFORMS]
102  line_whitelist = config[CONF_WHITELIST_LINES]
103  name = config[CONF_NAME]
104  show_on_map = config[CONF_SHOW_ON_MAP]
105  stop_ids = config[CONF_STOP_IDS]
106  omit_non_boarding = config[CONF_OMIT_NON_BOARDING]
107  number_of_departures = config[CONF_NUMBER_OF_DEPARTURES]
108 
109  stops = [s for s in stop_ids if "StopPlace" in s]
110  quays = [s for s in stop_ids if "Quay" in s]
111 
112  data = EnturPublicTransportData(
113  API_CLIENT_NAME.format(str(randint(100000, 999999))),
114  stops=stops,
115  quays=quays,
116  line_whitelist=line_whitelist,
117  omit_non_boarding=omit_non_boarding,
118  number_of_departures=number_of_departures,
119  web_session=async_get_clientsession(hass),
120  )
121 
122  if expand:
123  await data.expand_all_quays()
124  await data.update()
125 
126  proxy = EnturProxy(data)
127 
128  entities = []
129  for place in data.all_stop_places_quays():
130  try:
131  given_name = f"{name} {data.get_stop_info(place).name}"
132  except KeyError:
133  given_name = f"{name} {place}"
134 
135  entities.append(
136  EnturPublicTransportSensor(proxy, given_name, place, show_on_map)
137  )
138 
139  async_add_entities(entities, True)
140 
141 
143  """Proxy for the Entur client.
144 
145  Ensure throttle to not hit rate limiting on the API.
146  """
147 
148  def __init__(self, api):
149  """Initialize the proxy."""
150  self._api_api = api
151 
152  @Throttle(timedelta(seconds=15))
153  async def async_update(self) -> None:
154  """Update data in client."""
155  await self._api_api.update()
156 
157  def get_stop_info(self, stop_id: str) -> dict:
158  """Get info about specific stop place."""
159  return self._api_api.get_stop_info(stop_id)
160 
161 
163  """Implementation of a Entur public transport sensor."""
164 
165  _attr_attribution = "Data provided by entur.org under NLOD"
166 
167  def __init__(
168  self, api: EnturProxy, name: str, stop: str, show_on_map: bool
169  ) -> None:
170  """Initialize the sensor."""
171  self.apiapi = api
172  self._stop_stop = stop
173  self._show_on_map_show_on_map = show_on_map
174  self._name_name = name
175  self._state_state: int | None = None
176  self._icon_icon = ICONS[DEFAULT_ICON_KEY]
177  self._attributes_attributes: dict[str, str] = {}
178 
179  @property
180  def name(self) -> str:
181  """Return the name of the sensor."""
182  return self._name_name
183 
184  @property
185  def native_value(self) -> int | None:
186  """Return the state of the sensor."""
187  return self._state_state
188 
189  @property
190  def extra_state_attributes(self) -> dict[str, str]:
191  """Return the state attributes."""
192  self._attributes_attributes[ATTR_STOP_ID] = self._stop_stop
193  return self._attributes_attributes
194 
195  @property
196  def native_unit_of_measurement(self) -> str:
197  """Return the unit this state is expressed in."""
198  return UnitOfTime.MINUTES
199 
200  @property
201  def icon(self) -> str:
202  """Icon to use in the frontend."""
203  return self._icon_icon
204 
205  async def async_update(self) -> None:
206  """Get the latest data and update the states."""
207  await self.apiapi.async_update()
208 
209  self._attributes_attributes = {}
210 
211  data: EnturPublicTransportData = self.apiapi.get_stop_info(self._stop_stop)
212  if data is None:
213  self._state_state = None
214  return
215 
216  if self._show_on_map_show_on_map and data.latitude and data.longitude:
217  self._attributes_attributes[CONF_LATITUDE] = data.latitude
218  self._attributes_attributes[CONF_LONGITUDE] = data.longitude
219 
220  if not (calls := data.estimated_calls):
221  self._state_state = None
222  return
223 
224  self._state_state = due_in_minutes(calls[0].expected_departure_time)
225  self._icon_icon = ICONS.get(calls[0].transport_mode, ICONS[DEFAULT_ICON_KEY])
226 
227  self._attributes_attributes[ATTR_ROUTE] = calls[0].front_display
228  self._attributes_attributes[ATTR_ROUTE_ID] = calls[0].line_id
229  self._attributes_attributes[ATTR_EXPECTED_AT] = calls[0].expected_departure_time.strftime(
230  "%H:%M"
231  )
232  self._attributes_attributes[ATTR_REALTIME] = calls[0].is_realtime
233  self._attributes_attributes[ATTR_DELAY] = calls[0].delay_in_min
234 
235  number_of_calls = len(calls)
236  if number_of_calls < 2:
237  return
238 
239  self._attributes_attributes[ATTR_NEXT_UP_ROUTE] = calls[1].front_display
240  self._attributes_attributes[ATTR_NEXT_UP_ROUTE_ID] = calls[1].line_id
241  self._attributes_attributes[ATTR_NEXT_UP_AT] = calls[1].expected_departure_time.strftime(
242  "%H:%M"
243  )
244  self._attributes_attributes[ATTR_NEXT_UP_IN] = (
245  f"{due_in_minutes(calls[1].expected_departure_time)} min"
246  )
247  self._attributes_attributes[ATTR_NEXT_UP_REALTIME] = calls[1].is_realtime
248  self._attributes_attributes[ATTR_NEXT_UP_DELAY] = calls[1].delay_in_min
249 
250  if number_of_calls < 3:
251  return
252 
253  for i, call in enumerate(calls[2:]):
254  key_name = f"departure_#{i + 3}"
255  self._attributes_attributes[key_name] = (
256  f"{'' if bool(call.is_realtime) else 'ca. '}"
257  f"{call.expected_departure_time.strftime('%H:%M')} {call.front_display}"
258  )
None __init__(self, EnturProxy api, str name, str stop, bool show_on_map)
Definition: sensor.py:169
None async_setup_platform(HomeAssistant hass, ConfigType config, AddEntitiesCallback async_add_entities, DiscoveryInfoType|None discovery_info=None)
Definition: sensor.py:98
IssData update(pyiss.ISS iss)
Definition: __init__.py:33
aiohttp.ClientSession async_get_clientsession(HomeAssistant hass, bool verify_ssl=True, socket.AddressFamily family=socket.AF_UNSPEC, ssl_util.SSLCipherList ssl_cipher=ssl_util.SSLCipherList.PYTHON_DEFAULT)