core/homeassistant/components/switch/rainmachine.py

301 lines
10 KiB
Python

"""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
_LOGGER = getLogger(__name__)
REQUIREMENTS = ['regenmaschine==0.4.1']
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)
def setup_platform(hass, config, add_devices, discovery_info=None):
"""Set this component up under its platform."""
import regenmaschine as rm
_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_mac = client.provision.wifi()['macAddress']
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
class RainMachineEntity(SwitchDevice):
"""A class to represent a generic RainMachine entity."""
def __init__(self, client, device_name, device_mac, entity_json):
"""Initialize a generic RainMachine entity."""
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_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_entity_id(self) -> int:
"""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."""
@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'))
@property
def unique_id(self) -> str:
"""Return a unique, HASS-friendly identifier for this entity."""
return '{0}_program_{1}'.format(
self.device_mac.replace(':', ''), self.rainmachine_entity_id)
def turn_off(self, **kwargs) -> None:
"""Turn the program off."""
import regenmaschine.exceptions as exceptions
try:
self._client.programs.stop(self.rainmachine_entity_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.unique_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_entity_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.unique_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_entity_id)
except exceptions.HTTPError as exc_info:
_LOGGER.error('Unable to update info for program "%s"',
self.unique_id)
_LOGGER.debug(exc_info)
class RainMachineZone(RainMachineEntity):
"""A RainMachine zone."""
def __init__(self, client, device_name, device_mac, zone_json,
zone_run_time):
"""Initialize a RainMachine zone."""
super().__init__(client, device_name, device_mac, zone_json)
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'))
@property
def unique_id(self) -> str:
"""Return a unique, HASS-friendly identifier for this entity."""
return '{0}_zone_{1}'.format(
self.device_mac.replace(':', ''), self.rainmachine_entity_id)
def turn_off(self, **kwargs) -> None:
"""Turn the zone off."""
import regenmaschine.exceptions as exceptions
try:
self._client.zones.stop(self.rainmachine_entity_id)
except exceptions.HTTPError as exc_info:
_LOGGER.error('Unable to turn off zone "%s"', self.unique_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_entity_id,
self._run_time)
except exceptions.HTTPError as exc_info:
_LOGGER.error('Unable to turn on zone "%s"', self.unique_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_entity_id)
except exceptions.HTTPError as exc_info:
_LOGGER.error('Unable to update info for zone "%s"',
self.unique_id)
_LOGGER.debug(exc_info)