From 8aca2e84dc81961296eea2fdacf672f068acdcbb Mon Sep 17 00:00:00 2001 From: Aaron Bach Date: Sun, 10 Jun 2018 02:23:07 -0600 Subject: [PATCH] Make RainMachine async (#14879) * Make RainMachine async * Updated requirements * Dispatcher adjustments * Small verbiage change * Member-requested changes * Style consistency * Updated requirements --- .../components/binary_sensor/rainmachine.py | 15 +- .../components/rainmachine/__init__.py | 88 +++++---- .../components/sensor/rainmachine.py | 15 +- .../components/switch/rainmachine.py | 170 +++++++++--------- requirements_all.txt | 2 +- 5 files changed, 156 insertions(+), 134 deletions(-) diff --git a/homeassistant/components/binary_sensor/rainmachine.py b/homeassistant/components/binary_sensor/rainmachine.py index 601a73298af..b2f44696fbd 100644 --- a/homeassistant/components/binary_sensor/rainmachine.py +++ b/homeassistant/components/binary_sensor/rainmachine.py @@ -8,7 +8,7 @@ import logging from homeassistant.components.binary_sensor import BinarySensorDevice from homeassistant.components.rainmachine import ( - BINARY_SENSORS, DATA_RAINMACHINE, DATA_UPDATE_TOPIC, TYPE_FREEZE, + BINARY_SENSORS, DATA_RAINMACHINE, SENSOR_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 @@ -20,7 +20,8 @@ DEPENDENCIES = ['rainmachine'] _LOGGER = logging.getLogger(__name__) -def setup_platform(hass, config, add_devices, discovery_info=None): +async def async_setup_platform( + hass, config, async_add_devices, discovery_info=None): """Set up the RainMachine Switch platform.""" if discovery_info is None: return @@ -33,7 +34,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None): binary_sensors.append( RainMachineBinarySensor(rainmachine, sensor_type, name, icon)) - add_devices(binary_sensors, True) + async_add_devices(binary_sensors, True) class RainMachineBinarySensor(RainMachineEntity, BinarySensorDevice): @@ -70,16 +71,16 @@ class RainMachineBinarySensor(RainMachineEntity, BinarySensorDevice): self.rainmachine.device_mac.replace(':', ''), self._sensor_type) @callback - def update_data(self): + 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) + async_dispatcher_connect( + self.hass, SENSOR_UPDATE_TOPIC, self._update_data) - def update(self): + async def async_update(self): """Update the state.""" if self._sensor_type == TYPE_FREEZE: self._state = self.rainmachine.restrictions['current']['freeze'] diff --git a/homeassistant/components/rainmachine/__init__.py b/homeassistant/components/rainmachine/__init__.py index 7ee6b063720..38672dbc23b 100644 --- a/homeassistant/components/rainmachine/__init__.py +++ b/homeassistant/components/rainmachine/__init__.py @@ -13,12 +13,13 @@ 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 import ( + aiohttp_client, config_validation as cv, discovery) +from homeassistant.helpers.dispatcher import async_dispatcher_send from homeassistant.helpers.entity import Entity -from homeassistant.helpers.event import track_time_interval +from homeassistant.helpers.event import async_track_time_interval -REQUIREMENTS = ['regenmaschine==0.4.2'] +REQUIREMENTS = ['regenmaschine==1.0.2'] _LOGGER = logging.getLogger(__name__) @@ -28,8 +29,9 @@ 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) +SENSOR_UPDATE_TOPIC = '{0}_data_update'.format(DOMAIN) +ZONE_UPDATE_TOPIC = '{0}_zone_update'.format(DOMAIN) CONF_PROGRAM_ID = 'program_id' CONF_ZONE_ID = 'zone_id' @@ -114,10 +116,10 @@ CONFIG_SCHEMA = vol.Schema( extra=vol.ALLOW_EXTRA) -def setup(hass, config): +async def async_setup(hass, config): """Set up the RainMachine component.""" - from regenmaschine import Authenticator, Client - from regenmaschine.exceptions import RainMachineError + from regenmaschine import Client + from regenmaschine.errors import RequestError conf = config[DOMAIN] ip_address = conf[CONF_IP_ADDRESS] @@ -126,17 +128,18 @@ def setup(hass, config): ssl = conf[CONF_SSL] try: - auth = Authenticator.create_local( - ip_address, password, port=port, https=ssl) - rainmachine = RainMachine(hass, Client(auth)) - rainmachine.update() + websession = aiohttp_client.async_get_clientsession(hass) + client = Client(ip_address, websession, port=port, ssl=ssl) + await client.authenticate(password) + rainmachine = RainMachine(client) + await rainmachine.async_update() hass.data[DATA_RAINMACHINE] = rainmachine - except RainMachineError as exc: - _LOGGER.error('An error occurred: %s', str(exc)) + except RequestError as err: + _LOGGER.error('An error occurred: %s', str(err)) hass.components.persistent_notification.create( 'Error: {0}
' 'You will need to restart hass after fixing.' - ''.format(exc), + ''.format(err), title=NOTIFICATION_TITLE, notification_id=NOTIFICATION_ID) return False @@ -146,36 +149,43 @@ def setup(hass, config): ('sensor', conf[CONF_SENSORS]), ('switch', conf[CONF_SWITCHES]), ]: - discovery.load_platform(hass, component, DOMAIN, schema, config) + hass.async_add_job( + discovery.async_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) + async def refresh_sensors(event_time): + """Refresh RainMachine sensor data.""" + _LOGGER.debug('Updating RainMachine sensor data') + await rainmachine.async_update() + async_dispatcher_send(hass, SENSOR_UPDATE_TOPIC) - track_time_interval(hass, refresh, DEFAULT_SCAN_INTERVAL) + async_track_time_interval(hass, refresh_sensors, DEFAULT_SCAN_INTERVAL) - def start_program(service): + async def start_program(service): """Start a particular program.""" - rainmachine.client.programs.start(service.data[CONF_PROGRAM_ID]) + await rainmachine.client.programs.start(service.data[CONF_PROGRAM_ID]) + async_dispatcher_send(hass, PROGRAM_UPDATE_TOPIC) - def start_zone(service): + async 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]) + await rainmachine.client.zones.start(service.data[CONF_ZONE_ID], + service.data[CONF_ZONE_RUN_TIME]) + async_dispatcher_send(hass, ZONE_UPDATE_TOPIC) - def stop_all(service): + async def stop_all(service): """Stop all watering.""" - rainmachine.client.watering.stop_all() + await rainmachine.client.watering.stop_all() + async_dispatcher_send(hass, PROGRAM_UPDATE_TOPIC) - def stop_program(service): + async def stop_program(service): """Stop a program.""" - rainmachine.client.programs.stop(service.data[CONF_PROGRAM_ID]) + await rainmachine.client.programs.stop(service.data[CONF_PROGRAM_ID]) + async_dispatcher_send(hass, PROGRAM_UPDATE_TOPIC) - def stop_zone(service): + async def stop_zone(service): """Stop a zone.""" - rainmachine.client.zones.stop(service.data[CONF_ZONE_ID]) + await rainmachine.client.zones.stop(service.data[CONF_ZONE_ID]) + async_dispatcher_send(hass, ZONE_UPDATE_TOPIC) for service, method, schema in [ ('start_program', start_program, SERVICE_START_PROGRAM_SCHEMA), @@ -184,7 +194,7 @@ def setup(hass, config): ('stop_program', stop_program, SERVICE_STOP_PROGRAM_SCHEMA), ('stop_zone', stop_zone, SERVICE_STOP_ZONE_SCHEMA) ]: - hass.services.register(DOMAIN, service, method, schema=schema) + hass.services.async_register(DOMAIN, service, method, schema=schema) return True @@ -192,17 +202,17 @@ def setup(hass, config): class RainMachine(object): """Define a generic RainMachine object.""" - def __init__(self, hass, client): + def __init__(self, client): """Initialize.""" self.client = client - self.device_mac = self.client.provision.wifi()['macAddress'] + self.device_mac = self.client.mac self.restrictions = {} - def update(self): + async def async_update(self): """Update sensor/binary sensor data.""" self.restrictions.update({ - 'current': self.client.restrictions.current(), - 'global': self.client.restrictions.universal() + 'current': await self.client.restrictions.current(), + 'global': await self.client.restrictions.universal() }) diff --git a/homeassistant/components/sensor/rainmachine.py b/homeassistant/components/sensor/rainmachine.py index 8faf30acc38..f747a26df39 100644 --- a/homeassistant/components/sensor/rainmachine.py +++ b/homeassistant/components/sensor/rainmachine.py @@ -7,7 +7,7 @@ https://home-assistant.io/components/sensor.rainmachine/ import logging from homeassistant.components.rainmachine import ( - DATA_RAINMACHINE, DATA_UPDATE_TOPIC, SENSORS, RainMachineEntity) + DATA_RAINMACHINE, SENSOR_UPDATE_TOPIC, SENSORS, RainMachineEntity) from homeassistant.const import CONF_MONITORED_CONDITIONS from homeassistant.core import callback from homeassistant.helpers.dispatcher import async_dispatcher_connect @@ -17,7 +17,8 @@ DEPENDENCIES = ['rainmachine'] _LOGGER = logging.getLogger(__name__) -def setup_platform(hass, config, add_devices, discovery_info=None): +async def async_setup_platform( + hass, config, async_add_devices, discovery_info=None): """Set up the RainMachine Switch platform.""" if discovery_info is None: return @@ -30,7 +31,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None): sensors.append( RainMachineSensor(rainmachine, sensor_type, name, icon, unit)) - add_devices(sensors, True) + async_add_devices(sensors, True) class RainMachineSensor(RainMachineEntity): @@ -73,16 +74,16 @@ class RainMachineSensor(RainMachineEntity): return self._unit @callback - def update_data(self): + 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) + async_dispatcher_connect( + self.hass, SENSOR_UPDATE_TOPIC, self._update_data) - def update(self): + async def async_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 bdee64a3d54..b0cdf334cfa 100644 --- a/homeassistant/components/switch/rainmachine.py +++ b/homeassistant/components/switch/rainmachine.py @@ -8,12 +8,12 @@ import logging from homeassistant.components.rainmachine import ( CONF_ZONE_RUN_TIME, DATA_RAINMACHINE, DEFAULT_ZONE_RUN, - PROGRAM_UPDATE_TOPIC, RainMachineEntity) + PROGRAM_UPDATE_TOPIC, ZONE_UPDATE_TOPIC, RainMachineEntity) from homeassistant.const import ATTR_ID from homeassistant.components.switch import SwitchDevice from homeassistant.core import callback from homeassistant.helpers.dispatcher import ( - async_dispatcher_connect, dispatcher_send) + async_dispatcher_connect, async_dispatcher_send) DEPENDENCIES = ['rainmachine'] @@ -39,20 +39,11 @@ ATTR_VEGETATION_TYPE = 'vegetation_type' ATTR_ZONES = 'zones' DAYS = [ - 'Monday', - 'Tuesday', - 'Wednesday', - 'Thursday', - 'Friday', - 'Saturday', + 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday', 'Sunday' ] -PROGRAM_STATUS_MAP = { - 0: 'Not Running', - 1: 'Running', - 2: 'Queued' -} +PROGRAM_STATUS_MAP = {0: 'Not Running', 1: 'Running', 2: 'Queued'} SOIL_TYPE_MAP = { 0: 'Not Set', @@ -108,7 +99,8 @@ VEGETATION_MAP = { } -def setup_platform(hass, config, add_devices, discovery_info=None): +async def async_setup_platform( + hass, config, async_add_devices, discovery_info=None): """Set up the RainMachine Switch platform.""" if discovery_info is None: return @@ -120,21 +112,24 @@ def setup_platform(hass, config, add_devices, discovery_info=None): rainmachine = hass.data[DATA_RAINMACHINE] entities = [] - for program in rainmachine.client.programs.all().get('programs', {}): + + programs = await rainmachine.client.programs.all() + for program in programs: if not program.get('active'): continue _LOGGER.debug('Adding program: %s', program) entities.append(RainMachineProgram(rainmachine, program)) - for zone in rainmachine.client.zones.all().get('zones', {}): + zones = await rainmachine.client.zones.all() + for zone in zones: if not zone.get('active'): continue _LOGGER.debug('Adding zone: %s', zone) entities.append(RainMachineZone(rainmachine, zone, zone_run_time)) - add_devices(entities, True) + async_add_devices(entities, True) class RainMachineSwitch(RainMachineEntity, SwitchDevice): @@ -163,10 +158,14 @@ class RainMachineSwitch(RainMachineEntity, SwitchDevice): 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.device_mac.replace(':', ''), self._switch_type, self._rainmachine_entity_id) + @callback + def _program_updated(self): + """Update state, trigger updates.""" + self.async_schedule_update_ha_state(True) + class RainMachineProgram(RainMachineSwitch): """A RainMachine program.""" @@ -185,34 +184,42 @@ class RainMachineProgram(RainMachineSwitch): """Return a list of active zones associated with this program.""" return [z for z in self._obj['wateringTimes'] if z['active']] - def turn_off(self, **kwargs) -> None: + async def async_added_to_hass(self): + """Register callbacks.""" + async_dispatcher_connect( + self.hass, PROGRAM_UPDATE_TOPIC, self._program_updated) + + async def async_turn_off(self, **kwargs) -> None: """Turn the program off.""" - from regenmaschine.exceptions import RainMachineError + from regenmaschine.errors import RequestError try: - self.rainmachine.client.programs.stop(self._rainmachine_entity_id) - dispatcher_send(self.hass, PROGRAM_UPDATE_TOPIC) - except RainMachineError as exc_info: - _LOGGER.error('Unable to turn off program "%s"', self.unique_id) - _LOGGER.debug(exc_info) + await self.rainmachine.client.programs.stop( + self._rainmachine_entity_id) + async_dispatcher_send(self.hass, PROGRAM_UPDATE_TOPIC) + except RequestError as err: + _LOGGER.error( + 'Unable to turn off program "%s": %s', self.unique_id, + str(err)) - def turn_on(self, **kwargs) -> None: + async def async_turn_on(self, **kwargs) -> None: """Turn the program on.""" - from regenmaschine.exceptions import RainMachineError + from regenmaschine.errors import RequestError try: - self.rainmachine.client.programs.start(self._rainmachine_entity_id) - dispatcher_send(self.hass, PROGRAM_UPDATE_TOPIC) - except RainMachineError as exc_info: - _LOGGER.error('Unable to turn on program "%s"', self.unique_id) - _LOGGER.debug(exc_info) + await self.rainmachine.client.programs.start( + self._rainmachine_entity_id) + async_dispatcher_send(self.hass, PROGRAM_UPDATE_TOPIC) + except RequestError as err: + _LOGGER.error( + 'Unable to turn on program "%s": %s', self.unique_id, str(err)) - def update(self) -> None: + async def async_update(self) -> None: """Update info for the program.""" - from regenmaschine.exceptions import RainMachineError + from regenmaschine.errors import RequestError try: - self._obj = self.rainmachine.client.programs.get( + self._obj = await self.rainmachine.client.programs.get( self._rainmachine_entity_id) self._attrs.update({ @@ -221,10 +228,10 @@ class RainMachineProgram(RainMachineSwitch): ATTR_STATUS: PROGRAM_STATUS_MAP[self._obj.get('status')], ATTR_ZONES: ', '.join(z['name'] for z in self.zones) }) - except RainMachineError as exc_info: - _LOGGER.error('Unable to update info for program "%s"', - self.unique_id) - _LOGGER.debug(exc_info) + except RequestError as err: + _LOGGER.error( + 'Unable to update info for program "%s": %s', self.unique_id, + str(err)) class RainMachineZone(RainMachineSwitch): @@ -242,62 +249,65 @@ class RainMachineZone(RainMachineSwitch): """Return whether the zone is running.""" return bool(self._obj.get('state')) - @callback - def _program_updated(self): - """Update state, trigger updates.""" - self.async_schedule_update_ha_state(True) - async def async_added_to_hass(self): """Register callbacks.""" - async_dispatcher_connect(self.hass, PROGRAM_UPDATE_TOPIC, - self._program_updated) + async_dispatcher_connect( + self.hass, PROGRAM_UPDATE_TOPIC, self._program_updated) + async_dispatcher_connect( + self.hass, ZONE_UPDATE_TOPIC, self._program_updated) - def turn_off(self, **kwargs) -> None: + async def async_turn_off(self, **kwargs) -> None: """Turn the zone off.""" - from regenmaschine.exceptions import RainMachineError + from regenmaschine.errors import RequestError try: - self.rainmachine.client.zones.stop(self._rainmachine_entity_id) - except RainMachineError as exc_info: - _LOGGER.error('Unable to turn off zone "%s"', self.unique_id) - _LOGGER.debug(exc_info) + await self.rainmachine.client.zones.stop( + self._rainmachine_entity_id) + except RequestError as err: + _LOGGER.error( + 'Unable to turn off zone "%s": %s', self.unique_id, str(err)) - def turn_on(self, **kwargs) -> None: + async def async_turn_on(self, **kwargs) -> None: """Turn the zone on.""" - from regenmaschine.exceptions import RainMachineError + from regenmaschine.errors import RequestError try: - self.rainmachine.client.zones.start(self._rainmachine_entity_id, - self._run_time) - except RainMachineError as exc_info: - _LOGGER.error('Unable to turn on zone "%s"', self.unique_id) - _LOGGER.debug(exc_info) + await self.rainmachine.client.zones.start( + self._rainmachine_entity_id, self._run_time) + except RequestError as err: + _LOGGER.error( + 'Unable to turn on zone "%s": %s', self.unique_id, str(err)) - def update(self) -> None: + async def async_update(self) -> None: """Update info for the zone.""" - from regenmaschine.exceptions import RainMachineError + from regenmaschine.errors import RequestError try: - self._obj = self.rainmachine.client.zones.get( + self._obj = await self.rainmachine.client.zones.get( self._rainmachine_entity_id) - self._properties_json = self.rainmachine.client.zones.get( - self._rainmachine_entity_id, properties=True) + self._properties_json = await self.rainmachine.client.zones.get( + self._rainmachine_entity_id, details=True) self._attrs.update({ - ATTR_ID: self._obj['uid'], - ATTR_AREA: self._properties_json.get('waterSense').get('area'), - ATTR_CURRENT_CYCLE: self._obj.get('cycle'), + ATTR_ID: + self._obj['uid'], + ATTR_AREA: + self._properties_json.get('waterSense').get('area'), + ATTR_CURRENT_CYCLE: + self._obj.get('cycle'), ATTR_FIELD_CAPACITY: - self._properties_json.get( - 'waterSense').get('fieldCapacity'), - ATTR_NO_CYCLES: self._obj.get('noOfCycles'), + self._properties_json.get('waterSense') + .get('fieldCapacity'), + ATTR_NO_CYCLES: + self._obj.get('noOfCycles'), ATTR_PRECIP_RATE: - self._properties_json.get( - 'waterSense').get('precipitationRate'), - ATTR_RESTRICTIONS: self._obj.get('restriction'), - ATTR_SLOPE: SLOPE_TYPE_MAP.get( - self._properties_json.get('slope')), + self._properties_json.get('waterSense') + .get('precipitationRate'), + ATTR_RESTRICTIONS: + self._obj.get('restriction'), + ATTR_SLOPE: + SLOPE_TYPE_MAP.get(self._properties_json.get('slope')), ATTR_SOIL_TYPE: SOIL_TYPE_MAP.get(self._properties_json.get('sun')), ATTR_SPRINKLER_TYPE: @@ -308,7 +318,7 @@ class RainMachineZone(RainMachineSwitch): ATTR_VEGETATION_TYPE: VEGETATION_MAP.get(self._obj.get('type')), }) - except RainMachineError as exc_info: - _LOGGER.error('Unable to update info for zone "%s"', - self.unique_id) - _LOGGER.debug(exc_info) + except RequestError as err: + _LOGGER.error( + 'Unable to update info for zone "%s": %s', self.unique_id, + str(err)) diff --git a/requirements_all.txt b/requirements_all.txt index f47bbbdf23e..b941021d017 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1162,7 +1162,7 @@ raincloudy==0.0.4 # raspihats==2.2.3 # homeassistant.components.rainmachine -regenmaschine==0.4.2 +regenmaschine==1.0.2 # homeassistant.components.python_script restrictedpython==4.0b4