New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Add RMV public transport sensor #15814
Changes from 16 commits
c3f61e0
b9b59aa
41c34ea
f9e88b6
221167e
504049b
db76dd0
2d9c122
485ae89
6fcc48d
741a711
1403293
f67f22e
74117c3
2accb78
661bad1
26f1227
b714364
8be8b07
d21cec3
4341b8a
72c3b7e
9f29d30
844c439
cacfd46
a9949a4
1642b47
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,210 @@ | ||
""" | ||
Support for real-time departure information for Rhein-Main public transport. | ||
|
||
For more details about this platform, please refer to the documentation at | ||
https://home-assistant.io/components/sensor.rmvdeparture/ | ||
""" | ||
import logging | ||
from datetime import timedelta | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 'datetime.timedelta' imported but unused |
||
|
||
import voluptuous as vol | ||
|
||
import homeassistant.helpers.config_validation as cv | ||
from homeassistant.helpers.entity import Entity | ||
from homeassistant.components.sensor import PLATFORM_SCHEMA | ||
from homeassistant.const import ( | ||
CONF_NAME, ATTR_ATTRIBUTION, STATE_UNKNOWN | ||
) | ||
|
||
REQUIREMENTS = ['PyRMVtransport==0.0.6'] | ||
|
||
_LOGGER = logging.getLogger(__name__) | ||
|
||
CONF_NEXT_DEPARTURE = 'nextdeparture' | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. CONF_NEXT_DEPARTURE = 'next_departure' |
||
|
||
CONF_STATION = 'station' | ||
CONF_DESTINATIONS = 'destinations' | ||
CONF_DIRECTIONS = 'directions' | ||
CONF_LINES = 'lines' | ||
CONF_PRODUCTS = 'products' | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
|
||
CONF_TIMEOFFSET = 'timeoffset' | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Is it not explicit enough or is the documentation lacking detailed information? |
||
CONF_MAXJOURNEYS = 'max' | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. You're correct. I added it to the documentation. |
||
|
||
DEFAULT_NAME = 'RMV Journey' | ||
|
||
VALID_PRODUCTS = ['U-Bahn', 'Tram', 'Bus', 'S', 'RB', 'RE', 'EC', 'IC', 'ICE'] | ||
|
||
ICONS = { | ||
'U-Bahn': 'mdi:subway', | ||
'Tram': 'mdi:tram', | ||
'Bus': 'mdi:bus', | ||
'S': 'mdi:train', | ||
'RB': 'mdi:train', | ||
'RE': 'mdi:train', | ||
'EC': 'mdi:train', | ||
'IC': 'mdi:train', | ||
'ICE': 'mdi:train', | ||
'SEV': 'mdi:checkbox-blank-circle-outline', | ||
None: 'mdi:clock' | ||
} | ||
ATTRIBUTION = "Data provided by opendata.rmv.de" | ||
|
||
SCAN_INTERVAL = timedelta(seconds=30) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Do we need to update more frequently than once a minute? The unit of measurement is minutes. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. You're probably right. Every minute should be sufficient. 30 seconds was simply to make sure not to query just before the values actually get updated and therefore miss "the bus". Does that make sense? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Ok, you decide. |
||
|
||
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ | ||
vol.Required(CONF_NEXT_DEPARTURE): [{ | ||
vol.Required(CONF_STATION): cv.string, | ||
vol.Optional(CONF_DESTINATIONS, default=['']): cv.ensure_list_csv, | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We shouldn't use list csv. Just ensure list. List csv is only to allow non breaking change when migrating config schemas. Default to empty list instead. |
||
vol.Optional(CONF_DIRECTIONS, default=['']): cv.ensure_list_csv, | ||
vol.Optional(CONF_LINES, default=['']): cv.ensure_list_csv, | ||
vol.Optional(CONF_PRODUCTS, default=VALID_PRODUCTS): | ||
vol.All(cv.ensure_list, [vol.In(VALID_PRODUCTS)]), | ||
vol.Optional(CONF_TIMEOFFSET, default=0): cv.positive_int, | ||
vol.Optional(CONF_MAXJOURNEYS, default=5): cv.positive_int, | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. What do we call this in other sensor commuting platforms? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Ok. It would be good to come up with a descriptive name and then have all platforms use the same term. But it can wait to a different PR. But please separate words by underscore in variable and constant names and strings. So change to |
||
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string}] | ||
}) | ||
|
||
|
||
def setup_platform(hass, config, add_entities, discovery_info=None): | ||
"""Set up the RMV departure sensor.""" | ||
sensors = [] | ||
for nextdeparture in config.get(CONF_NEXT_DEPARTURE): | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. for next_departure in ... |
||
sensors.append( | ||
RMVDepartureSensor( | ||
nextdeparture.get(CONF_STATION), | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Don't use |
||
nextdeparture.get(CONF_DESTINATIONS), | ||
nextdeparture.get(CONF_DIRECTIONS), | ||
nextdeparture.get(CONF_LINES), | ||
nextdeparture.get(CONF_PRODUCTS), | ||
nextdeparture.get(CONF_TIMEOFFSET), | ||
nextdeparture.get(CONF_MAXJOURNEYS), | ||
nextdeparture.get(CONF_NAME))) | ||
add_entities(sensors, True) | ||
|
||
|
||
class RMVDepartureSensor(Entity): | ||
"""Implementation of an RMV departure sensor.""" | ||
|
||
def __init__(self, station, destinations, directions, | ||
lines, products, timeoffset, maxjourneys, name): | ||
"""Initialize the sensor.""" | ||
self._station = station | ||
self._name = name | ||
self.data = RMVDepartureData(station, destinations, directions, | ||
lines, products, timeoffset, maxjourneys) | ||
self._state = STATE_UNKNOWN | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Init state as |
||
self._icon = ICONS[None] | ||
|
||
@property | ||
def name(self): | ||
"""Return the name of the sensor.""" | ||
return self._name | ||
|
||
@property | ||
def available(self): | ||
"""Return True if entity is available.""" | ||
return self._state != STATE_UNKNOWN | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Don't use return self._state is not None Is there no use to differ between unknown state and unavailable? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'll check that. |
||
|
||
@property | ||
def state(self): | ||
"""Return the next departure time.""" | ||
self._state = self.data.departures[0].get('minutes', None) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Remove this. It's already present in |
||
return self._state | ||
|
||
@property | ||
def state_attributes(self): | ||
"""Return the state attributes.""" | ||
result = {} | ||
try: | ||
result = { | ||
'next_departures': [val for val in self.data.departures[1:]], | ||
'direction': self.data.departures[0].get('direction'), | ||
'line': self.data.departures[0].get('line'), | ||
'minutes': self.data.departures[0].get('minutes'), | ||
'departure_time': | ||
self.data.departures[0].get('departure_time'), | ||
'product': self.data.departures[0].get('product'), | ||
} | ||
except IndexError: | ||
pass | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We could return empty dict here instead of defining it at the top. |
||
return result | ||
|
||
@property | ||
def icon(self): | ||
"""Icon to use in the frontend, if any.""" | ||
return self._icon | ||
|
||
@property | ||
def unit_of_measurement(self): | ||
"""Return the unit this state is expressed in.""" | ||
return "min" | ||
|
||
def update(self): | ||
"""Get the latest data and update the state.""" | ||
self.data.update() | ||
if not self.data.departures: | ||
self._state = None | ||
self._icon = ICONS[None] | ||
return | ||
if self._name == DEFAULT_NAME: | ||
self._name = self.data.station | ||
self._station = self.data.station | ||
self._state = self.data.departures[0].get('minutes') | ||
self._state = self.data.departures[0].get('departure_time') | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Why overwrite state here? We just set it at the line above. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Well spotted. I already fixed that in my local branch. I simply forgot to remove the second call. |
||
self._icon = ICONS[self.data.departures[0].get('product')] | ||
return | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Not needed return. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. So true. :-) |
||
|
||
|
||
class RMVDepartureData: | ||
"""Pull data from the opendata.rmv.de web page.""" | ||
|
||
def __init__(self, station_id, destinations, directions, | ||
lines, products, timeoffset, maxjourneys): | ||
"""Initialize the sensor.""" | ||
import RMVtransport | ||
self.station = None | ||
self._station_id = station_id | ||
self._destinations = destinations | ||
self._directions = directions | ||
self._lines = lines | ||
self._products = products | ||
self._timeoffset = timeoffset | ||
self._maxjourneys = maxjourneys | ||
self.rmv = RMVtransport.RMVtransport() | ||
self.departures = [] | ||
|
||
def update(self): | ||
"""Update the connection data.""" | ||
try: | ||
_data = self.rmv.get_departures(self._station_id, | ||
products=self._products, | ||
maxJourneys=50) | ||
except ValueError: | ||
self.departures = {} | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We set this as a list in init. We probably shouldn't change the type. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Well spotted. Not sure when that happened. ;) There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I rather initialise as a dict. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. But we use |
||
_LOGGER.warning("Returned data not understood") | ||
return | ||
self.station = _data.get('station', None) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
|
||
_deps = [] | ||
for journey in _data['journeys']: | ||
# find the first departure meeting the criteria | ||
_nextdep = {ATTR_ATTRIBUTION: ATTRIBUTION} | ||
if '' not in self._destinations[:1]: | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Just do: if self._destinations: |
||
dest_found = False | ||
for dest in self._destinations: | ||
if dest in journey['stops']: | ||
dest_found = True | ||
_nextdep['destination'] = dest | ||
if not dest_found: | ||
continue | ||
elif ('' not in self._lines[:1] and | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. elif self._lines and journey['number'] not in self._lines: |
||
journey['number'] not in self._lines): | ||
continue | ||
elif journey['minutes'] < self._timeoffset: | ||
continue | ||
for k in ['direction', 'departure_time', 'product', 'minutes']: | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. for attr in ... There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
|
||
_nextdep[k] = journey.get(k, '') | ||
_nextdep['line'] = journey.get('number', '') | ||
_deps.append(_nextdep) | ||
if len(_deps) > self._maxjourneys: | ||
break | ||
self.departures = _deps |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The url is wrong. It should match the module name.