diff --git a/.gitignore b/.gitignore
index b250500..75a4557 100644
--- a/.gitignore
+++ b/.gitignore
@@ -101,4 +101,5 @@ ENV/
.mypy_cache/
.idea/
-.vscode/
\ No newline at end of file
+.vscode/
+index.py
\ No newline at end of file
diff --git a/raincloudy/controller.py b/raincloudy/controller.py
index 0f5f82f..29fb9a6 100644
--- a/raincloudy/controller.py
+++ b/raincloudy/controller.py
@@ -3,16 +3,14 @@
import raincloudy
from raincloudy.faucet import RainCloudyFaucet
-from raincloudy.const import (
- STATUS_ENDPOINT, HEADERS, HOME_ENDPOINT, SETUP_ENDPOINT)
-from raincloudy.helpers import (
- generate_soup_html, find_controller_or_faucet_name)
+from raincloudy.const import SETUP_ENDPOINT
+from raincloudy.helpers import find_controller_or_faucet_name
class RainCloudyController():
"""RainCloudy Controller object."""
- def __init__(self, parent, controller_id, faucets=None):
+ def __init__(self, parent, controller_id, index, faucets=None):
"""
Initialize RainCloudy Controller object.
@@ -25,14 +23,16 @@ def __init__(self, parent, controller_id, faucets=None):
:return: RainCloudyController object
:rtype: RainCloudyController object
"""
- self.attributes = None
+ self.attributes = {}
self._parent = parent
+ self.home = parent.html['home']
self._controller_id = controller_id
+ self.index = index
self._verify_parent()
# faucets associated with controller
- self.faucets = []
+ self._faucets = []
# load assigned faucets
self._assign_faucets(faucets)
@@ -46,13 +46,13 @@ def _verify_parent(self):
raise TypeError("Invalid parent object.")
def _assign_faucets(self, faucets):
- """Assign RainCloudyFaucet objects to self.faucets."""
+ """Assign RainCloudyFaucet objects to self._faucets."""
if not faucets:
raise TypeError("Controller does not have a faucet assigned.")
- for faucet_id in faucets:
- self.faucets.append(
- RainCloudyFaucet(self._parent, self, faucet_id))
+ for index, faucet_id in enumerate(faucets):
+ self._faucets.append(
+ RainCloudyFaucet(self._parent, self, faucet_id, index))
def __repr__(self):
"""Object representation."""
@@ -61,70 +61,13 @@ def __repr__(self):
except AttributeError:
return "<{0}: {1}>".format(self.__class__.__name__, self.id)
- def post(self, ddata, url=SETUP_ENDPOINT, referer=SETUP_ENDPOINT):
- """Method to update some attributes on namespace."""
- headers = HEADERS.copy()
- if referer is None:
- headers.pop('Referer')
- else:
- headers['Referer'] = referer
-
- # append csrftoken
- if 'csrfmiddlewaretoken' not in ddata.keys():
- ddata['csrfmiddlewaretoken'] = self._parent.csrftoken
-
- req = self._parent.client.post(url, headers=headers, data=ddata)
- if req.status_code == 200:
- self.update()
-
- def _get_cu_and_fu_status(self):
- """Submit GET request to update information."""
- # adjust headers
- headers = HEADERS.copy()
- headers['Accept'] = '*/*'
- headers['X-Requested-With'] = 'XMLHttpRequest'
- headers['X-CSRFToken'] = self._parent.csrftoken
-
- args = '?controller_serial=' + self.serial \
- + '&faucet_serial=' + self.faucet.serial
-
- req = self._parent.client.get(STATUS_ENDPOINT + args,
- headers=headers)
-
- # token probably expired, then try again
- if req.status_code == 403:
- self._parent.login()
- self.update()
- elif req.status_code == 200:
- self.attributes = req.json()
- else:
- req.raise_for_status()
-
- def _refresh_html_home(self):
- """
- Function to refresh the self._parent.html['home'] object
- which provides the status if zones are scheduled to
- start automatically (program_toggle).
- """
- req = self._parent.client.get(HOME_ENDPOINT)
- if req.status_code == 403:
- self._parent.login()
- self.update()
- elif req.status_code == 200:
- self._parent.html['home'] = generate_soup_html(req.text)
- else:
- req.raise_for_status()
-
def update(self):
"""
- Call 2 methods to update zone attributes and html['home'] object
+ Call 1 method to update zone attributes
"""
# update zone attributes
- self._get_cu_and_fu_status()
-
- # update self._parent.html['home'] for gathering
- # auto_watering status (program_toggle tag)
- self._refresh_html_home()
+ for faucet in self._faucets:
+ faucet.update()
@property
def serial(self):
@@ -142,16 +85,18 @@ def name(self):
"""Return controller name."""
return \
find_controller_or_faucet_name(self._parent.html['home'],
- 'controller')
+ 'controller',
+ self.index)
@name.setter
def name(self, value):
"""Set a new name to controller."""
data = {
+ 'select_controller': self.index,
'_set_controller_name': 'Set Name',
'controller_name': value,
}
- self.post(data, url=SETUP_ENDPOINT, referer=SETUP_ENDPOINT)
+ self._parent.post(data, url=SETUP_ENDPOINT, referer=SETUP_ENDPOINT)
@property
def status(self):
@@ -164,13 +109,10 @@ def current_time(self):
return self.attributes['current_time']
@property
- def faucet(self):
+ def faucets(self):
"""Show current linked faucet."""
- if hasattr(self, 'faucets'):
- if len(self.faucets) > 1:
- # in the future, we should support more faucets
- raise TypeError("Only one faucet per account.")
- return self.faucets[0]
- raise AttributeError("There is no faucet assigned.")
+ if hasattr(self, '_faucets'):
+ return self._faucets
+ raise AttributeError("There are no faucets assigned.")
# vim:sw=4:ts=4:et:
diff --git a/raincloudy/core.py b/raincloudy/core.py
index 44484f8..9d8879e 100644
--- a/raincloudy/core.py
+++ b/raincloudy/core.py
@@ -3,8 +3,10 @@
import requests
import urllib3
from raincloudy.const import (
- INITIAL_DATA, HEADERS, LOGIN_ENDPOINT, LOGOUT_ENDPOINT, SETUP_ENDPOINT)
-from raincloudy.helpers import generate_soup_html, serial_finder
+ INITIAL_DATA, HEADERS, LOGIN_ENDPOINT, LOGOUT_ENDPOINT, SETUP_ENDPOINT,
+ HOME_ENDPOINT)
+from raincloudy.helpers import generate_soup_html, faucet_serial_finder, \
+ controller_serial_finder
from raincloudy.controller import RainCloudyController
@@ -39,7 +41,7 @@ def __init__(self, username, password, http_proxy=None, https_proxy=None,
self._password = password
# initialize future attributes
- self.controllers = []
+ self._controllers = []
self.client = None
self.is_connected = False
self.html = {
@@ -60,13 +62,15 @@ def __init__(self, username, password, http_proxy=None, https_proxy=None,
def __repr__(self):
"""Object representation."""
- return "<{0}: {1}>".format(self.__class__.__name__,
- self.controller.serial)
+ for controller in self.controllers:
+ return "<{0}: {1}>".format(self.__class__.__name__,
+ controller.serial)
def login(self):
"""Call login."""
- self._authenticate()
+ return self._authenticate
+ @property
def _authenticate(self):
"""Authenticate."""
# to obtain csrftoken, remove Referer from headers
@@ -91,44 +95,84 @@ def _authenticate(self):
if req.status_code != 302:
req.raise_for_status()
+ home = self.client.get(url=HOME_ENDPOINT)
+
+ self.html['home'] = generate_soup_html(home.text)
+
setup = self.client.get(SETUP_ENDPOINT, headers=HEADERS)
# populate device list
self.html['setup'] = generate_soup_html(setup.text)
- # currently only one faucet is supported on the code
- # we have future plans to support it
- parsed_controller = serial_finder(self.html['setup'])
- self.controllers.append(
- RainCloudyController(
- self,
- parsed_controller['controller_serial'],
- parsed_controller['faucet_serial']
+ controller_serials = controller_serial_finder(self.html['setup'])
+
+ for index, controller_serial in enumerate(controller_serials):
+
+ # We need to do a form submit for other controllers to get
+ # faucet serials
+ if index > 0:
+ data = {
+ 'select_controller': index
+ }
+ self.html['setup'] = \
+ generate_soup_html(self.post(data,
+ url=SETUP_ENDPOINT,
+ referer=SETUP_ENDPOINT).text)
+
+ faucet_serials = faucet_serial_finder(self.html['setup'])
+ self._controllers.append(
+ RainCloudyController(
+ self,
+ controller_serial,
+ index,
+ faucet_serials
+ )
)
- )
self.is_connected = True
return True
@property
def csrftoken(self):
- '''Return current csrftoken from request session.'''
+ """Return current csrftoken from request session."""
if self.client:
return self.client.cookies.get('csrftoken')
return None
def update(self):
"""Update controller._attributes."""
- self.controller.update()
+ for controller in self._controllers:
+ controller.update()
@property
- def controller(self):
+ def controllers(self):
"""Show current linked controllers."""
- if hasattr(self, 'controllers'):
- if len(self.controllers) > 1:
- # in the future, we should support more controllers
- raise TypeError("Only one controller per account.")
- return self.controllers[0]
+ if hasattr(self, '_controllers'):
+ return self._controllers
raise AttributeError("There is no controller assigned.")
+ def update_home(self, data):
+ """Update home html"""
+ if not isinstance(data, str):
+ raise TypeError("Function requires string response")
+ self.html['home'] = generate_soup_html(data)
+
+ def post(self, ddata, url=SETUP_ENDPOINT, referer=SETUP_ENDPOINT):
+ """Method to update some attributes on namespace."""
+ headers = HEADERS.copy()
+ if referer is None:
+ headers.pop('Referer')
+ else:
+ headers['Referer'] = referer
+
+ # append csrftoken
+ if 'csrfmiddlewaretoken' not in ddata.keys():
+ ddata['csrfmiddlewaretoken'] = self.csrftoken
+
+ req = self.client.post(url, headers=headers, data=ddata)
+ if not req.status_code == 200:
+ return None
+
+ return req
+
def logout(self):
"""Logout."""
self.client.get(LOGOUT_ENDPOINT)
@@ -137,7 +181,7 @@ def logout(self):
def _cleanup(self):
"""Cleanup object when logging out."""
self.client = None
- self.controllers = []
+ self._controllers = []
self.is_connected = False
# vim:sw=4:ts=4:et:
diff --git a/raincloudy/faucet.py b/raincloudy/faucet.py
index c44f346..5914af3 100644
--- a/raincloudy/faucet.py
+++ b/raincloudy/faucet.py
@@ -2,15 +2,16 @@
"""RainCloud Faucet."""
from raincloudy.const import (
HOME_ENDPOINT, MANUAL_OP_DATA, MANUAL_WATERING_ALLOWED,
- MAX_RAIN_DELAY_DAYS, MAX_WATERING_MINUTES)
+ MAX_RAIN_DELAY_DAYS, MAX_WATERING_MINUTES, HEADERS, STATUS_ENDPOINT)
from raincloudy.helpers import (
- find_controller_or_faucet_name, find_zone_name)
+ find_controller_or_faucet_name, find_zone_name,
+ find_selected_controller_or_faucet_index)
class RainCloudyFaucetCore():
"""RainCloudyFaucetCore object."""
- def __init__(self, parent, controller, faucet_id):
+ def __init__(self, parent, controller, faucet_id, index):
"""
Initialize RainCloudy Controller object.
@@ -24,9 +25,11 @@ def __init__(self, parent, controller, faucet_id):
:rtype: RainCloudyFaucet object
"""
+ self.index = index
self._parent = parent
self._controller = controller
self._id = faucet_id
+ self._attributes = {}
# zones associated with faucet
self.zones = []
@@ -55,9 +58,9 @@ def __repr__(self):
return "<{0}: {1}>".format(self.__class__.__name__, self.id)
@property
- def _attributes(self):
- """Callback to self._controller attributes."""
- return self._controller.attributes
+ def attributes(self):
+ """Return faucet id."""
+ return self._attributes
@property
def serial(self):
@@ -80,18 +83,20 @@ def name(self):
"""Return faucet name."""
return \
find_controller_or_faucet_name(
- self._parent.html['home'],
- 'faucet')
+ self._controller.home,
+ 'faucet',
+ self.index
+ )
@name.setter
def name(self, value):
"""Set a new name to faucet."""
data = {
'_set_faucet_name': 'Set Name',
- 'select_faucet': 0,
+ 'select_faucet': self.index,
'faucet_name': value,
}
- self._controller.post(data)
+ self._parent.post(data)
@property
def status(self):
@@ -107,8 +112,28 @@ def battery(self):
return battery.strip('%')
def update(self):
- """Callback self._controller.update()."""
- self._controller.update()
+ """Submit GET request to update information."""
+ # adjust headers
+ headers = HEADERS.copy()
+ headers['Accept'] = '*/*'
+ headers['X-Requested-With'] = 'XMLHttpRequest'
+ headers['X-CSRFToken'] = self._parent.csrftoken
+
+ args = '?controller_serial=' + self._controller.serial \
+ + '&faucet_serial=' + self.id
+
+ req = self._parent.client.get(STATUS_ENDPOINT + args,
+ headers=headers)
+
+ # token probably expired, then try again
+ if req.status_code == 403:
+ self._parent.login()
+ self.update()
+ elif req.status_code == 200:
+ self._attributes = req.json()
+ self._controller.attributes = self._attributes
+ else:
+ req.raise_for_status()
def _find_zone_by_id(self, zone_id):
"""Return zone by id."""
@@ -182,16 +207,18 @@ def _set_zone_name(self, zoneid, name):
# zone starts with index 0
zoneid -= 1
data = {
+ 'select_controller': self._controller.index,
+ 'select_faucet': self._faucet.index,
'_set_zone_name': 'Set Name',
'select_zone': str(zoneid),
'zone_name': name,
}
- self._controller.post(data)
+ self._parent.post(data)
@property
def name(self):
"""Return zone name."""
- return find_zone_name(self._parent.html['home'], self.id)
+ return find_zone_name(self._controller.home, self.id)
@name.setter
def name(self, value):
@@ -298,6 +325,7 @@ def _set_auto_watering(self, zoneid, value):
ddata[attr] = 'on'
except KeyError:
pass
+
self.submit_action(ddata)
return True
@@ -318,7 +346,8 @@ def is_watering(self):
def lookup_attr(self, attr):
"""Returns rain_delay_mode attributes by zone index"""
- return self._attributes['rain_delay_mode'][int(self.id) - 1][attr]
+ return self._faucet.attributes['rain_delay_mode'][int(self.id) - 1][
+ attr]
def _to_dict(self):
"""Method to build zone dict."""
@@ -343,13 +372,17 @@ def report(self):
"""Return status from zone."""
return self._to_dict()
+ def update(self):
+ """Request faucet to update"""
+ return self._faucet.update()
+
def preupdate(self, force_refresh=True):
"""Return a dict with all current options prior submitting request."""
ddata = MANUAL_OP_DATA.copy()
# force update to make sure status is accurate
if force_refresh:
- self.update()
+ self._faucet.update()
# select current controller and faucet
ddata['select_controller'] = \
@@ -386,6 +419,30 @@ def preupdate(self, force_refresh=True):
def submit_action(self, ddata):
"""Post data."""
- self._controller.post(ddata,
+
+ controller_index = self._parent.controllers.index(self._controller)
+ faucet_index = self._controller.faucets.index(self._faucet)
+
+ current_controller_index = find_selected_controller_or_faucet_index(
+ self._parent.html['home'], 'controller')
+
+ current_faucet_index = find_selected_controller_or_faucet_index(
+ self._parent.html['home'], 'faucet')
+
+ # This is an artifact of how the web-page we're impersonating works.
+ # The form submit will only apply actions to _selected_ controllers
+ # and faucets. So if the active controller and/or faucet on the page
+ # isn't the faucet we're trying to submit an action for we need to
+ # send the response twice. The first time we send it will switch us
+ # to the action
+ if current_controller_index != controller_index or \
+ current_faucet_index != faucet_index:
+ self._parent.post(ddata,
url=HOME_ENDPOINT,
referer=HOME_ENDPOINT)
+
+ response = self._parent.post(ddata,
+ url=HOME_ENDPOINT,
+ referer=HOME_ENDPOINT)
+
+ self._parent.update_home(response.text)
diff --git a/raincloudy/helpers.py b/raincloudy/helpers.py
index 18b3346..729861c 100644
--- a/raincloudy/helpers.py
+++ b/raincloudy/helpers.py
@@ -7,24 +7,23 @@
def generate_soup_html(data):
"""Return an BeautifulSoup HTML parser document."""
try:
- return BeautifulSoup(data, 'html.parser')
+ return BeautifulSoup(data, 'html5lib')
except:
raise TypeError(
'Invalid data passed to BeautifulSoup')
-def serial_finder(data):
+def faucet_serial_finder(data):
"""
- Find controller serial and faucet_serial from the setup page.
+ Find faucet_serial from the setup page.
+
+ :param data: text to be parsed
+ :type data: BeautilSoup object
+ :return: an array of controller serials
+ :raises IndexError: if controller_serial was not found on the data
+ """
+
+ try:
+
+ controllers_element = data.find(id="id_select_controller2").find_all(
+ 'option')
+
+ controller_serials = []
+
+ for controller_element in controllers_element:
+ controller_serials.append(
+ controller_element.text.split('-')[1].strip())
+
+ return controller_serials
+
+ except (AttributeError, IndexError, ValueError):
+ raise RainCloudyException(
+ 'Could not find any valid controller serials')
+
+
+def find_controller_or_faucet_name(data, p_type, index=0):
"""
Find on the HTML document the controller name.
@@ -67,6 +88,7 @@ def find_controller_or_faucet_name(data, p_type):
name="select_controller" onchange="submit()" >