Skip to content

Commit

Permalink
Add a service to get hourly usage history
Browse files Browse the repository at this point in the history
  • Loading branch information
wbyoung committed Jul 11, 2024
1 parent 3302a3a commit 3e28826
Show file tree
Hide file tree
Showing 9 changed files with 1,758 additions and 12 deletions.
18 changes: 10 additions & 8 deletions custom_components/watersmart/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,26 +2,26 @@

from __future__ import annotations

from dataclasses import dataclass

from homeassistant.config_entries import ConfigEntry
from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_USERNAME, Platform
from homeassistant.core import HomeAssistant
from homeassistant.helpers.aiohttp_client import async_get_clientsession
from homeassistant.helpers.typing import ConfigType

from .client import WaterSmartClient
from .const import DOMAIN
from .coordinator import WaterSmartUpdateCoordinator
from .services import async_setup_services
from .types import WaterSmartConfigEntry, WaterSmartData

PLATFORMS: list[Platform] = [Platform.SENSOR]

type WaterSmartConfigEntry = ConfigEntry[WaterSmartData]

async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
"""Set up WaterSmart services."""

@dataclass
class WaterSmartData:
"""Runtime data definition."""
async_setup_services(hass)

coordinator: WaterSmartUpdateCoordinator
return True


async def async_setup_entry(hass: HomeAssistant, entry: WaterSmartConfigEntry) -> bool:
Expand All @@ -48,6 +48,8 @@ async def async_setup_entry(hass: HomeAssistant, entry: WaterSmartConfigEntry) -
coordinator=coordinator,
)

hass.data.setdefault(DOMAIN, {})[entry.entry_id] = entry.runtime_data

await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)

return True
Expand Down
8 changes: 4 additions & 4 deletions custom_components/watersmart/coordinator.py
Original file line number Diff line number Diff line change
Expand Up @@ -121,7 +121,7 @@ def _sensor_data_for_most_recent_hour(data: dict[str, Any]) -> dict[str, Any]:
"state": record["gallons"],
"attrs": {
"start": record_date.isoformat(),
"related": _records_for_attrs(records),
"related": _serialize_records(records),
},
}

Expand All @@ -136,7 +136,7 @@ def _sensor_data_for_most_recent_full_day(data: dict[str, Any]) -> dict[str, Any
return {
"state": gallons,
"attrs": {
"related": _records_for_attrs(records),
"related": _serialize_records(records),
},
}

Expand Down Expand Up @@ -166,8 +166,8 @@ def _records_from_first_full_day(data):
return list(reversed(full_day_records))


def _records_for_attrs(records: list[dict[str, Any]]) -> list[dict[str, Any]]:
"""Convert records for returning in attributes."""
def _serialize_records(records: list[dict[str, Any]]) -> list[dict[str, Any]]:
"""Convert records for returning in attributes & service calls."""

return [
{
Expand Down
140 changes: 140 additions & 0 deletions custom_components/watersmart/services.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,140 @@
"""Support for the WaterSmart integration."""

from datetime import date, datetime
from functools import partial
from typing import Final

import voluptuous as vol

from homeassistant.config_entries import ConfigEntry, ConfigEntryState
from homeassistant.core import (
HomeAssistant,
ServiceCall,
ServiceResponse,
SupportsResponse,
callback,
)
from homeassistant.exceptions import ServiceValidationError
from homeassistant.helpers import selector
from homeassistant.util import dt

from .const import DOMAIN
from .coordinator import (
WaterSmartUpdateCoordinator,
_from_timestamp,
_serialize_records,
)
from .types import WaterSmartData

ATTR_CONFIG_ENTRY: Final = "config_entry"
ATTR_FROM_CACHE: Final = "cached"
ATTR_START: Final = "start"
ATTR_END: Final = "end"
HOURLY_HISTORY_SERVICE_NAME: Final = "get_water_usage_hourly_history"

SERVICE_SCHEMA: Final = vol.Schema(
{
vol.Required(ATTR_CONFIG_ENTRY): selector.ConfigEntrySelector(
{
"integration": DOMAIN,
}
),
vol.Optional(ATTR_FROM_CACHE): bool,
vol.Optional(ATTR_START): vol.Any(str, int),
vol.Optional(ATTR_END): vol.Any(str, int),
}
)


def __get_date(date_input: str | int | None) -> date | datetime | None:
"""Get date."""

if not date_input:
return None

if isinstance(date_input, int) and (value := dt.utc_from_timestamp(date_input)):
return value

if isinstance(date_input, str) and (value := dt.parse_datetime(date_input)):
return value

raise ServiceValidationError(
translation_domain=DOMAIN,
translation_key="invalid_date",
translation_placeholders={
"date": date_input,
},
)


def __get_coordinator(
hass: HomeAssistant, call: ServiceCall
) -> WaterSmartUpdateCoordinator:
"""Get the coordinator from the entry."""

entry_id: str = call.data[ATTR_CONFIG_ENTRY]
entry: ConfigEntry | None = hass.config_entries.async_get_entry(entry_id)

if not entry:
raise ServiceValidationError(
translation_domain=DOMAIN,
translation_key="invalid_config_entry",
translation_placeholders={
"config_entry": entry_id,
},
)

if entry.state != ConfigEntryState.LOADED:
raise ServiceValidationError(
translation_domain=DOMAIN,
translation_key="unloaded_config_entry",
translation_placeholders={
"config_entry": entry.title,
},
)

runtime_data: WaterSmartData = hass.data[DOMAIN][entry_id]

return runtime_data.coordinator


async def __get_hourly_history(
call: ServiceCall,
*,
hass: HomeAssistant,
) -> ServiceResponse:
coordinator = __get_coordinator(hass, call)

start = __get_date(call.data.get(ATTR_START))
end = __get_date(call.data.get(ATTR_END))

if call.data.get(ATTR_FROM_CACHE) is False:
await coordinator.async_refresh()

records = []

for record in coordinator.data["hourly"]:
record_date = _from_timestamp(record["read_datetime"])

if start and dt.as_local(record_date) < dt.as_local(start):
continue

if end and dt.as_local(record_date) > dt.as_local(end):
continue

records.append(record)

return {"history": _serialize_records(records)}


@callback
def async_setup_services(hass: HomeAssistant) -> None:
"""Set up WaterSmart services."""

hass.services.async_register(
DOMAIN,
HOURLY_HISTORY_SERVICE_NAME,
partial(__get_hourly_history, hass=hass),
schema=SERVICE_SCHEMA,
supports_response=SupportsResponse.ONLY,
)
22 changes: 22 additions & 0 deletions custom_components/watersmart/services.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
get_water_usage_hourly_history:
fields:
config_entry:
required: true
selector:
config_entry:
integration: watersmart
cached:
required: false
default: false
selector:
boolean:
start:
required: false
example: "2024-01-01 00:00:00"
selector:
datetime:
end:
required: false
example: "2024-01-01 00:00:00"
selector:
datetime:
35 changes: 35 additions & 0 deletions custom_components/watersmart/strings.json
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,17 @@
"already_configured": "[%key:common::config_flow::abort::already_configured_device%]"
}
},
"exceptions": {
"invalid_date": {
"message": "Invalid date provided. Got {date}"
},
"invalid_config_entry": {
"message": "Invalid config entry provided. Got {config_entry}"
},
"unloaded_config_entry": {
"message": "Invalid config entry provided. {config_entry} is not loaded."
}
},
"entity": {
"sensor": {
"gallons_for_most_recent_hour": {
Expand All @@ -29,5 +40,29 @@
"name": "Most recent full day usage"
}
}
},
"services": {
"get_water_usage_hourly_history": {
"name": "Get hourly water usage history",
"description": "Request hourly water usage from WaterSmart.",
"fields": {
"config_entry": {
"name": "Config Entry",
"description": "The config entry to use for this service."
},
"cached": {
"name": "Cached Data",
"description": "Accept data from the integration cache instead of re-fetching."
},
"start": {
"name": "Start",
"description": "Specifies the date and time from which to retrieve usage."
},
"end": {
"name": "End",
"description": "Specifies the date and time until which to retrieve usage."
}
}
}
}
}
35 changes: 35 additions & 0 deletions custom_components/watersmart/translations/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -29,5 +29,40 @@
"name": "Most recent hour usage"
}
}
},
"exceptions": {
"invalid_config_entry": {
"message": "Invalid config entry provided. Got {config_entry}"
},
"invalid_date": {
"message": "Invalid date provided. Got {date}"
},
"unloaded_config_entry": {
"message": "Invalid config entry provided. {config_entry} is not loaded."
}
},
"services": {
"get_water_usage_hourly_history": {
"description": "Request hourly water usage from WaterSmart.",
"fields": {
"cached": {
"description": "Accept data from the integration cache instead of re-fetching.",
"name": "Cached Data"
},
"config_entry": {
"description": "The config entry to use for this service.",
"name": "Config Entry"
},
"end": {
"description": "Specifies the date and time until which to retrieve usage.",
"name": "End"
},
"start": {
"description": "Specifies the date and time from which to retrieve usage.",
"name": "Start"
}
},
"name": "Get hourly water usage history"
}
}
}
17 changes: 17 additions & 0 deletions custom_components/watersmart/types.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
"""WaterSmart dataclasses and typing."""

from __future__ import annotations

from dataclasses import dataclass
from homeassistant.config_entries import ConfigEntry

from .coordinator import WaterSmartUpdateCoordinator

type WaterSmartConfigEntry = ConfigEntry[WaterSmartData]


@dataclass
class WaterSmartData:
"""Runtime data definition."""

coordinator: WaterSmartUpdateCoordinator
Loading

0 comments on commit 3e28826

Please sign in to comment.