Skip to content

Commit

Permalink
feat: add support for multiple valves and multiple controllers (#42)
Browse files Browse the repository at this point in the history
  • Loading branch information
vanstinator authored May 22, 2020
1 parent 36e0e1a commit 8e6cfbc
Show file tree
Hide file tree
Showing 15 changed files with 302 additions and 213 deletions.
3 changes: 2 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -101,4 +101,5 @@ ENV/
.mypy_cache/

.idea/
.vscode/
.vscode/
index.py
102 changes: 22 additions & 80 deletions raincloudy/controller.py
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -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)
Expand All @@ -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."""
Expand All @@ -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):
Expand All @@ -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):
Expand All @@ -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:
92 changes: 68 additions & 24 deletions raincloudy/core.py
Original file line number Diff line number Diff line change
Expand Up @@ -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


Expand Down Expand Up @@ -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 = {
Expand All @@ -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
Expand All @@ -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)
Expand All @@ -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:
Loading

0 comments on commit 8e6cfbc

Please sign in to comment.