Skip to content

Commit

Permalink
Implement service
Browse files Browse the repository at this point in the history
  • Loading branch information
kdeyev committed Aug 28, 2023
1 parent 6f3679c commit 27cb710
Show file tree
Hide file tree
Showing 10 changed files with 149 additions and 63 deletions.
10 changes: 5 additions & 5 deletions .pre-commit-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -25,11 +25,11 @@ repos:
- id: isort
language_version: python

- repo: https://github.com/charliermarsh/ruff-pre-commit
rev: v0.0.285
hooks:
- id: ruff
args: ["--fix"]
# - repo: https://github.com/charliermarsh/ruff-pre-commit
# rev: v0.0.285
# hooks:
# - id: ruff
# args: ["--fix"]

- repo: https://github.com/pre-commit/mirrors-mypy
rev: v1.5.1
Expand Down
31 changes: 24 additions & 7 deletions custom_components/eyeonwater/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,9 @@
DATA_SMART_METER,
DEBOUNCE_COOLDOWN,
DOMAIN,
IMPORT_HISTORICAL_DATA_DAYS_DEFAULT,
IMPORT_HISTORICAL_DATA_DAYS_NAME,
IMPORT_HISTORICAL_DATA_SERVICE_NAME,
SCAN_INTERVAL,
)
from .coordinator import EyeOnWaterData
Expand All @@ -41,16 +44,16 @@ 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
except Exception:
_LOGGER.exception("Fetching meters failed")
raise

# Fetch actual meter_info for all meters
try:
await eye_on_water_data.read_meters(days_to_load=30)
except Exception as e:
_LOGGER.error(f"Reading meters failed: {e}")
raise e
except Exception:
_LOGGER.exception("Reading meters failed")
raise

for meter in eye_on_water_data.meters:
_LOGGER.debug(meter.meter_uuid, meter.meter_id, meter.meter_info)
Expand Down Expand Up @@ -80,11 +83,25 @@ 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)
_LOGGER.debug("End setup platforms")

async def async_service_handler(call):
days = call.data.get(
IMPORT_HISTORICAL_DATA_DAYS_NAME,
IMPORT_HISTORICAL_DATA_DAYS_DEFAULT,
)
await eye_on_water_data.import_historical_data(days)

hass.services.async_register(
DOMAIN,
IMPORT_HISTORICAL_DATA_SERVICE_NAME,
async_service_handler,
)

return True


Expand Down
2 changes: 2 additions & 0 deletions custom_components/eyeonwater/binary_sensor.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@

@dataclass
class Description:
"""Binary sensor description."""

key: str
device_class: BinarySensorDeviceClass
translation_key: str | None = None
Expand Down
2 changes: 0 additions & 2 deletions custom_components/eyeonwater/config_flow.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,6 @@

def get_hostname_for_country(hass) -> str:
"""Return EOW hostname based on HA country."""

CountryCode = hass.config.country
if CountryCode == "CA":
return CONF_EOW_HOSTNAME_CA
Expand All @@ -43,7 +42,6 @@ def create_account_from_config(
data: dict[str, Any],
) -> Account:
"""Create account login from config."""

eow_hostname = get_hostname_for_country(hass)

metric_measurement_system = hass.config.units is METRIC_SYSTEM
Expand Down
4 changes: 4 additions & 0 deletions custom_components/eyeonwater/const.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,3 +9,7 @@

DOMAIN = "eyeonwater"
WATER_METER_NAME = "Water Meter"

IMPORT_HISTORICAL_DATA_SERVICE_NAME = "import_historical_data"
IMPORT_HISTORICAL_DATA_DAYS_NAME = "days"
IMPORT_HISTORICAL_DATA_DAYS_DEFAULT = 365
21 changes: 20 additions & 1 deletion custom_components/eyeonwater/coordinator.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,18 @@
"""EyeOnWater coordinator."""
import logging

from pyonwater import Account, Client, EyeOnWaterAPIError, EyeOnWaterAuthError, Meter

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 pyonwater import Account, Client, EyeOnWaterAPIError, EyeOnWaterAuthError, Meter

from .sensor import (
async_import_statistics,
convert_statistic_data,
get_statistic_metadata,
)

_LOGGER = logging.getLogger(__name__)

Expand Down Expand Up @@ -40,3 +47,15 @@ async def read_meters(self, days_to_load=3):
except (EyeOnWaterAPIError, EyeOnWaterAuthError) as error:
raise UpdateFailed(error) from error
return self.meters

async def import_historical_data(self, days: int):
"""Import historical data."""
for meter in self.meters:
data = await meter.reader.read_historical_data(
client=self.client,
days_to_load=days,
)
_LOGGER.info("%i data points will be imported", len(data))
statistics = convert_statistic_data(data)
metadata = get_statistic_metadata(meter)
async_import_statistics(self.hass, metadata, statistics)
119 changes: 76 additions & 43 deletions custom_components/eyeonwater/sensor.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,19 +4,14 @@
from typing import Any

from pyonwater import DataPoint, Meter
import pytz

from homeassistant.components.recorder import get_instance
from homeassistant.components.recorder.models import StatisticData, StatisticMetaData
from homeassistant.components.recorder.statistics import (
async_import_statistics,
get_last_statistics,
)
from homeassistant.components.sensor import (
SensorDeviceClass,
SensorEntity,
SensorStateClass,
)
from homeassistant.components.sensor import SensorDeviceClass, SensorEntity
from homeassistant.core import callback
from homeassistant.helpers.entity import DeviceInfo
from homeassistant.helpers.update_coordinator import (
Expand All @@ -31,27 +26,61 @@
_LOGGER.addHandler(logging.StreamHandler())


def get_statistics_id(meter: Meter) -> str:
return f"sensor.water_meter_{meter.meter_id.lower()}"
def get_statistics_id(meter_id: str) -> str:
"""Generate statistics ID for meter."""
return f"sensor.water_meter_{meter_id.lower()}"


def get_statistic_metadata(meter: Meter) -> StatisticMetaData:
"""Build statistic metadata for a given meter."""
name = f"{WATER_METER_NAME} {meter.meter_id}"
statistic_id = get_statistics_id(meter.meter_id)

return StatisticMetaData(
has_mean=False,
has_sum=True,
name=name,
source="recorder",
statistic_id=statistic_id,
unit_of_measurement=meter.native_unit_of_measurement,
)


def convert_statistic_data(data: list[DataPoint]) -> list[StatisticData]:
"""Convert statistics data to HA StatisticData format."""
return [
StatisticData(
start=row.dt,
sum=row.reading,
state=row.reading,
)
for row in data
]


async def get_last_imported_time(hass, meter):
"""Return last imported data datetime."""
# https://github.com/home-assistant/core/blob/74e2d5c5c312cf3ba154b5206ceb19ba884c6fb4/homeassistant/components/tibber/sensor.py#L11

statistic_id = get_statistics_id(meter)
statistic_id = get_statistics_id(meter.meter_id)

last_stats = await get_instance(hass).async_add_executor_job(
get_last_statistics, hass, 1, statistic_id, True, set(["start", "sum"])
get_last_statistics,
hass,
1,
statistic_id,
True,
{"start", "sum"},
)
_LOGGER.debug(f"last_stats", last_stats)
_LOGGER.debug("last_stats %s", last_stats)

if last_stats:
date = last_stats[statistic_id][0]["start"]
_LOGGER.debug("date", date)
date = datetime.datetime.fromtimestamp(date)
_LOGGER.debug("date", date)
_LOGGER.debug("date %d", date)
date = datetime.datetime.fromtimestamp(date, tz=dtutil.DEFAULT_TIME_ZONE)
_LOGGER.debug("date %d", date)
date = dtutil.as_local(date)
_LOGGER.debug("date", date)
_LOGGER.debug("date %d", date)

return date
return None
Expand All @@ -67,7 +96,20 @@ async def async_setup_entry(hass, config_entry, async_add_entities):
last_imported_time = await get_last_imported_time(hass=hass, meter=meter)
sensors.append(EyeOnWaterSensor(meter, last_imported_time, coordinator))

async_add_entities(sensors, False)
async_add_entities(sensors, update_before_add=False)

# async def async_service_handler(service):

# key: value for key, value in service.data.items() if key != ATTR_ENTITY_ID
# if entity_ids:

# for entity in target:
# if not hasattr(entity, method_name):

# hass.services.async_register(
# DOMAIN,
# IMPORT_HISTORICAL_DATA_SERVICE_NAME,
# async_service_handler,


class EyeOnWaterSensor(CoordinatorEntity, SensorEntity):
Expand All @@ -78,7 +120,6 @@ class EyeOnWaterSensor(CoordinatorEntity, SensorEntity):
_attr_device_class = SensorDeviceClass.WATER

# We should not specify the state_class for workarounding the #30 issue
# _attr_state_class = SensorStateClass.TOTAL_INCREASING

def __init__(
self,
Expand Down Expand Up @@ -129,16 +170,19 @@ def _state_update(self):
self._last_historical_data = self.meter.last_historical_data.copy()
if self._last_imported_time and self._last_historical_data:
_LOGGER.info(
f"_last_imported_time {self._last_imported_time} - self._last_historical_data {self._last_historical_data[-1].dt}"
"_last_imported_time %d - self._last_historical_data %d",
self._last_imported_time,
self._last_historical_data[-1].dt,
)
self._last_historical_data = list(
filter(
lambda r: r.dt > self._last_imported_time,
self._last_historical_data,
)
),
)
_LOGGER.info(
f"{len(self._last_historical_data)} data points will be imported"
"%i data points will be imported",
len(self._last_historical_data),
)

if self._last_historical_data:
Expand All @@ -161,32 +205,21 @@ async def async_added_to_hass(self):

def import_historical_data(self):
"""Import historical data for today and past N days."""

if not self._last_historical_data:
_LOGGER.info("There is no new historical data")
# Nothing to import
return

_LOGGER.info(f"{len(self._last_historical_data)} data points will be imported")

statistics = [
StatisticData(
start=row.dt,
sum=row.reading,
state=row.reading,
)
for row in self._last_historical_data
]

name = f"{WATER_METER_NAME} {self.meter.meter_id}"
statistic_id = get_statistics_id(self.meter)

metadata = StatisticMetaData(
has_mean=False,
has_sum=True,
name=name,
source="recorder",
statistic_id=statistic_id,
unit_of_measurement=self.meter.native_unit_of_measurement,
)
_LOGGER.info("%i data points will be imported", len(self._last_historical_data))
statistics = convert_statistic_data(self._last_historical_data)
metadata = get_statistic_metadata(self.meter)

async_import_statistics(self.hass, metadata, statistics)

async def import_historical_data_handler(self, days: int):
"""Import historical data."""
data = await self.meter.reader.read_historical_data(days)
_LOGGER.info("%i data points will be imported", len(data))
statistics = convert_statistic_data(data)
metadata = get_statistic_metadata(self.meter)
async_import_statistics(self.hass, metadata, statistics)
13 changes: 13 additions & 0 deletions custom_components/eyeonwater/services.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import_historical_data:
description: Import historical data
fields:
# entity_id:
# description: Water meter
# example: "switch.backyard_zone"
days:
description: Number of deys of history to load
required: true
example: 365
selector:
number:
min: 0
9 changes: 5 additions & 4 deletions custom_components/eyeonwater/system_health.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,19 +7,20 @@

@callback
def async_register(
hass: HomeAssistant, register: system_health.SystemHealthRegistration
hass: HomeAssistant,
register: system_health.SystemHealthRegistration,
) -> None:
"""Register system health callbacks."""
register.async_register_info(system_health_info)


async def system_health_info(hass):
"""Get info for the info page."""

eow_hostname = get_hostname_for_country(hass)

return {
"api_endpoint_reachable": system_health.async_check_can_reach_url(
hass, f"https://{eow_hostname}"
)
hass,
f"https://{eow_hostname}",
),
}
1 change: 0 additions & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,6 @@ profile = "black"
force_sort_within_sections = true
known_first_party = [
"homeassistant",
"pyonwater"
]
combine_as_imports = true

Expand Down

0 comments on commit 27cb710

Please sign in to comment.