11"""Support for departure information for public transport in Munich."""
22
3- # mypy: ignore-errors
43from __future__ import annotations
54
5+ from collections .abc import Mapping
66from copy import deepcopy
77from datetime import timedelta
88import logging
9+ from typing import Any
910
10- import MVGLive
11+ from mvg import MvgApi , MvgApiError , TransportType
1112import voluptuous as vol
1213
1314from homeassistant .components .sensor import (
1920from homeassistant .helpers import config_validation as cv
2021from homeassistant .helpers .entity_platform import AddEntitiesCallback
2122from homeassistant .helpers .typing import ConfigType , DiscoveryInfoType
23+ import homeassistant .util .dt as dt_util
2224
2325_LOGGER = logging .getLogger (__name__ )
2426
4446 "SEV" : "mdi:checkbox-blank-circle-outline" ,
4547 "-" : "mdi:clock" ,
4648}
47- ATTRIBUTION = "Data provided by MVG-live.de"
49+
50+ ATTRIBUTION = "Data provided by mvg.de"
4851
4952SCAN_INTERVAL = timedelta (seconds = 30 )
5053
51- PLATFORM_SCHEMA = SENSOR_PLATFORM_SCHEMA .extend (
52- {
53- vol .Required (CONF_NEXT_DEPARTURE ): [
54- {
55- vol .Required (CONF_STATION ): cv .string ,
56- vol .Optional (CONF_DESTINATIONS , default = ["" ]): cv .ensure_list_csv ,
57- vol .Optional (CONF_DIRECTIONS , default = ["" ]): cv .ensure_list_csv ,
58- vol .Optional (CONF_LINES , default = ["" ]): cv .ensure_list_csv ,
59- vol .Optional (
60- CONF_PRODUCTS , default = DEFAULT_PRODUCT
61- ): cv .ensure_list_csv ,
62- vol .Optional (CONF_TIMEOFFSET , default = 0 ): cv .positive_int ,
63- vol .Optional (CONF_NUMBER , default = 1 ): cv .positive_int ,
64- vol .Optional (CONF_NAME ): cv .string ,
65- }
66- ]
67- }
54+ PLATFORM_SCHEMA = vol .All (
55+ cv .deprecated (CONF_DIRECTIONS ),
56+ SENSOR_PLATFORM_SCHEMA .extend (
57+ {
58+ vol .Required (CONF_NEXT_DEPARTURE ): [
59+ {
60+ vol .Required (CONF_STATION ): cv .string ,
61+ vol .Optional (CONF_DESTINATIONS , default = ["" ]): cv .ensure_list_csv ,
62+ vol .Optional (CONF_DIRECTIONS , default = ["" ]): cv .ensure_list_csv ,
63+ vol .Optional (CONF_LINES , default = ["" ]): cv .ensure_list_csv ,
64+ vol .Optional (
65+ CONF_PRODUCTS , default = DEFAULT_PRODUCT
66+ ): cv .ensure_list_csv ,
67+ vol .Optional (CONF_TIMEOFFSET , default = 0 ): cv .positive_int ,
68+ vol .Optional (CONF_NUMBER , default = 1 ): cv .positive_int ,
69+ vol .Optional (CONF_NAME ): cv .string ,
70+ }
71+ ]
72+ }
73+ ),
6874)
6975
7076
71- def setup_platform (
77+ async def async_setup_platform (
7278 hass : HomeAssistant ,
7379 config : ConfigType ,
7480 add_entities : AddEntitiesCallback ,
7581 discovery_info : DiscoveryInfoType | None = None ,
7682) -> None :
7783 """Set up the MVGLive sensor."""
78- add_entities (
79- (
80- MVGLiveSensor (
81- nextdeparture .get (CONF_STATION ),
82- nextdeparture .get (CONF_DESTINATIONS ),
83- nextdeparture .get (CONF_DIRECTIONS ),
84- nextdeparture .get (CONF_LINES ),
85- nextdeparture .get (CONF_PRODUCTS ),
86- nextdeparture .get (CONF_TIMEOFFSET ),
87- nextdeparture .get (CONF_NUMBER ),
88- nextdeparture .get (CONF_NAME ),
89- )
90- for nextdeparture in config [CONF_NEXT_DEPARTURE ]
91- ),
92- True ,
93- )
84+ sensors = [
85+ MVGLiveSensor (
86+ hass ,
87+ nextdeparture .get (CONF_STATION ),
88+ nextdeparture .get (CONF_DESTINATIONS ),
89+ nextdeparture .get (CONF_LINES ),
90+ nextdeparture .get (CONF_PRODUCTS ),
91+ nextdeparture .get (CONF_TIMEOFFSET ),
92+ nextdeparture .get (CONF_NUMBER ),
93+ nextdeparture .get (CONF_NAME ),
94+ )
95+ for nextdeparture in config [CONF_NEXT_DEPARTURE ]
96+ ]
97+ add_entities (sensors , True )
9498
9599
96100class MVGLiveSensor (SensorEntity ):
@@ -100,38 +104,38 @@ class MVGLiveSensor(SensorEntity):
100104
101105 def __init__ (
102106 self ,
103- station ,
107+ hass : HomeAssistant ,
108+ station_name ,
104109 destinations ,
105- directions ,
106110 lines ,
107111 products ,
108112 timeoffset ,
109113 number ,
110114 name ,
111- ):
115+ ) -> None :
112116 """Initialize the sensor."""
113- self ._station = station
114117 self ._name = name
118+ self ._station_name = station_name
115119 self .data = MVGLiveData (
116- station , destinations , directions , lines , products , timeoffset , number
120+ hass , station_name , destinations , lines , products , timeoffset , number
117121 )
118122 self ._state = None
119123 self ._icon = ICONS ["-" ]
120124
121125 @property
122- def name (self ):
126+ def name (self ) -> str | None :
123127 """Return the name of the sensor."""
124128 if self ._name :
125129 return self ._name
126- return self ._station
130+ return self ._station_name
127131
128132 @property
129- def native_value (self ):
133+ def native_value (self ) -> str | None :
130134 """Return the next departure time."""
131135 return self ._state
132136
133137 @property
134- def extra_state_attributes (self ):
138+ def extra_state_attributes (self ) -> Mapping [ str , Any ] | None :
135139 """Return the state attributes."""
136140 if not (dep := self .data .departures ):
137141 return None
@@ -140,88 +144,114 @@ def extra_state_attributes(self):
140144 return attr
141145
142146 @property
143- def icon (self ):
147+ def icon (self ) -> str | None :
144148 """Icon to use in the frontend, if any."""
145149 return self ._icon
146150
147151 @property
148- def native_unit_of_measurement (self ):
152+ def native_unit_of_measurement (self ) -> str | None :
149153 """Return the unit this state is expressed in."""
150154 return UnitOfTime .MINUTES
151155
152- def update (self ) -> None :
156+ async def async_update (self ) -> None :
153157 """Get the latest data and update the state."""
154- self .data .update ()
158+ await self .data .update ()
155159 if not self .data .departures :
156- self ._state = "-"
160+ self ._state = None
157161 self ._icon = ICONS ["-" ]
158162 else :
159- self ._state = self .data .departures [0 ].get ("time" , "-" )
160- self ._icon = ICONS [self .data .departures [0 ].get ("product" , "-" )]
163+ self ._state = self .data .departures [0 ].get ("time_in_mins" , "-" )
164+ self ._icon = self .data .departures [0 ].get ("icon" , ICONS ["-" ])
165+
166+
167+ def _get_minutes_until_departure (departure_time : int ) -> int :
168+ """Calculate the time difference in minutes between the current time and a given departure time.
169+
170+ Args:
171+ departure_time: Unix timestamp of the departure time, in seconds.
172+
173+ Returns:
174+ The time difference in minutes, as an integer.
175+
176+ """
177+ current_time = dt_util .utcnow ()
178+ departure_datetime = dt_util .utc_from_timestamp (departure_time )
179+ time_difference = (departure_datetime - current_time ).total_seconds ()
180+ return int (time_difference / 60.0 )
161181
162182
163183class MVGLiveData :
164- """Pull data from the mvg-live .de web page."""
184+ """Pull data from the mvg.de web page."""
165185
166186 def __init__ (
167- self , station , destinations , directions , lines , products , timeoffset , number
168- ):
187+ self ,
188+ hass : HomeAssistant ,
189+ station_name ,
190+ destinations ,
191+ lines ,
192+ products ,
193+ timeoffset ,
194+ number ,
195+ ) -> None :
169196 """Initialize the sensor."""
170- self ._station = station
197+ self ._hass = hass
198+ self ._station_name = station_name
199+ self ._station_id = None
171200 self ._destinations = destinations
172- self ._directions = directions
173201 self ._lines = lines
174202 self ._products = products
175203 self ._timeoffset = timeoffset
176204 self ._number = number
177- self ._include_ubahn = "U-Bahn" in self ._products
178- self ._include_tram = "Tram" in self ._products
179- self ._include_bus = "Bus" in self ._products
180- self ._include_sbahn = "S-Bahn" in self ._products
181- self .mvg = MVGLive .MVGLive ()
182- self .departures = []
205+ self .departures : list [dict [str , Any ]] = []
183206
184- def update (self ):
207+ async def update (self ):
185208 """Update the connection data."""
209+ if self ._station_id is None :
210+ try :
211+ station = await MvgApi .station_async (self ._station_name )
212+ self ._station_id = station ["id" ]
213+ except MvgApiError as err :
214+ _LOGGER .error (
215+ "Failed to resolve station %s: %s" , self ._station_name , err
216+ )
217+ self .departures = []
218+ return
219+
186220 try :
187- _departures = self .mvg .getlivedata (
188- station = self ._station ,
189- timeoffset = self ._timeoffset ,
190- ubahn = self ._include_ubahn ,
191- tram = self ._include_tram ,
192- bus = self ._include_bus ,
193- sbahn = self ._include_sbahn ,
221+ _departures = await MvgApi .departures_async (
222+ station_id = self ._station_id ,
223+ offset = self ._timeoffset ,
224+ limit = self ._number ,
225+ transport_types = [
226+ transport_type
227+ for transport_type in TransportType
228+ if transport_type .value [0 ] in self ._products
229+ ]
230+ if self ._products
231+ else None ,
194232 )
195233 except ValueError :
196234 self .departures = []
197235 _LOGGER .warning ("Returned data not understood" )
198236 return
199237 self .departures = []
200- for i , _departure in enumerate (_departures ):
201- # find the first departure meeting the criteria
238+ for _departure in _departures :
202239 if (
203240 "" not in self ._destinations [:1 ]
204241 and _departure ["destination" ] not in self ._destinations
205242 ):
206243 continue
207244
208- if (
209- "" not in self ._directions [:1 ]
210- and _departure ["direction" ] not in self ._directions
211- ):
245+ if "" not in self ._lines [:1 ] and _departure ["line" ] not in self ._lines :
212246 continue
213247
214- if "" not in self ._lines [:1 ] and _departure ["linename" ] not in self ._lines :
215- continue
248+ time_to_departure = _get_minutes_until_departure (_departure ["time" ])
216249
217- if _departure [ "time" ] < self ._timeoffset :
250+ if time_to_departure < self ._timeoffset :
218251 continue
219252
220- # now select the relevant data
221253 _nextdep = {}
222- for k in ("destination" , "linename " , "time " , "direction " , "product " ):
254+ for k in ("destination" , "line " , "type " , "cancelled " , "icon " ):
223255 _nextdep [k ] = _departure .get (k , "" )
224- _nextdep ["time " ] = int ( _nextdep [ "time" ])
256+ _nextdep ["time_in_mins " ] = time_to_departure
225257 self .departures .append (_nextdep )
226- if i == self ._number - 1 :
227- break
0 commit comments