diff --git a/rachiopy/__init__.py b/rachiopy/__init__.py index e5fc760..21d0910 100644 --- a/rachiopy/__init__.py +++ b/rachiopy/__init__.py @@ -7,11 +7,15 @@ from rachiopy.notification import Notification from rachiopy.schedulerule import Schedulerule from rachiopy.zone import Zone +from rachiopy.valve import Valve +from rachiopy.summary import SummaryServce +from rachiopy.program import Program class Rachio(RachioObject): """Object representing the Rachio API.""" + # pylint: disable=too-many-instance-attributes def __init__(self, authtoken: str): """Initialze the Rachio API wrapper. @@ -25,3 +29,6 @@ def __init__(self, authtoken: str): self.notification = Notification(authtoken) self.schedulerule = Schedulerule(authtoken) self.zone = Zone(authtoken) + self.valve = Valve(authtoken) + self.summary = SummaryServce(authtoken) + self.program = Program(authtoken) diff --git a/rachiopy/program.py b/rachiopy/program.py new file mode 100644 index 0000000..6505195 --- /dev/null +++ b/rachiopy/program.py @@ -0,0 +1,86 @@ +"""Program module for the smart hose timer.""" + +from rachiopy.rachioobject import RachioObject + + +class Program(RachioObject): + """Program class for the smart hose timer.""" + + def list_programs(self, valve_id: str): + """Retreive the list of programs (schedules) for a valve. + + For more info of the content in the response see: + https://rachio.readme.io/docs/programservice_listprograms + + :param valve_id: Valve's unique id + :type valve_id: str + + :return: The return value is a tuple of (response, content), the first + being and instance of the httplib2.Response class, the second + being a string that contains the response entity body. + :rtype: tuple + """ + path = f"program/listPrograms/{valve_id}" + return self.valve_get_request(path) + + def get_program(self, program_id: str): + """Retreive the information for a specific program. + + For more info of the content in the response see: + https://rachio.readme.io/docs/programservice_getprogram + + :param program_id: Program's unique id + :type program_id: str + + :return: The return value is a tuple of (response, content), the first + being and instance of the httplib2.Response class, the second + being a string that contains the response entity body (Python + object if it contains JSON). + :rtype: tuple + """ + path = f"program/getProgram/{program_id}" + return self.valve_get_request(path) + + def create_skip_overrides(self, program_id: str, timestamp: str): + """Create manual skips for the specific program run time. + You can retrieve the runtimes from SummaryService.getValveDayViews + + For more info of the content in the response see: + https://rachio.readme.io/docs/programservice_createskipoverrides + + :param program_id: Program's unique id + :type program_id: str + + :param timestamp: Timestamp of the run to skip + :type timestamp: timestamp + + :return: The return value is a tuple of (response, content), the first + being and instance of the httplib2.Response class, the second + being a string that contains the response entity body (Python + object if it contains JSON). + :rtype: tuple + """ + payload = {"programId": program_id, "timestamp": timestamp} + return self.valve_post_request("program/createSkipOverrides", payload) + + def delete_skip_overrides(self, program_id: str, timestamp: str): + """Cancel program skips for the specified program run time. + You can retrieve upcoming skips from SummaryService.getValveDayViews + + For more info of the content in the response see: + https://rachio.readme.io/docs/programservice_deleteskipoverrides + + :param program_id: Program's unique id + :type program_id: str + + :param timestamp: Timestamp of the run skip to delete + :type timestamp: timestamp + + :return: The return value is a tuple of (response, content), the first + being and instance of the httplib2.Response class, the second + being a string that contains the response entity body (Python + object if it contains JSON). + :rtype: tuple + """ + payload = {"programId": program_id, "timestamp": timestamp} + return self.valve_post_request("program/deleteSkipOverrides", payload) diff --git a/rachiopy/rachioobject.py b/rachiopy/rachioobject.py index f519908..4eec922 100644 --- a/rachiopy/rachioobject.py +++ b/rachiopy/rachioobject.py @@ -4,6 +4,7 @@ from requests import Session _API_URL = "https://api.rach.io/1/public" +_VALVE_URL = "https://cloud-rest.rach.io" class RachioObject: @@ -32,7 +33,7 @@ def __init__(self, authtoken: str, http_session=None, timeout=25): self.timeout = timeout def _request(self, path: str, method: str, body=None): - """Make a request from the API. + """Make a request to the API. :return: The return value is a tuple of (response, content), the first being and instance of the httplib2.Response class, the second @@ -100,3 +101,74 @@ def delete_request(self, path: str, body=None): :rtype: tuple """ return self._request(path, "DELETE", body) + + def _valve_request(self, path: str, method: str, body=None): + """Make a request to the API. + + :return: The return value is a tuple of (response, content), the first + being and instance of the httplib2.Response class, the second + being a string that contains the response entity body (Python + object if it contains JSON). + :rtype: tuple + """ + + if body is not None: + body = json.dumps(body) + + url = f"{_VALVE_URL}/{path}" + response = self._http_session.request( + method, url, headers=self._headers, data=body, timeout=self.timeout + ) + + content_type = response.headers.get("content-type") + headers = {k.lower(): v for k, v in response.headers.items()} + headers["status"] = response.status_code + + if content_type and content_type.startswith("application/json"): + return headers, response.json() + + return headers, response.text + + def valve_get_request(self, path: str, body=None): + """Make a GET request to the valve API. + + :return: The return value is a tuple of (response, content), the first + being and instance of the httplib2.Response class, the second + being a string that contains the response entity body (Python + object if it contains JSON). + :rtype: tuple + """ + return self._valve_request(path, "GET", body) + + def valve_put_request(self, path: str, body=None): + """Make a PUT request to the valve API. + + :return: The return value is a tuple of (response, content), the first + being and instance of the httplib2.Response class, the second + being a string that contains the response entity body (Python + object if it contains JSON). + :rtype: tuple + """ + return self._valve_request(path, "PUT", body) + + def valve_post_request(self, path: str, body=None): + """Make a POST request to the valve API. + + :return: The return value is a tuple of (response, content), the first + being and instance of the httplib2.Response class, the second + being a string that contains the response entity body (Python + object if it contains JSON). + :rtype: tuple + """ + return self._valve_request(path, "POST", body) + + def valve_delete_request(self, path: str, body=None): + """Make a DELETE request to the valve API. + + :return: The return value is a tuple of (response, content), the first + being and instance of the httplib2.Response class, the second + being a string that contains the response entity body (Python + object if it contains JSON). + :rtype: tuple + """ + return self._valve_request(path, "DELETE", body) diff --git a/rachiopy/summary.py b/rachiopy/summary.py new file mode 100644 index 0000000..66805a6 --- /dev/null +++ b/rachiopy/summary.py @@ -0,0 +1,34 @@ +"""Smart Hose Timer scheudle summary calls.""" + +from rachiopy.rachioobject import RachioObject + + +class SummaryServce(RachioObject): + """Scheudle summary class.""" + + def get_valve_day_views(self, base_id: str, start, end): + """List historical and upcoming valve runs and skips. + + For more info of the content in the response see: + https://rachio.readme.io/docs/summaryservice_getvalvedayviews + + :param base_id: Base's unique id + :type dev_id: str + + :param start: Start date + :type start: Object[] + + :param end: End date + :type end: Object[] + + :return: The return value is a tuple of (response, content), the first + being and instance of the httplib2.Response class, the second + being a string that contains the response entity body. + :rtype: tuple + """ + payload = { + "resourceId": {"baseStationId": base_id}, + "start": start, + "end": end, + } + return self.valve_post_request("summary/getValveDayViews", payload) diff --git a/rachiopy/valve.py b/rachiopy/valve.py new file mode 100644 index 0000000..383999c --- /dev/null +++ b/rachiopy/valve.py @@ -0,0 +1,140 @@ +"""Valve Service.""" + +from rachiopy.rachioobject import RachioObject + + +class Valve(RachioObject): + """Valve class for smart hose timer.""" + + def get_base_station(self, base_id: str): + """Retreive the information for a specific base station. + + For more info of the content in the response see: + https://rachio.readme.io/docs/valveservice_getbasestation + + :param base_id: Base station's unique id + :type user_id: str + + :return: The return value is a tuple of (response, content), the first + being and instance of the httplib2.Response class, the second + being a string that contains the response entity body (Python + object if it contains JSON). + :rtype: tuple + """ + path = f"valve/getBaseStation/{base_id}" + return self.valve_get_request(path) + + def get_valve(self, valve_id: str): + """Retrieve the information for a specific smart valve. + + For more info of the content in the response see: + https://rachio.readme.io/docs/valveservice_getvalve + + :param valve_id: Valve's unique id + :type user_id: str + + :return: The return value is a tuple of (response, content), the first + being and instance of the httplib2.Response class, the second + being a string that contains the response entity body (Python + object if it contains JSON). + :rtype: tuple + """ + path = f"valve/getValve/{valve_id}" + return self.valve_get_request(path) + + def list_base_stations(self, user_id: str): + """Retrieve all base stations for a given user ID. + + For more info of the content in the response see: + https://rachio.readme.io/docs/valveservice_listbasestations + + :param user_id: Person's unique id + :type user_id: str + + :return: The return value is a tuple of (response, content), the first + being and instance of the httplib2.Response class, the second + being a string that contains the response entity body (Python + object if it contains JSON). + :rtype: tuple + """ + path = f"valve/listBaseStations/{user_id}" + return self.valve_get_request(path) + + def list_valves(self, base_id: str): + """Retreive all valves on a given base station. + + For more info of the content in the response see: + https://rachio.readme.io/docs/valveservice_listvalves + + :param base_id: Base station's unique id + :type user_id: str + + :return: The return value is a tuple of (response, content), the first + being and instance of the httplib2.Response class, the second + being a string that contains the response entity body (Python + object if it contains JSON). + :rtype: tuple + """ + path = f"valve/listValves/{base_id}" + return self.valve_get_request(path) + + def set_default_runtime(self, valve_id: str, duration: int): + """Set the runtime for a valve when the button is pressed. + + For more info of the content in the response see: + https://rachio.readme.io/docs/valveservice_setdefaultruntime + + :param valve_id: Valve's unique id + :type user_id: str + + :param duration: Duration in seconds + :type duration: int + + :return: The return value is a tuple of (response, content), the first + being and instance of the httplib2.Response class, the second + being a string that contains the response entity body (Python + object if it contains JSON). + :rtype: tuple + """ + payload = {"valveId": valve_id, "defaultRuntimeSeconds": duration} + return self.valve_put_request("valve/setDefaultRuntime", payload) + + def start_watering(self, valve_id: str, duration: int): + """Start a valve. + + For more info of the content in the response see: + https://rachio.readme.io/docs/valveservice_startwatering + + :param valve_id: Valve's unique id + :type user_id: str + + :param duration: Duration in seconds + :type duration: int + + :return: The return value is a tuple of (response, content), the first + being and instance of the httplib2.Response class, the second + being a string that contains the response entity body (Python + object if it contains JSON). + :rtype: tuple + """ + assert 0 <= duration <= 86400, "duration must be in range 0-86400" + payload = {"valveId": valve_id, "durationSeconds": duration} + return self.valve_put_request("valve/startWatering", payload) + + def stop_watering(self, valve_id: str): + """Stop a valve. + + For more info of the content in the response see: + https://rachio.readme.io/docs/valveservice_stopwatering + + :param valve_id: Valve's unique id + :type user_id: str + + :return: The return value is a tuple of (response, content), the first + being and instance of the httplib2.Response class, the second + being a string that contains the response entity body (Python + object if it contains JSON). + :rtype: tuple + """ + payload = {"valveId": valve_id} + return self.valve_put_request("valve/stopWatering", payload) diff --git a/tests/constants.py b/tests/constants.py index eec3442..c7b2a9c 100644 --- a/tests/constants.py +++ b/tests/constants.py @@ -5,6 +5,7 @@ from requests import Response BASE_API_URL = "https://api.rach.io/1/public" +VALVE_API_URL = "https://cloud-rest.rach.io" AUTHTOKEN = "1c1d9f3d-39c9-42b1-abc0-066f5a05cdef" diff --git a/tests/test_program.py b/tests/test_program.py new file mode 100644 index 0000000..185e79d --- /dev/null +++ b/tests/test_program.py @@ -0,0 +1,100 @@ +"""Program object test module""" + +import unittest +import uuid +import json +from unittest.mock import patch + +from rachiopy import Program +from tests.constants import VALVE_API_URL, AUTHTOKEN, RESPONSE200 + + +class TestProgramMethods(unittest.TestCase): + """Class containing the Program object tests.""" + + def setUp(self): + self.program = Program(AUTHTOKEN) + + def test_init(self): + """Test if the constructor works as expected.""" + self.assertEqual(self.program.authtoken, AUTHTOKEN) + + @patch("requests.Session.request") + def test_list_programs(self, mock): + """Test if the list programs method works as expected.""" + mock.return_value = RESPONSE200 + + valveid = str(uuid.uuid4()) + + self.program.list_programs(valveid) + + args, kwargs = mock.call_args + + # Check that the mock function is called with the rights args. + self.assertEqual( + args[1], f"{VALVE_API_URL}/program/listPrograms/{valveid}" + ) + self.assertEqual(args[0], "GET") + self.assertEqual(kwargs["data"], None) + + @patch("requests.Session.request") + def test_get_program(self, mock): + """Test if the get program method works as expected.""" + mock.return_value = RESPONSE200 + + programid = str(uuid.uuid4()) + + self.program.get_program(programid) + + args, kwargs = mock.call_args + + # Check that the mock function is called with the rights args. + self.assertEqual( + args[1], f"{VALVE_API_URL}/program/getProgram/{programid}" + ) + self.assertEqual(args[0], "GET") + self.assertEqual(kwargs["data"], None) + + @patch("requests.Session.request") + def test_create_skip_overrides(self, mock): + """Test if the create skip overrides method works as expected.""" + mock.return_value = RESPONSE200 + + programid = str(uuid.uuid4()) + timestamp = 1414818000000 + + self.program.create_skip_overrides(programid, timestamp) + + args, kwargs = mock.call_args + + # Check that the mock function is called with the rights args. + self.assertEqual( + args[1], f"{VALVE_API_URL}/program/createSkipOverrides" + ) + self.assertEqual(args[0], "POST") + self.assertEqual( + kwargs["data"], + json.dumps({"programId": programid, "timestamp": timestamp}), + ) + + @patch("requests.Session.request") + def test_delete_skip_overrides(self, mock): + """Test if the delete skip overrides method works as expected.""" + mock.return_value = RESPONSE200 + + programid = str(uuid.uuid4()) + timestamp = 1414818000000 + + self.program.delete_skip_overrides(programid, timestamp) + + args, kwargs = mock.call_args + + # Check that the mock function is called with the rights args. + self.assertEqual( + args[1], f"{VALVE_API_URL}/program/deleteSkipOverrides" + ) + self.assertEqual(args[0], "POST") + self.assertEqual( + kwargs["data"], + json.dumps({"programId": programid, "timestamp": timestamp}), + ) diff --git a/tests/test_rachio.py b/tests/test_rachio.py index cb6fac8..7140614 100644 --- a/tests/test_rachio.py +++ b/tests/test_rachio.py @@ -19,3 +19,6 @@ def test_init(self): self.assertEqual(rachio.schedulerule.authtoken, AUTHTOKEN) self.assertEqual(rachio.flexschedulerule.authtoken, AUTHTOKEN) self.assertEqual(rachio.notification.authtoken, AUTHTOKEN) + self.assertEqual(rachio.valve.authtoken, AUTHTOKEN) + self.assertEqual(rachio.summary.authtoken, AUTHTOKEN) + self.assertEqual(rachio.program.authtoken, AUTHTOKEN) diff --git a/tests/test_summary.py b/tests/test_summary.py new file mode 100644 index 0000000..651a0e7 --- /dev/null +++ b/tests/test_summary.py @@ -0,0 +1,47 @@ +"""Summary object test module""" + +import unittest +import uuid +import json +from unittest.mock import patch + +from rachiopy import SummaryServce +from tests.constants import VALVE_API_URL, AUTHTOKEN, RESPONSE200 + + +class TestSummaryMethod(unittest.TestCase): + """Class containing the Summary object test.""" + + def setUp(self): + self.summary = SummaryServce(AUTHTOKEN) + + def test_init(self): + """Test if the constructor works as expected.""" + self.assertEqual(self.summary.authtoken, AUTHTOKEN) + + @patch("requests.Session.request") + def test_get_valve_day_views(self, mock): + """Test if the get day views method works as expected.""" + mock.return_value = RESPONSE200 + + deviceid = str(uuid.uuid4()) + start = {"year": 2023, "month": 1, "day": 1} + end = {"year": 2023, "month": 1, "day": 30} + + self.summary.get_valve_day_views(deviceid, start, end) + + args, kwargs = mock.call_args + + # Check that the mock function is called with the rights args. + self.assertEqual(args[1], f"{VALVE_API_URL}/summary/getValveDayViews") + self.assertEqual(args[0], "POST") + self.assertEqual( + kwargs["data"], + json.dumps( + { + "resourceId": {"baseStationId": deviceid}, + "start": start, + "end": end, + } + ), + ) diff --git a/tests/test_valve.py b/tests/test_valve.py new file mode 100644 index 0000000..d9ab8e7 --- /dev/null +++ b/tests/test_valve.py @@ -0,0 +1,181 @@ +"""Valve object test module""" + +import unittest +from unittest.mock import patch +import uuid +import json + +from random import randrange +from rachiopy import Valve +from tests.constants import VALVE_API_URL, AUTHTOKEN, RESPONSE200, RESPONSE204 + + +class TestValveMethods(unittest.TestCase): + """Class containing the Valve object test cases.""" + + def setUp(self): + self.valve = Valve(AUTHTOKEN) + + def test_init(self): + """Test if the constructor works as expected.""" + self.assertEqual(self.valve.authtoken, AUTHTOKEN) + + @patch("requests.Session.request") + def test_get_valve(self, mock): + """Test if the get_valve method works as expected.""" + mock.return_value = RESPONSE200 + + valveid = uuid.uuid4() + + self.valve.get_valve(valveid) + + args, kwargs = mock.call_args + + # Check that the mock function is called with the rights args. + self.assertEqual(args[1], f"{VALVE_API_URL}/valve/getValve/{valveid}") + self.assertEqual(args[0], "GET") + self.assertEqual(kwargs["data"], None) + + @patch("requests.Session.request") + def test_get_base_station(self, mock): + """Test if the get_base_station method works as expected.""" + mock.return_value = RESPONSE200 + + baseid = str(uuid.uuid4()) + + self.valve.get_base_station(baseid) + + args, kwargs = mock.call_args + + # Check that the mock function is called with the rights args. + self.assertEqual( + args[1], f"{VALVE_API_URL}/valve/getBaseStation/{baseid}" + ) + self.assertEqual(args[0], "GET") + self.assertEqual(kwargs["data"], None) + + @patch("requests.Session.request") + def test_list_base_stations(self, mock): + """Test if the list_base_stations method works as expected.""" + mock.return_value = RESPONSE200 + + userid = str(uuid.uuid4()) + + self.valve.list_base_stations(userid) + + args, kwargs = mock.call_args + + # Check that the mock function is called with the rights args. + self.assertEqual( + args[1], f"{VALVE_API_URL}/valve/listBaseStations/{userid}" + ) + self.assertEqual(args[0], "GET") + self.assertEqual(kwargs["data"], None) + + @patch("requests.Session.request") + def test_list_valves(self, mock): + """Test if the list_valves method works as expected.""" + mock.return_value = RESPONSE200 + + baseid = str(uuid.uuid4()) + + self.valve.list_valves(baseid) + + args, kwargs = mock.call_args + + # Check that the mock function is called with the rights args. + self.assertEqual(args[1], f"{VALVE_API_URL}/valve/listValves/{baseid}") + self.assertEqual(args[0], "GET") + self.assertEqual(kwargs["data"], None) + + @patch("requests.Session.request") + def test_set_default_runtime(self, mock): + """Test if the set_default_runtime method works as expected.""" + mock.return_value = RESPONSE200 + + valveid = str(uuid.uuid4()) + duration = randrange(86400) + + self.valve.set_default_runtime(valveid, duration) + + args, kwargs = mock.call_args + + # Check that the mock function is called with the rights args. + self.assertEqual(args[1], f"{VALVE_API_URL}/valve/setDefaultRuntime") + self.assertEqual(args[0], "PUT") + self.assertEqual( + kwargs["data"], + json.dumps( + {"valveId": valveid, "defaultRuntimeSeconds": duration} + ), + ) + + @patch("requests.Session.request") + def test_set_default_runtime_exception(self, mock): + """Test if the set_default_runtime method catches incorrect values.""" + mock.return_value = RESPONSE200 + + valveid = str(uuid.uuid4()) + duration1 = randrange(-50, -1) + duration2 = randrange(86401, 86500) + + # Check that values should be within range. + self.assertRaises( + AssertionError, self.valve.start_watering, valveid, duration1 + ) + self.assertRaises( + AssertionError, self.valve.start_watering, valveid, duration2 + ) + + @patch("requests.Session.request") + def test_start_watering(self, mock): + """Test if the start_watering method works as expected.""" + mock.return_value = RESPONSE204 + + valveid = str(uuid.uuid4()) + duration = randrange(86400) + + self.valve.start_watering(valveid, duration) + + args, kwargs = mock.call_args + + # Check that the mock function is called with the rights args. + self.assertEqual(args[1], f"{VALVE_API_URL}/valve/startWatering") + self.assertEqual(args[0], "PUT") + self.assertEqual( + kwargs["data"], + json.dumps({"valveId": valveid, "durationSeconds": duration}), + ) + + @patch("requests.Session.request") + def test_start_watering_exception(self, mock): + """Test if the start_watering method catches incorrect values.""" + mock.return_value = RESPONSE204 + + valveid = str(uuid.uuid4()) + duration1 = randrange(-50, -1) + duration2 = randrange(86401, 86500) + + # Check that values should be within range. + self.assertRaises( + AssertionError, self.valve.start_watering, valveid, duration1 + ) + self.assertRaises( + AssertionError, self.valve.start_watering, valveid, duration2 + ) + + @patch("requests.Session.request") + def test_stop_watering(self, mock): + """Test if the stop_watering method works as expected.""" + mock.return_value = RESPONSE204 + + valveid = str(uuid.uuid4()) + + self.valve.stop_watering(valveid) + + args, kwargs = mock.call_args + + # Check that the mock function is called with the rights args. + self.assertEqual(args[1], f"{VALVE_API_URL}/valve/stopWatering") + self.assertEqual(args[0], "PUT") + self.assertEqual(kwargs["data"], json.dumps({"valveId": valveid}))