From 8bc497ba1d8b588fcb165b8c7b4773741ed47d32 Mon Sep 17 00:00:00 2001 From: Aaron Bach Date: Sat, 28 Apr 2018 07:46:58 -0600 Subject: [PATCH] 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 --- .coveragerc | 4 +- homeassistant/components/rainmachine.py | 71 ++++++++ .../components/switch/rainmachine.py | 167 +++++------------- requirements_all.txt | 2 +- 4 files changed, 117 insertions(+), 127 deletions(-) create mode 100644 homeassistant/components/rainmachine.py diff --git a/.coveragerc b/.coveragerc index 452dbec7559..c1c879aef09 100644 --- a/.coveragerc +++ b/.coveragerc @@ -208,6 +208,9 @@ omit = homeassistant/components/raincloud.py homeassistant/components/*/raincloud.py + homeassistant/components/rainmachine.py + homeassistant/components/*/rainmachine.py + homeassistant/components/raspihats.py homeassistant/components/*/raspihats.py @@ -711,7 +714,6 @@ omit = homeassistant/components/switch/orvibo.py homeassistant/components/switch/pulseaudio_loopback.py homeassistant/components/switch/rainbird.py - homeassistant/components/switch/rainmachine.py homeassistant/components/switch/rest.py homeassistant/components/switch/rpi_rf.py homeassistant/components/switch/snmp.py diff --git a/homeassistant/components/rainmachine.py b/homeassistant/components/rainmachine.py new file mode 100644 index 00000000000..4c8b8a1114f --- /dev/null +++ b/homeassistant/components/rainmachine.py @@ -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}
' + 'You will need to restart hass after fixing.' + ''.format(exc_info), + title=NOTIFICATION_TITLE, + notification_id=NOTIFICATION_ID) + return False + return True diff --git a/homeassistant/components/switch/rainmachine.py b/homeassistant/components/switch/rainmachine.py index 99d41bdd9c3..cdada7ce274 100644 --- a/homeassistant/components/switch/rainmachine.py +++ b/homeassistant/components/switch/rainmachine.py @@ -1,130 +1,61 @@ """Implements a RainMachine sprinkler controller for Home Assistant.""" -from datetime import timedelta from logging import getLogger import voluptuous as vol import homeassistant.helpers.config_validation as cv -from homeassistant.components.switch import SwitchDevice -from homeassistant.const import ( - ATTR_ATTRIBUTION, ATTR_DEVICE_CLASS, CONF_EMAIL, CONF_IP_ADDRESS, - CONF_PASSWORD, CONF_PLATFORM, CONF_PORT, CONF_SCAN_INTERVAL, CONF_SSL) +from homeassistant.components.rainmachine import ( + DATA_RAINMACHINE, DEFAULT_ATTRIBUTION, MIN_SCAN_TIME, MIN_SCAN_TIME_FORCED) +from homeassistant.components.switch import PLATFORM_SCHEMA, SwitchDevice +from homeassistant.const import ATTR_ATTRIBUTION, ATTR_DEVICE_CLASS from homeassistant.util import Throttle _LOGGER = getLogger(__name__) -REQUIREMENTS = ['regenmaschine==0.4.1'] +DEPENDENCIES = ['rainmachine'] ATTR_CYCLES = 'cycles' ATTR_TOTAL_DURATION = 'total_duration' CONF_ZONE_RUN_TIME = 'zone_run_time' -DEFAULT_PORT = 8080 -DEFAULT_SSL = True DEFAULT_ZONE_RUN_SECONDS = 60 * 10 -MIN_SCAN_TIME_LOCAL = timedelta(seconds=1) -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): - cv.positive_int - }), - extra=vol.ALLOW_EXTRA) +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ + vol.Optional(CONF_ZONE_RUN_TIME, default=DEFAULT_ZONE_RUN_SECONDS): + cv.positive_int +}) def setup_platform(hass, config, add_devices, discovery_info=None): """Set this component up under its platform.""" - import regenmaschine as rm + client = hass.data.get(DATA_RAINMACHINE) + device_name = client.provision.device_name()['name'] + device_mac = client.provision.wifi()['macAddress'] - _LOGGER.debug('Config data: %s', config) + _LOGGER.debug('Config received: %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') + entities = [] + for program in client.programs.all().get('programs', {}): + if not program.get('active'): + continue - 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('Adding program: %s', program) + entities.append( + RainMachineProgram(client, device_name, device_mac, program)) - _LOGGER.debug('Querying against: %s', auth.url) + for zone in client.zones.all().get('zones', {}): + if not zone.get('active'): + continue - client = rm.Client(auth) - device_name = client.provision.device_name()['name'] - device_mac = client.provision.wifi()['macAddress'] + _LOGGER.debug('Adding zone: %s', zone) + entities.append( + RainMachineZone(client, device_name, device_mac, zone, + zone_run_time)) - entities = [] - for program in client.programs.all().get('programs', {}): - if not program.get('active'): - continue - - _LOGGER.debug('Adding program: %s', program) - entities.append( - RainMachineProgram(client, device_name, device_mac, program)) - - for zone in client.zones.all().get('zones', {}): - if not zone.get('active'): - continue - - _LOGGER.debug('Adding zone: %s', zone) - entities.append( - RainMachineZone(client, device_name, device_mac, zone, - zone_run_time)) - - add_devices(entities) - 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 + add_devices(entities, True) class RainMachineEntity(SwitchDevice): @@ -135,19 +66,24 @@ class RainMachineEntity(SwitchDevice): self._api_type = 'remote' if client.auth.using_remote_api else 'local' self._client = client self._entity_json = entity_json + self.device_mac = device_mac self.device_name = device_name self._attrs = { - ATTR_ATTRIBUTION: '© RainMachine', + ATTR_ATTRIBUTION: DEFAULT_ATTRIBUTION, ATTR_DEVICE_CLASS: self.device_name } @property def device_state_attributes(self) -> dict: """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 def is_enabled(self) -> bool: @@ -159,27 +95,6 @@ class RainMachineEntity(SwitchDevice): """Return the RainMachine ID for this entity.""" 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): """A RainMachine program.""" @@ -192,7 +107,7 @@ class RainMachineProgram(RainMachineEntity): @property def name(self) -> str: """Return the name of the program.""" - return 'Program: {}'.format(self._entity_json.get('name')) + return 'Program: {0}'.format(self._entity_json.get('name')) @property def unique_id(self) -> str: @@ -224,7 +139,8 @@ class RainMachineProgram(RainMachineEntity): _LOGGER.error('Unable to turn on program "%s"', self.unique_id) _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.""" import regenmaschine.exceptions as exceptions @@ -258,7 +174,7 @@ class RainMachineZone(RainMachineEntity): @property def name(self) -> str: """Return the name of the zone.""" - return 'Zone: {}'.format(self._entity_json.get('name')) + return 'Zone: {0}'.format(self._entity_json.get('name')) @property def unique_id(self) -> str: @@ -287,7 +203,8 @@ class RainMachineZone(RainMachineEntity): _LOGGER.error('Unable to turn on zone "%s"', self.unique_id) _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.""" import regenmaschine.exceptions as exceptions diff --git a/requirements_all.txt b/requirements_all.txt index d6cb477ab51..f6fdf81a2e4 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1116,7 +1116,7 @@ raincloudy==0.0.4 # homeassistant.components.raspihats # raspihats==2.2.3 -# homeassistant.components.switch.rainmachine +# homeassistant.components.rainmachine regenmaschine==0.4.1 # homeassistant.components.python_script