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:
parent
312872961f
commit
8aa1283adc
15 changed files with 400 additions and 87 deletions
|
@ -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
|
||||||
|
|
|
@ -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."""
|
||||||
|
|
19
homeassistant/components/rainmachine/.translations/en.json
Normal file
19
homeassistant/components/rainmachine/.translations/en.json
Normal 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"
|
||||||
|
}
|
||||||
|
}
|
|
@ -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
|
||||||
|
|
||||||
|
|
85
homeassistant/components/rainmachine/config_flow.py
Normal file
85
homeassistant/components/rainmachine/config_flow.py
Normal 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)
|
14
homeassistant/components/rainmachine/const.py
Normal file
14
homeassistant/components/rainmachine/const.py
Normal 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}'
|
19
homeassistant/components/rainmachine/strings.json
Normal file
19
homeassistant/components/rainmachine/strings.json
Normal 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"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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."""
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -149,6 +149,7 @@ FLOWS = [
|
||||||
'mqtt',
|
'mqtt',
|
||||||
'nest',
|
'nest',
|
||||||
'openuv',
|
'openuv',
|
||||||
|
'rainmachine',
|
||||||
'simplisafe',
|
'simplisafe',
|
||||||
'smhi',
|
'smhi',
|
||||||
'sonos',
|
'sonos',
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
||||||
|
|
|
@ -95,6 +95,7 @@ TEST_REQUIREMENTS = (
|
||||||
'pyunifi',
|
'pyunifi',
|
||||||
'pyupnp-async',
|
'pyupnp-async',
|
||||||
'pywebpush',
|
'pywebpush',
|
||||||
|
'regenmaschine',
|
||||||
'restrictedpython',
|
'restrictedpython',
|
||||||
'rflink',
|
'rflink',
|
||||||
'ring_doorbell',
|
'ring_doorbell',
|
||||||
|
|
1
tests/components/rainmachine/__init__.py
Normal file
1
tests/components/rainmachine/__init__.py
Normal file
|
@ -0,0 +1 @@
|
||||||
|
"""Define tests for the RainMachine component."""
|
109
tests/components/rainmachine/test_config_flow.py
Normal file
109
tests/components/rainmachine/test_config_flow.py
Normal 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,
|
||||||
|
}
|
Loading…
Add table
Reference in a new issue