Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Introducing support to Melnor RainCloud sprinkler systems #9287

Merged
merged 12 commits into from
Sep 29, 2017
3 changes: 3 additions & 0 deletions .coveragerc
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
102 changes: 102 additions & 0 deletions homeassistant/components/binary_sensor/raincloud.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
"""
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 CONF_ATTRIBUTION
from homeassistant.components.binary_sensor import (
BinarySensorDevice, PLATFORM_SCHEMA)
from homeassistant.const import (
CONF_MONITORED_CONDITIONS, ATTR_ATTRIBUTION)

DEPENDENCIES = ['raincloud']

_LOGGER = logging.getLogger(__name__)

SENSOR_TYPES = {
'is_watering': ['Watering', ''],
'status': ['Status', ''],
}

PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
vol.Optional(CONF_MONITORED_CONDITIONS, default=list(SENSOR_TYPES)):
vol.All(cv.ensure_list, [vol.In(SENSOR_TYPES)]),
})


def setup_platform(hass, config, add_devices, discovery_info=None):
"""Set up a sensor for a raincloud device."""
raincloud = hass.data.get('raincloud').data
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

use short form for this hass.data['raincloud'].data and make a DATA_RAINCLOUD on your component and use this constant that you need import.


sensors = []
for sensor_type in config.get(CONF_MONITORED_CONDITIONS):
if sensor_type == 'status':
sensors.append(
RainCloudBinarySensor(hass,
raincloud.controller,
sensor_type))
sensors.append(
RainCloudBinarySensor(hass,
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(hass, zone, sensor_type))

add_devices(sensors, True)
return True


class RainCloudBinarySensor(BinarySensorDevice):
"""A sensor implementation for raincloud device."""

def __init__(self, hass, data, sensor_type):
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Don't pass hass, see above.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I suggest to move the whole init method to the intermediate class, since most device init method have the same logic. Only the switch class is different.

"""Initialize a sensor for raincloud device."""
super().__init__()
self._sensor_type = sensor_type
self._data = data
self._name = "{0} {1}".format(
self._data.name, SENSOR_TYPES.get(self._sensor_type)[0])
self._state = None

@property
def name(self):
"""Return the name of the sensor."""
return self._name

@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 SENSOR_TYPES.get(self._sensor_type)[1]

@property
def device_state_attributes(self):
"""Return the state attributes."""
attrs = {}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Use like:

return {
   'XY': dksl,
}


attrs[ATTR_ATTRIBUTION] = CONF_ATTRIBUTION
attrs['current_time'] = self._data.current_time
attrs['identifier'] = self._data.serial
return attrs
95 changes: 95 additions & 0 deletions homeassistant/components/raincloud.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
"""
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 logging
from datetime import timedelta

import voluptuous as vol
import homeassistant.helpers.config_validation as cv

from homeassistant.const import CONF_USERNAME, CONF_PASSWORD
from homeassistant.helpers.entity import Entity

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

'homeassistant.helpers.entity.Entity' imported but unused

from homeassistant.helpers.event import track_time_interval

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'

DOMAIN = 'raincloud'
DEFAULT_ENTITY_NAMESPACE = 'raincloud'
DEFAULT_WATERING_TIME = 15

SCAN_INTERVAL_HUB = timedelta(seconds=20)

CONFIG_SCHEMA = vol.Schema({
DOMAIN: vol.Schema({
vol.Required(CONF_USERNAME): cv.string,
vol.Required(CONF_PASSWORD): cv.string,
vol.Optional(CONF_WATERING_TIME, default=DEFAULT_WATERING_TIME):
vol.All(vol.In(ALLOWED_WATERING_TIME)),
}),
}, 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)
default_watering_timer = conf.get(CONF_WATERING_TIME)

try:
from raincloudy.core import RainCloudy

raincloud = RainCloudy(username=username, password=password)
if not raincloud.is_connected:
return False
hass.data['raincloud'] = RainCloudHub(hass,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

use a constante for 'raincloud' like DATA_

raincloud,
default_watering_timer)
except (ConnectTimeout, HTTPError) as ex:
_LOGGER.error("Unable to connect to Rain Cloud service: %s", str(ex))
hass.components.persistent_notification.create(
'Error: {}<br />'
'You will need to restart hass after fixing.'
''.format(ex),
title=NOTIFICATION_TITLE,
notification_id=NOTIFICATION_ID)
return False
return True


class RainCloudHub(Entity):
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You shouldn't inherit from Entity if you won't inherit this class in the entities that you add to home assistant. Just make a data class that inherits from object.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good point

"""Base class for all Raincloud entities."""
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Stale docstring.


def __init__(self, hass, data, default_watering_timer):
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

remove hass from init

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I had to re-add this due to the dispatcher signal.

"""Initialize the entity."""
self.data = data
self.default_watering_timer = default_watering_timer

# Load data
track_time_interval(hass, self._update_hub, SCAN_INTERVAL_HUB)
self._update_hub(SCAN_INTERVAL_HUB)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

move this into async_added_to_hass


@property
def should_poll(self):
"""Return false. RainCloud Hub object updates variables."""
return False

def _update_hub(self, now):
"""Refresh data from for all child objects."""
_LOGGER.debug("Updating RainCloud Hub component.")
self.data.update()
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Isn't the point of the signal to update the entity state? The data has already been updated before the signal is dispatched at line 81 above.

108 changes: 108 additions & 0 deletions homeassistant/components/sensor/raincloud.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
"""
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 CONF_ATTRIBUTION
from homeassistant.components.sensor import PLATFORM_SCHEMA
from homeassistant.const import (
CONF_MONITORED_CONDITIONS, STATE_UNKNOWN, ATTR_ATTRIBUTION)
from homeassistant.helpers.entity import Entity

DEPENDENCIES = ['raincloud']

_LOGGER = logging.getLogger(__name__)

# Sensor types: label, desc, unit, icon
SENSOR_TYPES = {
'battery': ['Battery', '%', ''],
'next_cycle': ['Next Cycle', '', 'calendar-clock'],
'rain_delay': ['Rain Delay', 'days', 'weather-rainy'],
'watering_time': ['Remaining Watering Time', 'min', 'water-pump'],
}

PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
vol.Optional(CONF_MONITORED_CONDITIONS, default=list(SENSOR_TYPES)):
vol.All(cv.ensure_list, [vol.In(SENSOR_TYPES)]),
})


def setup_platform(hass, config, add_devices, discovery_info=None):
"""Set up a sensor for a raincloud device."""
raincloud = hass.data.get('raincloud').data
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

same


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(Entity):
"""A sensor implementation for raincloud device."""

def __init__(self, data, sensor_type):
"""Initialize a sensor for raincloud device."""
self._data = data
self._sensor_type = sensor_type
self._icon = 'mdi:{}'.format(SENSOR_TYPES.get(self._sensor_type)[2])
self._name = "{0} {1}".format(
self._data.name, SENSOR_TYPES.get(self._sensor_type)[0])
self._state = STATE_UNKNOWN

@property
def name(self):
"""Return the name of the sensor."""
return self._name

@property
def state(self):
"""Return the state of the sensor."""
_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)
return self._state

@property
def icon(self):
"""Icon to use in the frontend, if any."""
if self._sensor_type == 'battery' and self._state is not STATE_UNKNOWN:
rounded_level = round(int(self._state), -1)
if rounded_level < 10:
self._icon = 'mdi:battery-outline'
elif self._state == 100:
self._icon = 'mdi:battery'
else:
self._icon = 'mdi:battery-{}'.format(str(rounded_level))
return self._icon
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Use the battery helper from utils/icon.py


@property
def unit_of_measurement(self):
"""Return the units of measurement."""
return SENSOR_TYPES.get(self._sensor_type)[1]

@property
def device_state_attributes(self):
"""Return the state attributes."""
attrs = {}

attrs[ATTR_ATTRIBUTION] = CONF_ATTRIBUTION
attrs['identifier'] = self._data.serial
attrs['current_time'] = self._data.current_time
return attrs
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

same

Loading