Skip to content

Commit

Permalink
Add Sonos device attribute with grouping information (home-assistant#…
Browse files Browse the repository at this point in the history
…13553)

Moves RainMachine to component/hub model

Updated requirements

Updated coverage

Hound violations
  • Loading branch information
amelchio authored and bachya committed Apr 26, 2018
1 parent a94864c commit af96a56
Show file tree
Hide file tree
Showing 5 changed files with 171 additions and 104 deletions.
4 changes: 3 additions & 1 deletion .coveragerc
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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
Expand Down
13 changes: 11 additions & 2 deletions homeassistant/components/media_player/sonos.py
Original file line number Diff line number Diff line change
Expand Up @@ -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']

Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -688,14 +689,22 @@ 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:
slave = _get_entity_from_soco_uid(self.hass, slave_uid)
if slave:
# pylint: disable=protected-access
slave._coordinator = self
slave._sonos_group = sonos_group
slave.schedule_update_ha_state()

@property
Expand Down Expand Up @@ -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
Expand Down
113 changes: 113 additions & 0 deletions homeassistant/components/rainmachine.py
Original file line number Diff line number Diff line change
@@ -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}<br />'
'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
143 changes: 43 additions & 100 deletions homeassistant/components/switch/rainmachine.py
Original file line number Diff line number Diff line change
@@ -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):
Expand All @@ -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

Expand All @@ -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:
Expand Down
2 changes: 1 addition & 1 deletion requirements_all.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down

0 comments on commit af96a56

Please sign in to comment.