Skip to content

Commit

Permalink
Merge remote-tracking branch 'origin/master' into feature/fix-statist…
Browse files Browse the repository at this point in the history
…ics-neg-value
  • Loading branch information
Konstantin Deev committed Aug 20, 2023
2 parents b7b90b6 + 68f75e1 commit 1a820bf
Show file tree
Hide file tree
Showing 14 changed files with 195 additions and 66 deletions.
14 changes: 14 additions & 0 deletions .github/workflows/hassfest.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
name: Validate with hassfest

on:
push:
pull_request:
schedule:
- cron: '0 0 * * *'

jobs:
validate:
runs-on: "ubuntu-latest"
steps:
- uses: "actions/checkout@v3"
- uses: "home-assistant/actions/hassfest@master"
15 changes: 15 additions & 0 deletions .github/workflows/pre-commit.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
name: pre-commit

on:
push:
pull_request:
schedule:
- cron: '0 0 * * *'

jobs:
pre-commit:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions/setup-python@v3
- uses: pre-commit/action@v3.0.0
24 changes: 24 additions & 0 deletions .pre-commit-config.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
repos:
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v4.4.0
hooks:
- id: check-added-large-files
- id: trailing-whitespace
- id: end-of-file-fixer
- id: mixed-line-ending
args: ["--fix=lf"]
# - repo: https://github.com/astral-sh/ruff-pre-commit
# rev: v0.0.284
# hooks:
# - id: ruff
# args: ["--fix"]

- repo: https://github.com/psf/black
rev: 23.7.0
hooks:
- id: black

- repo: https://github.com/PyCQA/isort
rev: 5.12.0
hooks:
- id: isort
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ Add `https://github.com/kdeyev/eyeonwater` as Repository and select the "Integra

2. Add EyeOnWater integration following [HACS instructions](https://github.com/hacs/integration)

Follow the configuration dialog:
Follow the configuration dialog:
- Choose EyeOnWater hostname (choose eyeonwater.com unless you are in Canada).
- Choose the measurement system you prefer to use. "Imperial" will create a water sensor counting gallons, "Metric" will create a water sensor counting cubic meters.
- Use your username and password, which you use to log in on eyeonwater.com
Expand Down
7 changes: 5 additions & 2 deletions custom_components/eyeonwater/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -66,7 +66,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,
),
)

Expand Down
1 change: 1 addition & 0 deletions custom_components/eyeonwater/binary_sensor.py
Original file line number Diff line number Diff line change
Expand Up @@ -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]
Expand Down
23 changes: 10 additions & 13 deletions custom_components/eyeonwater/config_flow.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
"""Config flow for EyeOnWater integration."""
import asyncio
import contextlib
import logging
from typing import Any

Expand All @@ -24,35 +23,32 @@
{
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
if CountryCode == "US":
eow_hostname = CONF_EOW_HOSTNAME_COM
elif CountryCode == "CA":
if CountryCode == "CA":
eow_hostname = CONF_EOW_HOSTNAME_CA
else:
raise CannotConnect(
f"Unsupported country ({CountryCode}) setup in HomeAssistant."
)
# There are some users from Europe that use .com domain
eow_hostname = CONF_EOW_HOSTNAME_COM

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):
Expand Down Expand Up @@ -82,7 +78,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:
Expand All @@ -103,7 +98,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,
)


Expand Down
12 changes: 5 additions & 7 deletions custom_components/eyeonwater/coordinator.py
Original file line number Diff line number Diff line change
@@ -1,26 +1,24 @@
"""EyeOnWater coordinator."""
import logging
import datetime
import logging
from typing import List

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 .const import WATER_METER_NAME
from .eow import (
Account,
Client,
Meter,
EyeOnWaterAPIError,
EyeOnWaterAuthError,
EyeOnWaterResponseIsEmpty,
Meter,
)

_LOGGER = logging.getLogger(__name__)
Expand Down
50 changes: 30 additions & 20 deletions custom_components/eyeonwater/eow.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,16 +2,18 @@
from __future__ import annotations

import datetime
from dateutil import parser
import pytz
import json
import logging
from typing import Any
from typing import TYPE_CHECKING, Any
import urllib.parse

from aiohttp import ClientSession
from dateutil import parser
import pytz
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"
Expand Down Expand Up @@ -99,17 +101,19 @@ async def read_meter(self, client: Client, days_to_load=3) -> 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"]

try:
self.last_historical_data = await self.get_historical_datas(days_to_load=days_to_load, client=client)
self.last_historical_data = await self.get_historical_datas(
days_to_load=days_to_load, client=client
)
except EyeOnWaterResponseIsEmpty:
self.last_historical_data = []


@property
def attributes(self):
"""Define attributes."""
Expand All @@ -119,7 +123,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
Expand All @@ -130,7 +135,8 @@ def reading(self):

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])
Expand Down Expand Up @@ -163,7 +169,7 @@ def convert(self, read_unit_upper, amount):
f"Unsupported measurement unit: {read_unit_upper}"
)
return amount

async def get_historical_datas(self, days_to_load: int, client: Client):
"""Retrieve historical data for today and past N days."""

Expand Down Expand Up @@ -198,7 +204,7 @@ async def get_historical_data(self, date: datetime, client: Client):
units = "CM"
else:
units = self.native_unit_of_measurement.upper()

query = {
"params": {
"source": "barnacle",
Expand Down Expand Up @@ -226,7 +232,6 @@ async def get_historical_data(self, date: datetime, client: Client):

timezone = data["hit"]["meter.timezone"][0]
timezone = pytz.timezone(timezone)
# tzinfos = {data["timezone"] : timezone }

data = data["timeseries"][key]["series"]
statistics = []
Expand All @@ -242,9 +247,11 @@ async def get_historical_data(self, date: datetime, client: Client):
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"])

Expand Down Expand Up @@ -279,8 +286,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]
Expand Down Expand Up @@ -329,7 +337,6 @@ async def request(
f"{self.base_url}{path}",
cookies=self.cookies,
**kwargs,
# ssl=self.ssl_context,
)
if resp.status == 403:
_LOGGER.error("Reached ratelimit")
Expand All @@ -348,7 +355,7 @@ async def request(
if resp.status != 200:
_LOGGER.error(f"Request failed: {resp.status} {data}")
raise EyeOnWaterException(f"Request failed: {resp.status} {data}")

return data

async def authenticate(self):
Expand All @@ -367,13 +374,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 = f"Username or password was not accepted by {self.base_url}"
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()
Expand Down
7 changes: 4 additions & 3 deletions custom_components/eyeonwater/manifest.json
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
{
"domain": "eyeonwater",
"name": "EyeOnWater",
"codeowners": [],
"config_flow": true,
"dependencies": ["recorder"],
"documentation": "https://github.com/kdeyev/eyeonwater",
"requirements": [],
"codeowners": [],
"iot_class": "cloud_polling",
"version": "1.0"
"requirements": [],
"version": "2.0.0"
}
Loading

0 comments on commit 1a820bf

Please sign in to comment.