From 8aa1283adc6eb9050e6c5b5594eb3bcf1cf7e01f Mon Sep 17 00:00:00 2001 From: Aaron Bach Date: Wed, 14 Nov 2018 13:23:49 -0700 Subject: [PATCH] Add Rainmachine config entry (#18419) * Initial stuff * More work in place * Starting with tests * Device registry in place * Hound * Linting * Member comments (including extracting device registry) * Member comments (plus I forgot cleanup!) * Hound * More Hound * Removed old import * Adding config entry test to coverage * Updated strings --- .coveragerc | 2 +- .../components/binary_sensor/rainmachine.py | 40 +++--- .../rainmachine/.translations/en.json | 19 +++ .../components/rainmachine/__init__.py | 128 ++++++++++++------ .../components/rainmachine/config_flow.py | 85 ++++++++++++ homeassistant/components/rainmachine/const.py | 14 ++ .../components/rainmachine/strings.json | 19 +++ .../components/sensor/rainmachine.py | 36 +++-- .../components/switch/rainmachine.py | 27 ++-- homeassistant/config_entries.py | 1 + requirements_all.txt | 2 +- requirements_test_all.txt | 3 + script/gen_requirements_all.py | 1 + tests/components/rainmachine/__init__.py | 1 + .../rainmachine/test_config_flow.py | 109 +++++++++++++++ 15 files changed, 400 insertions(+), 87 deletions(-) create mode 100644 homeassistant/components/rainmachine/.translations/en.json create mode 100644 homeassistant/components/rainmachine/config_flow.py create mode 100644 homeassistant/components/rainmachine/const.py create mode 100644 homeassistant/components/rainmachine/strings.json create mode 100644 tests/components/rainmachine/__init__.py create mode 100644 tests/components/rainmachine/test_config_flow.py diff --git a/.coveragerc b/.coveragerc index 21589759084..2762dffbeb1 100644 --- a/.coveragerc +++ b/.coveragerc @@ -271,7 +271,7 @@ omit = homeassistant/components/raincloud.py homeassistant/components/*/raincloud.py - homeassistant/components/rainmachine/* + homeassistant/components/rainmachine/__init__.py homeassistant/components/*/rainmachine.py homeassistant/components/raspihats.py diff --git a/homeassistant/components/binary_sensor/rainmachine.py b/homeassistant/components/binary_sensor/rainmachine.py index 12c9b3e98f0..88b2dd22d52 100644 --- a/homeassistant/components/binary_sensor/rainmachine.py +++ b/homeassistant/components/binary_sensor/rainmachine.py @@ -8,28 +8,29 @@ import logging from homeassistant.components.binary_sensor import BinarySensorDevice from homeassistant.components.rainmachine import ( - BINARY_SENSORS, DATA_RAINMACHINE, SENSOR_UPDATE_TOPIC, TYPE_FREEZE, - TYPE_FREEZE_PROTECTION, TYPE_HOT_DAYS, TYPE_HOURLY, TYPE_MONTH, - TYPE_RAINDELAY, TYPE_RAINSENSOR, TYPE_WEEKDAY, RainMachineEntity) -from homeassistant.const import CONF_MONITORED_CONDITIONS + BINARY_SENSORS, DATA_CLIENT, DOMAIN as RAINMACHINE_DOMAIN, + SENSOR_UPDATE_TOPIC, TYPE_FREEZE, TYPE_FREEZE_PROTECTION, TYPE_HOT_DAYS, + TYPE_HOURLY, TYPE_MONTH, TYPE_RAINDELAY, TYPE_RAINSENSOR, TYPE_WEEKDAY, + RainMachineEntity) from homeassistant.core import callback from homeassistant.helpers.dispatcher import async_dispatcher_connect DEPENDENCIES = ['rainmachine'] - _LOGGER = logging.getLogger(__name__) async def async_setup_platform( hass, config, async_add_entities, discovery_info=None): - """Set up the RainMachine Switch platform.""" - if discovery_info is None: - return + """Set up RainMachine binary sensors based on the old way.""" + pass - rainmachine = hass.data[DATA_RAINMACHINE] + +async def async_setup_entry(hass, entry, async_add_entities): + """Set up RainMachine binary sensors based on a config entry.""" + rainmachine = hass.data[RAINMACHINE_DOMAIN][DATA_CLIENT][entry.entry_id] binary_sensors = [] - for sensor_type in discovery_info[CONF_MONITORED_CONDITIONS]: + for sensor_type in rainmachine.binary_sensor_conditions: name, icon = BINARY_SENSORS[sensor_type] binary_sensors.append( RainMachineBinarySensor(rainmachine, sensor_type, name, icon)) @@ -70,15 +71,20 @@ class RainMachineBinarySensor(RainMachineEntity, BinarySensorDevice): return '{0}_{1}'.format( self.rainmachine.device_mac.replace(':', ''), self._sensor_type) - @callback - def _update_data(self): - """Update the state.""" - self.async_schedule_update_ha_state(True) - async def async_added_to_hass(self): """Register callbacks.""" - async_dispatcher_connect( - self.hass, SENSOR_UPDATE_TOPIC, self._update_data) + @callback + def update(self): + """Update the state.""" + self.async_schedule_update_ha_state(True) + + self._async_unsub_dispatcher_connect = async_dispatcher_connect( + self.hass, SENSOR_UPDATE_TOPIC, 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): """Update the state.""" diff --git a/homeassistant/components/rainmachine/.translations/en.json b/homeassistant/components/rainmachine/.translations/en.json new file mode 100644 index 00000000000..54b67066f2b --- /dev/null +++ b/homeassistant/components/rainmachine/.translations/en.json @@ -0,0 +1,19 @@ +{ + "config": { + "error": { + "identifier_exists": "Account already registered", + "invalid_credentials": "Invalid credentials" + }, + "step": { + "user": { + "data": { + "ip_address": "Hostname or IP Address", + "password": "Password", + "port": "Port" + }, + "title": "Fill in your information" + } + }, + "title": "RainMachine" + } +} \ No newline at end of file diff --git a/homeassistant/components/rainmachine/__init__.py b/homeassistant/components/rainmachine/__init__.py index 9f15c8b373f..5778d9db4df 100644 --- a/homeassistant/components/rainmachine/__init__.py +++ b/homeassistant/components/rainmachine/__init__.py @@ -9,25 +9,25 @@ from datetime import timedelta import voluptuous as vol +from homeassistant.config_entries import SOURCE_IMPORT from homeassistant.const import ( ATTR_ATTRIBUTION, CONF_BINARY_SENSORS, CONF_IP_ADDRESS, CONF_PASSWORD, CONF_PORT, CONF_SCAN_INTERVAL, CONF_SENSORS, CONF_SSL, CONF_MONITORED_CONDITIONS, CONF_SWITCHES) -from homeassistant.helpers import ( - aiohttp_client, config_validation as cv, discovery) +from homeassistant.exceptions import ConfigEntryNotReady +from homeassistant.helpers import aiohttp_client, config_validation as cv from homeassistant.helpers.dispatcher import async_dispatcher_send from homeassistant.helpers.entity import Entity from homeassistant.helpers.event import async_track_time_interval -REQUIREMENTS = ['regenmaschine==1.0.2'] +from .config_flow import configured_instances +from .const import DATA_CLIENT, DEFAULT_PORT, DEFAULT_SCAN_INTERVAL, DOMAIN + +REQUIREMENTS = ['regenmaschine==1.0.7'] _LOGGER = logging.getLogger(__name__) -DATA_RAINMACHINE = 'data_rainmachine' -DOMAIN = 'rainmachine' - -NOTIFICATION_ID = 'rainmachine_notification' -NOTIFICATION_TITLE = 'RainMachine Component Setup' +DATA_LISTENER = 'listener' PROGRAM_UPDATE_TOPIC = '{0}_program_update'.format(DOMAIN) SENSOR_UPDATE_TOPIC = '{0}_data_update'.format(DOMAIN) @@ -39,8 +39,6 @@ CONF_ZONE_RUN_TIME = 'zone_run_time' DEFAULT_ATTRIBUTION = 'Data provided by Green Electronics LLC' DEFAULT_ICON = 'mdi:water' -DEFAULT_PORT = 8080 -DEFAULT_SCAN_INTERVAL = timedelta(seconds=60) DEFAULT_SSL = True DEFAULT_ZONE_RUN = 60 * 10 @@ -120,48 +118,73 @@ CONFIG_SCHEMA = vol.Schema( async def async_setup(hass, config): """Set up the RainMachine component.""" - from regenmaschine import Client - from regenmaschine.errors import RequestError + hass.data[DOMAIN] = {} + hass.data[DOMAIN][DATA_CLIENT] = {} + hass.data[DOMAIN][DATA_LISTENER] = {} + + if DOMAIN not in config: + return True conf = config[DOMAIN] - ip_address = conf[CONF_IP_ADDRESS] - password = conf[CONF_PASSWORD] - port = conf[CONF_PORT] - ssl = conf[CONF_SSL] + + if conf[CONF_IP_ADDRESS] in configured_instances(hass): + return True + + hass.async_create_task( + hass.config_entries.flow.async_init( + DOMAIN, + context={'source': SOURCE_IMPORT}, + data=conf)) + + return True + + +async def async_setup_entry(hass, config_entry): + """Set up RainMachine as config entry.""" + from regenmaschine import login + from regenmaschine.errors import RainMachineError + + ip_address = config_entry.data[CONF_IP_ADDRESS] + password = config_entry.data[CONF_PASSWORD] + port = config_entry.data[CONF_PORT] + ssl = config_entry.data.get(CONF_SSL, DEFAULT_SSL) + + websession = aiohttp_client.async_get_clientsession(hass) try: - websession = aiohttp_client.async_get_clientsession(hass) - client = Client(ip_address, websession, port=port, ssl=ssl) - await client.authenticate(password) - rainmachine = RainMachine(client) + client = await login( + ip_address, password, websession, port=port, ssl=ssl) + rainmachine = RainMachine( + client, + config_entry.data.get(CONF_BINARY_SENSORS, {}).get( + CONF_MONITORED_CONDITIONS, list(BINARY_SENSORS)), + config_entry.data.get(CONF_SENSORS, {}).get( + CONF_MONITORED_CONDITIONS, list(SENSORS)), + config_entry.data.get(CONF_ZONE_RUN_TIME, DEFAULT_ZONE_RUN) + ) await rainmachine.async_update() - hass.data[DATA_RAINMACHINE] = rainmachine - except RequestError as err: - _LOGGER.error('An error occurred: %s', str(err)) - hass.components.persistent_notification.create( - 'Error: {0}
' - 'You will need to restart hass after fixing.' - ''.format(err), - title=NOTIFICATION_TITLE, - notification_id=NOTIFICATION_ID) - return False + except RainMachineError as err: + _LOGGER.error('An error occurred: %s', err) + raise ConfigEntryNotReady - for component, schema in [ - ('binary_sensor', conf[CONF_BINARY_SENSORS]), - ('sensor', conf[CONF_SENSORS]), - ('switch', conf[CONF_SWITCHES]), - ]: + hass.data[DOMAIN][DATA_CLIENT][config_entry.entry_id] = rainmachine + + for component in ('binary_sensor', 'sensor', 'switch'): hass.async_create_task( - discovery.async_load_platform(hass, component, DOMAIN, schema, - config)) + hass.config_entries.async_forward_entry_setup( + config_entry, component)) - async def refresh_sensors(event_time): + async def refresh(event_time): """Refresh RainMachine sensor data.""" _LOGGER.debug('Updating RainMachine sensor data') await rainmachine.async_update() async_dispatcher_send(hass, SENSOR_UPDATE_TOPIC) - async_track_time_interval(hass, refresh_sensors, conf[CONF_SCAN_INTERVAL]) + hass.data[DOMAIN][DATA_LISTENER][ + config_entry.entry_id] = async_track_time_interval( + hass, + refresh, + timedelta(seconds=config_entry.data[CONF_SCAN_INTERVAL])) async def start_program(service): """Start a particular program.""" @@ -170,8 +193,8 @@ async def async_setup(hass, config): async def start_zone(service): """Start a particular zone for a certain amount of time.""" - await rainmachine.client.zones.start(service.data[CONF_ZONE_ID], - service.data[CONF_ZONE_RUN_TIME]) + await rainmachine.client.zones.start( + service.data[CONF_ZONE_ID], service.data[CONF_ZONE_RUN_TIME]) async_dispatcher_send(hass, ZONE_UPDATE_TOPIC) async def stop_all(service): @@ -201,14 +224,34 @@ async def async_setup(hass, config): return True +async def async_unload_entry(hass, config_entry): + """Unload an OpenUV config entry.""" + hass.data[DOMAIN][DATA_CLIENT].pop(config_entry.entry_id) + + remove_listener = hass.data[DOMAIN][DATA_LISTENER].pop( + config_entry.entry_id) + remove_listener() + + for component in ('binary_sensor', 'sensor', 'switch'): + await hass.config_entries.async_forward_entry_unload( + config_entry, component) + + return True + + class RainMachine: """Define a generic RainMachine object.""" - def __init__(self, client): + def __init__( + self, client, binary_sensor_conditions, sensor_conditions, + default_zone_runtime): """Initialize.""" + self.binary_sensor_conditions = binary_sensor_conditions self.client = client + self.default_zone_runtime = default_zone_runtime self.device_mac = self.client.mac self.restrictions = {} + self.sensor_conditions = sensor_conditions async def async_update(self): """Update sensor/binary sensor data.""" @@ -224,6 +267,7 @@ class RainMachineEntity(Entity): def __init__(self, rainmachine): """Initialize.""" self._attrs = {ATTR_ATTRIBUTION: DEFAULT_ATTRIBUTION} + self._async_unsub_dispatcher_connect = None self._name = None self.rainmachine = rainmachine diff --git a/homeassistant/components/rainmachine/config_flow.py b/homeassistant/components/rainmachine/config_flow.py new file mode 100644 index 00000000000..ecf497333cb --- /dev/null +++ b/homeassistant/components/rainmachine/config_flow.py @@ -0,0 +1,85 @@ +"""Config flow to configure the RainMachine component.""" + +from collections import OrderedDict + +import voluptuous as vol + +from homeassistant import config_entries +from homeassistant.core import callback +from homeassistant.const import ( + CONF_IP_ADDRESS, CONF_PASSWORD, CONF_PORT, CONF_SCAN_INTERVAL) +from homeassistant.helpers import aiohttp_client + +from .const import DEFAULT_PORT, DEFAULT_SCAN_INTERVAL, DOMAIN + + +@callback +def configured_instances(hass): + """Return a set of configured RainMachine instances.""" + return set( + entry.data[CONF_IP_ADDRESS] + for entry in hass.config_entries.async_entries(DOMAIN)) + + +@config_entries.HANDLERS.register(DOMAIN) +class RainMachineFlowHandler(config_entries.ConfigFlow): + """Handle a RainMachine config flow.""" + + VERSION = 1 + CONNECTION_CLASS = config_entries.CONN_CLASS_LOCAL_POLL + + def __init__(self): + """Initialize the config flow.""" + self.data_schema = OrderedDict() + self.data_schema[vol.Required(CONF_IP_ADDRESS)] = str + self.data_schema[vol.Required(CONF_PASSWORD)] = str + self.data_schema[vol.Optional(CONF_PORT, default=DEFAULT_PORT)] = int + + async def _show_form(self, errors=None): + """Show the form to the user.""" + return self.async_show_form( + step_id='user', + data_schema=vol.Schema(self.data_schema), + errors=errors if errors else {}, + ) + + 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 regenmaschine import login + from regenmaschine.errors import RainMachineError + + if not user_input: + return await self._show_form() + + if user_input[CONF_IP_ADDRESS] in configured_instances(self.hass): + return await self._show_form({ + CONF_IP_ADDRESS: 'identifier_exists' + }) + + websession = aiohttp_client.async_get_clientsession(self.hass) + + try: + await login( + user_input[CONF_IP_ADDRESS], + user_input[CONF_PASSWORD], + websession, + port=user_input.get(CONF_PORT, DEFAULT_PORT), + ssl=True) + except RainMachineError: + return await self._show_form({ + CONF_PASSWORD: 'invalid_credentials' + }) + + scan_interval = user_input.get( + CONF_SCAN_INTERVAL, DEFAULT_SCAN_INTERVAL) + user_input[CONF_SCAN_INTERVAL] = scan_interval.seconds + + # Unfortunately, RainMachine doesn't provide a way to refresh the + # access token without using the IP address and password, so we have to + # store it: + return self.async_create_entry( + title=user_input[CONF_IP_ADDRESS], data=user_input) diff --git a/homeassistant/components/rainmachine/const.py b/homeassistant/components/rainmachine/const.py new file mode 100644 index 00000000000..ec1f0436ccb --- /dev/null +++ b/homeassistant/components/rainmachine/const.py @@ -0,0 +1,14 @@ +"""Define constants for the SimpliSafe component.""" +import logging +from datetime import timedelta + +LOGGER = logging.getLogger('homeassistant.components.rainmachine') + +DOMAIN = 'rainmachine' + +DATA_CLIENT = 'client' + +DEFAULT_PORT = 8080 +DEFAULT_SCAN_INTERVAL = timedelta(seconds=60) + +TOPIC_UPDATE = 'update_{0}' diff --git a/homeassistant/components/rainmachine/strings.json b/homeassistant/components/rainmachine/strings.json new file mode 100644 index 00000000000..6e26192ec82 --- /dev/null +++ b/homeassistant/components/rainmachine/strings.json @@ -0,0 +1,19 @@ +{ + "config": { + "title": "RainMachine", + "step": { + "user": { + "title": "Fill in your information", + "data": { + "ip_address": "Hostname or IP Address", + "password": "Password", + "port": "Port" + } + } + }, + "error": { + "identifier_exists": "Account already registered", + "invalid_credentials": "Invalid credentials" + } + } +} diff --git a/homeassistant/components/sensor/rainmachine.py b/homeassistant/components/sensor/rainmachine.py index 20e95f0e98f..59efd4c47f6 100644 --- a/homeassistant/components/sensor/rainmachine.py +++ b/homeassistant/components/sensor/rainmachine.py @@ -7,26 +7,27 @@ https://home-assistant.io/components/sensor.rainmachine/ import logging from homeassistant.components.rainmachine import ( - DATA_RAINMACHINE, SENSOR_UPDATE_TOPIC, SENSORS, RainMachineEntity) -from homeassistant.const import CONF_MONITORED_CONDITIONS + DATA_CLIENT, DOMAIN as RAINMACHINE_DOMAIN, SENSOR_UPDATE_TOPIC, SENSORS, + RainMachineEntity) from homeassistant.core import callback from homeassistant.helpers.dispatcher import async_dispatcher_connect DEPENDENCIES = ['rainmachine'] - _LOGGER = logging.getLogger(__name__) async def async_setup_platform( hass, config, async_add_entities, discovery_info=None): - """Set up the RainMachine Switch platform.""" - if discovery_info is None: - return + """Set up RainMachine sensors based on the old way.""" + pass - rainmachine = hass.data[DATA_RAINMACHINE] + +async def async_setup_entry(hass, entry, async_add_entities): + """Set up RainMachine sensors based on a config entry.""" + rainmachine = hass.data[RAINMACHINE_DOMAIN][DATA_CLIENT][entry.entry_id] sensors = [] - for sensor_type in discovery_info[CONF_MONITORED_CONDITIONS]: + for sensor_type in rainmachine.sensor_conditions: name, icon, unit = SENSORS[sensor_type] sensors.append( RainMachineSensor(rainmachine, sensor_type, name, icon, unit)) @@ -73,15 +74,20 @@ class RainMachineSensor(RainMachineEntity): """Return the unit the value is expressed in.""" return self._unit - @callback - def _update_data(self): - """Update the state.""" - self.async_schedule_update_ha_state(True) - async def async_added_to_hass(self): """Register callbacks.""" - async_dispatcher_connect( - self.hass, SENSOR_UPDATE_TOPIC, self._update_data) + @callback + def update(self): + """Update the state.""" + self.async_schedule_update_ha_state(True) + + self._async_unsub_dispatcher_connect = async_dispatcher_connect( + self.hass, SENSOR_UPDATE_TOPIC, 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): """Update the sensor's state.""" diff --git a/homeassistant/components/switch/rainmachine.py b/homeassistant/components/switch/rainmachine.py index 633a3e50a09..5d03b2691eb 100644 --- a/homeassistant/components/switch/rainmachine.py +++ b/homeassistant/components/switch/rainmachine.py @@ -7,8 +7,8 @@ https://home-assistant.io/components/switch.rainmachine/ import logging from homeassistant.components.rainmachine import ( - CONF_ZONE_RUN_TIME, DATA_RAINMACHINE, DEFAULT_ZONE_RUN, - PROGRAM_UPDATE_TOPIC, ZONE_UPDATE_TOPIC, RainMachineEntity) + DATA_CLIENT, DOMAIN as RAINMACHINE_DOMAIN, PROGRAM_UPDATE_TOPIC, + ZONE_UPDATE_TOPIC, RainMachineEntity) from homeassistant.const import ATTR_ID from homeassistant.components.switch import SwitchDevice from homeassistant.core import callback @@ -101,15 +101,13 @@ VEGETATION_MAP = { async def async_setup_platform( hass, config, async_add_entities, discovery_info=None): - """Set up the RainMachine Switch platform.""" - if discovery_info is None: - return + """Set up RainMachine switches sensor based on the old way.""" + pass - _LOGGER.debug('Config received: %s', discovery_info) - zone_run_time = discovery_info.get(CONF_ZONE_RUN_TIME, DEFAULT_ZONE_RUN) - - rainmachine = hass.data[DATA_RAINMACHINE] +async def async_setup_entry(hass, entry, async_add_entities): + """Set up RainMachine switches based on a config entry.""" + rainmachine = hass.data[RAINMACHINE_DOMAIN][DATA_CLIENT][entry.entry_id] entities = [] @@ -127,7 +125,9 @@ async def async_setup_platform( continue _LOGGER.debug('Adding zone: %s', zone) - entities.append(RainMachineZone(rainmachine, zone, zone_run_time)) + entities.append( + RainMachineZone( + rainmachine, zone, rainmachine.default_zone_runtime)) async_add_entities(entities, True) @@ -186,9 +186,14 @@ class RainMachineProgram(RainMachineSwitch): async def async_added_to_hass(self): """Register callbacks.""" - async_dispatcher_connect( + self._async_unsub_dispatcher_connect = async_dispatcher_connect( self.hass, PROGRAM_UPDATE_TOPIC, self._program_updated) + 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_turn_off(self, **kwargs) -> None: """Turn the program off.""" from regenmaschine.errors import RequestError diff --git a/homeassistant/config_entries.py b/homeassistant/config_entries.py index 513f225db03..6669d5240d8 100644 --- a/homeassistant/config_entries.py +++ b/homeassistant/config_entries.py @@ -149,6 +149,7 @@ FLOWS = [ 'mqtt', 'nest', 'openuv', + 'rainmachine', 'simplisafe', 'smhi', 'sonos', diff --git a/requirements_all.txt b/requirements_all.txt index 429567f9bf2..1545440f8b7 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1336,7 +1336,7 @@ raincloudy==0.0.5 # raspihats==2.2.3 # homeassistant.components.rainmachine -regenmaschine==1.0.2 +regenmaschine==1.0.7 # homeassistant.components.python_script restrictedpython==4.0b6 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index b94a0ac00ab..49cc4dd1102 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -210,6 +210,9 @@ pyunifi==2.13 # homeassistant.components.notify.html5 pywebpush==1.6.0 +# homeassistant.components.rainmachine +regenmaschine==1.0.7 + # homeassistant.components.python_script restrictedpython==4.0b6 diff --git a/script/gen_requirements_all.py b/script/gen_requirements_all.py index 698b35e776f..cf5791ef38d 100755 --- a/script/gen_requirements_all.py +++ b/script/gen_requirements_all.py @@ -95,6 +95,7 @@ TEST_REQUIREMENTS = ( 'pyunifi', 'pyupnp-async', 'pywebpush', + 'regenmaschine', 'restrictedpython', 'rflink', 'ring_doorbell', diff --git a/tests/components/rainmachine/__init__.py b/tests/components/rainmachine/__init__.py new file mode 100644 index 00000000000..d6bd6a5dd95 --- /dev/null +++ b/tests/components/rainmachine/__init__.py @@ -0,0 +1 @@ +"""Define tests for the RainMachine component.""" diff --git a/tests/components/rainmachine/test_config_flow.py b/tests/components/rainmachine/test_config_flow.py new file mode 100644 index 00000000000..2291ac23749 --- /dev/null +++ b/tests/components/rainmachine/test_config_flow.py @@ -0,0 +1,109 @@ +"""Define tests for the OpenUV config flow.""" +from unittest.mock import patch + +from homeassistant import data_entry_flow +from homeassistant.components.rainmachine import DOMAIN, config_flow +from homeassistant.const import ( + CONF_IP_ADDRESS, CONF_PASSWORD, CONF_PORT, CONF_SSL, CONF_SCAN_INTERVAL) + +from tests.common import MockConfigEntry, mock_coro + + +async def test_duplicate_error(hass): + """Test that errors are shown when duplicates are added.""" + conf = { + CONF_IP_ADDRESS: '192.168.1.100', + CONF_PASSWORD: 'password', + CONF_PORT: 8080, + CONF_SSL: True, + } + + MockConfigEntry(domain=DOMAIN, data=conf).add_to_hass(hass) + flow = config_flow.RainMachineFlowHandler() + flow.hass = hass + + result = await flow.async_step_user(user_input=conf) + assert result['errors'] == {CONF_IP_ADDRESS: 'identifier_exists'} + + +async def test_invalid_password(hass): + """Test that an invalid password throws an error.""" + from regenmaschine.errors import RainMachineError + + conf = { + CONF_IP_ADDRESS: '192.168.1.100', + CONF_PASSWORD: 'bad_password', + CONF_PORT: 8080, + CONF_SSL: True, + } + + flow = config_flow.RainMachineFlowHandler() + flow.hass = hass + + with patch('regenmaschine.login', + return_value=mock_coro(exception=RainMachineError)): + result = await flow.async_step_user(user_input=conf) + assert result['errors'] == {CONF_PASSWORD: 'invalid_credentials'} + + +async def test_show_form(hass): + """Test that the form is served with no input.""" + flow = config_flow.RainMachineFlowHandler() + 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_IP_ADDRESS: '192.168.1.100', + CONF_PASSWORD: 'password', + CONF_PORT: 8080, + CONF_SSL: True, + } + + flow = config_flow.RainMachineFlowHandler() + flow.hass = hass + + with patch('regenmaschine.login', 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'] == '192.168.1.100' + assert result['data'] == { + CONF_IP_ADDRESS: '192.168.1.100', + CONF_PASSWORD: 'password', + CONF_PORT: 8080, + CONF_SSL: True, + CONF_SCAN_INTERVAL: 60, + } + + +async def test_step_user(hass): + """Test that the user step works.""" + conf = { + CONF_IP_ADDRESS: '192.168.1.100', + CONF_PASSWORD: 'password', + CONF_PORT: 8080, + CONF_SSL: True, + } + + flow = config_flow.RainMachineFlowHandler() + flow.hass = hass + + with patch('regenmaschine.login', 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'] == '192.168.1.100' + assert result['data'] == { + CONF_IP_ADDRESS: '192.168.1.100', + CONF_PASSWORD: 'password', + CONF_PORT: 8080, + CONF_SSL: True, + CONF_SCAN_INTERVAL: 60, + }