Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .coveragerc
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,9 @@ omit =
homeassistant/components/eight_sleep.py
homeassistant/components/*/eight_sleep.py

homeassistant/components/etherrain.py
homeassistant/components/*/etherrain.py

homeassistant/components/ecobee.py
homeassistant/components/*/ecobee.py

Expand Down
172 changes: 172 additions & 0 deletions homeassistant/components/etherrain.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,172 @@
""".
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Please remove the dot.


Support for Etherrain/8.

"""
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Please follow the file header style of the other files.

import logging

import requests
import voluptuous as vol

from homeassistant.const import (
CONF_HOST, CONF_PASSWORD, CONF_USERNAME)
import homeassistant.helpers.config_validation as cv


_LOGGER = logging.getLogger(__name__)

DEFAULT_SSL = False
DEFAULT_TIMEOUT = 10
DOMAIN = 'etherrain'
STATE = 1
WATER_ON = 2
WATER_OFF = 3

LOGIN_RETRIES = 2

ER = {}

CONFIG_SCHEMA = vol.Schema({
DOMAIN: vol.Schema({
vol.Required(CONF_HOST): cv.string,
vol.Required(CONF_USERNAME): cv.string,
vol.Required(CONF_PASSWORD): cv.string
})
}, extra=vol.ALLOW_EXTRA)


def setup(hass, config):
"""Set up the Etherrain component."""
global ER
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Don't use global, use hass.data[] directly.

ER = {}

conf = config[DOMAIN]
schema = 'http'
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Looks superfluous as there is only support for http.


server_origin = '{}://{}'.format(schema, conf[CONF_HOST])
username = conf.get(CONF_USERNAME, None)
password = conf.get(CONF_PASSWORD, None)

ER['server_origin'] = server_origin
ER['username'] = username
ER['password'] = password

hass.data[DOMAIN] = ER

return login()


# pylint: disable=no-member
def login():
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Move the communication with the EtherRain API to a Python module, publish it on PyPI, and make it a requirement.

"""Login to the EtherRain API."""
# _LOGGER.info("Attempting to login to EtherRain")

# ergetcfg.cgi?lu=admin\&lp=deadbeef
url = '{0}/ergetcfg.cgi?lu={1}&lp={2}'.format(
ER['server_origin'], ER['username'], ER['password'])
req = requests.get(url, timeout=DEFAULT_TIMEOUT)
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Add a timeout to all requests.


if not req.ok:
_LOGGER.error("Connection error logging into EtherRain")
return False

return True


# http://er_addr/result.cgi?xi=0:1:0:0:0:0:0:0:0
def _er_request(data=None):
"""Perform an EtherRain request."""
valve = 0
duration = 0
cmd = 0
if data is not 'None':
cmd = data["command"]
valve = data["valve"]
duration = data["duration"]
if cmd == STATE:
# _LOGGER.info("Get state".format(valve,duration))
url = '{0}/result.cgi?xs'.format(ER['server_origin'])
duration = 0
if cmd == WATER_OFF:
# _LOGGER.info("Water off".format(valve,duration))
url = '{0}/result.cgi?xr'.format(ER['server_origin'])
if cmd == WATER_ON:
# _LOGGER.info("Set {0} to {1} minutes".format(valve, duration))
valves = ["0", "0", "0", "0", "0", "0", "0", "0", "0"]
valves[valve] = duration
url = '{0}/result.cgi?xi=0:{1}:{2}:{3}:{4}:{5}:{6}:{7}:{8}'.format(
ER['server_origin'], valves[1], valves[2], valves[3], valves[4],
valves[5], valves[6], valves[7], valves[8])

# Always log in. The etherrain/8 will only take commands from the last IP
# that logged in to it. So if a different host on the network is also
# issuing commands to the ER/8, then our commands will fail. There is
# obviously a bit of a race condition here. This at least somewhat
# mitigates the problem.
login()
# _LOGGER.info("url is {0}".format(url))
req = requests.get(url)
# _LOGGER.info("Returned: {0}".format(req.status_code))

if not req.ok:
_LOGGER.error("Unable to get API response from EtherRain")

return req


# retrieve current status
# http://<er_addr>/result.cgi?xs
#
# <body>
# EtherRain Device Status <br>
# un:EtherRain 8
# ma: 01.00.44.03.0A.01 <br>
# ac: <br>
# os: RD <br>
# cs: OK <br>
# rz: UK <br>
# ri: 0 <br>
# rn: 0 <br>
# </body>
def get_state(valve):
"""Get the current state of a valve."""
data = {}
data["valve"] = valve
data["duration"] = 0
data["command"] = STATE
state = _er_request(data)
# _LOGGER.info("got {0} from etherrain".format(state.text))
status = {}
for b_line in state.iter_lines():
line = b_line.decode('utf8').strip()
# _LOGGER.info("iterating: {0}".format(line))
if ":" in line:
attr, value = line.split(":")
value = value.replace(" <br>", "").strip()
attr = attr.strip()
if attr in ['ac', 'os', 'cs', 'rz', 'ri', 'rn']:
status[attr] = value

# ri contains the last valve to run. os is the current state of that
# valve. (ready or busy)
# (XXX: The valve number returned is 0-7 but the watering command
# takes 1-8)
if 'os' in status and status['os'] == 'WT':
# _LOGGER.info("valve={0} and waiting".format(valve, status['ri']))
return 1
if 'ri' in status and int(status['ri']) == valve-1:
if 'os' in status and status['os'] == 'RD':
# _LOGGER.info("valve={0} and ready".format(valve, status['ri']))
return 0
if 'os' in status and status['os'] == 'BZ':
# _LOGGER.info("valve={0} and busy".format(valve, status['ri']))
return 1
else:
return 0


# pylint: disable=no-member
def change_state(valve_data):
"""Change the state of a valve."""
# _LOGGER.info("Change State: {0}".format(valve_data))
return _er_request(data=valve_data)
88 changes: 88 additions & 0 deletions homeassistant/components/switch/etherrain.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
""".

Support for Etherrain valves.

"""
import logging

import voluptuous as vol

from homeassistant.components.switch import (SwitchDevice, PLATFORM_SCHEMA)
import homeassistant.components.etherrain as er
import homeassistant.helpers.config_validation as cv


_LOGGER = logging.getLogger(__name__)

DEPENDENCIES = ['etherrain']

PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
vol.Required("valve_id"): cv.positive_int,
})


def setup_platform(hass, config, add_devices, discovery_info=None):
"""Set up the Etherrain irrigation platform."""
valve_id = config.get("valve_id")
valve_name = config.get("name")
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Must go through the configuration validation as optional.

duration = config.get("duration")
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Dito.


add_devices([ERValveSwitches(valve_id, valve_name, duration)])


class ERValveSwitches(SwitchDevice):
"""Representation of an Etherrain valve."""

def __init__(self, valve_id, valve_name, duration):
"""Initialize ERValveSwitches."""
self._valve_id = valve_id
self._valve_name = valve_name
if duration is not None:
self._duration = duration
else:
self._duration = 60
self._on_state = 1
self._off_state = 0
self._state = None

@property
def name(self):
"""Get valve name."""
return self._valve_name

def update(self):
"""Update valve state."""
state = er.get_state(self._valve_id)

self._state = state
# _LOGGER.info("update etherrain switch {0} - {1}".format(
# self._valve_id, self._state))

@property
def is_on(self):
"""Return valve state."""
# _LOGGER.info("is_on: etherrain switch {0} - {1}".format(
# self._valve_id, self._state))
return self._state

def turn_on(self):
"""Turn a valve on."""
valve = {}
valve["duration"] = self._duration
valve["valve"] = self._valve_id
valve["command"] = er.WATER_ON
# _LOGGER.info("turn on etherrain switch {0}".format(self._valve_id))
self._state = True
er.change_state(valve)

def turn_off(self):
"""Turn a valve off."""
# We should first check the state and if it's "BZ" and the valve_id
# matches, then turn it off. For now, just turn it off regardless.
valve = {}
valve["duration"] = 0
valve["valve"] = 0
valve["command"] = er.WATER_OFF
self._state = False
# _LOGGER.info("turn off etherrain switch {0}".format(self._valve_id))
er.change_state(valve)