-
-
Notifications
You must be signed in to change notification settings - Fork 37.6k
Component/Switch/Sensor for QuickSmart Etherrain/8 network irrigation #8900
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
Changes from all commits
d3006b6
c5b453c
c838277
866f046
5438059
60a5e36
78b8ebd
3839efc
ff7d133
79b692c
86494a1
16ffe8f
eb4bbe5
e26e496
783974a
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,172 @@ | ||
| """. | ||
|
|
||
| Support for Etherrain/8. | ||
|
|
||
| """ | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Don't use |
||
| ER = {} | ||
|
|
||
| conf = config[DOMAIN] | ||
| schema = 'http' | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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(): | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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) | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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) | ||
| 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") | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Must go through the configuration validation as optional. |
||
| duration = config.get("duration") | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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) | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Please remove the dot.