diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 837dba4..f9749ee 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -8,7 +8,7 @@ repos: - id: mixed-line-ending args: ["--fix=lf"] - repo: https://github.com/astral-sh/ruff-pre-commit - rev: v0.0.280 + rev: v0.0.284 hooks: - id: ruff args: ["--fix"] @@ -18,12 +18,12 @@ repos: hooks: - id: black - - repo: https://github.com/PyCQA/flake8 - rev: 6.0.0 - hooks: - - id: flake8 + # - repo: https://github.com/PyCQA/flake8 + # rev: 6.1.0 + # hooks: + # - id: flake8 - - repo: https://github.com/PyCQA/isort - rev: 5.12.0 - hooks: - - id: isort + # - repo: https://github.com/PyCQA/isort + # rev: 5.12.0 + # hooks: + # - id: isort diff --git a/custom_components/eyeonwater/__init__.py b/custom_components/eyeonwater/__init__.py index f2c64f7..998f923 100644 --- a/custom_components/eyeonwater/__init__.py +++ b/custom_components/eyeonwater/__init__.py @@ -33,7 +33,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: try: await eye_on_water_data.client.authenticate() except EyeOnWaterAuthError: - _LOGGER.error("Username or password was not accepted") + _LOGGER.exception("Username or password was not accepted") return False except asyncio.TimeoutError as error: raise ConfigEntryNotReady from error @@ -41,15 +41,15 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: try: await eye_on_water_data.setup() except Exception as e: - _LOGGER.error(f"Fetching meters failed: {e}") - raise e - + _LOGGER.exception(f"Fetching meters failed: {e}") + raise + # Fetch actual meter_info for all meters try: await eye_on_water_data.read_meters() except Exception as e: _LOGGER.error(f"Reading meters failed: {e}") - raise e + raise # load old hostorical data _LOGGER.info("Start loading historical data") @@ -75,7 +75,10 @@ async def async_update_data(): update_method=async_update_data, update_interval=SCAN_INTERVAL, request_refresh_debouncer=debounce.Debouncer( - hass, _LOGGER, cooldown=DEBOUNCE_COOLDOWN, immediate=True + hass, + _LOGGER, + cooldown=DEBOUNCE_COOLDOWN, + immediate=True, ), ) @@ -85,7 +88,7 @@ async def async_update_data(): DATA_SMART_METER: eye_on_water_data, } - watch_task = asyncio.create_task(coordinator.async_refresh()) + asyncio.create_task(coordinator.async_refresh()) _LOGGER.debug("Start setup platforms") await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) diff --git a/custom_components/eyeonwater/binary_sensor.py b/custom_components/eyeonwater/binary_sensor.py index 3b76694..7c67510 100644 --- a/custom_components/eyeonwater/binary_sensor.py +++ b/custom_components/eyeonwater/binary_sensor.py @@ -51,6 +51,7 @@ ), ] + async def async_setup_entry(hass, config_entry, async_add_entities): """Set up the EyeOnWater sensors.""" coordinator = hass.data[DOMAIN][config_entry.entry_id][DATA_COORDINATOR] diff --git a/custom_components/eyeonwater/config_flow.py b/custom_components/eyeonwater/config_flow.py index 7d84bd9..0bdae84 100644 --- a/custom_components/eyeonwater/config_flow.py +++ b/custom_components/eyeonwater/config_flow.py @@ -1,12 +1,10 @@ """Config flow for EyeOnWater integration.""" import asyncio -import contextlib import logging from typing import Any -from aiohttp import ClientError import voluptuous as vol - +from aiohttp import ClientError from homeassistant import config_entries, core, exceptions from homeassistant.const import CONF_PASSWORD, CONF_USERNAME from homeassistant.helpers import aiohttp_client @@ -24,12 +22,13 @@ { vol.Required(CONF_USERNAME): str, vol.Required(CONF_PASSWORD): str, - } + }, ) def create_account_from_config( - hass: core.HomeAssistant, data: dict[str, Any] + hass: core.HomeAssistant, + data: dict[str, Any], ) -> Account: """Create account login from config.""" CountryCode = hass.config.country @@ -38,21 +37,21 @@ def create_account_from_config( elif CountryCode == "CA": eow_hostname = CONF_EOW_HOSTNAME_CA else: + msg = f"Unsupported country ({CountryCode}) setup in HomeAssistant." raise CannotConnect( - f"Unsupported country ({CountryCode}) setup in HomeAssistant." + msg, ) metric_measurement_system = hass.config.units is METRIC_SYSTEM username = data[CONF_USERNAME] password = data[CONF_PASSWORD] - account = Account( + return Account( eow_hostname=eow_hostname, username=username, password=password, metric_measurement_system=metric_measurement_system, ) - return account async def validate_input(hass: core.HomeAssistant, data): @@ -82,7 +81,6 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): async def async_step_user(self, user_input=None): """Handle the initial step.""" - errors = {} if user_input is not None: try: @@ -103,7 +101,9 @@ async def async_step_user(self, user_input=None): return self.async_create_entry(title=info["title"], data=user_input) return self.async_show_form( - step_id="user", data_schema=DATA_SCHEMA, errors=errors + step_id="user", + data_schema=DATA_SCHEMA, + errors=errors, ) diff --git a/custom_components/eyeonwater/coordinator.py b/custom_components/eyeonwater/coordinator.py index 71ceb07..27bad11 100644 --- a/custom_components/eyeonwater/coordinator.py +++ b/custom_components/eyeonwater/coordinator.py @@ -1,26 +1,22 @@ """EyeOnWater coordinator.""" -import logging import datetime -from typing import List +import logging +from homeassistant.components.recorder.models import StatisticData, StatisticMetaData +from homeassistant.components.recorder.statistics import async_import_statistics from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant from homeassistant.helpers import aiohttp_client from homeassistant.helpers.update_coordinator import UpdateFailed -from homeassistant.components.recorder.models import StatisticData, StatisticMetaData -from homeassistant.components.recorder.statistics import async_import_statistics from .const import WATER_METER_NAME - - -from .config_flow import create_account_from_config from .eow import ( Account, Client, - Meter, EyeOnWaterAPIError, EyeOnWaterAuthError, EyeOnWaterResponseIsEmpty, + Meter, ) _LOGGER = logging.getLogger(__name__) @@ -59,7 +55,6 @@ async def read_meters(self): async def import_historical_data(self, days_to_load: int = 2): """Import historical data for today and past N days.""" - for meter in self.meters: statistics = await self.get_historical_data(meter, days_to_load) @@ -78,12 +73,16 @@ async def import_historical_data(self, days_to_load: int = 2): async_import_statistics(self.hass, metadata, statistics) async def get_historical_data( - self, meter: Meter, days_to_load: int = 2 - ) -> List[StatisticData]: + self, + meter: Meter, + days_to_load: int = 2, + ) -> list[StatisticData]: """Retrieve historical data for today and past N days.""" - today = datetime.datetime.now().replace( - hour=0, minute=0, second=0, microsecond=0 + hour=0, + minute=0, + second=0, + microsecond=0, ) date_list = [today - datetime.timedelta(days=x) for x in range(0, days_to_load)] @@ -94,18 +93,20 @@ async def get_historical_data( units = meter.native_unit_of_measurement.upper() _LOGGER.info( - f"adding historical statistics for {meter.meter_id} on {date_list} with units {units}" + f"adding historical statistics for {meter.meter_id} on {date_list} with units {units}", ) statistics = [] for date in date_list: _LOGGER.debug( - f"requesting historical statistics for {meter.meter_id} on {date} with units {units}" + f"requesting historical statistics for {meter.meter_id} on {date} with units {units}", ) try: data = await meter.get_historical_data( - date=date, units=units, client=self.client + date=date, + units=units, + client=self.client, ) except EyeOnWaterResponseIsEmpty: # Suppress this exception. It's valid situation when data was not reported by EOW for the requested day @@ -120,7 +121,7 @@ async def get_historical_data( StatisticData( start=row["start"], sum=row["sum"], - ) + ), ) return statistics diff --git a/custom_components/eyeonwater/eow.py b/custom_components/eyeonwater/eow.py index e704698..2247ae5 100644 --- a/custom_components/eyeonwater/eow.py +++ b/custom_components/eyeonwater/eow.py @@ -2,16 +2,18 @@ from __future__ import annotations import datetime -from dateutil import parser -import pytz import json import logging -from typing import Any import urllib.parse +from typing import TYPE_CHECKING, Any -from aiohttp import ClientSession +import pytz +from dateutil import parser from tenacity import retry, retry_if_exception_type +if TYPE_CHECKING: + from aiohttp import ClientSession + AUTH_ENDPOINT = "account/signin" DASHBOARD_ENDPOINT = "/dashboard/" SEARCH_ENDPOINT = "/api/2/residential/new_search" @@ -97,7 +99,8 @@ async def read_meter(self, client: Client) -> dict[str, Any]: data = json.loads(data) meters = data["elastic_results"]["hits"]["hits"] if len(meters) > 1: - raise Exception("More than one meter reading found") + msg = "More than one meter reading found" + raise Exception(msg) self.meter_info = meters[0]["_source"] self.reading_data = self.meter_info["register_0"] @@ -111,7 +114,8 @@ def get_flags(self, flag) -> bool: """Define flags.""" flags = self.reading_data["flags"] if flag not in flags: - raise EyeOnWaterAPIError(f"Cannot find {flag} field") + msg = f"Cannot find {flag} field" + raise EyeOnWaterAPIError(msg) return flags[flag] @property @@ -119,20 +123,21 @@ def reading(self): """Returns the latest meter reading in gal.""" reading = self.reading_data["latest_read"] if READ_UNITS_FIELD not in reading: - raise EyeOnWaterAPIError("Cannot find read units in reading data") + msg = "Cannot find read units in reading data" + raise EyeOnWaterAPIError(msg) read_unit = reading[READ_UNITS_FIELD] read_unit_upper = read_unit.upper() amount = float(reading[READ_AMOUNT_FIELD]) - amount = self.convert(read_unit_upper, amount) - return amount + return self.convert(read_unit_upper, amount) def convert(self, read_unit_upper, amount): if self.metric_measurement_system: if read_unit_upper in MEASUREMENT_CUBICMETERS: pass else: + msg = f"Unsupported measurement unit: {read_unit_upper}" raise EyeOnWaterAPIError( - f"Unsupported measurement unit: {read_unit_upper}" + msg, ) else: if read_unit_upper == MEASUREMENT_KILOGALLONS: @@ -148,13 +153,14 @@ def convert(self, read_unit_upper, amount): elif read_unit_upper in MEASUREMENT_CF: amount = amount * 7.48052 else: + msg = f"Unsupported measurement unit: {read_unit_upper}" raise EyeOnWaterAPIError( - f"Unsupported measurement unit: {read_unit_upper}" + msg, ) return amount async def get_historical_data(self, date: datetime, units: str, client: Client): - """Retrieve the historical hourly water readings for a requested day""" + """Retrieve the historical hourly water readings for a requested day.""" query = { "params": { "source": "barnacle", @@ -172,17 +178,19 @@ async def get_historical_data(self, date: datetime, units: str, client: Client): "query": {"query": {"terms": {"meter.meter_uuid": [self.meter_uuid]}}}, } data = await client.request( - path=CONSUMPTION_ENDPOINT, method="post", json=query + path=CONSUMPTION_ENDPOINT, + method="post", + json=query, ) data = json.loads(data) key = f"{self.meter_uuid},0" if key not in data["timeseries"]: - raise EyeOnWaterResponseIsEmpty("Response is empty") + msg = "Response is empty" + raise EyeOnWaterResponseIsEmpty(msg) timezone = data["hit"]["meter.timezone"][0] timezone = pytz.timezone(timezone) - # tzinfos = {data["timezone"] : timezone } data = data["timeseries"][key]["series"] statistics = [] @@ -192,17 +200,18 @@ async def get_historical_data(self, date: datetime, units: str, client: Client): { "start": timezone.localize(parser.parse(d["date"])), "sum": self.convert(response_unit, d["bill_read"]), - } + }, ) for statistic in statistics: start = statistic["start"] if start.tzinfo is None or start.tzinfo.utcoffset(start) is None: - raise Exception("Naive timestamp") + msg = "Naive timestamp" + raise Exception(msg) if start.minute != 0 or start.second != 0 or start.microsecond != 0: - raise Exception("Invalid timestamp") + msg = "Invalid timestamp" + raise Exception(msg) - # statistics.sort(key=lambda d: d["start"]) return statistics @@ -234,8 +243,9 @@ async def fetch_meters(self, client: Client): meter_infos = client.extract_json(line, Meter.info_prefix) for meter_info in meter_infos: if METER_UUID_FIELD not in meter_info: + msg = f"Cannot find {METER_UUID_FIELD} field" raise EyeOnWaterAPIError( - f"Cannot find {METER_UUID_FIELD} field" + msg, ) meter_uuid = meter_info[METER_UUID_FIELD] @@ -284,11 +294,11 @@ async def request( f"{self.base_url}{path}", cookies=self.cookies, **kwargs, - # ssl=self.ssl_context, ) if resp.status == 403: _LOGGER.error("Reached ratelimit") - raise EyeOnWaterRateLimitError("Reached ratelimit") + msg = "Reached ratelimit" + raise EyeOnWaterRateLimitError(msg) elif resp.status == 401: _LOGGER.debug("Authentication token expired; requesting new token") self.authenticated = False @@ -302,8 +312,9 @@ async def request( if resp.status != 200: _LOGGER.error(f"Request failed: {resp.status} {data}") - raise EyeOnWaterException(f"Request failed: {resp.status} {data}") - + msg = f"Request failed: {resp.status} {data}" + raise EyeOnWaterException(msg) + return data async def authenticate(self): @@ -322,13 +333,16 @@ async def authenticate(self): if "dashboard" not in str(resp.url): _LOGGER.warning("METER NOT FOUND!") - raise EyeOnWaterAuthError("No meter found") + msg = "No meter found" + raise EyeOnWaterAuthError(msg) if resp.status == 400: - raise EyeOnWaterAuthError("Username or password was not accepted") + msg = "Username or password was not accepted" + raise EyeOnWaterAuthError(msg) if resp.status == 403: - raise EyeOnWaterRateLimitError("Reached ratelimit") + msg = "Reached ratelimit" + raise EyeOnWaterRateLimitError(msg) self.cookies = resp.cookies self._update_token_expiration() diff --git a/pyproject.toml b/pyproject.toml index b13d44a..fa2c235 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -17,5 +17,5 @@ forced_separate = [ combine_as_imports = true [flake8] -; ignore = E211, E999, F401, F821, W503 -max-doc-length = 72 +# ignore = E211, E999, F401, F821, W503 +#max-doc-length = 72