Skip to content
Open
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
29 changes: 26 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ Hydrawise official site: https://hydrawise.com<br/>
Source code documentation: https://hydrawiser.readthedocs.io<br/>
Python Package Index: https://pypi.org/project/Hydrawiser/

## Usage
## Usage API v1
```python
from hydrawiser.core import Hydrawiser

Expand Down Expand Up @@ -74,7 +74,30 @@ True
hw.time_remaining(3)
247
```
### Limitations

## Limitations
- Only one controller is supported
- No sensor data


## Hydrawise GraphQL API (v2)

```python

from hydrawiser.core_v2 import HydrawiserV2

hw = HydrawiserV2('<you email>', '<your hydrawise password>')

# Information about your account
hw.customer()

# List all controllers
hw.controllers()

# List all controllers and their sensors
hw.sensors()

# List all controllers and their zones
hw.zones()
```

* Only one controller is supported
2 changes: 1 addition & 1 deletion hydrawiser/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,6 @@
"""

__author__ = 'David Ryan'
__version__ = '0.2'
__version__ = '0.3.0'
__copyright__ = 'Copyright (c) 2020 David Ryan'
__license__ = 'MIT'
217 changes: 217 additions & 0 deletions hydrawiser/core_v2.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,217 @@
"""
A GraphQL (API v2) implementation
"""

from gql import Client, gql
from gql.transport.requests import RequestsHTTPTransport
import requests


class HydrawiserV2():
"""
:param email: E-mail used during Hydrawise account registration
:type email: string
:param password: Hydrawise account password
:type password: string
:param timeout: Requests timeout in seconds
:type timeout: int
:returns: Hydrawiser object.
:rtype: object
"""
CLIENT_ID = 'hydrawise_app'
CLIENT_SECRET = 'zn3CrjglwNV1'
TOKEN_ENDPOINT = 'https://app.hydrawise.com/api/v2/oauth/access-token'
API_ENDPOINT = 'https://app.hydrawise.com/api/v2/graph'

def __init__(self, email, password, timeout=10):
self.timeout = timeout
self._token = self.__fetch_token(email, password)
self._client = self.__client(self._token)

def __fetch_token(self, username, password):
"""
Retrieves OAuth2 token and refresh token

:param username: E-mail used during Hydrawise account registration
:type username: string
:param password: Hydrawise account password
:type password: string

:returns: JSON object containing access_token, refresh_token
:rtype: JSON
"""

payload = {
'grant_type': 'password',
'client_id': self.CLIENT_ID,
'client_secret': self.CLIENT_SECRET,
'username': username,
'password': password,
'scope': 'all',
}

resp = requests.post(
self.TOKEN_ENDPOINT,
headers={"Content-Type": "application/x-www-form-urlencoded"},
data=payload,
timeout=self.timeout
)

if resp.status_code != 200:
print(resp.json())

return resp.json()

def __client(self, token):
"""
Create gql client for communication with remote GraphQL API.

:returns: GraphQL Client object
:rtype: gql.Client
"""

headers = {
'Authorization': token['token_type'] + ' ' + token['access_token']
}

_transport = RequestsHTTPTransport(
url=self.API_ENDPOINT,
headers=headers,
use_json=True,
verify=True,
timeout=self.timeout,
retries=3,
)

return Client(
transport=_transport,
fetch_schema_from_transport=True,
)

def customer(self):
"""
Fetches basic information about current user including all registered
controllers.

:returns: Information about current user
:rtype: dict
"""

query = gql("""{
me {
id
customerId
name
email
customerId
validated
controllers {
id
name
deviceId
}
}
}
""")
result = self._client.execute(query)
return result['me']

def controllers(self):
"""
List all registered controllers, their IDs, version and status.

:returns: Controllers information
:rtype: array
"""

query = gql("""{
me {
controllers {
id
name
online
deviceId
wizardComplete
hardware {
serialNumber
version
status
installationDate
}
softwareVersion
boc
lastAction {
value
timestamp
}
}
}
}
""")
result = self._client.execute(query)
return result['me']['controllers']

def zones(self, controller_id=None):
"""
List all zones and its controllers, their id and names.

:returns: Available zones
:rtype: array
"""

query = gql("""{
me {
controllers {
id
name
zones {
id
name
}
}
}
}
""")
result = self._client.execute(query)
return result['me']['controllers']

def sensors(self, controller_id=None):
"""
List all sensors connected to a controller, their id and status
information.

:returns: Sensors list and state.
:rtype: array
"""

query = gql("""{
me {
controllers {
id
name
sensors {
id
name
model {
id
name
modeType
active
offLevel
offTimer
delay
divisor
flowRate
customerId
sensorType
category {
name
}
}
}
}
}
}
""")
result = self._client.execute(query)
return result['me']['controllers']
1 change: 1 addition & 0 deletions requirements.txt
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
requests>=2.18.4
gql[requests]>=3.0.0
4 changes: 2 additions & 2 deletions setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
setup(
name='Hydrawiser',
packages=['hydrawiser'],
version='0.2',
version='0.3.0',
description='A Python library to communicate with Hunter ' +
'Wi-Fi irrigation controllers ' +
'(https://www.hunter.com) that support the ' +
Expand All @@ -14,7 +14,7 @@
url='https://github.com/ptcryan/hydrawiser',
license='MIT',
include_package_data=True,
install_requires=['requests>=2.0'],
install_requires=['requests>=2.0', 'gql[requests]>=3.0.0', ],
platforms='any',
test_suite='tests',
keywords=[
Expand Down
1 change: 1 addition & 0 deletions tests/fixtures/oauth2_resp.json
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
{"token_type": "Bearer", "expires_in": 3600, "access_token": "eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiIsImp0aSI6ImEyNTYyZmIxMWI5YzljYjg2NzUwNGM5NGZkMmQ3NmVkOWM0NTE0YjhhZGI3MzQxZTdlZWJiY2Y2N2RlOTYyNjAyMWJlYWQxYjliMDgxNDkyIn0.eyJhdWQiOiJoeWRyYXdpc2VfYXBwIiwianRpIjoiYTI1NjJmYjExYjljOWNiODY3NTA0Yzk0ZmQyZDc2ZWQ5YzQ1MTRiOGFkYjczNDFlN2VlYmJjZjY3ZGU5NjI2MDIxYmVhZDFiOWIwODE0OTIiLCJpYXQiOjE2NDg3NDA3MjcsIm5iZiI6MTY0ODc0MDcyNywiZXhwIjoxNjQ4NzQ0MzI3LCJzdWIiOiIxMjM0NTYiLCJzY29wZXMiOlsiYWxsIl19.A0cjscbqE_QDaFtt_k9Db0EYeKFupZokcOz44xhS7gwGbKMgevUckgHukCUJ1wQxQpXrbFA8CH9Oj4TYPASheG-PiB9CEsguiqLdY9d8hz5c3ztU6Gi9dqOzrKSfuMewcko4eF-VHmrQPD24RAFBd_pseK7TsGC66WhhfhuizSeHnhTMOzAgbzYZzWD0C3RuDvKdZmCmgj9W6y2MCHxGm8_wDwYKVN_QM1IVlDOZkwWqXHGG3N9lPSXSZIBrry6VToO31DWEBUbiJWm4GTUuLFFkWfVAL8uoZ6PEVziBkaPdltB3Yo11iXZr8uNrThFcmzjbs751fofx9DLpdv05Cw", "refresh_token": "def50200109bb8172ece79bf2664ca3738f1e9a58694a9ffcc1d25af01bffabcfcb3b5c74b0a3affd1ebd29c19a489228b40865dea880ce409947124b9c403272cebc8907a4fc92c0896d992ed0c90b6ada003f89c0113431bf14b7df723230ede5009cd4df32bb386cacc7eb357e355374e247ac13ffc08cb40e7e876c92002e6f3ee807f1156d3c5ef148e6d3c2fee2bb812a1e0921bfb1fa491ebd5bc32c2e348708eb2a856aab4a72237b224885f0c58b4b1bc171fb51d4a41a01e30c1aaf365ec72403a8169f7844e64f99a70d88584e19550aefffbbc1d888f7192756fb132395ae0afccbf14ba8f2eff81298f69bfb5d465c2c58d48bf4e228994664dc9cc8f7b5f435cccf3da5f0985afb5a5253b00373388febfab24064d7ae265fc26bc7404ab0ecf8eec1f2445d9fae7f6b9a704aaf0e760ca764da54c97d0876085a2cded26834f825e20eb9c92a184a6e9b44b631e1c9af86d0db604e4cc39508db038c4a6a14741f1f571e3221a7e0487ebdc181bf76b", "expires_at": 1648744327}
39 changes: 39 additions & 0 deletions tests/test_core_v2.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
"""Define basic data for unittests."""
import unittest
import requests_mock

from tests.extras import load_fixture


class UnitTestBase(unittest.TestCase):
"""Top level test class"""

@requests_mock.Mocker()
def setUp(self, mock):
"""Initialize rdy object for unittests."""
from hydrawiser.core_v2 import HydrawiserV2

mock.post(
HydrawiserV2.TOKEN_ENDPOINT,
text=load_fixture('oauth2_resp.json')
)

# initialize self.rdy object
self.rdy = HydrawiserV2('test@example.com', 's3cr3tP1ssw0rd')

def cleanUp(self):
"""Cleanup any data created from the tests."""
self.rdy = None

def tearDown(self):
"""Stop everything initialized."""
self.cleanUp()


class TestHydrawiserV2(UnitTestBase):

def test_attributes(self):
""" Test if attributes exist. """

self.assertTrue(hasattr(self.rdy, 'customer'))
self.assertTrue(hasattr(self.rdy, 'controllers'))
3 changes: 2 additions & 1 deletion tox.ini
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
# and then run "tox" from this directory.

[tox]
envlist = py27, py38, lint
envlist = py38, lint
skip_missing_interpreters = True

[testenv]
Expand All @@ -14,6 +14,7 @@ commands =
py.test --basetemp={envtmpdir} --cov-report term-missing --cov --verbose
deps =
requests>=2.18.4
gql[requests]>=3.0.0
requests_mock
pytest
pytest-cov
Expand Down