From 2e517ab6bc6ae76def082d9473e092faae94dcba Mon Sep 17 00:00:00 2001 From: Fabian Affolter Date: Tue, 6 Nov 2018 14:27:52 +0100 Subject: [PATCH] Enable config flow for Luftdaten (#17700) * Move file to new location * Update requirement * Enable config flow * Add luftdaten * Add tests * Update * Add constants * Changes according to the review comments * Remove wrong entry from flows * Fix dict handling * Add callback and use OrderedDict * Remve leftover * Fix * Remove await --- CODEOWNERS | 3 +- .../components/luftdaten/__init__.py | 170 +++++++++++++++++ .../components/luftdaten/config_flow.py | 75 ++++++++ homeassistant/components/luftdaten/const.py | 10 + .../components/luftdaten/strings.json | 20 ++ homeassistant/components/sensor/luftdaten.py | 174 +++++++----------- homeassistant/config_entries.py | 1 + requirements_all.txt | 4 +- requirements_test_all.txt | 3 + script/gen_requirements_all.py | 5 +- tests/components/luftdaten/__init__.py | 1 + .../components/luftdaten/test_config_flow.py | 114 ++++++++++++ tests/components/luftdaten/test_init.py | 36 ++++ 13 files changed, 508 insertions(+), 108 deletions(-) create mode 100644 homeassistant/components/luftdaten/__init__.py create mode 100644 homeassistant/components/luftdaten/config_flow.py create mode 100644 homeassistant/components/luftdaten/const.py create mode 100644 homeassistant/components/luftdaten/strings.json create mode 100644 tests/components/luftdaten/__init__.py create mode 100644 tests/components/luftdaten/test_config_flow.py create mode 100644 tests/components/luftdaten/test_init.py diff --git a/CODEOWNERS b/CODEOWNERS index bf4c342b474..0d498d89cae 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -109,7 +109,6 @@ homeassistant/components/sensor/gpsd.py @fabaff homeassistant/components/sensor/irish_rail_transport.py @ttroy50 homeassistant/components/sensor/jewish_calendar.py @tsvi homeassistant/components/sensor/linux_battery.py @fabaff -homeassistant/components/sensor/luftdaten.py @fabaff homeassistant/components/sensor/miflora.py @danielhiversen @ChristianKuehnel homeassistant/components/sensor/min_max.py @fabaff homeassistant/components/sensor/moon.py @fabaff @@ -189,6 +188,8 @@ homeassistant/components/*/konnected.py @heythisisnate # L homeassistant/components/lifx.py @amelchio homeassistant/components/*/lifx.py @amelchio +homeassistant/components/luftdaten/* @fabaff +homeassistant/components/*/luftdaten.py @fabaff # M homeassistant/components/matrix.py @tinloaf diff --git a/homeassistant/components/luftdaten/__init__.py b/homeassistant/components/luftdaten/__init__.py new file mode 100644 index 00000000000..b00fca7d3c0 --- /dev/null +++ b/homeassistant/components/luftdaten/__init__.py @@ -0,0 +1,170 @@ +""" +Support for Luftdaten stations. + +For more details about this platform, please refer to the documentation at +https://home-assistant.io/components/luftdaten/ +""" +import logging + +import voluptuous as vol + +from homeassistant.config_entries import SOURCE_IMPORT +from homeassistant.const import ( + CONF_MONITORED_CONDITIONS, CONF_SCAN_INTERVAL, CONF_SENSORS, + CONF_SHOW_ON_MAP, TEMP_CELSIUS) +from homeassistant.exceptions import ConfigEntryNotReady +from homeassistant.helpers.aiohttp_client import async_get_clientsession +import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.dispatcher import async_dispatcher_send +from homeassistant.helpers.event import async_track_time_interval + +from .config_flow import configured_sensors +from .const import CONF_SENSOR_ID, DEFAULT_SCAN_INTERVAL, DOMAIN + +REQUIREMENTS = ['luftdaten==0.3.4'] + +_LOGGER = logging.getLogger(__name__) + +DATA_LUFTDATEN = 'luftdaten' +DATA_LUFTDATEN_CLIENT = 'data_luftdaten_client' +DATA_LUFTDATEN_LISTENER = 'data_luftdaten_listener' +DEFAULT_ATTRIBUTION = "Data provided by luftdaten.info" + +SENSOR_HUMIDITY = 'humidity' +SENSOR_PM10 = 'P1' +SENSOR_PM2_5 = 'P2' +SENSOR_PRESSURE = 'pressure' +SENSOR_TEMPERATURE = 'temperature' + +TOPIC_UPDATE = '{0}_data_update'.format(DOMAIN) + +VOLUME_MICROGRAMS_PER_CUBIC_METER = 'µg/m3' + +SENSORS = { + SENSOR_TEMPERATURE: ['Temperature', 'mdi:thermometer', TEMP_CELSIUS], + SENSOR_HUMIDITY: ['Humidity', 'mdi:water-percent', '%'], + SENSOR_PRESSURE: ['Pressure', 'mdi:arrow-down-bold', 'Pa'], + SENSOR_PM10: ['PM10', 'mdi:thought-bubble', + VOLUME_MICROGRAMS_PER_CUBIC_METER], + SENSOR_PM2_5: ['PM2.5', 'mdi:thought-bubble-outline', + VOLUME_MICROGRAMS_PER_CUBIC_METER] +} + +SENSOR_SCHEMA = vol.Schema({ + vol.Optional(CONF_MONITORED_CONDITIONS, default=list(SENSORS)): + vol.All(cv.ensure_list, [vol.In(SENSORS)]) +}) + +CONFIG_SCHEMA = vol.Schema({ + DOMAIN: + vol.Schema({ + vol.Required(CONF_SENSOR_ID): cv.positive_int, + vol.Optional(CONF_SENSORS, default={}): SENSOR_SCHEMA, + vol.Optional(CONF_SHOW_ON_MAP, default=False): cv.boolean, + vol.Optional(CONF_SCAN_INTERVAL, default=DEFAULT_SCAN_INTERVAL): + cv.time_period, + }) +}, extra=vol.ALLOW_EXTRA) + + +async def async_setup(hass, config): + """Set up the Luftdaten component.""" + hass.data[DOMAIN] = {} + hass.data[DOMAIN][DATA_LUFTDATEN_CLIENT] = {} + hass.data[DOMAIN][DATA_LUFTDATEN_LISTENER] = {} + + if DOMAIN not in config: + return True + + conf = config[DOMAIN] + station_id = conf.get(CONF_SENSOR_ID) + + if station_id not in configured_sensors(hass): + hass.async_create_task( + hass.config_entries.flow.async_init( + DOMAIN, + context={'source': SOURCE_IMPORT}, + data={ + CONF_SENSORS: conf[CONF_SENSORS], + CONF_SENSOR_ID: conf[CONF_SENSOR_ID], + CONF_SHOW_ON_MAP: conf[CONF_SHOW_ON_MAP], + } + ) + ) + + hass.data[DOMAIN][CONF_SCAN_INTERVAL] = conf[CONF_SCAN_INTERVAL] + + return True + + +async def async_setup_entry(hass, config_entry): + """Set up Luftdaten as config entry.""" + from luftdaten import Luftdaten + from luftdaten.exceptions import LuftdatenError + + session = async_get_clientsession(hass) + + try: + luftdaten = LuftDatenData( + Luftdaten( + config_entry.data[CONF_SENSOR_ID], hass.loop, session), + config_entry.data.get(CONF_SENSORS, {}).get( + CONF_MONITORED_CONDITIONS, list(SENSORS))) + await luftdaten.async_update() + hass.data[DOMAIN][DATA_LUFTDATEN_CLIENT][config_entry.entry_id] = \ + luftdaten + except LuftdatenError: + raise ConfigEntryNotReady + + hass.async_create_task(hass.config_entries.async_forward_entry_setup( + config_entry, 'sensor')) + + async def refresh_sensors(event_time): + """Refresh Luftdaten data.""" + await luftdaten.async_update() + async_dispatcher_send(hass, TOPIC_UPDATE) + + hass.data[DOMAIN][DATA_LUFTDATEN_LISTENER][ + config_entry.entry_id] = async_track_time_interval( + hass, refresh_sensors, + hass.data[DOMAIN].get(CONF_SCAN_INTERVAL, DEFAULT_SCAN_INTERVAL)) + + return True + + +async def async_unload_entry(hass, config_entry): + """Unload an Luftdaten config entry.""" + remove_listener = hass.data[DOMAIN][DATA_LUFTDATEN_LISTENER].pop( + config_entry.entry_id) + remove_listener() + + for component in ('sensor', ): + await hass.config_entries.async_forward_entry_unload( + config_entry, component) + + hass.data[DOMAIN][DATA_LUFTDATEN_CLIENT].pop(config_entry.entry_id) + + return True + + +class LuftDatenData: + """Define a generic Luftdaten object.""" + + def __init__(self, client, sensor_conditions): + """Initialize the Luftdata object.""" + self.client = client + self.data = {} + self.sensor_conditions = sensor_conditions + + async def async_update(self): + """Update sensor/binary sensor data.""" + from luftdaten.exceptions import LuftdatenError + + try: + await self.client.get_data() + + self.data[DATA_LUFTDATEN] = self.client.values + self.data[DATA_LUFTDATEN].update(self.client.meta) + + except LuftdatenError: + _LOGGER.error("Unable to retrieve data from luftdaten.info") diff --git a/homeassistant/components/luftdaten/config_flow.py b/homeassistant/components/luftdaten/config_flow.py new file mode 100644 index 00000000000..33715c3c0c1 --- /dev/null +++ b/homeassistant/components/luftdaten/config_flow.py @@ -0,0 +1,75 @@ +"""Config flow to configure the Luftdaten component.""" +from collections import OrderedDict + +import voluptuous as vol + +from homeassistant import config_entries +from homeassistant.const import CONF_SCAN_INTERVAL, CONF_SHOW_ON_MAP +from homeassistant.core import callback +from homeassistant.helpers import aiohttp_client + +from .const import CONF_SENSOR_ID, DEFAULT_SCAN_INTERVAL, DOMAIN + + +@callback +def configured_sensors(hass): + """Return a set of configured Luftdaten sensors.""" + return set( + '{0}'.format(entry.data[CONF_SENSOR_ID]) + for entry in hass.config_entries.async_entries(DOMAIN)) + + +@config_entries.HANDLERS.register(DOMAIN) +class LuftDatenFlowHandler(config_entries.ConfigFlow): + """Handle a Luftdaten config flow.""" + + VERSION = 1 + CONNECTION_CLASS = config_entries.CONN_CLASS_CLOUD_POLL + + @callback + def _show_form(self, errors=None): + """Show the form to the user.""" + data_schema = OrderedDict() + data_schema[vol.Required(CONF_SENSOR_ID)] = str + data_schema[vol.Optional(CONF_SHOW_ON_MAP, default=False)] = bool + + return self.async_show_form( + step_id='user', + data_schema=vol.Schema(data_schema), + errors=errors or {} + ) + + async def async_step_import(self, import_config): + """Import a config entry from configuration.yaml.""" + return await self.async_step_user(import_config) + + async def async_step_user(self, user_input=None): + """Handle the start of the config flow.""" + from luftdaten import Luftdaten, exceptions + + if not user_input: + return self._show_form() + + sensor_id = user_input[CONF_SENSOR_ID] + + if sensor_id in configured_sensors(self.hass): + return self._show_form({CONF_SENSOR_ID: 'sensor_exists'}) + + session = aiohttp_client.async_get_clientsession(self.hass) + luftdaten = Luftdaten( + user_input[CONF_SENSOR_ID], self.hass.loop, session) + try: + await luftdaten.get_data() + valid = await luftdaten.validate_sensor() + except exceptions.LuftdatenConnectionError: + return self._show_form( + {CONF_SENSOR_ID: 'communication_error'}) + + if not valid: + return self._show_form({CONF_SENSOR_ID: 'invalid_sensor'}) + + scan_interval = user_input.get( + CONF_SCAN_INTERVAL, DEFAULT_SCAN_INTERVAL) + user_input.update({CONF_SCAN_INTERVAL: scan_interval.seconds}) + + return self.async_create_entry(title=sensor_id, data=user_input) diff --git a/homeassistant/components/luftdaten/const.py b/homeassistant/components/luftdaten/const.py new file mode 100644 index 00000000000..2f87f857545 --- /dev/null +++ b/homeassistant/components/luftdaten/const.py @@ -0,0 +1,10 @@ +"""Define constants for the Luftdaten component.""" +from datetime import timedelta + +ATTR_SENSOR_ID = 'sensor_id' + +CONF_SENSOR_ID = 'sensor_id' + +DEFAULT_SCAN_INTERVAL = timedelta(minutes=10) + +DOMAIN = 'luftdaten' diff --git a/homeassistant/components/luftdaten/strings.json b/homeassistant/components/luftdaten/strings.json new file mode 100644 index 00000000000..2ba15087c48 --- /dev/null +++ b/homeassistant/components/luftdaten/strings.json @@ -0,0 +1,20 @@ +{ + "config": { + "title": "Luftdaten", + "step": { + "user": { + "title": "Define Luftdaten", + "data": { + "station_id": "Luftdaten Sensor ID", + "show_on_map": "Show on map" + + } + } + }, + "error": { + "sensor_exists": "Sensor already registered", + "invalid_sensor": "Sensor not available or invalid", + "communication_error": "Unable to communicate with the Luftdaten API" + } + } +} diff --git a/homeassistant/components/sensor/luftdaten.py b/homeassistant/components/sensor/luftdaten.py index 445ccb7214e..4752286b9b2 100644 --- a/homeassistant/components/sensor/luftdaten.py +++ b/homeassistant/components/sensor/luftdaten.py @@ -4,152 +4,120 @@ Support for Luftdaten sensors. For more details about this platform, please refer to the documentation at https://home-assistant.io/components/sensor.luftdaten/ """ -from datetime import timedelta import logging -import voluptuous as vol - -from homeassistant.components.sensor import PLATFORM_SCHEMA +from homeassistant.components.luftdaten import ( + DATA_LUFTDATEN, DATA_LUFTDATEN_CLIENT, DEFAULT_ATTRIBUTION, DOMAIN, + SENSORS, TOPIC_UPDATE) +from homeassistant.components.luftdaten.const import ATTR_SENSOR_ID from homeassistant.const import ( - ATTR_ATTRIBUTION, ATTR_LATITUDE, ATTR_LONGITUDE, CONF_MONITORED_CONDITIONS, - CONF_NAME, CONF_SHOW_ON_MAP, TEMP_CELSIUS) -from homeassistant.helpers.aiohttp_client import async_get_clientsession -import homeassistant.helpers.config_validation as cv + ATTR_ATTRIBUTION, ATTR_LATITUDE, ATTR_LONGITUDE, CONF_SHOW_ON_MAP) +from homeassistant.core import callback +from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.entity import Entity -from homeassistant.util import Throttle - -REQUIREMENTS = ['luftdaten==0.2.0'] _LOGGER = logging.getLogger(__name__) -ATTR_SENSOR_ID = 'sensor_id' - -CONF_ATTRIBUTION = "Data provided by luftdaten.info" - - -VOLUME_MICROGRAMS_PER_CUBIC_METER = 'µg/m3' - -SENSOR_TEMPERATURE = 'temperature' -SENSOR_HUMIDITY = 'humidity' -SENSOR_PM10 = 'P1' -SENSOR_PM2_5 = 'P2' -SENSOR_PRESSURE = 'pressure' - -SENSOR_TYPES = { - SENSOR_TEMPERATURE: ['Temperature', TEMP_CELSIUS], - SENSOR_HUMIDITY: ['Humidity', '%'], - SENSOR_PRESSURE: ['Pressure', 'Pa'], - SENSOR_PM10: ['PM10', VOLUME_MICROGRAMS_PER_CUBIC_METER], - SENSOR_PM2_5: ['PM2.5', VOLUME_MICROGRAMS_PER_CUBIC_METER] -} - -DEFAULT_NAME = 'Luftdaten' - -CONF_SENSORID = 'sensorid' - -MIN_TIME_BETWEEN_UPDATES = timedelta(minutes=5) - -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ - vol.Required(CONF_SENSORID): cv.positive_int, - vol.Required(CONF_MONITORED_CONDITIONS): - vol.All(cv.ensure_list, [vol.In(SENSOR_TYPES)]), - vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, - vol.Optional(CONF_SHOW_ON_MAP, default=False): cv.boolean, -}) +DEPENDENCIES = ['luftdaten'] async def async_setup_platform( hass, config, async_add_entities, discovery_info=None): - """Set up the Luftdaten sensor.""" - from luftdaten import Luftdaten + """Set up an Luftdaten sensor based on existing config.""" + pass - name = config.get(CONF_NAME) - show_on_map = config.get(CONF_SHOW_ON_MAP) - sensor_id = config.get(CONF_SENSORID) - session = async_get_clientsession(hass) - luftdaten = LuftdatenData(Luftdaten(sensor_id, hass.loop, session)) +async def async_setup_entry(hass, entry, async_add_entities): + """Set up a Luftdaten sensor based on a config entry.""" + luftdaten = hass.data[DOMAIN][DATA_LUFTDATEN_CLIENT][entry.entry_id] - await luftdaten.async_update() + sensors = [] + for sensor_type in luftdaten.sensor_conditions: + name, icon, unit = SENSORS[sensor_type] + sensors.append( + LuftdatenSensor( + luftdaten, sensor_type, name, icon, unit, + entry.data[CONF_SHOW_ON_MAP]) + ) - if luftdaten.data is None: - _LOGGER.error("Sensor is not available: %s", sensor_id) - return - - devices = [] - for variable in config[CONF_MONITORED_CONDITIONS]: - if luftdaten.data.values[variable] is None: - _LOGGER.warning("It might be that sensor %s is not providing " - "measurements for %s", sensor_id, variable) - devices.append( - LuftdatenSensor(luftdaten, name, variable, sensor_id, show_on_map)) - - async_add_entities(devices) + async_add_entities(sensors, True) class LuftdatenSensor(Entity): """Implementation of a Luftdaten sensor.""" - def __init__(self, luftdaten, name, sensor_type, sensor_id, show): + def __init__( + self, luftdaten, sensor_type, name, icon, unit, show): """Initialize the Luftdaten sensor.""" + self._async_unsub_dispatcher_connect = None self.luftdaten = luftdaten + self._icon = icon self._name = name - self._state = None - self._sensor_id = sensor_id + self._data = None self.sensor_type = sensor_type - self._unit_of_measurement = SENSOR_TYPES[sensor_type][1] + self._unit_of_measurement = unit self._show_on_map = show + self._attrs = {} @property - def name(self): - """Return the name of the sensor.""" - return '{} {}'.format(self._name, SENSOR_TYPES[self.sensor_type][0]) + def icon(self): + """Return the icon.""" + return self._icon @property def state(self): """Return the state of the device.""" - return self.luftdaten.data.values[self.sensor_type] + return self._data[self.sensor_type] @property def unit_of_measurement(self): """Return the unit of measurement of this entity, if any.""" return self._unit_of_measurement + @property + def should_poll(self): + """Disable polling.""" + return False + + @property + def unique_id(self) -> str: + """Return a unique, friendly identifier for this entity.""" + return '{0}_{1}'.format(self._data['sensor_id'], self.sensor_type) + @property def device_state_attributes(self): """Return the state attributes.""" - onmap = ATTR_LATITUDE, ATTR_LONGITUDE - nomap = 'lat', 'long' - lat_format, lon_format = onmap if self._show_on_map else nomap + self._attrs[ATTR_SENSOR_ID] = self._data['sensor_id'] + self._attrs[ATTR_ATTRIBUTION] = DEFAULT_ATTRIBUTION + + on_map = ATTR_LATITUDE, ATTR_LONGITUDE + no_map = 'lat', 'long' + lat_format, lon_format = on_map if self._show_on_map else no_map try: - attr = { - ATTR_ATTRIBUTION: CONF_ATTRIBUTION, - ATTR_SENSOR_ID: self._sensor_id, - lat_format: self.luftdaten.data.meta['latitude'], - lon_format: self.luftdaten.data.meta['longitude'], - } - return attr + self._attrs[lon_format] = self._data['longitude'] + self._attrs[lat_format] = self._data['latitude'] + return self._attrs except KeyError: return + async def async_added_to_hass(self): + """Register callbacks.""" + @callback + def update(): + """Update the state.""" + self.async_schedule_update_ha_state(True) + + self._async_unsub_dispatcher_connect = async_dispatcher_connect( + self.hass, TOPIC_UPDATE, update) + + async def async_will_remove_from_hass(self): + """Disconnect dispatcher listener when removed.""" + if self._async_unsub_dispatcher_connect: + self._async_unsub_dispatcher_connect() + async def async_update(self): - """Get the latest data from luftdaten.info and update the state.""" - await self.luftdaten.async_update() - - -class LuftdatenData: - """Class for handling the data retrieval.""" - - def __init__(self, data): - """Initialize the data object.""" - self.data = data - - @Throttle(MIN_TIME_BETWEEN_UPDATES) - async def async_update(self): - """Get the latest data from luftdaten.info.""" - from luftdaten.exceptions import LuftdatenError - + """Get the latest data and update the state.""" try: - await self.data.async_get_data() - except LuftdatenError: - _LOGGER.error("Unable to retrieve data from luftdaten.info") + self._data = self.luftdaten.data[DATA_LUFTDATEN] + except KeyError: + return diff --git a/homeassistant/config_entries.py b/homeassistant/config_entries.py index eaef97011fc..513f225db03 100644 --- a/homeassistant/config_entries.py +++ b/homeassistant/config_entries.py @@ -145,6 +145,7 @@ FLOWS = [ 'ios', 'lifx', 'mailgun', + 'luftdaten', 'mqtt', 'nest', 'openuv', diff --git a/requirements_all.txt b/requirements_all.txt index a4933db7a6b..971800082ca 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -590,8 +590,8 @@ locationsharinglib==3.0.7 # homeassistant.components.logi_circle logi_circle==0.1.7 -# homeassistant.components.sensor.luftdaten -luftdaten==0.2.0 +# homeassistant.components.luftdaten +luftdaten==0.3.4 # homeassistant.components.light.lw12wifi lw12==0.9.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 8246079fe26..1cf0de72104 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -112,6 +112,9 @@ libpurecoollink==0.4.2 # homeassistant.components.media_player.soundtouch libsoundtouch==0.7.2 +# homeassistant.components.luftdaten +luftdaten==0.3.4 + # homeassistant.components.sensor.mfi # homeassistant.components.switch.mfi mficlient==0.3.0 diff --git a/script/gen_requirements_all.py b/script/gen_requirements_all.py index 97711b5e893..e5da8b48360 100755 --- a/script/gen_requirements_all.py +++ b/script/gen_requirements_all.py @@ -50,12 +50,12 @@ TEST_REQUIREMENTS = ( 'evohomeclient', 'feedparser', 'foobot_async', - 'gTTS-token', 'geojson_client', 'georss_client', + 'gTTS-token', + 'ha-ffmpeg', 'hangups', 'HAP-python', - 'ha-ffmpeg', 'haversine', 'hbmqtt', 'hdate', @@ -65,6 +65,7 @@ TEST_REQUIREMENTS = ( 'influxdb', 'libpurecoollink', 'libsoundtouch', + 'luftdaten', 'mficlient', 'numpy', 'paho-mqtt', diff --git a/tests/components/luftdaten/__init__.py b/tests/components/luftdaten/__init__.py new file mode 100644 index 00000000000..d4249f69da2 --- /dev/null +++ b/tests/components/luftdaten/__init__.py @@ -0,0 +1 @@ +"""Define tests for the Luftdaten component.""" diff --git a/tests/components/luftdaten/test_config_flow.py b/tests/components/luftdaten/test_config_flow.py new file mode 100644 index 00000000000..5c005507fbc --- /dev/null +++ b/tests/components/luftdaten/test_config_flow.py @@ -0,0 +1,114 @@ +"""Define tests for the Luftdaten config flow.""" +from datetime import timedelta +from unittest.mock import patch + +from homeassistant import data_entry_flow +from homeassistant.components.luftdaten import DOMAIN, config_flow +from homeassistant.components.luftdaten.const import CONF_SENSOR_ID +from homeassistant.const import CONF_SCAN_INTERVAL, CONF_SHOW_ON_MAP + +from tests.common import MockConfigEntry, mock_coro + + +async def test_duplicate_error(hass): + """Test that errors are shown when duplicates are added.""" + conf = { + CONF_SENSOR_ID: '12345abcde', + } + + MockConfigEntry(domain=DOMAIN, data=conf).add_to_hass(hass) + flow = config_flow.LuftDatenFlowHandler() + flow.hass = hass + + result = await flow.async_step_user(user_input=conf) + assert result['errors'] == {CONF_SENSOR_ID: 'sensor_exists'} + + +async def test_communication_error(hass): + """Test that no sensor is added while unable to communicate with API.""" + conf = { + CONF_SENSOR_ID: '12345abcde', + } + + flow = config_flow.LuftDatenFlowHandler() + flow.hass = hass + + with patch('luftdaten.Luftdaten.get_data', return_value=mock_coro(None)): + result = await flow.async_step_user(user_input=conf) + assert result['errors'] == {CONF_SENSOR_ID: 'invalid_sensor'} + + +async def test_invalid_sensor(hass): + """Test that an invalid sensor throws an error.""" + conf = { + CONF_SENSOR_ID: '12345abcde', + } + + flow = config_flow.LuftDatenFlowHandler() + flow.hass = hass + + with patch('luftdaten.Luftdaten.get_data', return_value=mock_coro(False)),\ + patch('luftdaten.Luftdaten.validate_sensor', + return_value=mock_coro(False)): + result = await flow.async_step_user(user_input=conf) + assert result['errors'] == {CONF_SENSOR_ID: 'invalid_sensor'} + + +async def test_show_form(hass): + """Test that the form is served with no input.""" + flow = config_flow.LuftDatenFlowHandler() + flow.hass = hass + + result = await flow.async_step_user(user_input=None) + + assert result['type'] == data_entry_flow.RESULT_TYPE_FORM + assert result['step_id'] == 'user' + + +async def test_step_import(hass): + """Test that the import step works.""" + conf = { + CONF_SENSOR_ID: '12345abcde', + CONF_SHOW_ON_MAP: False, + } + + flow = config_flow.LuftDatenFlowHandler() + flow.hass = hass + + with patch('luftdaten.Luftdaten.get_data', return_value=mock_coro(True)), \ + patch('luftdaten.Luftdaten.validate_sensor', + return_value=mock_coro(True)): + result = await flow.async_step_import(import_config=conf) + + assert result['type'] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result['title'] == '12345abcde' + assert result['data'] == { + CONF_SENSOR_ID: '12345abcde', + CONF_SHOW_ON_MAP: False, + CONF_SCAN_INTERVAL: 600, + } + + +async def test_step_user(hass): + """Test that the user step works.""" + conf = { + CONF_SENSOR_ID: '12345abcde', + CONF_SHOW_ON_MAP: False, + CONF_SCAN_INTERVAL: timedelta(minutes=5), + } + + flow = config_flow.LuftDatenFlowHandler() + flow.hass = hass + + with patch('luftdaten.Luftdaten.get_data', return_value=mock_coro(True)), \ + patch('luftdaten.Luftdaten.validate_sensor', + return_value=mock_coro(True)): + result = await flow.async_step_user(user_input=conf) + + assert result['type'] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result['title'] == '12345abcde' + assert result['data'] == { + CONF_SENSOR_ID: '12345abcde', + CONF_SHOW_ON_MAP: False, + CONF_SCAN_INTERVAL: 300, + } diff --git a/tests/components/luftdaten/test_init.py b/tests/components/luftdaten/test_init.py new file mode 100644 index 00000000000..eb2c0895c59 --- /dev/null +++ b/tests/components/luftdaten/test_init.py @@ -0,0 +1,36 @@ +"""Test the Luftdaten component setup.""" +from unittest.mock import patch + +from homeassistant.components import luftdaten +from homeassistant.components.luftdaten.const import CONF_SENSOR_ID, DOMAIN +from homeassistant.const import CONF_SCAN_INTERVAL, CONF_SHOW_ON_MAP +from homeassistant.setup import async_setup_component + + +async def test_config_with_sensor_passed_to_config_entry(hass): + """Test that configured options for a sensor are loaded.""" + conf = { + CONF_SENSOR_ID: '12345abcde', + CONF_SHOW_ON_MAP: False, + CONF_SCAN_INTERVAL: 600, + } + + with patch.object(hass, 'config_entries') as mock_config_entries, \ + patch.object(luftdaten, 'configured_sensors', return_value=[]): + assert await async_setup_component(hass, DOMAIN, conf) is True + + assert len(mock_config_entries.flow.mock_calls) == 0 + + +async def test_config_already_registered_not_passed_to_config_entry(hass): + """Test that an already registered sensor does not initiate an import.""" + conf = { + CONF_SENSOR_ID: '12345abcde', + } + + with patch.object(hass, 'config_entries') as mock_config_entries, \ + patch.object(luftdaten, 'configured_sensors', + return_value=['12345abcde']): + assert await async_setup_component(hass, DOMAIN, conf) is True + + assert len(mock_config_entries.flow.mock_calls) == 0