diff --git a/.coveragerc b/.coveragerc index 5e27aed0182534..da3968870772f7 100644 --- a/.coveragerc +++ b/.coveragerc @@ -146,6 +146,9 @@ omit = homeassistant/components/rachio.py homeassistant/components/*/rachio.py + homeassistant/components/raincloud.py + homeassistant/components/*/raincloud.py + homeassistant/components/raspihats.py homeassistant/components/*/raspihats.py diff --git a/homeassistant/components/binary_sensor/raincloud.py b/homeassistant/components/binary_sensor/raincloud.py new file mode 100644 index 00000000000000..874f7a81a1780a --- /dev/null +++ b/homeassistant/components/binary_sensor/raincloud.py @@ -0,0 +1,70 @@ +""" +Support for Melnor RainCloud sprinkler water timer. + +For more details about this platform, please refer to the documentation at +https://home-assistant.io/components/binary_sensor.raincloud/ +""" +import logging + +import voluptuous as vol + +import homeassistant.helpers.config_validation as cv +from homeassistant.components.raincloud import ( + BINARY_SENSORS, DATA_RAINCLOUD, ICON_MAP, RainCloudEntity) +from homeassistant.components.binary_sensor import ( + BinarySensorDevice, PLATFORM_SCHEMA) +from homeassistant.const import CONF_MONITORED_CONDITIONS + +DEPENDENCIES = ['raincloud'] + +_LOGGER = logging.getLogger(__name__) + +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ + vol.Optional(CONF_MONITORED_CONDITIONS, default=list(BINARY_SENSORS)): + vol.All(cv.ensure_list, [vol.In(BINARY_SENSORS)]), +}) + + +def setup_platform(hass, config, add_devices, discovery_info=None): + """Set up a sensor for a raincloud device.""" + raincloud = hass.data[DATA_RAINCLOUD].data + + sensors = [] + for sensor_type in config.get(CONF_MONITORED_CONDITIONS): + if sensor_type == 'status': + sensors.append( + RainCloudBinarySensor(raincloud.controller, sensor_type)) + sensors.append( + RainCloudBinarySensor(raincloud.controller.faucet, + sensor_type)) + + else: + # create an sensor for each zone managed by faucet + for zone in raincloud.controller.faucet.zones: + sensors.append(RainCloudBinarySensor(zone, sensor_type)) + + add_devices(sensors, True) + return True + + +class RainCloudBinarySensor(RainCloudEntity, BinarySensorDevice): + """A sensor implementation for raincloud device.""" + + @property + def is_on(self): + """Return true if the binary sensor is on.""" + return self._state + + def update(self): + """Get the latest data and updates the state.""" + _LOGGER.debug("Updating RainCloud sensor: %s", self._name) + self._state = getattr(self.data, self._sensor_type) + + @property + def icon(self): + """Return the icon of this device.""" + if self._sensor_type == 'is_watering': + return 'mdi:water' if self.is_on else 'mdi:water-off' + elif self._sensor_type == 'status': + return 'mdi:pipe' if self.is_on else 'mdi:pipe-disconnected' + return ICON_MAP.get(self._sensor_type) diff --git a/homeassistant/components/raincloud.py b/homeassistant/components/raincloud.py new file mode 100644 index 00000000000000..0cc91576daebd0 --- /dev/null +++ b/homeassistant/components/raincloud.py @@ -0,0 +1,179 @@ +""" +Support for Melnor RainCloud sprinkler water timer. + +For more details about this platform, please refer to the documentation at +https://home-assistant.io/components/raincloud/ +""" +import asyncio +import logging +from datetime import timedelta + +import voluptuous as vol +import homeassistant.helpers.config_validation as cv + +from homeassistant.const import ( + ATTR_ATTRIBUTION, CONF_USERNAME, CONF_PASSWORD, CONF_SCAN_INTERVAL) +from homeassistant.helpers.event import track_time_interval +from homeassistant.helpers.entity import Entity +from homeassistant.helpers.dispatcher import ( + async_dispatcher_connect, dispatcher_send) + +from requests.exceptions import HTTPError, ConnectTimeout + +REQUIREMENTS = ['raincloudy==0.0.1'] + +_LOGGER = logging.getLogger(__name__) + +ALLOWED_WATERING_TIME = [5, 10, 15, 30, 45, 60] + +CONF_ATTRIBUTION = "Data provided by Melnor Aquatimer.com" +CONF_WATERING_TIME = 'watering_minutes' + +NOTIFICATION_ID = 'raincloud_notification' +NOTIFICATION_TITLE = 'Rain Cloud Setup' + +DATA_RAINCLOUD = 'raincloud' +DOMAIN = 'raincloud' +DEFAULT_WATERING_TIME = 15 + +KEY_MAP = { + 'auto_watering': 'Automatic Watering', + 'battery': 'Battery', + 'is_watering': 'Watering', + 'manual_watering': 'Manual Watering', + 'next_cycle': 'Next Cycle', + 'rain_delay': 'Rain Delay', + 'status': 'Status', + 'watering_time': 'Remaining Watering Time', +} + +ICON_MAP = { + 'auto_watering': 'mdi:autorenew', + 'battery': '', + 'is_watering': '', + 'manual_watering': 'mdi:water-pump', + 'next_cycle': 'mdi:calendar-clock', + 'rain_delay': 'mdi:weather-rainy', + 'status': '', + 'watering_time': 'mdi:water-pump', +} + +UNIT_OF_MEASUREMENT_MAP = { + 'auto_watering': '', + 'battery': '%', + 'is_watering': '', + 'manual_watering': '', + 'next_cycle': '', + 'rain_delay': 'days', + 'status': '', + 'watering_time': 'min', +} + +BINARY_SENSORS = ['is_watering', 'status'] + +SENSORS = ['battery', 'next_cycle', 'rain_delay', 'watering_time'] + +SWITCHES = ['auto_watering', 'manual_watering'] + +SCAN_INTERVAL = timedelta(seconds=20) + +SIGNAL_UPDATE_RAINCLOUD = "raincloud_update" + +CONFIG_SCHEMA = vol.Schema({ + DOMAIN: vol.Schema({ + vol.Required(CONF_USERNAME): cv.string, + vol.Required(CONF_PASSWORD): cv.string, + vol.Optional(CONF_SCAN_INTERVAL, default=SCAN_INTERVAL): + cv.time_period, + }), +}, extra=vol.ALLOW_EXTRA) + + +def setup(hass, config): + """Set up the Melnor RainCloud component.""" + conf = config[DOMAIN] + username = conf.get(CONF_USERNAME) + password = conf.get(CONF_PASSWORD) + scan_interval = conf.get(CONF_SCAN_INTERVAL) + + try: + from raincloudy.core import RainCloudy + + raincloud = RainCloudy(username=username, password=password) + if not raincloud.is_connected: + raise HTTPError + hass.data[DATA_RAINCLOUD] = RainCloudHub(raincloud) + except (ConnectTimeout, HTTPError) as ex: + _LOGGER.error("Unable to connect to Rain Cloud service: %s", str(ex)) + hass.components.persistent_notification.create( + 'Error: {}
' + 'You will need to restart hass after fixing.' + ''.format(ex), + title=NOTIFICATION_TITLE, + notification_id=NOTIFICATION_ID) + return False + + def hub_refresh(event_time): + """Call Raincloud hub to refresh information.""" + _LOGGER.debug("Updating RainCloud Hub component.") + hass.data[DATA_RAINCLOUD].data.update() + dispatcher_send(hass, SIGNAL_UPDATE_RAINCLOUD) + + # Call the Raincloud API to refresh updates + track_time_interval(hass, hub_refresh, scan_interval) + + return True + + +class RainCloudHub(object): + """Representation of a base RainCloud device.""" + + def __init__(self, data): + """Initialize the entity.""" + self.data = data + + +class RainCloudEntity(Entity): + """Entity class for RainCloud devices.""" + + def __init__(self, data, sensor_type): + """Initialize the RainCloud entity.""" + self.data = data + self._sensor_type = sensor_type + self._name = "{0} {1}".format( + self.data.name, KEY_MAP.get(self._sensor_type)) + self._state = None + + @property + def name(self): + """Return the name of the sensor.""" + return self._name + + @asyncio.coroutine + def async_added_to_hass(self): + """Register callbacks.""" + async_dispatcher_connect( + self.hass, SIGNAL_UPDATE_RAINCLOUD, self._update_callback) + + def _update_callback(self): + """Callback update method.""" + self.schedule_update_ha_state(True) + + @property + def unit_of_measurement(self): + """Return the units of measurement.""" + return UNIT_OF_MEASUREMENT_MAP.get(self._sensor_type) + + @property + def device_state_attributes(self): + """Return the state attributes.""" + return { + ATTR_ATTRIBUTION: CONF_ATTRIBUTION, + 'current_time': self.data.current_time, + 'identifier': self.data.serial, + } + + @property + def icon(self): + """Icon to use in the frontend, if any.""" + return ICON_MAP.get(self._sensor_type) diff --git a/homeassistant/components/sensor/raincloud.py b/homeassistant/components/sensor/raincloud.py new file mode 100644 index 00000000000000..ab073917e8e43c --- /dev/null +++ b/homeassistant/components/sensor/raincloud.py @@ -0,0 +1,69 @@ +""" +Support for Melnor RainCloud sprinkler water timer. + +For more details about this platform, please refer to the documentation at +https://home-assistant.io/components/sensor.raincloud/ +""" +import logging + +import voluptuous as vol + +import homeassistant.helpers.config_validation as cv +from homeassistant.components.raincloud import ( + DATA_RAINCLOUD, ICON_MAP, RainCloudEntity, SENSORS) +from homeassistant.components.sensor import PLATFORM_SCHEMA +from homeassistant.const import CONF_MONITORED_CONDITIONS +from homeassistant.util.icon import icon_for_battery_level + +DEPENDENCIES = ['raincloud'] + +_LOGGER = logging.getLogger(__name__) + +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ + vol.Optional(CONF_MONITORED_CONDITIONS, default=list(SENSORS)): + vol.All(cv.ensure_list, [vol.In(SENSORS)]), +}) + + +def setup_platform(hass, config, add_devices, discovery_info=None): + """Set up a sensor for a raincloud device.""" + raincloud = hass.data[DATA_RAINCLOUD].data + + sensors = [] + for sensor_type in config.get(CONF_MONITORED_CONDITIONS): + if sensor_type == 'battery': + sensors.append( + RainCloudSensor(raincloud.controller.faucet, + sensor_type)) + else: + # create an sensor for each zone managed by a faucet + for zone in raincloud.controller.faucet.zones: + sensors.append(RainCloudSensor(zone, sensor_type)) + + add_devices(sensors, True) + return True + + +class RainCloudSensor(RainCloudEntity): + """A sensor implementation for raincloud device.""" + + @property + def state(self): + """Return the state of the sensor.""" + return self._state + + def update(self): + """Get the latest data and updates the states.""" + _LOGGER.debug("Updating RainCloud sensor: %s", self._name) + if self._sensor_type == 'battery': + self._state = self.data.battery.strip('%') + else: + self._state = getattr(self.data, self._sensor_type) + + @property + def icon(self): + """Icon to use in the frontend, if any.""" + if self._sensor_type == 'battery' and self._state is not None: + return icon_for_battery_level(battery_level=int(self._state), + charging=False) + return ICON_MAP.get(self._sensor_type) diff --git a/homeassistant/components/switch/raincloud.py b/homeassistant/components/switch/raincloud.py new file mode 100644 index 00000000000000..f373a6aad84639 --- /dev/null +++ b/homeassistant/components/switch/raincloud.py @@ -0,0 +1,94 @@ +""" +Support for Melnor RainCloud sprinkler water timer. + +For more details about this platform, please refer to the documentation at +https://home-assistant.io/components/switch.raincloud/ +""" +import logging + +import voluptuous as vol + +import homeassistant.helpers.config_validation as cv +from homeassistant.components.raincloud import ( + ALLOWED_WATERING_TIME, CONF_ATTRIBUTION, CONF_WATERING_TIME, + DATA_RAINCLOUD, DEFAULT_WATERING_TIME, RainCloudEntity, SWITCHES) +from homeassistant.components.switch import SwitchDevice, PLATFORM_SCHEMA +from homeassistant.const import ( + CONF_MONITORED_CONDITIONS, ATTR_ATTRIBUTION) + +DEPENDENCIES = ['raincloud'] + +_LOGGER = logging.getLogger(__name__) + +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ + vol.Optional(CONF_MONITORED_CONDITIONS, default=list(SWITCHES)): + vol.All(cv.ensure_list, [vol.In(SWITCHES)]), + vol.Optional(CONF_WATERING_TIME, default=DEFAULT_WATERING_TIME): + vol.All(vol.In(ALLOWED_WATERING_TIME)), +}) + + +def setup_platform(hass, config, add_devices, discovery_info=None): + """Set up a sensor for a raincloud device.""" + raincloud = hass.data[DATA_RAINCLOUD].data + default_watering_timer = config.get(CONF_WATERING_TIME) + + sensors = [] + for sensor_type in config.get(CONF_MONITORED_CONDITIONS): + # create an sensor for each zone managed by faucet + for zone in raincloud.controller.faucet.zones: + sensors.append( + RainCloudSwitch(default_watering_timer, + zone, + sensor_type)) + + add_devices(sensors, True) + return True + + +class RainCloudSwitch(RainCloudEntity, SwitchDevice): + """A switch implementation for raincloud device.""" + + def __init__(self, default_watering_timer, *args): + """Initialize a switch for raincloud device.""" + super().__init__(*args) + self._default_watering_timer = default_watering_timer + + @property + def is_on(self): + """Return true if device is on.""" + return self._state + + def turn_on(self): + """Turn the device on.""" + if self._sensor_type == 'manual_watering': + self.data.watering_time = self._default_watering_timer + elif self._sensor_type == 'auto_watering': + self.data.auto_watering = True + self._state = True + + def turn_off(self): + """Turn the device off.""" + if self._sensor_type == 'manual_watering': + self.data.watering_time = 'off' + elif self._sensor_type == 'auto_watering': + self.data.auto_watering = False + self._state = False + + def update(self): + """Update device state.""" + _LOGGER.debug("Updating RainCloud switch: %s", self._name) + if self._sensor_type == 'manual_watering': + self._state = bool(self.data.watering_time) + elif self._sensor_type == 'auto_watering': + self._state = self.data.auto_watering + + @property + def device_state_attributes(self): + """Return the state attributes.""" + return { + ATTR_ATTRIBUTION: CONF_ATTRIBUTION, + 'current_time': self.data.current_time, + 'default_manual_timer': self._default_watering_timer, + 'identifier': self.data.serial + } diff --git a/requirements_all.txt b/requirements_all.txt index ae91099165b09d..2289f8fcb1f012 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -841,6 +841,9 @@ rachiopy==0.1.2 # homeassistant.components.climate.radiotherm radiotherm==1.3 +# homeassistant.components.raincloud +raincloudy==0.0.1 + # homeassistant.components.raspihats # raspihats==2.2.1