diff --git a/pyproject.toml b/pyproject.toml index b4f5406..1e41240 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -3,7 +3,7 @@ name = "py-watersmart" authors = [ {name = "J.C. Jones", email = "jc@insufficient.coffee"}, ] -version = "0.1.1" +version = "0.1.2" readme = "README.md" description = "Obtain water usage data from Watersmart.com" classifiers = [ @@ -14,7 +14,8 @@ classifiers = [ keywords = ["water meter"] dependencies = [ "aiohttp", - "aiohttp-client-cache[sqlite]" + "aiohttp-client-cache[sqlite]", + "async_timeout" ] requires-python = ">=3.11" diff --git a/src/client/client.py b/src/client/client.py index 1ef0577..5391034 100644 --- a/src/client/client.py +++ b/src/client/client.py @@ -3,7 +3,12 @@ import asyncio import argparse import logging -from watersmart import WatersmartClient +from watersmart import ( + WatersmartClient, + WatersmartClientAuthenticationError, + WatersmartClientCommunicationError, + WatersmartClientError, +) PARSER = argparse.ArgumentParser(description=__doc__) @@ -24,15 +29,22 @@ async def main(): log_format = "%(asctime)s - %(name)s - %(levelname)s - %(message)s" logging.basicConfig(level=args.log_level, format=log_format) wc = WatersmartClient(args.url, args.email, args.password) - data = await wc.usage() - for datapoint in sorted(data, key=lambda x: x["read_datetime"]): - parts = [ - f"{datapoint['local_datetime']}", - f"usage: {datapoint['gallons']:8}gal", - f"leak: {datapoint['leak_gallons']:8}gal", - f"flags: {datapoint['flags']}", - ] - print(" | ".join(parts)) + try: + data = await wc.usage() + for datapoint in sorted(data, key=lambda x: x["read_datetime"]): + parts = [ + f"{datapoint['local_datetime']}", + f"usage: {datapoint['gallons']:8}gal", + f"leak: {datapoint['leak_gallons']:8}gal", + f"flags: {datapoint['flags']}", + ] + print(" | ".join(parts)) + except WatersmartClientAuthenticationError: + logging.exception("Login failure") + except WatersmartClientCommunicationError: + logging.exception("Communications error") + except WatersmartClientError: + logging.exception("Unknown error") def start(): diff --git a/src/watersmart/__init__.py b/src/watersmart/__init__.py index d61f31f..d6b375d 100644 --- a/src/watersmart/__init__.py +++ b/src/watersmart/__init__.py @@ -1,12 +1,28 @@ """Main WaterSmart module""" +import aiohttp +import asyncio +import async_timeout import logging +import socket import time from aiohttp_client_cache import CachedSession, SQLiteBackend from importlib.metadata import version +class WatersmartClientError(Exception): + """Exception to indicate a general API error.""" + + +class WatersmartClientCommunicationError(WatersmartClientError): + """Exception to indicate a communication error.""" + + +class WatersmartClientAuthenticationError(WatersmartClientError): + """Exception to indicate an authentication error.""" + + class WatersmartClient: def __init__(self, url, email, password): self._url = url @@ -30,11 +46,14 @@ def __init__(self, url, email, password): async def _login(self): url = f"{self._url}/index.php/welcome/login?forceEmail=1" login = {"token": "", "email": self._email, "password": self._password} - await self._session.post(url, data=login) + result = await self._session.post(url, data=login) + logging.debug(result) async def _populate_data(self): url = f"{self._url}/index.php/rest/v1/Chart/RealTimeChart" chart_rsp = await self._session.get(url) + if chart_rsp.status != 200: + raise WatersmartClientAuthenticationError() data = await chart_rsp.json() self._data_series = data["data"]["series"] @@ -49,16 +68,31 @@ def _amend_with_local_ts(cls, datapoint): async def usage(self): if not self._data_series: - logging.debug("Loading watersmart data") - await self._login() - await self._populate_data() - await self._close() + try: + async with async_timeout.timeout(10): + logging.debug("Loading watersmart data from %s", self._url) + await self._login() + await self._populate_data() + except WatersmartClientAuthenticationError as e: + raise e + except asyncio.TimeoutError as exception: + raise WatersmartClientCommunicationError( + "Timeout error fetching information", + ) from exception + except (aiohttp.ClientError, socket.gaierror) as exception: + raise WatersmartClientCommunicationError( + "Error fetching information", + ) from exception + except Exception as exception: # pylint: disable=broad-except + raise WatersmartClientError( + "Something really wrong happened!" + ) from exception + finally: + await self._close() result = [] - for datapoint in self._data_series: result.append(WatersmartClient._amend_with_local_ts(datapoint)) - return result async def _close(self):