pax_global_header00006660000000000000000000000064146107654430014524gustar00rootroot0000000000000052 comment=636992c158473c90821bd5b8b7ede556dbb8aae4 JeromeHXP-ondilo-8adc3a2/000077500000000000000000000000001461076544300152765ustar00rootroot00000000000000JeromeHXP-ondilo-8adc3a2/.github/000077500000000000000000000000001461076544300166365ustar00rootroot00000000000000JeromeHXP-ondilo-8adc3a2/.github/workflows/000077500000000000000000000000001461076544300206735ustar00rootroot00000000000000JeromeHXP-ondilo-8adc3a2/.github/workflows/publish-to-test-pypi.yml000066400000000000000000000015201461076544300254360ustar00rootroot00000000000000# This workflows will upload a Python Package using Twine when a release is created # For more information see: https://help.github.com/en/actions/language-and-framework-guides/using-python-with-github-actions#publishing-to-package-registries name: Upload Python Package on: release: types: [created] jobs: deploy: runs-on: ubuntu-latest steps: - uses: actions/checkout@v2 - name: Set up Python uses: actions/setup-python@v2 with: python-version: '3.8' - name: Install dependencies run: | python -m pip install --upgrade pip pip install setuptools wheel twine - name: Build and publish env: TWINE_USERNAME: '__token__' TWINE_PASSWORD: ${{ secrets.PYPI_API_TOKEN }} run: | python setup.py sdist bdist_wheel twine upload dist/*JeromeHXP-ondilo-8adc3a2/.gitignore000066400000000000000000000004321461076544300172650ustar00rootroot00000000000000/.venv /.vscode # ====== # # Python # # ====== # __pycache__/ .pytest_cache/ *.py[cod] *.egg-info/ .installed.cfg *.egg *.ipynb # C extensions *.so # Packages *.egg *.egg-info dist build eggs .eggs parts bin var sdist develop-eggs .installed.cfg lib lib64 pip-wheel-metadata httpJeromeHXP-ondilo-8adc3a2/LICENSE.txt000066400000000000000000000020441461076544300171210ustar00rootroot00000000000000Copyright (c) 2020 Jérôme Mainguet Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.JeromeHXP-ondilo-8adc3a2/README.md000066400000000000000000000041461461076544300165620ustar00rootroot00000000000000Ondilo ICO ========== [![PyPi](https://img.shields.io/pypi/v/ondilo.svg)](https://pypi.python.org/pypi/ondilo) [![PyPi](https://img.shields.io/pypi/l/ondilo.svg)](https://github.com/JeromeHXP/ondilo/blob/master/LICENSE.txt) A simple client used to access Ondilo ICO APIs. Implemented to be used in Home Assistant, but can be used anywhere else. Install ------- To install ondilo, run: pip install ondilo Example usage ------------- Ondilo is using the Authorization Code Grant flow, so each user must be individually authenticated. The ```client_id``` and ```client_secret``` are always the same, there is no need to create a specific app on Ondilo side. So they are hard coded. However, if needed, they can also be passed during initialization. A very basic implementation could look like: from ondilo import Ondilo client = Ondilo(redirect_uri="https://example.com/api") print('Please go here and authorize,', client.get_authurl()) redirect_response = input('Paste the full redirect URL here:') client.request_token(authorization_response=redirect_response) print("Found all those pools: ", client.get_pools()) If the Oauth2 flow is handled externally and a token is already available, one can also use the package this way: from ondilo import Ondilo client = Ondilo(token) print("Found all those pools: ", client.get_pools()) Available APIs -------------- More information about the returned objects can be found here: https://interop.ondilo.com/docs/api/customer/v1/ - ```get_pools```: Get list of available pools / spa - ```get_ICO_details```: Get details of a pool/spa - ```get_last_pool_measures```: Get the last measures from an ICO - ```get_pool_recommendations```: Get the list of recommendations from an ICO - ```validate_pool_recommendation```: Acknowledge a recommendation - ```get_user_units```: Get user units - ```get_user_info```: Get user infos - ```get_pool_config```: Get pool/spa ranges for temperature, pH, ORP, salt and TDS - ```get_pool_shares```: Get list of users with whom the pool/spa is shared - ```get_pool_histo```: Get measurements historical data JeromeHXP-ondilo-8adc3a2/example/000077500000000000000000000000001461076544300167315ustar00rootroot00000000000000JeromeHXP-ondilo-8adc3a2/example/example.py000066400000000000000000000006671461076544300207470ustar00rootroot00000000000000from ondilo import Ondilo client = Ondilo(redirect_uri="https://example.com/api") print('Please go here and authorize,', client.get_authurl()) redirect_response = input('Paste the full redirect URL here:') client.request_token(authorization_response=redirect_response) pools = client.get_pools() print("Found all those pools: ", pools) print(client.get_ICO_details(pools[0]['id'])) print(client.get_last_pool_measures(pools[0]['id'])) JeromeHXP-ondilo-8adc3a2/setup.py000066400000000000000000000014031461076544300170060ustar00rootroot00000000000000import setuptools with open("README.md", "r") as fh: long_description = fh.read() setuptools.setup( name="ondilo", version="0.5.0", author="Jérôme Mainguet", author_email="dartdoka@mainguet.fr", description="A client to access Ondilo ICO APIs", long_description=long_description, long_description_content_type="text/markdown", url="https://github.com/JeromeHXP/ondilo", packages=setuptools.find_packages('src'), package_dir={"": "src"}, license="MIT", classifiers=[ "Programming Language :: Python :: 3", "License :: OSI Approved :: MIT License", "Operating System :: OS Independent", ], python_requires='>=3.6', install_requires=["requests", "requests_oauthlib", "oauthlib"], ) JeromeHXP-ondilo-8adc3a2/src/000077500000000000000000000000001461076544300160655ustar00rootroot00000000000000JeromeHXP-ondilo-8adc3a2/src/ondilo/000077500000000000000000000000001461076544300173515ustar00rootroot00000000000000JeromeHXP-ondilo-8adc3a2/src/ondilo/__init__.py000066400000000000000000000000501461076544300214550ustar00rootroot00000000000000from .ondilo import Ondilo, OndiloError JeromeHXP-ondilo-8adc3a2/src/ondilo/ondilo.py000066400000000000000000000214541461076544300212150ustar00rootroot00000000000000from typing import Optional, Union, Callable, Dict from requests import Response from requests_oauthlib import OAuth2Session from oauthlib.oauth2 import TokenExpiredError API_HOST = "https://interop.ondilo.com" API_URL = API_HOST + "/api/customer/v1" ENDPOINT_TOKEN = "/oauth2/token" ENDPOINT_AUTHORIZE = "/oauth2/authorize" DEFAULT_CLIENT_ID = "customer_api" DEFAULT_CLIENT_SECRET = "" DEFAULT_SCOPE = "api" class OndiloError(Exception): """Ondilo API error. Provides status code and error message from the API call""" def __init__(self, status_code: int, message: str): self.status_code = status_code self.message = message def __str__(self): return f"{self.status_code}: {self.message}" class Ondilo: """Ondilo API client. Handles OAuth2 authorization and API requests.""" def __init__( self, token: Optional[Dict[str, str]] = None, client_id: str = DEFAULT_CLIENT_ID, client_secret: str = DEFAULT_CLIENT_SECRET, redirect_uri: str = None, token_updater: Optional[Callable[[str], None]] = None, ): self.host = API_HOST self.client_id = client_id self.client_secret = client_secret self.redirect_uri = redirect_uri self.token_updater = token_updater self.scope = DEFAULT_SCOPE extra = {"client_id": self.client_id, "client_secret": self.client_secret} self._oauth = OAuth2Session( auto_refresh_kwargs=extra, redirect_uri=redirect_uri, client_id=client_id, token=token, token_updater=token_updater, scope=self.scope, ) def refresh_tokens(self) -> Dict[str, Union[str, int]]: """Refresh and return new tokens.""" token = self._oauth.refresh_token(f"{self.host}{ENDPOINT_TOKEN}") if self.token_updater is not None: self.token_updater(token) return token def request_token( self, authorization_response: Optional[str] = None, code: Optional[str] = None, ) -> Dict[str, str]: """ Generic method for fetching an access token. :param authorization_response: Authorization response URL, the callback URL of the request back to you. :param code: Authorization code :return: A token dict """ return self._oauth.fetch_token( f"{self.host}{ENDPOINT_TOKEN}", authorization_response=authorization_response, code=code, include_client_id=True, ) def get_authurl(self): """Get the URL needed for the authorization code grant flow.""" authorization_url, _ = self._oauth.authorization_url( f"{self.host}{ENDPOINT_AUTHORIZE}" ) return authorization_url def request(self, method: str, path: str, **kwargs) -> Response: """Make a request. We don't use the built-in token refresh mechanism of OAuth2 session because we want to allow overriding the token refresh logic. """ url = f"{API_URL}{path}" try: return getattr(self._oauth, method)(url, **kwargs) except TokenExpiredError: self._oauth.token = self.refresh_tokens() return getattr(self._oauth, method)(url, **kwargs) def get_pools(self): """Get all pools/spas associated with the user.""" req = self.request("get", "/pools") if req.status_code != 200: raise OndiloError(req.status_code, req.text) return req.json() def get_ICO_details(self, pool_id: int) -> dict: """ Retrieves the details of an ICO device associated with a specific pool. Args: pool_id (int): The ID of the pool. Returns: dict: A dictionary containing the details of the ICO device. Raises: OndiloError: If the request to retrieve the device details fails. """ req = self.request("get", "/pools/" + str(pool_id) + "/device") if req.status_code != 200: raise OndiloError(req.status_code, req.text) return req.json() def get_last_pool_measures(self, pool_id: int) -> dict: """ Retrieves the last measures of a specific pool. Args: pool_id (int): The ID of the pool. Returns: dict: A dictionary containing the last measures for the pool. Raises: OndiloError: If the request to retrieve the last measures fails. """ qstr = "?types[]=temperature&types[]=ph&types[]=orp&types[]=salt&types[]=battery&types[]=tds&types[]=rssi" req = self.request("get", f"/pools/{str(pool_id)}/lastmeasures{qstr}") if req.status_code != 200: raise OndiloError(req.status_code, req.text) return req.json() def get_pool_recommendations(self, pool_id: int) -> dict: """ Retrieves the recommendations for a specific pool. Args: pool_id (int): The ID of the pool. Returns: dict: A dictionary containing the recommendations for the pool. Raises: OndiloError: If the request to retrieve the recommendations fails. """ req = self.request("get", "/pools/" + str(pool_id) + "/recommendations") if req.status_code != 200: raise OndiloError(req.status_code, req.text) return req.json() def validate_pool_recommendation(self, pool_id: int, recommendation_id: int) -> str: """ Validates a pool recommendation. Args: pool_id (int): The ID of the pool. recommendation_id (int): The ID of the recommendation. Returns: str: The JSON response from the API. Should be "Done". Raises: OndiloError: If the API request fails. """ req = self.request("put", "/pools/" + str(pool_id) + "/recommendations/" + str(recommendation_id)) if req.status_code != 200: raise OndiloError(req.status_code, req.text) return req.json() def get_user_units(self) -> dict: """ Retrieves the units of measurement for the user. Returns: A dictionary containing the user's units of measurement. Raises: OndiloError: If the request to retrieve the units fails. """ req = self.request("get", "/user/units") if req.status_code != 200: raise OndiloError(req.status_code, req.text) return req.json() def get_user_info(self) -> dict: """ Retrieves the user information from the Ondilo API. Returns: A dictionary containing the user information. Raises: OndiloError: If the request to retrieve the user info fails. """ req = self.request("get", "/user/info") if req.status_code != 200: raise OndiloError(req.status_code, req.text) return req.json() def get_pool_config(self, pool_id: int) -> dict: """ Retrieves the configuration of a pool. Args: pool_id (int): The ID of the pool. Returns: dict: The configuration of the pool. Raises: OndiloError: If the request to retrieve the configuration fails. """ req = self.request("get", "/pools/" + str(pool_id) + "/configuration") if req.status_code != 200: raise OndiloError(req.status_code, req.text) return req.json() def get_pool_shares(self, pool_id: int) -> dict: """ Retrieves the shares of a specific pool. Args: pool_id (int): The ID of the pool. Returns: dict: A dictionary containing the shares of the pool. Raises: OndiloError: If the request to retrieve the shares fails. """ req = self.request("get", "/pools/" + str(pool_id) + "/shares") if req.status_code != 200: raise OndiloError(req.status_code, req.text) return req.json() def get_pool_histo(self, pool_id: int, measure: str, period: str) -> dict: """ Retrieves the historical data for a specific pool. Args: pool_id (int): The ID of the pool. measure (str): The type of measure to retrieve. period (str): The time period for the historical data. Allowed values are "day", "week", "month". Returns: dict: The historical data for the pool. Raises: OndiloError: If the request to retrieve the historical data fails. """ req = self.request("get", "/pools/" + str(pool_id) + "/measures?type=" + measure + "&period=" + period) if req.status_code != 200: raise OndiloError(req.status_code, req.text) return req.json()