From cfc175d71d15dbeda7975638da5ef205aa11a9b5 Mon Sep 17 00:00:00 2001 From: cgtobi Date: Wed, 10 Oct 2018 08:10:42 +0200 Subject: [PATCH] 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 --- .../components/sensor/rmvtransport.py | 56 ++++--- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/sensor/test_rmvtransport.py | 148 +++++++++--------- 4 files changed, 114 insertions(+), 94 deletions(-) diff --git a/homeassistant/components/sensor/rmvtransport.py b/homeassistant/components/sensor/rmvtransport.py index 0916765e12d..79ec8c7a5e7 100644 --- a/homeassistant/components/sensor/rmvtransport.py +++ b/homeassistant/components/sensor/rmvtransport.py @@ -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") diff --git a/requirements_all.txt b/requirements_all.txt index 0370373e818..68bb64844c4 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -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 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 754a8947a70..229655bdea7 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -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 diff --git a/tests/components/sensor/test_rmvtransport.py b/tests/components/sensor/test_rmvtransport.py index 9db19ecde49..d917edf0029 100644 --- a/tests/components/sensor/test_rmvtransport.py +++ b/tests/components/sensor/test_rmvtransport.py @@ -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