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
This commit is contained in:
Aaron Bach 2018-11-14 13:23:49 -07:00 committed by GitHub
parent 312872961f
commit 8aa1283adc
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
15 changed files with 400 additions and 87 deletions

View file

@ -271,7 +271,7 @@ omit =
homeassistant/components/raincloud.py homeassistant/components/raincloud.py
homeassistant/components/*/raincloud.py homeassistant/components/*/raincloud.py
homeassistant/components/rainmachine/* homeassistant/components/rainmachine/__init__.py
homeassistant/components/*/rainmachine.py homeassistant/components/*/rainmachine.py
homeassistant/components/raspihats.py homeassistant/components/raspihats.py

View file

@ -8,28 +8,29 @@ import logging
from homeassistant.components.binary_sensor import BinarySensorDevice from homeassistant.components.binary_sensor import BinarySensorDevice
from homeassistant.components.rainmachine import ( from homeassistant.components.rainmachine import (
BINARY_SENSORS, DATA_RAINMACHINE, SENSOR_UPDATE_TOPIC, TYPE_FREEZE, BINARY_SENSORS, DATA_CLIENT, DOMAIN as RAINMACHINE_DOMAIN,
TYPE_FREEZE_PROTECTION, TYPE_HOT_DAYS, TYPE_HOURLY, TYPE_MONTH, SENSOR_UPDATE_TOPIC, TYPE_FREEZE, TYPE_FREEZE_PROTECTION, TYPE_HOT_DAYS,
TYPE_RAINDELAY, TYPE_RAINSENSOR, TYPE_WEEKDAY, RainMachineEntity) TYPE_HOURLY, TYPE_MONTH, TYPE_RAINDELAY, TYPE_RAINSENSOR, TYPE_WEEKDAY,
from homeassistant.const import CONF_MONITORED_CONDITIONS RainMachineEntity)
from homeassistant.core import callback from homeassistant.core import callback
from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.dispatcher import async_dispatcher_connect
DEPENDENCIES = ['rainmachine'] DEPENDENCIES = ['rainmachine']
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
async def async_setup_platform( async def async_setup_platform(
hass, config, async_add_entities, discovery_info=None): hass, config, async_add_entities, discovery_info=None):
"""Set up the RainMachine Switch platform.""" """Set up RainMachine binary sensors based on the old way."""
if discovery_info is None: pass
return
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 = [] 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] name, icon = BINARY_SENSORS[sensor_type]
binary_sensors.append( binary_sensors.append(
RainMachineBinarySensor(rainmachine, sensor_type, name, icon)) RainMachineBinarySensor(rainmachine, sensor_type, name, icon))
@ -70,15 +71,20 @@ class RainMachineBinarySensor(RainMachineEntity, BinarySensorDevice):
return '{0}_{1}'.format( return '{0}_{1}'.format(
self.rainmachine.device_mac.replace(':', ''), self._sensor_type) 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): async def async_added_to_hass(self):
"""Register callbacks.""" """Register callbacks."""
async_dispatcher_connect( @callback
self.hass, SENSOR_UPDATE_TOPIC, self._update_data) 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): async def async_update(self):
"""Update the state.""" """Update the state."""

View file

@ -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"
}
}

View file

@ -9,25 +9,25 @@ from datetime import timedelta
import voluptuous as vol import voluptuous as vol
from homeassistant.config_entries import SOURCE_IMPORT
from homeassistant.const import ( from homeassistant.const import (
ATTR_ATTRIBUTION, CONF_BINARY_SENSORS, CONF_IP_ADDRESS, CONF_PASSWORD, ATTR_ATTRIBUTION, CONF_BINARY_SENSORS, CONF_IP_ADDRESS, CONF_PASSWORD,
CONF_PORT, CONF_SCAN_INTERVAL, CONF_SENSORS, CONF_SSL, CONF_PORT, CONF_SCAN_INTERVAL, CONF_SENSORS, CONF_SSL,
CONF_MONITORED_CONDITIONS, CONF_SWITCHES) CONF_MONITORED_CONDITIONS, CONF_SWITCHES)
from homeassistant.helpers import ( from homeassistant.exceptions import ConfigEntryNotReady
aiohttp_client, config_validation as cv, discovery) from homeassistant.helpers import aiohttp_client, config_validation as cv
from homeassistant.helpers.dispatcher import async_dispatcher_send from homeassistant.helpers.dispatcher import async_dispatcher_send
from homeassistant.helpers.entity import Entity from homeassistant.helpers.entity import Entity
from homeassistant.helpers.event import async_track_time_interval 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__) _LOGGER = logging.getLogger(__name__)
DATA_RAINMACHINE = 'data_rainmachine' DATA_LISTENER = 'listener'
DOMAIN = 'rainmachine'
NOTIFICATION_ID = 'rainmachine_notification'
NOTIFICATION_TITLE = 'RainMachine Component Setup'
PROGRAM_UPDATE_TOPIC = '{0}_program_update'.format(DOMAIN) PROGRAM_UPDATE_TOPIC = '{0}_program_update'.format(DOMAIN)
SENSOR_UPDATE_TOPIC = '{0}_data_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_ATTRIBUTION = 'Data provided by Green Electronics LLC'
DEFAULT_ICON = 'mdi:water' DEFAULT_ICON = 'mdi:water'
DEFAULT_PORT = 8080
DEFAULT_SCAN_INTERVAL = timedelta(seconds=60)
DEFAULT_SSL = True DEFAULT_SSL = True
DEFAULT_ZONE_RUN = 60 * 10 DEFAULT_ZONE_RUN = 60 * 10
@ -120,48 +118,73 @@ CONFIG_SCHEMA = vol.Schema(
async def async_setup(hass, config): async def async_setup(hass, config):
"""Set up the RainMachine component.""" """Set up the RainMachine component."""
from regenmaschine import Client hass.data[DOMAIN] = {}
from regenmaschine.errors import RequestError hass.data[DOMAIN][DATA_CLIENT] = {}
hass.data[DOMAIN][DATA_LISTENER] = {}
if DOMAIN not in config:
return True
conf = config[DOMAIN] conf = config[DOMAIN]
ip_address = conf[CONF_IP_ADDRESS]
password = conf[CONF_PASSWORD] if conf[CONF_IP_ADDRESS] in configured_instances(hass):
port = conf[CONF_PORT] return True
ssl = conf[CONF_SSL]
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: try:
websession = aiohttp_client.async_get_clientsession(hass) client = await login(
client = Client(ip_address, websession, port=port, ssl=ssl) ip_address, password, websession, port=port, ssl=ssl)
await client.authenticate(password) rainmachine = RainMachine(
rainmachine = RainMachine(client) 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() await rainmachine.async_update()
hass.data[DATA_RAINMACHINE] = rainmachine except RainMachineError as err:
except RequestError as err: _LOGGER.error('An error occurred: %s', err)
_LOGGER.error('An error occurred: %s', str(err)) raise ConfigEntryNotReady
hass.components.persistent_notification.create(
'Error: {0}<br />'
'You will need to restart hass after fixing.'
''.format(err),
title=NOTIFICATION_TITLE,
notification_id=NOTIFICATION_ID)
return False
for component, schema in [ hass.data[DOMAIN][DATA_CLIENT][config_entry.entry_id] = rainmachine
('binary_sensor', conf[CONF_BINARY_SENSORS]),
('sensor', conf[CONF_SENSORS]), for component in ('binary_sensor', 'sensor', 'switch'):
('switch', conf[CONF_SWITCHES]),
]:
hass.async_create_task( hass.async_create_task(
discovery.async_load_platform(hass, component, DOMAIN, schema, hass.config_entries.async_forward_entry_setup(
config)) config_entry, component))
async def refresh_sensors(event_time): async def refresh(event_time):
"""Refresh RainMachine sensor data.""" """Refresh RainMachine sensor data."""
_LOGGER.debug('Updating RainMachine sensor data') _LOGGER.debug('Updating RainMachine sensor data')
await rainmachine.async_update() await rainmachine.async_update()
async_dispatcher_send(hass, SENSOR_UPDATE_TOPIC) 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): async def start_program(service):
"""Start a particular program.""" """Start a particular program."""
@ -170,8 +193,8 @@ async def async_setup(hass, config):
async def start_zone(service): async def start_zone(service):
"""Start a particular zone for a certain amount of time.""" """Start a particular zone for a certain amount of time."""
await rainmachine.client.zones.start(service.data[CONF_ZONE_ID], await rainmachine.client.zones.start(
service.data[CONF_ZONE_RUN_TIME]) service.data[CONF_ZONE_ID], service.data[CONF_ZONE_RUN_TIME])
async_dispatcher_send(hass, ZONE_UPDATE_TOPIC) async_dispatcher_send(hass, ZONE_UPDATE_TOPIC)
async def stop_all(service): async def stop_all(service):
@ -201,14 +224,34 @@ async def async_setup(hass, config):
return True 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: class RainMachine:
"""Define a generic RainMachine object.""" """Define a generic RainMachine object."""
def __init__(self, client): def __init__(
self, client, binary_sensor_conditions, sensor_conditions,
default_zone_runtime):
"""Initialize.""" """Initialize."""
self.binary_sensor_conditions = binary_sensor_conditions
self.client = client self.client = client
self.default_zone_runtime = default_zone_runtime
self.device_mac = self.client.mac self.device_mac = self.client.mac
self.restrictions = {} self.restrictions = {}
self.sensor_conditions = sensor_conditions
async def async_update(self): async def async_update(self):
"""Update sensor/binary sensor data.""" """Update sensor/binary sensor data."""
@ -224,6 +267,7 @@ class RainMachineEntity(Entity):
def __init__(self, rainmachine): def __init__(self, rainmachine):
"""Initialize.""" """Initialize."""
self._attrs = {ATTR_ATTRIBUTION: DEFAULT_ATTRIBUTION} self._attrs = {ATTR_ATTRIBUTION: DEFAULT_ATTRIBUTION}
self._async_unsub_dispatcher_connect = None
self._name = None self._name = None
self.rainmachine = rainmachine self.rainmachine = rainmachine

View file

@ -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)

View file

@ -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}'

View file

@ -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"
}
}
}

View file

@ -7,26 +7,27 @@ https://home-assistant.io/components/sensor.rainmachine/
import logging import logging
from homeassistant.components.rainmachine import ( from homeassistant.components.rainmachine import (
DATA_RAINMACHINE, SENSOR_UPDATE_TOPIC, SENSORS, RainMachineEntity) DATA_CLIENT, DOMAIN as RAINMACHINE_DOMAIN, SENSOR_UPDATE_TOPIC, SENSORS,
from homeassistant.const import CONF_MONITORED_CONDITIONS RainMachineEntity)
from homeassistant.core import callback from homeassistant.core import callback
from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.dispatcher import async_dispatcher_connect
DEPENDENCIES = ['rainmachine'] DEPENDENCIES = ['rainmachine']
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
async def async_setup_platform( async def async_setup_platform(
hass, config, async_add_entities, discovery_info=None): hass, config, async_add_entities, discovery_info=None):
"""Set up the RainMachine Switch platform.""" """Set up RainMachine sensors based on the old way."""
if discovery_info is None: pass
return
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 = [] sensors = []
for sensor_type in discovery_info[CONF_MONITORED_CONDITIONS]: for sensor_type in rainmachine.sensor_conditions:
name, icon, unit = SENSORS[sensor_type] name, icon, unit = SENSORS[sensor_type]
sensors.append( sensors.append(
RainMachineSensor(rainmachine, sensor_type, name, icon, unit)) RainMachineSensor(rainmachine, sensor_type, name, icon, unit))
@ -73,15 +74,20 @@ class RainMachineSensor(RainMachineEntity):
"""Return the unit the value is expressed in.""" """Return the unit the value is expressed in."""
return self._unit 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): async def async_added_to_hass(self):
"""Register callbacks.""" """Register callbacks."""
async_dispatcher_connect( @callback
self.hass, SENSOR_UPDATE_TOPIC, self._update_data) 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): async def async_update(self):
"""Update the sensor's state.""" """Update the sensor's state."""

View file

@ -7,8 +7,8 @@ https://home-assistant.io/components/switch.rainmachine/
import logging import logging
from homeassistant.components.rainmachine import ( from homeassistant.components.rainmachine import (
CONF_ZONE_RUN_TIME, DATA_RAINMACHINE, DEFAULT_ZONE_RUN, DATA_CLIENT, DOMAIN as RAINMACHINE_DOMAIN, PROGRAM_UPDATE_TOPIC,
PROGRAM_UPDATE_TOPIC, ZONE_UPDATE_TOPIC, RainMachineEntity) ZONE_UPDATE_TOPIC, RainMachineEntity)
from homeassistant.const import ATTR_ID from homeassistant.const import ATTR_ID
from homeassistant.components.switch import SwitchDevice from homeassistant.components.switch import SwitchDevice
from homeassistant.core import callback from homeassistant.core import callback
@ -101,15 +101,13 @@ VEGETATION_MAP = {
async def async_setup_platform( async def async_setup_platform(
hass, config, async_add_entities, discovery_info=None): hass, config, async_add_entities, discovery_info=None):
"""Set up the RainMachine Switch platform.""" """Set up RainMachine switches sensor based on the old way."""
if discovery_info is None: pass
return
_LOGGER.debug('Config received: %s', discovery_info)
zone_run_time = discovery_info.get(CONF_ZONE_RUN_TIME, DEFAULT_ZONE_RUN) async def async_setup_entry(hass, entry, async_add_entities):
"""Set up RainMachine switches based on a config entry."""
rainmachine = hass.data[DATA_RAINMACHINE] rainmachine = hass.data[RAINMACHINE_DOMAIN][DATA_CLIENT][entry.entry_id]
entities = [] entities = []
@ -127,7 +125,9 @@ async def async_setup_platform(
continue continue
_LOGGER.debug('Adding zone: %s', zone) _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) async_add_entities(entities, True)
@ -186,9 +186,14 @@ class RainMachineProgram(RainMachineSwitch):
async def async_added_to_hass(self): async def async_added_to_hass(self):
"""Register callbacks.""" """Register callbacks."""
async_dispatcher_connect( self._async_unsub_dispatcher_connect = async_dispatcher_connect(
self.hass, PROGRAM_UPDATE_TOPIC, self._program_updated) 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: async def async_turn_off(self, **kwargs) -> None:
"""Turn the program off.""" """Turn the program off."""
from regenmaschine.errors import RequestError from regenmaschine.errors import RequestError

View file

@ -149,6 +149,7 @@ FLOWS = [
'mqtt', 'mqtt',
'nest', 'nest',
'openuv', 'openuv',
'rainmachine',
'simplisafe', 'simplisafe',
'smhi', 'smhi',
'sonos', 'sonos',

View file

@ -1336,7 +1336,7 @@ raincloudy==0.0.5
# raspihats==2.2.3 # raspihats==2.2.3
# homeassistant.components.rainmachine # homeassistant.components.rainmachine
regenmaschine==1.0.2 regenmaschine==1.0.7
# homeassistant.components.python_script # homeassistant.components.python_script
restrictedpython==4.0b6 restrictedpython==4.0b6

View file

@ -210,6 +210,9 @@ pyunifi==2.13
# homeassistant.components.notify.html5 # homeassistant.components.notify.html5
pywebpush==1.6.0 pywebpush==1.6.0
# homeassistant.components.rainmachine
regenmaschine==1.0.7
# homeassistant.components.python_script # homeassistant.components.python_script
restrictedpython==4.0b6 restrictedpython==4.0b6

View file

@ -95,6 +95,7 @@ TEST_REQUIREMENTS = (
'pyunifi', 'pyunifi',
'pyupnp-async', 'pyupnp-async',
'pywebpush', 'pywebpush',
'regenmaschine',
'restrictedpython', 'restrictedpython',
'rflink', 'rflink',
'ring_doorbell', 'ring_doorbell',

View file

@ -0,0 +1 @@
"""Define tests for the RainMachine component."""

View file

@ -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,
}