1 """Real-time information about public transport departures in Norway."""
3 from __future__
import annotations
5 from datetime
import datetime, timedelta
6 from random
import randint
8 from enturclient
import EnturPublicTransportData
9 import voluptuous
as vol
12 PLATFORM_SCHEMA
as SENSOR_PLATFORM_SCHEMA,
30 API_CLIENT_NAME =
"homeassistant-{}"
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"
38 DEFAULT_NAME =
"Entur"
39 DEFAULT_ICON_KEY =
"bus"
42 "air":
"mdi:airplane",
44 "metro":
"mdi:subway",
52 PLATFORM_SCHEMA = SENSOR_PLATFORM_SCHEMA.extend(
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)
67 ATTR_STOP_ID =
"stop_id"
70 ATTR_ROUTE_ID =
"route_id"
71 ATTR_EXPECTED_AT =
"due_at"
73 ATTR_REALTIME =
"real_time"
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"
82 ATTR_TRANSPORT_MODE =
"transport_mode"
86 """Get the time in minutes from a timestamp."""
89 diff = timestamp - dt_util.now()
90 return int(diff.total_seconds() / 60)
96 async_add_entities: AddEntitiesCallback,
97 discovery_info: DiscoveryInfoType |
None =
None,
99 """Set up the Entur public transport sensor."""
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]
109 stops = [s
for s
in stop_ids
if "StopPlace" in s]
110 quays = [s
for s
in stop_ids
if "Quay" in s]
112 data = EnturPublicTransportData(
113 API_CLIENT_NAME.format(
str(randint(100000, 999999))),
116 line_whitelist=line_whitelist,
117 omit_non_boarding=omit_non_boarding,
118 number_of_departures=number_of_departures,
123 await data.expand_all_quays()
129 for place
in data.all_stop_places_quays():
131 given_name = f
"{name} {data.get_stop_info(place).name}"
133 given_name = f
"{name} {place}"
143 """Proxy for the Entur client.
145 Ensure throttle to not hit rate limiting on the API.
149 """Initialize the proxy."""
152 @Throttle(timedelta(seconds=15))
154 """Update data in client."""
158 """Get info about specific stop place."""
163 """Implementation of a Entur public transport sensor."""
165 _attr_attribution =
"Data provided by entur.org under NLOD"
168 self, api: EnturProxy, name: str, stop: str, show_on_map: bool
170 """Initialize the sensor."""
175 self.
_state_state: int |
None =
None
176 self.
_icon_icon = ICONS[DEFAULT_ICON_KEY]
181 """Return the name of the sensor."""
182 return self.
_name_name
186 """Return the state of the sensor."""
191 """Return the state attributes."""
197 """Return the unit this state is expressed in."""
198 return UnitOfTime.MINUTES
202 """Icon to use in the frontend."""
203 return self.
_icon_icon
206 """Get the latest data and update the states."""
211 data: EnturPublicTransportData = self.
apiapi.get_stop_info(self.
_stop_stop)
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
220 if not (calls := data.estimated_calls):
225 self.
_icon_icon = ICONS.get(calls[0].transport_mode, ICONS[DEFAULT_ICON_KEY])
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(
232 self.
_attributes_attributes[ATTR_REALTIME] = calls[0].is_realtime
233 self.
_attributes_attributes[ATTR_DELAY] = calls[0].delay_in_min
235 number_of_calls = len(calls)
236 if number_of_calls < 2:
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(
245 f
"{due_in_minutes(calls[1].expected_departure_time)} min"
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
250 if number_of_calls < 3:
253 for i, call
in enumerate(calls[2:]):
254 key_name = f
"departure_#{i + 3}"
256 f
"{'' if bool(call.is_realtime) else 'ca. '}"
257 f
"{call.expected_departure_time.strftime('%H:%M')} {call.front_display}"
dict get_stop_info(self, str stop_id)
dict[str, str] extra_state_attributes(self)
int|None native_value(self)
str native_unit_of_measurement(self)
None __init__(self, EnturProxy api, str name, str stop, bool show_on_map)
int due_in_minutes(datetime timestamp)
None async_setup_platform(HomeAssistant hass, ConfigType config, AddEntitiesCallback async_add_entities, DiscoveryInfoType|None discovery_info=None)
IssData update(pyiss.ISS iss)
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)