From 084b3287ab6755b70cd812a3c8fd83a8098159b5 Mon Sep 17 00:00:00 2001 From: Aaron Bach Date: Tue, 29 May 2018 13:02:16 -0600 Subject: [PATCH] Add sensors and services to RainMachine (#14326) * Starting to add attributes * All attributes added to programs * Basic zone attributes in place * Added advanced properties for zones * We shouldn't calculate the MAC with every entity * Small fixes * Basic framework for push in play * I THINK IT'S WORKING * Some state cleanup * Restart * Restart part 2 * Added stub for service schema * Update * Added services * Small service description update * Lint * Updated CODEOWNERS * Moving to async methods * Fixed coverage test * Lint * Removed unnecessary hass reference * Lint * Lint * Round 1 of Owner-requested changes * Round 2 of Owner-requested changes * Round 3 of Owner-requested changes * Round 4 (final for now) of Owner-requested changes * Hound * Updated package requirements * Lint * Collaborator-requested changes * Collaborator-requested changes * More small tweaks * One more small tweak * Bumping Travis and Coveralls --- .coveragerc | 2 +- CODEOWNERS | 3 +- .../components/binary_sensor/rainmachine.py | 102 ++++++++ homeassistant/components/rainmachine.py | 132 ---------- .../components/rainmachine/__init__.py | 226 ++++++++++++++++++ .../components/rainmachine/services.yaml | 32 +++ .../components/sensor/rainmachine.py | 88 +++++++ .../components/switch/rainmachine.py | 79 +++--- requirements_all.txt | 2 +- 9 files changed, 490 insertions(+), 176 deletions(-) create mode 100644 homeassistant/components/binary_sensor/rainmachine.py delete mode 100644 homeassistant/components/rainmachine.py create mode 100644 homeassistant/components/rainmachine/__init__.py create mode 100644 homeassistant/components/rainmachine/services.yaml create mode 100644 homeassistant/components/sensor/rainmachine.py diff --git a/.coveragerc b/.coveragerc index 3d1bbab8456..8d884dc53e6 100644 --- a/.coveragerc +++ b/.coveragerc @@ -219,7 +219,7 @@ omit = homeassistant/components/raincloud.py homeassistant/components/*/raincloud.py - homeassistant/components/rainmachine.py + homeassistant/components/rainmachine/* homeassistant/components/*/rainmachine.py homeassistant/components/raspihats.py diff --git a/CODEOWNERS b/CODEOWNERS index 32639fed43c..0da8353e5aa 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -78,7 +78,6 @@ homeassistant/components/sensor/sytadin.py @gautric homeassistant/components/sensor/tibber.py @danielhiversen homeassistant/components/sensor/upnp.py @dgomes homeassistant/components/sensor/waqi.py @andrey-git -homeassistant/components/switch/rainmachine.py @bachya homeassistant/components/switch/tplink.py @rytilahti homeassistant/components/vacuum/roomba.py @pschmitt homeassistant/components/xiaomi_aqara.py @danielhiversen @syssi @@ -100,6 +99,8 @@ homeassistant/components/matrix.py @tinloaf homeassistant/components/*/matrix.py @tinloaf homeassistant/components/qwikswitch.py @kellerza homeassistant/components/*/qwikswitch.py @kellerza +homeassistant/components/rainmachine/* @bachya +homeassistant/components/*/rainmachine.py @bachya homeassistant/components/*/rfxtrx.py @danielhiversen homeassistant/components/tahoma.py @philklei homeassistant/components/*/tahoma.py @philklei diff --git a/homeassistant/components/binary_sensor/rainmachine.py b/homeassistant/components/binary_sensor/rainmachine.py new file mode 100644 index 00000000000..601a73298af --- /dev/null +++ b/homeassistant/components/binary_sensor/rainmachine.py @@ -0,0 +1,102 @@ +""" +This platform provides binary sensors for key RainMachine data. + +For more details about this platform, please refer to the documentation at +https://home-assistant.io/components/binary_sensor.rainmachine/ +""" +import logging + +from homeassistant.components.binary_sensor import BinarySensorDevice +from homeassistant.components.rainmachine import ( + BINARY_SENSORS, DATA_RAINMACHINE, DATA_UPDATE_TOPIC, TYPE_FREEZE, + TYPE_FREEZE_PROTECTION, TYPE_HOT_DAYS, TYPE_HOURLY, TYPE_MONTH, + TYPE_RAINDELAY, TYPE_RAINSENSOR, TYPE_WEEKDAY, RainMachineEntity) +from homeassistant.const import CONF_MONITORED_CONDITIONS +from homeassistant.core import callback +from homeassistant.helpers.dispatcher import async_dispatcher_connect + +DEPENDENCIES = ['rainmachine'] + +_LOGGER = logging.getLogger(__name__) + + +def setup_platform(hass, config, add_devices, discovery_info=None): + """Set up the RainMachine Switch platform.""" + if discovery_info is None: + return + + rainmachine = hass.data[DATA_RAINMACHINE] + + binary_sensors = [] + for sensor_type in discovery_info[CONF_MONITORED_CONDITIONS]: + name, icon = BINARY_SENSORS[sensor_type] + binary_sensors.append( + RainMachineBinarySensor(rainmachine, sensor_type, name, icon)) + + add_devices(binary_sensors, True) + + +class RainMachineBinarySensor(RainMachineEntity, BinarySensorDevice): + """A sensor implementation for raincloud device.""" + + def __init__(self, rainmachine, sensor_type, name, icon): + """Initialize the sensor.""" + super().__init__(rainmachine) + + self._icon = icon + self._name = name + self._sensor_type = sensor_type + self._state = None + + @property + def icon(self) -> str: + """Return the icon.""" + return self._icon + + @property + def is_on(self): + """Return the status of the sensor.""" + return self._state + + @property + def should_poll(self): + """Disable polling.""" + return False + + @property + def unique_id(self) -> str: + """Return a unique, HASS-friendly identifier for this entity.""" + return '{0}_{1}'.format( + self.rainmachine.device_mac.replace(':', ''), self._sensor_type) + + @callback + def update_data(self): + """Update the state.""" + self.async_schedule_update_ha_state(True) + + async def async_added_to_hass(self): + """Register callbacks.""" + async_dispatcher_connect(self.hass, DATA_UPDATE_TOPIC, + self.update_data) + + def update(self): + """Update the state.""" + if self._sensor_type == TYPE_FREEZE: + self._state = self.rainmachine.restrictions['current']['freeze'] + elif self._sensor_type == TYPE_FREEZE_PROTECTION: + self._state = self.rainmachine.restrictions['global'][ + 'freezeProtectEnabled'] + elif self._sensor_type == TYPE_HOT_DAYS: + self._state = self.rainmachine.restrictions['global'][ + 'hotDaysExtraWatering'] + elif self._sensor_type == TYPE_HOURLY: + self._state = self.rainmachine.restrictions['current']['hourly'] + elif self._sensor_type == TYPE_MONTH: + self._state = self.rainmachine.restrictions['current']['month'] + elif self._sensor_type == TYPE_RAINDELAY: + self._state = self.rainmachine.restrictions['current']['rainDelay'] + elif self._sensor_type == TYPE_RAINSENSOR: + self._state = self.rainmachine.restrictions['current'][ + 'rainSensor'] + elif self._sensor_type == TYPE_WEEKDAY: + self._state = self.rainmachine.restrictions['current']['weekDay'] diff --git a/homeassistant/components/rainmachine.py b/homeassistant/components/rainmachine.py deleted file mode 100644 index f2d5893d60b..00000000000 --- a/homeassistant/components/rainmachine.py +++ /dev/null @@ -1,132 +0,0 @@ -""" -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 - -import voluptuous as vol - -from homeassistant.const import ( - ATTR_ATTRIBUTION, CONF_IP_ADDRESS, CONF_PASSWORD, CONF_PORT, CONF_SSL, - CONF_SWITCHES) -from homeassistant.helpers import config_validation as cv, discovery -from homeassistant.helpers.entity import Entity - -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_ZONE_RUN_TIME = 'zone_run_time' - -DEFAULT_ATTRIBUTION = 'Data provided by Green Electronics LLC' -DEFAULT_ICON = 'mdi:water' -DEFAULT_PORT = 8080 -DEFAULT_SSL = True - -PROGRAM_UPDATE_TOPIC = '{0}_program_update'.format(DOMAIN) - -SWITCH_SCHEMA = vol.Schema({ - vol.Optional(CONF_ZONE_RUN_TIME): - cv.positive_int -}) - -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, - vol.Optional(CONF_SWITCHES): SWITCH_SCHEMA, - }) - }, - extra=vol.ALLOW_EXTRA) - - -def setup(hass, config): - """Set up the RainMachine component.""" - from regenmaschine import Authenticator, Client - from regenmaschine.exceptions import HTTPError - from requests.exceptions import ConnectTimeout - - conf = config[DOMAIN] - ip_address = conf[CONF_IP_ADDRESS] - password = conf[CONF_PASSWORD] - port = conf[CONF_PORT] - ssl = conf[CONF_SSL] - - _LOGGER.debug('Setting up RainMachine client') - - try: - auth = Authenticator.create_local( - ip_address, password, port=port, https=ssl) - client = Client(auth) - hass.data[DATA_RAINMACHINE] = 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 - - _LOGGER.debug('Setting up switch platform') - switch_config = conf.get(CONF_SWITCHES, {}) - discovery.load_platform(hass, 'switch', DOMAIN, switch_config, config) - - _LOGGER.debug('Setup complete') - - return True - - -class RainMachine(object): - """Define a generic RainMachine object.""" - - def __init__(self, client): - """Initialize.""" - self.client = client - self.device_mac = self.client.provision.wifi()['macAddress'] - - -class RainMachineEntity(Entity): - """Define a generic RainMachine entity.""" - - def __init__(self, - rainmachine, - rainmachine_type, - rainmachine_entity_id, - icon=DEFAULT_ICON): - """Initialize.""" - self._attrs = {ATTR_ATTRIBUTION: DEFAULT_ATTRIBUTION} - self._icon = icon - self._rainmachine_type = rainmachine_type - self._rainmachine_entity_id = rainmachine_entity_id - self.rainmachine = rainmachine - - @property - def device_state_attributes(self) -> dict: - """Return the state attributes.""" - return self._attrs - - @property - def icon(self) -> str: - """Return the icon.""" - return self._icon - - @property - def unique_id(self) -> str: - """Return a unique, HASS-friendly identifier for this entity.""" - return '{0}_{1}_{2}'.format( - self.rainmachine.device_mac.replace( - ':', ''), self._rainmachine_type, - self._rainmachine_entity_id) diff --git a/homeassistant/components/rainmachine/__init__.py b/homeassistant/components/rainmachine/__init__.py new file mode 100644 index 00000000000..7ee6b063720 --- /dev/null +++ b/homeassistant/components/rainmachine/__init__.py @@ -0,0 +1,226 @@ +""" +Support for RainMachine devices. + +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 homeassistant.const import ( + ATTR_ATTRIBUTION, CONF_BINARY_SENSORS, CONF_IP_ADDRESS, CONF_PASSWORD, + CONF_PORT, CONF_SENSORS, CONF_SSL, CONF_MONITORED_CONDITIONS, + CONF_SWITCHES) +from homeassistant.helpers import config_validation as cv, discovery +from homeassistant.helpers.dispatcher import dispatcher_send +from homeassistant.helpers.entity import Entity +from homeassistant.helpers.event import track_time_interval + +REQUIREMENTS = ['regenmaschine==0.4.2'] + +_LOGGER = logging.getLogger(__name__) + +DATA_RAINMACHINE = 'data_rainmachine' +DOMAIN = 'rainmachine' + +NOTIFICATION_ID = 'rainmachine_notification' +NOTIFICATION_TITLE = 'RainMachine Component Setup' + +DATA_UPDATE_TOPIC = '{0}_data_update'.format(DOMAIN) +PROGRAM_UPDATE_TOPIC = '{0}_program_update'.format(DOMAIN) + +CONF_PROGRAM_ID = 'program_id' +CONF_ZONE_ID = 'zone_id' +CONF_ZONE_RUN_TIME = 'zone_run_time' + +DEFAULT_ATTRIBUTION = 'Data provided by Green Electronics LLC' +DEFAULT_ICON = 'mdi:water' +DEFAULT_PORT = 8080 +DEFAULT_SCAN_INTERVAL = timedelta(seconds=60) +DEFAULT_SSL = True +DEFAULT_ZONE_RUN = 60 * 10 + +TYPE_FREEZE = 'freeze' +TYPE_FREEZE_PROTECTION = 'freeze_protection' +TYPE_FREEZE_TEMP = 'freeze_protect_temp' +TYPE_HOT_DAYS = 'extra_water_on_hot_days' +TYPE_HOURLY = 'hourly' +TYPE_MONTH = 'month' +TYPE_RAINDELAY = 'raindelay' +TYPE_RAINSENSOR = 'rainsensor' +TYPE_WEEKDAY = 'weekday' + +BINARY_SENSORS = { + TYPE_FREEZE: ('Freeze Restrictions', 'mdi:cancel'), + TYPE_FREEZE_PROTECTION: ('Freeze Protection', 'mdi:weather-snowy'), + TYPE_HOT_DAYS: ('Extra Water on Hot Days', 'mdi:thermometer-lines'), + TYPE_HOURLY: ('Hourly Restrictions', 'mdi:cancel'), + TYPE_MONTH: ('Month Restrictions', 'mdi:cancel'), + TYPE_RAINDELAY: ('Rain Delay Restrictions', 'mdi:cancel'), + TYPE_RAINSENSOR: ('Rain Sensor Restrictions', 'mdi:cancel'), + TYPE_WEEKDAY: ('Weekday Restrictions', 'mdi:cancel'), +} + +SENSORS = { + TYPE_FREEZE_TEMP: ('Freeze Protect Temperature', 'mdi:thermometer', '°C'), +} + +BINARY_SENSOR_SCHEMA = vol.Schema({ + vol.Optional(CONF_MONITORED_CONDITIONS, default=list(BINARY_SENSORS)): + vol.All(cv.ensure_list, [vol.In(BINARY_SENSORS)]) +}) + +SENSOR_SCHEMA = vol.Schema({ + vol.Optional(CONF_MONITORED_CONDITIONS, default=list(SENSORS)): + vol.All(cv.ensure_list, [vol.In(SENSORS)]) +}) + +SERVICE_START_PROGRAM_SCHEMA = vol.Schema({ + vol.Required(CONF_PROGRAM_ID): cv.positive_int, +}) + +SERVICE_START_ZONE_SCHEMA = vol.Schema({ + vol.Required(CONF_ZONE_ID): cv.positive_int, + vol.Optional(CONF_ZONE_RUN_TIME, default=DEFAULT_ZONE_RUN): + cv.positive_int, +}) + +SERVICE_STOP_PROGRAM_SCHEMA = vol.Schema({ + vol.Required(CONF_PROGRAM_ID): cv.positive_int, +}) + +SERVICE_STOP_ZONE_SCHEMA = vol.Schema({ + vol.Required(CONF_ZONE_ID): cv.positive_int, +}) + +SWITCH_SCHEMA = vol.Schema({vol.Optional(CONF_ZONE_RUN_TIME): cv.positive_int}) + +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, + vol.Optional(CONF_BINARY_SENSORS, default={}): + BINARY_SENSOR_SCHEMA, + vol.Optional(CONF_SENSORS, default={}): SENSOR_SCHEMA, + vol.Optional(CONF_SWITCHES, default={}): SWITCH_SCHEMA, + }) + }, + extra=vol.ALLOW_EXTRA) + + +def setup(hass, config): + """Set up the RainMachine component.""" + from regenmaschine import Authenticator, Client + from regenmaschine.exceptions import RainMachineError + + 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) + rainmachine = RainMachine(hass, Client(auth)) + rainmachine.update() + hass.data[DATA_RAINMACHINE] = rainmachine + except RainMachineError as exc: + _LOGGER.error('An error occurred: %s', str(exc)) + hass.components.persistent_notification.create( + 'Error: {0}
' + 'You will need to restart hass after fixing.' + ''.format(exc), + title=NOTIFICATION_TITLE, + notification_id=NOTIFICATION_ID) + return False + + for component, schema in [ + ('binary_sensor', conf[CONF_BINARY_SENSORS]), + ('sensor', conf[CONF_SENSORS]), + ('switch', conf[CONF_SWITCHES]), + ]: + discovery.load_platform(hass, component, DOMAIN, schema, config) + + def refresh(event_time): + """Refresh RainMachine data.""" + _LOGGER.debug('Updating RainMachine data') + hass.data[DATA_RAINMACHINE].update() + dispatcher_send(hass, DATA_UPDATE_TOPIC) + + track_time_interval(hass, refresh, DEFAULT_SCAN_INTERVAL) + + def start_program(service): + """Start a particular program.""" + rainmachine.client.programs.start(service.data[CONF_PROGRAM_ID]) + + def start_zone(service): + """Start a particular zone for a certain amount of time.""" + rainmachine.client.zones.start(service.data[CONF_ZONE_ID], + service.data[CONF_ZONE_RUN_TIME]) + + def stop_all(service): + """Stop all watering.""" + rainmachine.client.watering.stop_all() + + def stop_program(service): + """Stop a program.""" + rainmachine.client.programs.stop(service.data[CONF_PROGRAM_ID]) + + def stop_zone(service): + """Stop a zone.""" + rainmachine.client.zones.stop(service.data[CONF_ZONE_ID]) + + for service, method, schema in [ + ('start_program', start_program, SERVICE_START_PROGRAM_SCHEMA), + ('start_zone', start_zone, SERVICE_START_ZONE_SCHEMA), + ('stop_all', stop_all, {}), + ('stop_program', stop_program, SERVICE_STOP_PROGRAM_SCHEMA), + ('stop_zone', stop_zone, SERVICE_STOP_ZONE_SCHEMA) + ]: + hass.services.register(DOMAIN, service, method, schema=schema) + + return True + + +class RainMachine(object): + """Define a generic RainMachine object.""" + + def __init__(self, hass, client): + """Initialize.""" + self.client = client + self.device_mac = self.client.provision.wifi()['macAddress'] + self.restrictions = {} + + def update(self): + """Update sensor/binary sensor data.""" + self.restrictions.update({ + 'current': self.client.restrictions.current(), + 'global': self.client.restrictions.universal() + }) + + +class RainMachineEntity(Entity): + """Define a generic RainMachine entity.""" + + def __init__(self, rainmachine): + """Initialize.""" + self._attrs = {ATTR_ATTRIBUTION: DEFAULT_ATTRIBUTION} + self._name = None + self.rainmachine = rainmachine + + @property + def device_state_attributes(self) -> dict: + """Return the state attributes.""" + return self._attrs + + @property + def name(self) -> str: + """Return the name of the entity.""" + return self._name diff --git a/homeassistant/components/rainmachine/services.yaml b/homeassistant/components/rainmachine/services.yaml new file mode 100644 index 00000000000..a8c77628c8f --- /dev/null +++ b/homeassistant/components/rainmachine/services.yaml @@ -0,0 +1,32 @@ +# Describes the format for available RainMachine services + +--- +start_program: + description: Start a program. + fields: + program_id: + description: The program to start. + example: 3 +start_zone: + description: Start a zone for a set number of seconds. + fields: + zone_id: + description: The zone to start. + example: 3 + zone_run_time: + description: The number of seconds to run the zone. + example: 120 +stop_all: + description: Stop all watering activities. +stop_program: + description: Stop a program. + fields: + program_id: + description: The program to stop. + example: 3 +stop_zone: + description: Stop a zone. + fields: + zone_id: + description: The zone to stop. + example: 3 diff --git a/homeassistant/components/sensor/rainmachine.py b/homeassistant/components/sensor/rainmachine.py new file mode 100644 index 00000000000..8faf30acc38 --- /dev/null +++ b/homeassistant/components/sensor/rainmachine.py @@ -0,0 +1,88 @@ +""" +This platform provides support for sensor data from RainMachine. + +For more details about this platform, please refer to the documentation at +https://home-assistant.io/components/sensor.rainmachine/ +""" +import logging + +from homeassistant.components.rainmachine import ( + DATA_RAINMACHINE, DATA_UPDATE_TOPIC, SENSORS, RainMachineEntity) +from homeassistant.const import CONF_MONITORED_CONDITIONS +from homeassistant.core import callback +from homeassistant.helpers.dispatcher import async_dispatcher_connect + +DEPENDENCIES = ['rainmachine'] + +_LOGGER = logging.getLogger(__name__) + + +def setup_platform(hass, config, add_devices, discovery_info=None): + """Set up the RainMachine Switch platform.""" + if discovery_info is None: + return + + rainmachine = hass.data[DATA_RAINMACHINE] + + sensors = [] + for sensor_type in discovery_info[CONF_MONITORED_CONDITIONS]: + name, icon, unit = SENSORS[sensor_type] + sensors.append( + RainMachineSensor(rainmachine, sensor_type, name, icon, unit)) + + add_devices(sensors, True) + + +class RainMachineSensor(RainMachineEntity): + """A sensor implementation for raincloud device.""" + + def __init__(self, rainmachine, sensor_type, name, icon, unit): + """Initialize.""" + super().__init__(rainmachine) + + self._icon = icon + self._name = name + self._sensor_type = sensor_type + self._state = None + self._unit = unit + + @property + def icon(self) -> str: + """Return the icon.""" + return self._icon + + @property + def should_poll(self): + """Disable polling.""" + return False + + @property + def state(self) -> str: + """Return the name of the entity.""" + return self._state + + @property + def unique_id(self) -> str: + """Return a unique, HASS-friendly identifier for this entity.""" + return '{0}_{1}'.format( + self.rainmachine.device_mac.replace(':', ''), self._sensor_type) + + @property + def unit_of_measurement(self): + """Return the unit the value is expressed in.""" + return self._unit + + @callback + def update_data(self): + """Update the state.""" + self.async_schedule_update_ha_state(True) + + async def async_added_to_hass(self): + """Register callbacks.""" + async_dispatcher_connect(self.hass, DATA_UPDATE_TOPIC, + self.update_data) + + def update(self): + """Update the sensor's state.""" + self._state = self.rainmachine.restrictions['global'][ + 'freezeProtectTemp'] diff --git a/homeassistant/components/switch/rainmachine.py b/homeassistant/components/switch/rainmachine.py index beb00eeca44..f4b2d780a9a 100644 --- a/homeassistant/components/switch/rainmachine.py +++ b/homeassistant/components/switch/rainmachine.py @@ -4,12 +4,11 @@ This component provides support for RainMachine programs and zones. For more details about this component, please refer to the documentation at https://home-assistant.io/components/switch.rainmachine/ """ - -from logging import getLogger +import logging from homeassistant.components.rainmachine import ( - CONF_ZONE_RUN_TIME, DATA_RAINMACHINE, PROGRAM_UPDATE_TOPIC, - RainMachineEntity) + CONF_ZONE_RUN_TIME, DATA_RAINMACHINE, DEFAULT_ZONE_RUN, + PROGRAM_UPDATE_TOPIC, RainMachineEntity) from homeassistant.const import ATTR_ID from homeassistant.components.switch import SwitchDevice from homeassistant.core import callback @@ -18,7 +17,7 @@ from homeassistant.helpers.dispatcher import ( DEPENDENCIES = ['rainmachine'] -_LOGGER = getLogger(__name__) +_LOGGER = logging.getLogger(__name__) ATTR_AREA = 'area' ATTR_CS_ON = 'cs_on' @@ -39,8 +38,6 @@ ATTR_SUN_EXPOSURE = 'sun_exposure' ATTR_VEGETATION_TYPE = 'vegetation_type' ATTR_ZONES = 'zones' -DEFAULT_ZONE_RUN = 60 * 10 - DAYS = [ 'Monday', 'Tuesday', @@ -141,26 +138,41 @@ def setup_platform(hass, config, add_devices, discovery_info=None): class RainMachineSwitch(RainMachineEntity, SwitchDevice): - """A class to represent a generic RainMachine entity.""" + """A class to represent a generic RainMachine switch.""" - def __init__(self, rainmachine, rainmachine_type, obj): - """Initialize a generic RainMachine entity.""" + def __init__(self, rainmachine, switch_type, obj): + """Initialize a generic RainMachine switch.""" + super().__init__(rainmachine) + + self._name = obj['name'] self._obj = obj - self._type = rainmachine_type + self._rainmachine_entity_id = obj['uid'] + self._switch_type = switch_type - super().__init__(rainmachine, rainmachine_type, obj.get('uid')) + @property + def icon(self) -> str: + """Return the icon.""" + return 'mdi:water' @property def is_enabled(self) -> bool: """Return whether the entity is enabled.""" return self._obj.get('active') + @property + def unique_id(self) -> str: + """Return a unique, HASS-friendly identifier for this entity.""" + return '{0}_{1}_{2}'.format( + self.rainmachine.device_mac.replace(':', ''), + self._switch_type, + self._rainmachine_entity_id) + class RainMachineProgram(RainMachineSwitch): """A RainMachine program.""" def __init__(self, rainmachine, obj): - """Initialize.""" + """Initialize a generic RainMachine switch.""" super().__init__(rainmachine, 'program', obj) @property @@ -168,11 +180,6 @@ class RainMachineProgram(RainMachineSwitch): """Return whether the program is running.""" return bool(self._obj.get('status')) - @property - def name(self) -> str: - """Return the name of the program.""" - return 'Program: {0}'.format(self._obj.get('name')) - @property def zones(self) -> list: """Return a list of active zones associated with this program.""" @@ -180,29 +187,29 @@ class RainMachineProgram(RainMachineSwitch): def turn_off(self, **kwargs) -> None: """Turn the program off.""" - from regenmaschine.exceptions import HTTPError + from regenmaschine.exceptions import RainMachineError try: self.rainmachine.client.programs.stop(self._rainmachine_entity_id) dispatcher_send(self.hass, PROGRAM_UPDATE_TOPIC) - except HTTPError as exc_info: + except RainMachineError 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.""" - from regenmaschine.exceptions import HTTPError + from regenmaschine.exceptions import RainMachineError try: self.rainmachine.client.programs.start(self._rainmachine_entity_id) dispatcher_send(self.hass, PROGRAM_UPDATE_TOPIC) - except HTTPError as exc_info: + except RainMachineError 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.""" - from regenmaschine.exceptions import HTTPError + from regenmaschine.exceptions import RainMachineError try: self._obj = self.rainmachine.client.programs.get( @@ -210,16 +217,11 @@ class RainMachineProgram(RainMachineSwitch): self._attrs.update({ ATTR_ID: self._obj['uid'], - ATTR_CS_ON: self._obj.get('cs_on'), - ATTR_CYCLES: self._obj.get('cycles'), - ATTR_DELAY: self._obj.get('delay'), - ATTR_DELAY_ON: self._obj.get('delay_on'), ATTR_SOAK: self._obj.get('soak'), - ATTR_STATUS: - PROGRAM_STATUS_MAP[self._obj.get('status')], + ATTR_STATUS: PROGRAM_STATUS_MAP[self._obj.get('status')], ATTR_ZONES: ', '.join(z['name'] for z in self.zones) }) - except HTTPError as exc_info: + except RainMachineError as exc_info: _LOGGER.error('Unable to update info for program "%s"', self.unique_id) _LOGGER.debug(exc_info) @@ -240,11 +242,6 @@ class RainMachineZone(RainMachineSwitch): """Return whether the zone is running.""" return bool(self._obj.get('state')) - @property - def name(self) -> str: - """Return the name of the zone.""" - return 'Zone: {0}'.format(self._obj.get('name')) - @callback def _program_updated(self): """Update state, trigger updates.""" @@ -257,28 +254,28 @@ class RainMachineZone(RainMachineSwitch): def turn_off(self, **kwargs) -> None: """Turn the zone off.""" - from regenmaschine.exceptions import HTTPError + from regenmaschine.exceptions import RainMachineError try: self.rainmachine.client.zones.stop(self._rainmachine_entity_id) - except HTTPError as exc_info: + except RainMachineError 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.""" - from regenmaschine.exceptions import HTTPError + from regenmaschine.exceptions import RainMachineError try: self.rainmachine.client.zones.start(self._rainmachine_entity_id, self._run_time) - except HTTPError as exc_info: + except RainMachineError 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.""" - from regenmaschine.exceptions import HTTPError + from regenmaschine.exceptions import RainMachineError try: self._obj = self.rainmachine.client.zones.get( @@ -309,7 +306,7 @@ class RainMachineZone(RainMachineSwitch): ATTR_VEGETATION_TYPE: VEGETATION_MAP[self._obj.get('type')], }) - except HTTPError as exc_info: + except RainMachineError as exc_info: _LOGGER.error('Unable to update info for zone "%s"', self.unique_id) _LOGGER.debug(exc_info) diff --git a/requirements_all.txt b/requirements_all.txt index 958b0f1027e..1b1db52daef 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1147,7 +1147,7 @@ raincloudy==0.0.4 # raspihats==2.2.3 # homeassistant.components.rainmachine -regenmaschine==0.4.1 +regenmaschine==0.4.2 # homeassistant.components.python_script restrictedpython==4.0b4