Skip to content

Commit 0c5e125

Browse files
Update mvglive component (home-assistant#146479)
Co-authored-by: Erik Montnemery <[email protected]>
1 parent 9db9732 commit 0c5e125

File tree

3 files changed

+121
-90
lines changed

3 files changed

+121
-90
lines changed

homeassistant/components/mvglive/manifest.json

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,8 @@
22
"domain": "mvglive",
33
"name": "MVG",
44
"codeowners": [],
5-
"disabled": "This integration is disabled because it uses non-open source code to operate.",
65
"documentation": "https://www.home-assistant.io/integrations/mvglive",
76
"iot_class": "cloud_polling",
8-
"loggers": ["MVGLive"],
9-
"quality_scale": "legacy",
10-
"requirements": ["PyMVGLive==1.1.4"]
7+
"loggers": ["MVG"],
8+
"requirements": ["mvg==1.4.0"]
119
}

homeassistant/components/mvglive/sensor.py

Lines changed: 116 additions & 86 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,14 @@
11
"""Support for departure information for public transport in Munich."""
22

3-
# mypy: ignore-errors
43
from __future__ import annotations
54

5+
from collections.abc import Mapping
66
from copy import deepcopy
77
from datetime import timedelta
88
import logging
9+
from typing import Any
910

10-
import MVGLive
11+
from mvg import MvgApi, MvgApiError, TransportType
1112
import voluptuous as vol
1213

1314
from homeassistant.components.sensor import (
@@ -19,6 +20,7 @@
1920
from homeassistant.helpers import config_validation as cv
2021
from homeassistant.helpers.entity_platform import AddEntitiesCallback
2122
from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType
23+
import homeassistant.util.dt as dt_util
2224

2325
_LOGGER = logging.getLogger(__name__)
2426

@@ -44,53 +46,55 @@
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

4952
SCAN_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

96100
class 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

163183
class 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

requirements_all.txt

Lines changed: 3 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)