mirror of
https://github.com/home-assistant/core.git
synced 2025-07-15 09:17:10 +00:00
Move RainMachine to component/hub model (#14085)
* Moves RainMachine to component/hub model * Updated requirements * Updated coverage * Hound violations * Collaborator-requested changes * Small formatting updates * Removed references to remote API * Collaborator-requested changes * Collaborator-requested changes * Fixed attribution
This commit is contained in:
parent
1d41321f8f
commit
8bc497ba1d
@ -208,6 +208,9 @@ omit =
|
|||||||
homeassistant/components/raincloud.py
|
homeassistant/components/raincloud.py
|
||||||
homeassistant/components/*/raincloud.py
|
homeassistant/components/*/raincloud.py
|
||||||
|
|
||||||
|
homeassistant/components/rainmachine.py
|
||||||
|
homeassistant/components/*/rainmachine.py
|
||||||
|
|
||||||
homeassistant/components/raspihats.py
|
homeassistant/components/raspihats.py
|
||||||
homeassistant/components/*/raspihats.py
|
homeassistant/components/*/raspihats.py
|
||||||
|
|
||||||
@ -711,7 +714,6 @@ omit =
|
|||||||
homeassistant/components/switch/orvibo.py
|
homeassistant/components/switch/orvibo.py
|
||||||
homeassistant/components/switch/pulseaudio_loopback.py
|
homeassistant/components/switch/pulseaudio_loopback.py
|
||||||
homeassistant/components/switch/rainbird.py
|
homeassistant/components/switch/rainbird.py
|
||||||
homeassistant/components/switch/rainmachine.py
|
|
||||||
homeassistant/components/switch/rest.py
|
homeassistant/components/switch/rest.py
|
||||||
homeassistant/components/switch/rpi_rf.py
|
homeassistant/components/switch/rpi_rf.py
|
||||||
homeassistant/components/switch/snmp.py
|
homeassistant/components/switch/snmp.py
|
||||||
|
71
homeassistant/components/rainmachine.py
Normal file
71
homeassistant/components/rainmachine.py
Normal file
@ -0,0 +1,71 @@
|
|||||||
|
"""
|
||||||
|
This component provides support for RainMachine sprinkler controllers.
|
||||||
|
|
||||||
|
For more details about this component, please refer to the documentation at
|
||||||
|
https://home-assistant.io/components/rainmachine/
|
||||||
|
"""
|
||||||
|
import logging
|
||||||
|
from datetime import timedelta
|
||||||
|
|
||||||
|
import voluptuous as vol
|
||||||
|
from requests.exceptions import ConnectTimeout
|
||||||
|
|
||||||
|
from homeassistant.helpers import config_validation as cv
|
||||||
|
from homeassistant.const import (
|
||||||
|
CONF_IP_ADDRESS, CONF_PASSWORD, CONF_PORT, CONF_SSL)
|
||||||
|
|
||||||
|
REQUIREMENTS = ['regenmaschine==0.4.1']
|
||||||
|
|
||||||
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
DATA_RAINMACHINE = 'data_rainmachine'
|
||||||
|
DOMAIN = 'rainmachine'
|
||||||
|
|
||||||
|
NOTIFICATION_ID = 'rainmachine_notification'
|
||||||
|
NOTIFICATION_TITLE = 'RainMachine Component Setup'
|
||||||
|
|
||||||
|
DEFAULT_ATTRIBUTION = 'Data provided by Green Electronics LLC'
|
||||||
|
DEFAULT_PORT = 8080
|
||||||
|
DEFAULT_SSL = True
|
||||||
|
|
||||||
|
MIN_SCAN_TIME = timedelta(seconds=1)
|
||||||
|
MIN_SCAN_TIME_FORCED = timedelta(milliseconds=100)
|
||||||
|
|
||||||
|
CONFIG_SCHEMA = vol.Schema(
|
||||||
|
{
|
||||||
|
DOMAIN: vol.Schema({
|
||||||
|
vol.Required(CONF_IP_ADDRESS): cv.string,
|
||||||
|
vol.Required(CONF_PASSWORD): cv.string,
|
||||||
|
vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port,
|
||||||
|
vol.Optional(CONF_SSL, default=DEFAULT_SSL): cv.boolean,
|
||||||
|
})
|
||||||
|
},
|
||||||
|
extra=vol.ALLOW_EXTRA)
|
||||||
|
|
||||||
|
|
||||||
|
def setup(hass, config):
|
||||||
|
"""Set up the RainMachine component."""
|
||||||
|
from regenmaschine import Authenticator, Client
|
||||||
|
from regenmaschine.exceptions import HTTPError
|
||||||
|
|
||||||
|
conf = config[DOMAIN]
|
||||||
|
ip_address = conf[CONF_IP_ADDRESS]
|
||||||
|
password = conf[CONF_PASSWORD]
|
||||||
|
port = conf[CONF_PORT]
|
||||||
|
ssl = conf[CONF_SSL]
|
||||||
|
|
||||||
|
try:
|
||||||
|
auth = Authenticator.create_local(
|
||||||
|
ip_address, password, port=port, https=ssl)
|
||||||
|
client = Client(auth)
|
||||||
|
hass.data[DATA_RAINMACHINE] = client
|
||||||
|
except (HTTPError, ConnectTimeout, UnboundLocalError) as exc_info:
|
||||||
|
_LOGGER.error('An error occurred: %s', str(exc_info))
|
||||||
|
hass.components.persistent_notification.create(
|
||||||
|
'Error: {0}<br />'
|
||||||
|
'You will need to restart hass after fixing.'
|
||||||
|
''.format(exc_info),
|
||||||
|
title=NOTIFICATION_TITLE,
|
||||||
|
notification_id=NOTIFICATION_ID)
|
||||||
|
return False
|
||||||
|
return True
|
@ -1,80 +1,42 @@
|
|||||||
"""Implements a RainMachine sprinkler controller for Home Assistant."""
|
"""Implements a RainMachine sprinkler controller for Home Assistant."""
|
||||||
|
|
||||||
from datetime import timedelta
|
|
||||||
from logging import getLogger
|
from logging import getLogger
|
||||||
|
|
||||||
import voluptuous as vol
|
import voluptuous as vol
|
||||||
|
|
||||||
import homeassistant.helpers.config_validation as cv
|
import homeassistant.helpers.config_validation as cv
|
||||||
from homeassistant.components.switch import SwitchDevice
|
from homeassistant.components.rainmachine import (
|
||||||
from homeassistant.const import (
|
DATA_RAINMACHINE, DEFAULT_ATTRIBUTION, MIN_SCAN_TIME, MIN_SCAN_TIME_FORCED)
|
||||||
ATTR_ATTRIBUTION, ATTR_DEVICE_CLASS, CONF_EMAIL, CONF_IP_ADDRESS,
|
from homeassistant.components.switch import PLATFORM_SCHEMA, SwitchDevice
|
||||||
CONF_PASSWORD, CONF_PLATFORM, CONF_PORT, CONF_SCAN_INTERVAL, CONF_SSL)
|
from homeassistant.const import ATTR_ATTRIBUTION, ATTR_DEVICE_CLASS
|
||||||
from homeassistant.util import Throttle
|
from homeassistant.util import Throttle
|
||||||
|
|
||||||
_LOGGER = getLogger(__name__)
|
_LOGGER = getLogger(__name__)
|
||||||
REQUIREMENTS = ['regenmaschine==0.4.1']
|
DEPENDENCIES = ['rainmachine']
|
||||||
|
|
||||||
ATTR_CYCLES = 'cycles'
|
ATTR_CYCLES = 'cycles'
|
||||||
ATTR_TOTAL_DURATION = 'total_duration'
|
ATTR_TOTAL_DURATION = 'total_duration'
|
||||||
|
|
||||||
CONF_ZONE_RUN_TIME = 'zone_run_time'
|
CONF_ZONE_RUN_TIME = 'zone_run_time'
|
||||||
|
|
||||||
DEFAULT_PORT = 8080
|
|
||||||
DEFAULT_SSL = True
|
|
||||||
DEFAULT_ZONE_RUN_SECONDS = 60 * 10
|
DEFAULT_ZONE_RUN_SECONDS = 60 * 10
|
||||||
|
|
||||||
MIN_SCAN_TIME_LOCAL = timedelta(seconds=1)
|
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
||||||
MIN_SCAN_TIME_REMOTE = timedelta(seconds=5)
|
|
||||||
MIN_SCAN_TIME_FORCED = timedelta(milliseconds=100)
|
|
||||||
|
|
||||||
PLATFORM_SCHEMA = vol.Schema(
|
|
||||||
vol.All(
|
|
||||||
cv.has_at_least_one_key(CONF_IP_ADDRESS, CONF_EMAIL),
|
|
||||||
{
|
|
||||||
vol.Required(CONF_PLATFORM): cv.string,
|
|
||||||
vol.Optional(CONF_SCAN_INTERVAL): cv.time_period,
|
|
||||||
vol.Exclusive(CONF_IP_ADDRESS, 'auth'): cv.string,
|
|
||||||
vol.Exclusive(CONF_EMAIL, 'auth'):
|
|
||||||
vol.Email(), # pylint: disable=no-value-for-parameter
|
|
||||||
vol.Required(CONF_PASSWORD): cv.string,
|
|
||||||
vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port,
|
|
||||||
vol.Optional(CONF_SSL, default=DEFAULT_SSL): cv.boolean,
|
|
||||||
vol.Optional(CONF_ZONE_RUN_TIME, default=DEFAULT_ZONE_RUN_SECONDS):
|
vol.Optional(CONF_ZONE_RUN_TIME, default=DEFAULT_ZONE_RUN_SECONDS):
|
||||||
cv.positive_int
|
cv.positive_int
|
||||||
}),
|
})
|
||||||
extra=vol.ALLOW_EXTRA)
|
|
||||||
|
|
||||||
|
|
||||||
def setup_platform(hass, config, add_devices, discovery_info=None):
|
def setup_platform(hass, config, add_devices, discovery_info=None):
|
||||||
"""Set this component up under its platform."""
|
"""Set this component up under its platform."""
|
||||||
import regenmaschine as rm
|
client = hass.data.get(DATA_RAINMACHINE)
|
||||||
|
|
||||||
_LOGGER.debug('Config data: %s', config)
|
|
||||||
|
|
||||||
ip_address = config.get(CONF_IP_ADDRESS, None)
|
|
||||||
email_address = config.get(CONF_EMAIL, None)
|
|
||||||
password = config[CONF_PASSWORD]
|
|
||||||
zone_run_time = config[CONF_ZONE_RUN_TIME]
|
|
||||||
|
|
||||||
try:
|
|
||||||
if ip_address:
|
|
||||||
_LOGGER.debug('Configuring local API')
|
|
||||||
|
|
||||||
port = config[CONF_PORT]
|
|
||||||
ssl = config[CONF_SSL]
|
|
||||||
auth = rm.Authenticator.create_local(
|
|
||||||
ip_address, password, port=port, https=ssl)
|
|
||||||
elif email_address:
|
|
||||||
_LOGGER.debug('Configuring remote API')
|
|
||||||
auth = rm.Authenticator.create_remote(email_address, password)
|
|
||||||
|
|
||||||
_LOGGER.debug('Querying against: %s', auth.url)
|
|
||||||
|
|
||||||
client = rm.Client(auth)
|
|
||||||
device_name = client.provision.device_name()['name']
|
device_name = client.provision.device_name()['name']
|
||||||
device_mac = client.provision.wifi()['macAddress']
|
device_mac = client.provision.wifi()['macAddress']
|
||||||
|
|
||||||
|
_LOGGER.debug('Config received: %s', config)
|
||||||
|
|
||||||
|
zone_run_time = config[CONF_ZONE_RUN_TIME]
|
||||||
|
|
||||||
entities = []
|
entities = []
|
||||||
for program in client.programs.all().get('programs', {}):
|
for program in client.programs.all().get('programs', {}):
|
||||||
if not program.get('active'):
|
if not program.get('active'):
|
||||||
@ -93,38 +55,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
|
|||||||
RainMachineZone(client, device_name, device_mac, zone,
|
RainMachineZone(client, device_name, device_mac, zone,
|
||||||
zone_run_time))
|
zone_run_time))
|
||||||
|
|
||||||
add_devices(entities)
|
add_devices(entities, True)
|
||||||
except rm.exceptions.HTTPError as exc_info:
|
|
||||||
_LOGGER.error('An HTTP error occurred while talking with RainMachine')
|
|
||||||
_LOGGER.debug(exc_info)
|
|
||||||
return False
|
|
||||||
except UnboundLocalError as exc_info:
|
|
||||||
_LOGGER.error('Could not authenticate against RainMachine')
|
|
||||||
_LOGGER.debug(exc_info)
|
|
||||||
return False
|
|
||||||
|
|
||||||
|
|
||||||
def aware_throttle(api_type):
|
|
||||||
"""Create an API type-aware throttler."""
|
|
||||||
_decorator = None
|
|
||||||
if api_type == 'local':
|
|
||||||
|
|
||||||
@Throttle(MIN_SCAN_TIME_LOCAL, MIN_SCAN_TIME_FORCED)
|
|
||||||
def decorator(function):
|
|
||||||
"""Create a local API throttler."""
|
|
||||||
return function
|
|
||||||
|
|
||||||
_decorator = decorator
|
|
||||||
else:
|
|
||||||
|
|
||||||
@Throttle(MIN_SCAN_TIME_REMOTE, MIN_SCAN_TIME_FORCED)
|
|
||||||
def decorator(function):
|
|
||||||
"""Create a remote API throttler."""
|
|
||||||
return function
|
|
||||||
|
|
||||||
_decorator = decorator
|
|
||||||
|
|
||||||
return _decorator
|
|
||||||
|
|
||||||
|
|
||||||
class RainMachineEntity(SwitchDevice):
|
class RainMachineEntity(SwitchDevice):
|
||||||
@ -135,20 +66,25 @@ class RainMachineEntity(SwitchDevice):
|
|||||||
self._api_type = 'remote' if client.auth.using_remote_api else 'local'
|
self._api_type = 'remote' if client.auth.using_remote_api else 'local'
|
||||||
self._client = client
|
self._client = client
|
||||||
self._entity_json = entity_json
|
self._entity_json = entity_json
|
||||||
|
|
||||||
self.device_mac = device_mac
|
self.device_mac = device_mac
|
||||||
self.device_name = device_name
|
self.device_name = device_name
|
||||||
|
|
||||||
self._attrs = {
|
self._attrs = {
|
||||||
ATTR_ATTRIBUTION: '© RainMachine',
|
ATTR_ATTRIBUTION: DEFAULT_ATTRIBUTION,
|
||||||
ATTR_DEVICE_CLASS: self.device_name
|
ATTR_DEVICE_CLASS: self.device_name
|
||||||
}
|
}
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def device_state_attributes(self) -> dict:
|
def device_state_attributes(self) -> dict:
|
||||||
"""Return the state attributes."""
|
"""Return the state attributes."""
|
||||||
if self._client:
|
|
||||||
return self._attrs
|
return self._attrs
|
||||||
|
|
||||||
|
@property
|
||||||
|
def icon(self) -> str:
|
||||||
|
"""Return the icon."""
|
||||||
|
return 'mdi:water'
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def is_enabled(self) -> bool:
|
def is_enabled(self) -> bool:
|
||||||
"""Return whether the entity is enabled."""
|
"""Return whether the entity is enabled."""
|
||||||
@ -159,27 +95,6 @@ class RainMachineEntity(SwitchDevice):
|
|||||||
"""Return the RainMachine ID for this entity."""
|
"""Return the RainMachine ID for this entity."""
|
||||||
return self._entity_json.get('uid')
|
return self._entity_json.get('uid')
|
||||||
|
|
||||||
@aware_throttle('local')
|
|
||||||
def _local_update(self) -> None:
|
|
||||||
"""Call an update with scan times appropriate for the local API."""
|
|
||||||
self._update()
|
|
||||||
|
|
||||||
@aware_throttle('remote')
|
|
||||||
def _remote_update(self) -> None:
|
|
||||||
"""Call an update with scan times appropriate for the remote API."""
|
|
||||||
self._update()
|
|
||||||
|
|
||||||
def _update(self) -> None: # pylint: disable=no-self-use
|
|
||||||
"""Logic for update method, regardless of API type."""
|
|
||||||
raise NotImplementedError()
|
|
||||||
|
|
||||||
def update(self) -> None:
|
|
||||||
"""Determine how the entity updates itself."""
|
|
||||||
if self._api_type == 'remote':
|
|
||||||
self._remote_update()
|
|
||||||
else:
|
|
||||||
self._local_update()
|
|
||||||
|
|
||||||
|
|
||||||
class RainMachineProgram(RainMachineEntity):
|
class RainMachineProgram(RainMachineEntity):
|
||||||
"""A RainMachine program."""
|
"""A RainMachine program."""
|
||||||
@ -192,7 +107,7 @@ class RainMachineProgram(RainMachineEntity):
|
|||||||
@property
|
@property
|
||||||
def name(self) -> str:
|
def name(self) -> str:
|
||||||
"""Return the name of the program."""
|
"""Return the name of the program."""
|
||||||
return 'Program: {}'.format(self._entity_json.get('name'))
|
return 'Program: {0}'.format(self._entity_json.get('name'))
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def unique_id(self) -> str:
|
def unique_id(self) -> str:
|
||||||
@ -224,7 +139,8 @@ class RainMachineProgram(RainMachineEntity):
|
|||||||
_LOGGER.error('Unable to turn on program "%s"', self.unique_id)
|
_LOGGER.error('Unable to turn on program "%s"', self.unique_id)
|
||||||
_LOGGER.debug(exc_info)
|
_LOGGER.debug(exc_info)
|
||||||
|
|
||||||
def _update(self) -> None:
|
@Throttle(MIN_SCAN_TIME, MIN_SCAN_TIME_FORCED)
|
||||||
|
def update(self) -> None:
|
||||||
"""Update info for the program."""
|
"""Update info for the program."""
|
||||||
import regenmaschine.exceptions as exceptions
|
import regenmaschine.exceptions as exceptions
|
||||||
|
|
||||||
@ -258,7 +174,7 @@ class RainMachineZone(RainMachineEntity):
|
|||||||
@property
|
@property
|
||||||
def name(self) -> str:
|
def name(self) -> str:
|
||||||
"""Return the name of the zone."""
|
"""Return the name of the zone."""
|
||||||
return 'Zone: {}'.format(self._entity_json.get('name'))
|
return 'Zone: {0}'.format(self._entity_json.get('name'))
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def unique_id(self) -> str:
|
def unique_id(self) -> str:
|
||||||
@ -287,7 +203,8 @@ class RainMachineZone(RainMachineEntity):
|
|||||||
_LOGGER.error('Unable to turn on zone "%s"', self.unique_id)
|
_LOGGER.error('Unable to turn on zone "%s"', self.unique_id)
|
||||||
_LOGGER.debug(exc_info)
|
_LOGGER.debug(exc_info)
|
||||||
|
|
||||||
def _update(self) -> None:
|
@Throttle(MIN_SCAN_TIME, MIN_SCAN_TIME_FORCED)
|
||||||
|
def update(self) -> None:
|
||||||
"""Update info for the zone."""
|
"""Update info for the zone."""
|
||||||
import regenmaschine.exceptions as exceptions
|
import regenmaschine.exceptions as exceptions
|
||||||
|
|
||||||
|
@ -1116,7 +1116,7 @@ raincloudy==0.0.4
|
|||||||
# homeassistant.components.raspihats
|
# homeassistant.components.raspihats
|
||||||
# raspihats==2.2.3
|
# raspihats==2.2.3
|
||||||
|
|
||||||
# homeassistant.components.switch.rainmachine
|
# homeassistant.components.rainmachine
|
||||||
regenmaschine==0.4.1
|
regenmaschine==0.4.1
|
||||||
|
|
||||||
# homeassistant.components.python_script
|
# homeassistant.components.python_script
|
||||||
|
Loading…
x
Reference in New Issue
Block a user