Make rmvtransport async (#17225)

* Make rmvtransport async

* Make rmv transport async

* Make async tests

* Update rmvtransport module version

* Remove unnecessary import

* Make rmvtransport async

* Make rmv transport async

* Make async tests

* Update rmvtransport module version

* Remove unnecessary import

* Update requirements

* Remove async loop

* Fix wrong import

* Fix stupidness

* Remove unnecessary import

* Bump upstream version

* Don't store the session

* Refactor tests

* Add test for no data

* Fix linter issues

* Fix stale docstring

* Fix stale docstring

* Remove unnecessary test code

* Remove unnecessary import

* Add configurable timeout

* Remove global variable
This commit is contained in:
cgtobi 2018-10-10 08:10:42 +02:00 committed by Martin Hjelmare
parent 8310f4a1cf
commit cfc175d71d
4 changed files with 114 additions and 94 deletions

View File

@ -5,15 +5,18 @@ For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/sensor.rmvtransport/
"""
import logging
from datetime import timedelta
import voluptuous as vol
import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.aiohttp_client import async_get_clientsession
from homeassistant.helpers.entity import Entity
from homeassistant.components.sensor import PLATFORM_SCHEMA
from homeassistant.const import (CONF_NAME, ATTR_ATTRIBUTION)
from homeassistant.util import Throttle
REQUIREMENTS = ['PyRMVtransport==0.1']
REQUIREMENTS = ['PyRMVtransport==0.1.3']
_LOGGER = logging.getLogger(__name__)
@ -26,6 +29,7 @@ CONF_LINES = 'lines'
CONF_PRODUCTS = 'products'
CONF_TIME_OFFSET = 'time_offset'
CONF_MAX_JOURNEYS = 'max_journeys'
CONF_TIMEOUT = 'timeout'
DEFAULT_NAME = 'RMV Journey'
@ -46,6 +50,8 @@ ICONS = {
}
ATTRIBUTION = "Data provided by opendata.rmv.de"
SCAN_INTERVAL = timedelta(seconds=60)
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
vol.Required(CONF_NEXT_DEPARTURE): [{
vol.Required(CONF_STATION): cv.string,
@ -59,16 +65,23 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
vol.All(cv.ensure_list, [vol.In(VALID_PRODUCTS)]),
vol.Optional(CONF_TIME_OFFSET, default=0): cv.positive_int,
vol.Optional(CONF_MAX_JOURNEYS, default=5): cv.positive_int,
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string}]
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string}],
vol.Optional(CONF_TIMEOUT, default=10): cv.positive_int
})
def setup_platform(hass, config, add_entities, discovery_info=None):
async def async_setup_platform(hass, config, async_add_entities,
discovery_info=None):
"""Set up the RMV departure sensor."""
timeout = config.get(CONF_TIMEOUT)
session = async_get_clientsession(hass)
sensors = []
for next_departure in config.get(CONF_NEXT_DEPARTURE):
sensors.append(
RMVDepartureSensor(
session,
next_departure[CONF_STATION],
next_departure.get(CONF_DESTINATIONS),
next_departure.get(CONF_DIRECTIONS),
@ -76,21 +89,23 @@ def setup_platform(hass, config, add_entities, discovery_info=None):
next_departure.get(CONF_PRODUCTS),
next_departure.get(CONF_TIME_OFFSET),
next_departure.get(CONF_MAX_JOURNEYS),
next_departure.get(CONF_NAME)))
add_entities(sensors, True)
next_departure.get(CONF_NAME),
timeout))
async_add_entities(sensors, True)
class RMVDepartureSensor(Entity):
"""Implementation of an RMV departure sensor."""
def __init__(self, station, destinations, directions,
lines, products, time_offset, max_journeys, name):
def __init__(self, session, station, destinations, directions, lines,
products, time_offset, max_journeys, name, timeout):
"""Initialize the sensor."""
self._station = station
self._name = name
self._state = None
self.data = RMVDepartureData(station, destinations, directions, lines,
products, time_offset, max_journeys)
self.data = RMVDepartureData(session, station, destinations,
directions, lines, products, time_offset,
max_journeys, timeout)
self._icon = ICONS[None]
@property
@ -134,9 +149,10 @@ class RMVDepartureSensor(Entity):
"""Return the unit this state is expressed in."""
return "min"
def update(self):
async def async_update(self):
"""Get the latest data and update the state."""
self.data.update()
await self.data.async_update()
if not self.data.departures:
self._state = None
self._icon = ICONS[None]
@ -151,10 +167,11 @@ class RMVDepartureSensor(Entity):
class RMVDepartureData:
"""Pull data from the opendata.rmv.de web page."""
def __init__(self, station_id, destinations, directions,
lines, products, time_offset, max_journeys):
def __init__(self, session, station_id, destinations, directions, lines,
products, time_offset, max_journeys, timeout):
"""Initialize the sensor."""
import RMVtransport
from RMVtransport import RMVtransport
self.station = None
self._station_id = station_id
self._destinations = destinations
@ -163,15 +180,16 @@ class RMVDepartureData:
self._products = products
self._time_offset = time_offset
self._max_journeys = max_journeys
self.rmv = RMVtransport.RMVtransport()
self.rmv = RMVtransport(session, timeout)
self.departures = []
def update(self):
@Throttle(SCAN_INTERVAL)
async def async_update(self):
"""Update the connection data."""
try:
_data = self.rmv.get_departures(self._station_id,
products=self._products,
maxJourneys=50)
_data = await self.rmv.get_departures(self._station_id,
products=self._products,
maxJourneys=50)
except ValueError:
self.departures = []
_LOGGER.warning("Returned data not understood")

View File

@ -52,7 +52,7 @@ PyMata==2.14
PyQRCode==1.2.1
# homeassistant.components.sensor.rmvtransport
PyRMVtransport==0.1
PyRMVtransport==0.1.3
# homeassistant.components.switch.switchbot
PySwitchbot==0.3

View File

@ -22,7 +22,7 @@ requests_mock==1.5.2
HAP-python==2.2.2
# homeassistant.components.sensor.rmvtransport
PyRMVtransport==0.1
PyRMVtransport==0.1.3
# homeassistant.components.notify.yessssms
YesssSMS==0.2.3

View File

@ -1,14 +1,17 @@
"""The tests for the rmvtransport platform."""
import unittest
from unittest.mock import patch
import datetime
from unittest.mock import patch
from homeassistant.setup import setup_component
from homeassistant.setup import async_setup_component
from tests.common import get_test_home_assistant
from tests.common import mock_coro
VALID_CONFIG_MINIMAL = {'sensor': {'platform': 'rmvtransport',
'next_departure': [{'station': '3000010'}]}}
VALID_CONFIG_MINIMAL = {'sensor': {
'platform': 'rmvtransport',
'next_departure': [
{'station': '3000010'}
]}}
VALID_CONFIG_NAME = {'sensor': {
'platform': 'rmvtransport',
@ -41,8 +44,7 @@ VALID_CONFIG_DEST = {'sensor': {
]}}
def get_departuresMock(stationId, maxJourneys,
products): # pylint: disable=invalid-name
def get_departures_mock():
"""Mock rmvtransport departures loading."""
data = {'station': 'Frankfurt (Main) Hauptbahnhof',
'stationId': '3000010', 'filter': '11111111111', 'journeys': [
@ -97,77 +99,77 @@ def get_departuresMock(stationId, maxJourneys,
return data
def get_errDeparturesMock(stationId, maxJourneys,
products): # pylint: disable=invalid-name
"""Mock rmvtransport departures erroneous loading."""
raise ValueError
def get_no_departures_mock():
"""Mock no departures in results."""
data = {'station': 'Frankfurt (Main) Hauptbahnhof',
'stationId': '3000010',
'filter': '11111111111',
'journeys': []}
return data
class TestRMVtransportSensor(unittest.TestCase):
"""Test the rmvtransport sensor."""
async def test_rmvtransport_min_config(hass):
"""Test minimal rmvtransport configuration."""
with patch('RMVtransport.RMVtransport.get_departures',
return_value=mock_coro(get_departures_mock())):
assert await async_setup_component(hass, 'sensor',
VALID_CONFIG_MINIMAL) is True
def setUp(self):
"""Set up things to run when tests begin."""
self.hass = get_test_home_assistant()
self.config = VALID_CONFIG_MINIMAL
self.reference = {}
self.entities = []
state = hass.states.get('sensor.frankfurt_main_hauptbahnhof')
assert state.state == '7'
assert state.attributes['departure_time'] == \
datetime.datetime(2018, 8, 6, 14, 21)
assert state.attributes['direction'] == \
'Frankfurt (Main) Hugo-Junkers-Straße/Schleife'
assert state.attributes['product'] == 'Tram'
assert state.attributes['line'] == 12
assert state.attributes['icon'] == 'mdi:tram'
assert state.attributes['friendly_name'] == 'Frankfurt (Main) Hauptbahnhof'
def tearDown(self):
"""Stop everything that was started."""
self.hass.stop()
@patch('RMVtransport.RMVtransport.get_departures',
side_effect=get_departuresMock)
def test_rmvtransport_min_config(self, mock_get_departures):
"""Test minimal rmvtransport configuration."""
assert setup_component(self.hass, 'sensor', VALID_CONFIG_MINIMAL)
state = self.hass.states.get('sensor.frankfurt_main_hauptbahnhof')
self.assertEqual(state.state, '7')
self.assertEqual(state.attributes['departure_time'],
datetime.datetime(2018, 8, 6, 14, 21))
self.assertEqual(state.attributes['direction'],
'Frankfurt (Main) Hugo-Junkers-Straße/Schleife')
self.assertEqual(state.attributes['product'], 'Tram')
self.assertEqual(state.attributes['line'], 12)
self.assertEqual(state.attributes['icon'], 'mdi:tram')
self.assertEqual(state.attributes['friendly_name'],
'Frankfurt (Main) Hauptbahnhof')
async def test_rmvtransport_name_config(hass):
"""Test custom name configuration."""
with patch('RMVtransport.RMVtransport.get_departures',
return_value=mock_coro(get_departures_mock())):
assert await async_setup_component(hass, 'sensor', VALID_CONFIG_NAME)
@patch('RMVtransport.RMVtransport.get_departures',
side_effect=get_departuresMock)
def test_rmvtransport_name_config(self, mock_get_departures):
"""Test custom name configuration."""
assert setup_component(self.hass, 'sensor', VALID_CONFIG_NAME)
state = self.hass.states.get('sensor.my_station')
self.assertEqual(state.attributes['friendly_name'], 'My Station')
state = hass.states.get('sensor.my_station')
assert state.attributes['friendly_name'] == 'My Station'
@patch('RMVtransport.RMVtransport.get_departures',
side_effect=get_errDeparturesMock)
def test_rmvtransport_err_config(self, mock_get_departures):
"""Test erroneous rmvtransport configuration."""
assert setup_component(self.hass, 'sensor', VALID_CONFIG_MINIMAL)
@patch('RMVtransport.RMVtransport.get_departures',
side_effect=get_departuresMock)
def test_rmvtransport_misc_config(self, mock_get_departures):
"""Test misc configuration."""
assert setup_component(self.hass, 'sensor', VALID_CONFIG_MISC)
state = self.hass.states.get('sensor.frankfurt_main_hauptbahnhof')
self.assertEqual(state.attributes['friendly_name'],
'Frankfurt (Main) Hauptbahnhof')
self.assertEqual(state.attributes['line'], 21)
async def test_rmvtransport_misc_config(hass):
"""Test misc configuration."""
with patch('RMVtransport.RMVtransport.get_departures',
return_value=mock_coro(get_departures_mock())):
assert await async_setup_component(hass, 'sensor', VALID_CONFIG_MISC)
@patch('RMVtransport.RMVtransport.get_departures',
side_effect=get_departuresMock)
def test_rmvtransport_dest_config(self, mock_get_departures):
"""Test misc configuration."""
assert setup_component(self.hass, 'sensor', VALID_CONFIG_DEST)
state = self.hass.states.get('sensor.frankfurt_main_hauptbahnhof')
self.assertEqual(state.state, '11')
self.assertEqual(state.attributes['direction'],
'Frankfurt (Main) Hugo-Junkers-Straße/Schleife')
self.assertEqual(state.attributes['line'], 12)
self.assertEqual(state.attributes['minutes'], 11)
self.assertEqual(state.attributes['departure_time'],
datetime.datetime(2018, 8, 6, 14, 25))
state = hass.states.get('sensor.frankfurt_main_hauptbahnhof')
assert state.attributes['friendly_name'] == 'Frankfurt (Main) Hauptbahnhof'
assert state.attributes['line'] == 21
async def test_rmvtransport_dest_config(hass):
"""Test destination configuration."""
with patch('RMVtransport.RMVtransport.get_departures',
return_value=mock_coro(get_departures_mock())):
assert await async_setup_component(hass, 'sensor', VALID_CONFIG_DEST)
state = hass.states.get('sensor.frankfurt_main_hauptbahnhof')
assert state.state == '11'
assert state.attributes['direction'] == \
'Frankfurt (Main) Hugo-Junkers-Straße/Schleife'
assert state.attributes['line'] == 12
assert state.attributes['minutes'] == 11
assert state.attributes['departure_time'] == \
datetime.datetime(2018, 8, 6, 14, 25)
async def test_rmvtransport_no_departures(hass):
"""Test for no departures."""
with patch('RMVtransport.RMVtransport.get_departures',
return_value=mock_coro(get_no_departures_mock())):
assert await async_setup_component(hass, 'sensor',
VALID_CONFIG_MINIMAL)
state = hass.states.get('sensor.frankfurt_main_hauptbahnhof')
assert not state