diff --git a/.coveragerc b/.coveragerc index 452dbec755974e..c1c879aef096e1 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/media_player/sonos.py b/homeassistant/components/media_player/sonos.py index b10c761d532d09..cc10355abe899a 100644 --- a/homeassistant/components/media_player/sonos.py +++ b/homeassistant/components/media_player/sonos.py @@ -67,7 +67,7 @@ ATTR_NIGHT_SOUND = 'night_sound' ATTR_SPEECH_ENHANCE = 'speech_enhance' -ATTR_IS_COORDINATOR = 'is_coordinator' +ATTR_SONOS_GROUP = 'sonos_group' UPNP_ERRORS_TO_IGNORE = ['701', '711'] @@ -340,6 +340,7 @@ def __init__(self, player): self._play_mode = None self._name = None self._coordinator = None + self._sonos_group = None self._status = None self._media_duration = None self._media_position = None @@ -688,7 +689,14 @@ def update_groups(self, event=None): if p.uid != coordinator_uid] if self.unique_id == coordinator_uid: + sonos_group = [] + for uid in (coordinator_uid, *slave_uids): + entity = _get_entity_from_soco_uid(self.hass, uid) + if entity: + sonos_group.append(entity.entity_id) + self._coordinator = None + self._sonos_group = sonos_group self.schedule_update_ha_state() for slave_uid in slave_uids: @@ -696,6 +704,7 @@ def update_groups(self, event=None): if slave: # pylint: disable=protected-access slave._coordinator = self + slave._sonos_group = sonos_group slave.schedule_update_ha_state() @property @@ -1038,7 +1047,7 @@ def set_option(self, **data): @property def device_state_attributes(self): """Return device specific state attributes.""" - attributes = {ATTR_IS_COORDINATOR: self.is_coordinator} + attributes = {ATTR_SONOS_GROUP: self._sonos_group} if self._night_sound is not None: attributes[ATTR_NIGHT_SOUND] = self._night_sound diff --git a/homeassistant/components/rainmachine.py b/homeassistant/components/rainmachine.py new file mode 100644 index 00000000000000..a28bece64e718a --- /dev/null +++ b/homeassistant/components/rainmachine.py @@ -0,0 +1,113 @@ +""" +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_EMAIL, CONF_IP_ADDRESS, CONF_PASSWORD, CONF_PORT, CONF_SSL) +from homeassistant.util import Throttle + +REQUIREMENTS = ['regenmaschine==0.4.1'] + +_LOGGER = logging.getLogger(__name__) + +DATA_RAINMACHINE = 'data_rainmachine' +DOMAIN = 'rainmachine' + +NOTIFICATION_ID = 'rainmachine_notification' +NOTIFICATION_TITLE = 'RainMachine Component Setup' + +CONF_ATTRIBUTION = 'Data provided by Green Electronics LLC' + +DEFAULT_PORT = 8080 +DEFAULT_SSL = True + +MIN_SCAN_TIME_LOCAL = timedelta(seconds=1) +MIN_SCAN_TIME_REMOTE = timedelta(seconds=1) +MIN_SCAN_TIME_FORCED = timedelta(milliseconds=100) + + +CONFIG_SCHEMA = vol.Schema({ + DOMAIN: vol.Schema( + vol.All( + cv.has_at_least_one_key(CONF_IP_ADDRESS, CONF_EMAIL), + { + 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, + })) +}, extra=vol.ALLOW_EXTRA) + + +def setup(hass, config): + """Set up an Arlo component.""" + from regenmaschine import Authenticator, Client + from regenmaschine.exceptions import HTTPError + + _LOGGER.debug('Config data: %s', config) + + conf = config[DOMAIN] + ip_address = conf.get(CONF_IP_ADDRESS, None) + email_address = conf.get(CONF_EMAIL, None) + password = conf[CONF_PASSWORD] + + try: + if ip_address: + port = conf[CONF_PORT] + ssl = conf[CONF_SSL] + auth = Authenticator.create_local( + ip_address, + password, + port=port, + https=ssl) + _LOGGER.debug('Configuring local API: %s', auth) + elif email_address: + auth = Authenticator.create_remote(email_address, password) + _LOGGER.debug('Configuring remote API: %s', auth) + + 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 + + +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 diff --git a/homeassistant/components/switch/rainmachine.py b/homeassistant/components/switch/rainmachine.py index 99d41bdd9c3934..d45ee838d239d0 100644 --- a/homeassistant/components/switch/rainmachine.py +++ b/homeassistant/components/switch/rainmachine.py @@ -1,130 +1,68 @@ """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.util import Throttle +from homeassistant.components.rainmachine import ( + DATA_RAINMACHINE, aware_throttle) +from homeassistant.components.switch import PLATFORM_SCHEMA, SwitchDevice +from homeassistant.const import ATTR_ATTRIBUTION, ATTR_DEVICE_CLASS _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,6 +73,7 @@ def __init__(self, client, device_name, device_mac, entity_json): 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 @@ -146,8 +85,12 @@ def __init__(self, client, device_name, device_mac, entity_json): @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: diff --git a/requirements_all.txt b/requirements_all.txt index f4810116a406c2..74633d53c1ad13 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1113,7 +1113,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