mirror of
https://github.com/home-assistant/core.git
synced 2025-06-25 01:21:51 +02:00
Add RainMachine switch platform (#8827)
* Add RainMachine switch platform * Updated requirements_all.txt * Cleaning up CI and coverage results * Small update to deal with older pylint * Fixed small indentation-based error * Added some more defensive try/except logic around calls * I'm not a fan of importing a library multiple times :) * Making PR-requested changes * Fixed ref to positional parameter * Attempting to fix broken linting * Ignoring no-value-for-parameter pylint error
This commit is contained in:
committed by
Martin Hjelmare
parent
57f3bed465
commit
289c88ff71
@ -539,6 +539,7 @@ omit =
|
||||
homeassistant/components/switch/orvibo.py
|
||||
homeassistant/components/switch/pilight.py
|
||||
homeassistant/components/switch/pulseaudio_loopback.py
|
||||
homeassistant/components/switch/rainmachine.py
|
||||
homeassistant/components/switch/rest.py
|
||||
homeassistant/components/switch/rpi_rf.py
|
||||
homeassistant/components/switch/tplink.py
|
||||
|
1
.gitignore
vendored
1
.gitignore
vendored
@ -73,6 +73,7 @@ pyvenv.cfg
|
||||
pip-selfcheck.json
|
||||
venv
|
||||
.venv
|
||||
Pipfile*
|
||||
|
||||
# vimmy stuff
|
||||
*.swp
|
||||
|
305
homeassistant/components/switch/rainmachine.py
Normal file
305
homeassistant/components/switch/rainmachine.py
Normal file
@ -0,0 +1,305 @@
|
||||
"""Implements a RainMachine sprinkler controller for Home Assistant."""
|
||||
|
||||
import asyncio
|
||||
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_SCAN_INTERVAL)
|
||||
from homeassistant.util import Throttle
|
||||
|
||||
_LOGGER = getLogger(__name__)
|
||||
REQUIREMENTS = ['regenmaschine==0.3.2']
|
||||
|
||||
ATTR_CYCLES = 'cycles'
|
||||
ATTR_TOTAL_DURATION = 'total_duration'
|
||||
|
||||
CONF_HIDE_DISABLED_ENTITIES = 'hide_disabled_entities'
|
||||
CONF_ZONE_RUN_TIME = 'zone_run_time'
|
||||
|
||||
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_ZONE_RUN_TIME, default=DEFAULT_ZONE_RUN_SECONDS):
|
||||
cv.positive_int,
|
||||
vol.Optional(CONF_HIDE_DISABLED_ENTITIES, default=True):
|
||||
cv.boolean
|
||||
}),
|
||||
extra=vol.ALLOW_EXTRA)
|
||||
|
||||
|
||||
@asyncio.coroutine
|
||||
def async_setup_platform(hass, config, async_add_devices, discovery_info=None):
|
||||
"""Set this component up under its platform."""
|
||||
import regenmaschine as rm
|
||||
|
||||
ip_address = config.get(CONF_IP_ADDRESS)
|
||||
_LOGGER.debug('IP address: %s', ip_address)
|
||||
|
||||
email_address = config.get(CONF_EMAIL)
|
||||
_LOGGER.debug('Email address: %s', email_address)
|
||||
|
||||
password = config.get(CONF_PASSWORD)
|
||||
_LOGGER.debug('Password: %s', password)
|
||||
|
||||
hide_disabled_entities = config.get(CONF_HIDE_DISABLED_ENTITIES)
|
||||
_LOGGER.debug('Show disabled entities: %s', hide_disabled_entities)
|
||||
|
||||
zone_run_time = config.get(CONF_ZONE_RUN_TIME)
|
||||
_LOGGER.debug('Zone run time: %s', zone_run_time)
|
||||
|
||||
try:
|
||||
if ip_address:
|
||||
_LOGGER.debug('Configuring local API...')
|
||||
auth = rm.Authenticator.create_local(ip_address, password)
|
||||
elif email_address:
|
||||
_LOGGER.debug('Configuring remote API...')
|
||||
auth = rm.Authenticator.create_remote(email_address, password)
|
||||
|
||||
_LOGGER.debug('Instantiating RainMachine client...')
|
||||
client = rm.Client(auth)
|
||||
|
||||
rainmachine_device_name = client.provision.device_name().get('name')
|
||||
|
||||
entities = []
|
||||
for program in client.programs.all().get('programs'):
|
||||
if hide_disabled_entities and program.get('active') is False:
|
||||
continue
|
||||
|
||||
_LOGGER.debug('Adding program: %s', program)
|
||||
entities.append(
|
||||
RainMachineProgram(
|
||||
client, program, device_name=rainmachine_device_name))
|
||||
|
||||
for zone in client.zones.all().get('zones'):
|
||||
if hide_disabled_entities and zone.get('active') is False:
|
||||
continue
|
||||
|
||||
_LOGGER.debug('Adding zone: %s', zone)
|
||||
entities.append(
|
||||
RainMachineZone(
|
||||
client,
|
||||
zone,
|
||||
zone_run_time,
|
||||
device_name=rainmachine_device_name, ))
|
||||
|
||||
async_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
|
||||
|
||||
|
||||
class RainMachineEntity(SwitchDevice):
|
||||
"""A class to represent a generic RainMachine entity."""
|
||||
|
||||
def __init__(self, client, entity_json, **kwargs):
|
||||
"""Initialize a generic RainMachine entity."""
|
||||
self._api_type = 'remote' if client.auth.using_remote_api else 'local'
|
||||
self._client = client
|
||||
self._device_name = kwargs.get('device_name')
|
||||
self._entity_json = entity_json
|
||||
|
||||
self._attrs = {
|
||||
ATTR_ATTRIBUTION: '© RainMachine',
|
||||
ATTR_DEVICE_CLASS: self._device_name
|
||||
}
|
||||
|
||||
@property
|
||||
def device_state_attributes(self) -> dict:
|
||||
"""Return the state attributes."""
|
||||
if self._client:
|
||||
return self._attrs
|
||||
|
||||
@property
|
||||
def is_enabled(self) -> bool:
|
||||
"""Return whether the entity is enabled."""
|
||||
return self._entity_json.get('active')
|
||||
|
||||
@property
|
||||
def rainmachine_id(self) -> int:
|
||||
"""Return the RainMachine ID for this entity."""
|
||||
return self._entity_json.get('uid')
|
||||
|
||||
@property
|
||||
def unique_id(self) -> str:
|
||||
"""Return a unique, HASS-friendly identifier for this entity."""
|
||||
return '{}.{}.{}'.format(self.__class__, self._device_name,
|
||||
self.rainmachine_id)
|
||||
|
||||
@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."""
|
||||
|
||||
@property
|
||||
def is_on(self) -> bool:
|
||||
"""Return whether the program is running."""
|
||||
return bool(self._entity_json.get('status'))
|
||||
|
||||
@property
|
||||
def name(self) -> str:
|
||||
"""Return the name of the program."""
|
||||
return 'Program: {}'.format(self._entity_json.get('name'))
|
||||
|
||||
def turn_off(self, **kwargs) -> None:
|
||||
"""Turn the program off."""
|
||||
import regenmaschine.exceptions as exceptions
|
||||
|
||||
try:
|
||||
self._client.programs.stop(self.rainmachine_id)
|
||||
except exceptions.BrokenAPICall:
|
||||
_LOGGER.error('programs.stop currently broken in remote API')
|
||||
except exceptions.HTTPError as exc_info:
|
||||
_LOGGER.error('Unable to turn off program "%s"',
|
||||
self.rainmachine_id)
|
||||
_LOGGER.debug(exc_info)
|
||||
|
||||
def turn_on(self, **kwargs) -> None:
|
||||
"""Turn the program on."""
|
||||
import regenmaschine.exceptions as exceptions
|
||||
|
||||
try:
|
||||
self._client.programs.start(self.rainmachine_id)
|
||||
except exceptions.BrokenAPICall:
|
||||
_LOGGER.error('programs.start currently broken in remote API')
|
||||
except exceptions.HTTPError as exc_info:
|
||||
_LOGGER.error('Unable to turn on program "%s"',
|
||||
self.rainmachine_id)
|
||||
_LOGGER.debug(exc_info)
|
||||
|
||||
def _update(self) -> None:
|
||||
"""Update info for the program."""
|
||||
import regenmaschine.exceptions as exceptions
|
||||
|
||||
try:
|
||||
self._entity_json = self._client.programs.get(self.rainmachine_id)
|
||||
except exceptions.HTTPError as exc_info:
|
||||
_LOGGER.error('Unable to update info for program "%s"',
|
||||
self.rainmachine_id)
|
||||
_LOGGER.debug(exc_info)
|
||||
|
||||
|
||||
class RainMachineZone(RainMachineEntity):
|
||||
"""A RainMachine zone."""
|
||||
|
||||
def __init__(self, client, zone_json, zone_run_time, **kwargs):
|
||||
"""Initialize a RainMachine zone."""
|
||||
super().__init__(client, zone_json, **kwargs)
|
||||
self._run_time = zone_run_time
|
||||
self._attrs.update({
|
||||
ATTR_CYCLES:
|
||||
self._entity_json.get('noOfCycles'),
|
||||
ATTR_TOTAL_DURATION:
|
||||
self._entity_json.get('userDuration')
|
||||
})
|
||||
|
||||
@property
|
||||
def is_on(self) -> bool:
|
||||
"""Return whether the zone is running."""
|
||||
return bool(self._entity_json.get('state'))
|
||||
|
||||
@property
|
||||
def name(self) -> str:
|
||||
"""Return the name of the zone."""
|
||||
return 'Zone: {}'.format(self._entity_json.get('name'))
|
||||
|
||||
def turn_off(self, **kwargs) -> None:
|
||||
"""Turn the zone off."""
|
||||
import regenmaschine.exceptions as exceptions
|
||||
|
||||
try:
|
||||
self._client.zones.stop(self.rainmachine_id)
|
||||
except exceptions.HTTPError as exc_info:
|
||||
_LOGGER.error('Unable to turn off zone "%s"', self.rainmachine_id)
|
||||
_LOGGER.debug(exc_info)
|
||||
|
||||
def turn_on(self, **kwargs) -> None:
|
||||
"""Turn the zone on."""
|
||||
import regenmaschine.exceptions as exceptions
|
||||
|
||||
try:
|
||||
self._client.zones.start(self.rainmachine_id, self._run_time)
|
||||
except exceptions.HTTPError as exc_info:
|
||||
_LOGGER.error('Unable to turn on zone "%s"', self.rainmachine_id)
|
||||
_LOGGER.debug(exc_info)
|
||||
|
||||
def _update(self) -> None:
|
||||
"""Update info for the zone."""
|
||||
import regenmaschine.exceptions as exceptions
|
||||
|
||||
try:
|
||||
self._entity_json = self._client.zones.get(self.rainmachine_id)
|
||||
except exceptions.HTTPError as exc_info:
|
||||
_LOGGER.error('Unable to update info for zone "%s"',
|
||||
self.rainmachine_id)
|
||||
_LOGGER.debug(exc_info)
|
@ -109,6 +109,7 @@ CONF_ICON = 'icon'
|
||||
CONF_ICON_TEMPLATE = 'icon_template'
|
||||
CONF_INCLUDE = 'include'
|
||||
CONF_ID = 'id'
|
||||
CONF_IP_ADDRESS = 'ip_address'
|
||||
CONF_LATITUDE = 'latitude'
|
||||
CONF_LONGITUDE = 'longitude'
|
||||
CONF_MAC = 'mac'
|
||||
|
@ -825,6 +825,9 @@ radiotherm==1.3
|
||||
# homeassistant.components.raspihats
|
||||
# raspihats==2.2.1
|
||||
|
||||
# homeassistant.components.switch.rainmachine
|
||||
regenmaschine==0.3.2
|
||||
|
||||
# homeassistant.components.python_script
|
||||
restrictedpython==4.0a3
|
||||
|
||||
|
Reference in New Issue
Block a user