From 3e288260258098cd0f3d7b1490b7139e7c3b0ed2 Mon Sep 17 00:00:00 2001 From: Whitney Young Date: Thu, 11 Jul 2024 14:48:35 -0700 Subject: [PATCH] Add a service to get hourly usage history --- custom_components/watersmart/__init__.py | 18 +- custom_components/watersmart/coordinator.py | 8 +- custom_components/watersmart/services.py | 140 ++ custom_components/watersmart/services.yaml | 22 + custom_components/watersmart/strings.json | 35 + .../watersmart/translations/en.json | 35 + custom_components/watersmart/types.py | 17 + tests/snapshots/test_services.ambr | 1325 +++++++++++++++++ tests/test_services.py | 170 +++ 9 files changed, 1758 insertions(+), 12 deletions(-) create mode 100644 custom_components/watersmart/services.py create mode 100644 custom_components/watersmart/services.yaml create mode 100644 custom_components/watersmart/types.py create mode 100644 tests/snapshots/test_services.ambr create mode 100644 tests/test_services.py diff --git a/custom_components/watersmart/__init__.py b/custom_components/watersmart/__init__.py index 3186143..8b95502 100644 --- a/custom_components/watersmart/__init__.py +++ b/custom_components/watersmart/__init__.py @@ -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: @@ -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 diff --git a/custom_components/watersmart/coordinator.py b/custom_components/watersmart/coordinator.py index bc16e32..2fd31b3 100644 --- a/custom_components/watersmart/coordinator.py +++ b/custom_components/watersmart/coordinator.py @@ -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), }, } @@ -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), }, } @@ -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 [ { diff --git a/custom_components/watersmart/services.py b/custom_components/watersmart/services.py new file mode 100644 index 0000000..57cd788 --- /dev/null +++ b/custom_components/watersmart/services.py @@ -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, + ) diff --git a/custom_components/watersmart/services.yaml b/custom_components/watersmart/services.yaml new file mode 100644 index 0000000..e01a6a4 --- /dev/null +++ b/custom_components/watersmart/services.yaml @@ -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: diff --git a/custom_components/watersmart/strings.json b/custom_components/watersmart/strings.json index d525775..36c8b64 100644 --- a/custom_components/watersmart/strings.json +++ b/custom_components/watersmart/strings.json @@ -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": { @@ -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." + } + } + } } } diff --git a/custom_components/watersmart/translations/en.json b/custom_components/watersmart/translations/en.json index 1b6f37f..8ff1f63 100644 --- a/custom_components/watersmart/translations/en.json +++ b/custom_components/watersmart/translations/en.json @@ -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" + } } } \ No newline at end of file diff --git a/custom_components/watersmart/types.py b/custom_components/watersmart/types.py new file mode 100644 index 0000000..aac0d5c --- /dev/null +++ b/custom_components/watersmart/types.py @@ -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 diff --git a/tests/snapshots/test_services.ambr b/tests/snapshots/test_services.ambr new file mode 100644 index 0000000..9c68576 --- /dev/null +++ b/tests/snapshots/test_services.ambr @@ -0,0 +1,1325 @@ +# serializer version: 1 +# name: test_service[cached0-2-get_water_usage_hourly_history] + dict({ + 'history': list([ + dict({ + 'gallons': 0, + 'start': '2024-06-19T22:00:00-07:00', + }), + dict({ + 'gallons': 7.48, + 'start': '2024-06-19T21:00:00-07:00', + }), + dict({ + 'gallons': 0, + 'start': '2024-06-19T20:00:00-07:00', + }), + dict({ + 'gallons': 7.48, + 'start': '2024-06-19T19:00:00-07:00', + }), + ]), + }) +# --- +# name: test_service[cached0-get_water_usage_hourly_history] + dict({ + 'history': list([ + dict({ + 'gallons': 0, + 'start': '2024-06-19T22:00:00-07:00', + }), + dict({ + 'gallons': 7.48, + 'start': '2024-06-19T21:00:00-07:00', + }), + dict({ + 'gallons': 0, + 'start': '2024-06-19T20:00:00-07:00', + }), + dict({ + 'gallons': 7.48, + 'start': '2024-06-19T19:00:00-07:00', + }), + ]), + }) +# --- +# name: test_service[cached1-1-get_water_usage_hourly_history] + dict({ + 'history': list([ + dict({ + 'gallons': 0, + 'start': '2024-06-19T22:00:00-07:00', + }), + dict({ + 'gallons': 7.48, + 'start': '2024-06-19T21:00:00-07:00', + }), + dict({ + 'gallons': 0, + 'start': '2024-06-19T20:00:00-07:00', + }), + dict({ + 'gallons': 7.48, + 'start': '2024-06-19T19:00:00-07:00', + }), + ]), + }) +# --- +# name: test_service[cached1-get_water_usage_hourly_history] + dict({ + 'history': list([ + dict({ + 'gallons': 0, + 'start': '2024-06-19T22:00:00-07:00', + }), + dict({ + 'gallons': 7.48, + 'start': '2024-06-19T21:00:00-07:00', + }), + dict({ + 'gallons': 0, + 'start': '2024-06-19T20:00:00-07:00', + }), + dict({ + 'gallons': 7.48, + 'start': '2024-06-19T19:00:00-07:00', + }), + ]), + }) +# --- +# name: test_service[cached2-1-get_water_usage_hourly_history] + dict({ + 'history': list([ + dict({ + 'gallons': 0, + 'start': '2024-06-19T22:00:00-07:00', + }), + dict({ + 'gallons': 7.48, + 'start': '2024-06-19T21:00:00-07:00', + }), + dict({ + 'gallons': 0, + 'start': '2024-06-19T20:00:00-07:00', + }), + dict({ + 'gallons': 7.48, + 'start': '2024-06-19T19:00:00-07:00', + }), + ]), + }) +# --- +# name: test_service[cached2-get_water_usage_hourly_history] + dict({ + 'history': list([ + dict({ + 'gallons': 0, + 'start': '2024-06-19T22:00:00-07:00', + }), + dict({ + 'gallons': 7.48, + 'start': '2024-06-19T21:00:00-07:00', + }), + dict({ + 'gallons': 0, + 'start': '2024-06-19T20:00:00-07:00', + }), + dict({ + 'gallons': 7.48, + 'start': '2024-06-19T19:00:00-07:00', + }), + ]), + }) +# --- +# name: test_service[end0-start0-cached0-2-get_water_usage_hourly_history] + dict({ + 'history': list([ + dict({ + 'gallons': 0, + 'start': '2024-06-19T20:00:00-07:00', + }), + dict({ + 'gallons': 7.48, + 'start': '2024-06-19T21:00:00-07:00', + }), + ]), + }) +# --- +# name: test_service[end0-start0-cached1-1-get_water_usage_hourly_history] + dict({ + 'history': list([ + dict({ + 'gallons': 0, + 'start': '2024-06-19T20:00:00-07:00', + }), + dict({ + 'gallons': 7.48, + 'start': '2024-06-19T21:00:00-07:00', + }), + ]), + }) +# --- +# name: test_service[end0-start0-cached2-1-get_water_usage_hourly_history] + dict({ + 'history': list([ + dict({ + 'gallons': 0, + 'start': '2024-06-19T20:00:00-07:00', + }), + dict({ + 'gallons': 7.48, + 'start': '2024-06-19T21:00:00-07:00', + }), + ]), + }) +# --- +# name: test_service[end0-start1-cached0-2-get_water_usage_hourly_history] + dict({ + 'history': list([ + dict({ + 'gallons': 0, + 'start': '2024-06-19T20:00:00-07:00', + }), + dict({ + 'gallons': 7.48, + 'start': '2024-06-19T21:00:00-07:00', + }), + ]), + }) +# --- +# name: test_service[end0-start1-cached1-1-get_water_usage_hourly_history] + dict({ + 'history': list([ + dict({ + 'gallons': 0, + 'start': '2024-06-19T20:00:00-07:00', + }), + dict({ + 'gallons': 7.48, + 'start': '2024-06-19T21:00:00-07:00', + }), + ]), + }) +# --- +# name: test_service[end0-start1-cached2-1-get_water_usage_hourly_history] + dict({ + 'history': list([ + dict({ + 'gallons': 0, + 'start': '2024-06-19T20:00:00-07:00', + }), + dict({ + 'gallons': 7.48, + 'start': '2024-06-19T21:00:00-07:00', + }), + ]), + }) +# --- +# name: test_service[end0-start2-cached0-2-get_water_usage_hourly_history] + dict({ + 'history': list([ + dict({ + 'gallons': 0, + 'start': '2024-06-19T20:00:00-07:00', + }), + dict({ + 'gallons': 7.48, + 'start': '2024-06-19T21:00:00-07:00', + }), + ]), + }) +# --- +# name: test_service[end0-start2-cached1-1-get_water_usage_hourly_history] + dict({ + 'history': list([ + dict({ + 'gallons': 0, + 'start': '2024-06-19T20:00:00-07:00', + }), + dict({ + 'gallons': 7.48, + 'start': '2024-06-19T21:00:00-07:00', + }), + ]), + }) +# --- +# name: test_service[end0-start2-cached2-1-get_water_usage_hourly_history] + dict({ + 'history': list([ + dict({ + 'gallons': 0, + 'start': '2024-06-19T20:00:00-07:00', + }), + dict({ + 'gallons': 7.48, + 'start': '2024-06-19T21:00:00-07:00', + }), + ]), + }) +# --- +# name: test_service[end0-start3-cached0-2-get_water_usage_hourly_history] + dict({ + 'history': list([ + dict({ + 'gallons': 0, + 'start': '2024-06-19T20:00:00-07:00', + }), + dict({ + 'gallons': 7.48, + 'start': '2024-06-19T21:00:00-07:00', + }), + ]), + }) +# --- +# name: test_service[end0-start3-cached1-1-get_water_usage_hourly_history] + dict({ + 'history': list([ + dict({ + 'gallons': 0, + 'start': '2024-06-19T20:00:00-07:00', + }), + dict({ + 'gallons': 7.48, + 'start': '2024-06-19T21:00:00-07:00', + }), + ]), + }) +# --- +# name: test_service[end0-start3-cached2-1-get_water_usage_hourly_history] + dict({ + 'history': list([ + dict({ + 'gallons': 0, + 'start': '2024-06-19T20:00:00-07:00', + }), + dict({ + 'gallons': 7.48, + 'start': '2024-06-19T21:00:00-07:00', + }), + ]), + }) +# --- +# name: test_service[end0-start4-cached0-2-get_water_usage_hourly_history] + dict({ + 'history': list([ + dict({ + 'gallons': 7.48, + 'start': '2024-06-19T19:00:00-07:00', + }), + dict({ + 'gallons': 0, + 'start': '2024-06-19T20:00:00-07:00', + }), + dict({ + 'gallons': 7.48, + 'start': '2024-06-19T21:00:00-07:00', + }), + ]), + }) +# --- +# name: test_service[end0-start4-cached1-1-get_water_usage_hourly_history] + dict({ + 'history': list([ + dict({ + 'gallons': 7.48, + 'start': '2024-06-19T19:00:00-07:00', + }), + dict({ + 'gallons': 0, + 'start': '2024-06-19T20:00:00-07:00', + }), + dict({ + 'gallons': 7.48, + 'start': '2024-06-19T21:00:00-07:00', + }), + ]), + }) +# --- +# name: test_service[end0-start4-cached2-1-get_water_usage_hourly_history] + dict({ + 'history': list([ + dict({ + 'gallons': 7.48, + 'start': '2024-06-19T19:00:00-07:00', + }), + dict({ + 'gallons': 0, + 'start': '2024-06-19T20:00:00-07:00', + }), + dict({ + 'gallons': 7.48, + 'start': '2024-06-19T21:00:00-07:00', + }), + ]), + }) +# --- +# name: test_service[end1-start0-cached0-2-get_water_usage_hourly_history] + dict({ + 'history': list([ + dict({ + 'gallons': 0, + 'start': '2024-06-19T20:00:00-07:00', + }), + dict({ + 'gallons': 7.48, + 'start': '2024-06-19T21:00:00-07:00', + }), + ]), + }) +# --- +# name: test_service[end1-start0-cached1-1-get_water_usage_hourly_history] + dict({ + 'history': list([ + dict({ + 'gallons': 0, + 'start': '2024-06-19T20:00:00-07:00', + }), + dict({ + 'gallons': 7.48, + 'start': '2024-06-19T21:00:00-07:00', + }), + ]), + }) +# --- +# name: test_service[end1-start0-cached2-1-get_water_usage_hourly_history] + dict({ + 'history': list([ + dict({ + 'gallons': 0, + 'start': '2024-06-19T20:00:00-07:00', + }), + dict({ + 'gallons': 7.48, + 'start': '2024-06-19T21:00:00-07:00', + }), + ]), + }) +# --- +# name: test_service[end1-start1-cached0-2-get_water_usage_hourly_history] + dict({ + 'history': list([ + dict({ + 'gallons': 0, + 'start': '2024-06-19T20:00:00-07:00', + }), + dict({ + 'gallons': 7.48, + 'start': '2024-06-19T21:00:00-07:00', + }), + ]), + }) +# --- +# name: test_service[end1-start1-cached1-1-get_water_usage_hourly_history] + dict({ + 'history': list([ + dict({ + 'gallons': 0, + 'start': '2024-06-19T20:00:00-07:00', + }), + dict({ + 'gallons': 7.48, + 'start': '2024-06-19T21:00:00-07:00', + }), + ]), + }) +# --- +# name: test_service[end1-start1-cached2-1-get_water_usage_hourly_history] + dict({ + 'history': list([ + dict({ + 'gallons': 0, + 'start': '2024-06-19T20:00:00-07:00', + }), + dict({ + 'gallons': 7.48, + 'start': '2024-06-19T21:00:00-07:00', + }), + ]), + }) +# --- +# name: test_service[end1-start2-cached0-2-get_water_usage_hourly_history] + dict({ + 'history': list([ + dict({ + 'gallons': 0, + 'start': '2024-06-19T20:00:00-07:00', + }), + dict({ + 'gallons': 7.48, + 'start': '2024-06-19T21:00:00-07:00', + }), + ]), + }) +# --- +# name: test_service[end1-start2-cached1-1-get_water_usage_hourly_history] + dict({ + 'history': list([ + dict({ + 'gallons': 0, + 'start': '2024-06-19T20:00:00-07:00', + }), + dict({ + 'gallons': 7.48, + 'start': '2024-06-19T21:00:00-07:00', + }), + ]), + }) +# --- +# name: test_service[end1-start2-cached2-1-get_water_usage_hourly_history] + dict({ + 'history': list([ + dict({ + 'gallons': 0, + 'start': '2024-06-19T20:00:00-07:00', + }), + dict({ + 'gallons': 7.48, + 'start': '2024-06-19T21:00:00-07:00', + }), + ]), + }) +# --- +# name: test_service[end1-start3-cached0-2-get_water_usage_hourly_history] + dict({ + 'history': list([ + dict({ + 'gallons': 0, + 'start': '2024-06-19T20:00:00-07:00', + }), + dict({ + 'gallons': 7.48, + 'start': '2024-06-19T21:00:00-07:00', + }), + ]), + }) +# --- +# name: test_service[end1-start3-cached1-1-get_water_usage_hourly_history] + dict({ + 'history': list([ + dict({ + 'gallons': 0, + 'start': '2024-06-19T20:00:00-07:00', + }), + dict({ + 'gallons': 7.48, + 'start': '2024-06-19T21:00:00-07:00', + }), + ]), + }) +# --- +# name: test_service[end1-start3-cached2-1-get_water_usage_hourly_history] + dict({ + 'history': list([ + dict({ + 'gallons': 0, + 'start': '2024-06-19T20:00:00-07:00', + }), + dict({ + 'gallons': 7.48, + 'start': '2024-06-19T21:00:00-07:00', + }), + ]), + }) +# --- +# name: test_service[end1-start4-cached0-2-get_water_usage_hourly_history] + dict({ + 'history': list([ + dict({ + 'gallons': 7.48, + 'start': '2024-06-19T19:00:00-07:00', + }), + dict({ + 'gallons': 0, + 'start': '2024-06-19T20:00:00-07:00', + }), + dict({ + 'gallons': 7.48, + 'start': '2024-06-19T21:00:00-07:00', + }), + ]), + }) +# --- +# name: test_service[end1-start4-cached1-1-get_water_usage_hourly_history] + dict({ + 'history': list([ + dict({ + 'gallons': 7.48, + 'start': '2024-06-19T19:00:00-07:00', + }), + dict({ + 'gallons': 0, + 'start': '2024-06-19T20:00:00-07:00', + }), + dict({ + 'gallons': 7.48, + 'start': '2024-06-19T21:00:00-07:00', + }), + ]), + }) +# --- +# name: test_service[end1-start4-cached2-1-get_water_usage_hourly_history] + dict({ + 'history': list([ + dict({ + 'gallons': 7.48, + 'start': '2024-06-19T19:00:00-07:00', + }), + dict({ + 'gallons': 0, + 'start': '2024-06-19T20:00:00-07:00', + }), + dict({ + 'gallons': 7.48, + 'start': '2024-06-19T21:00:00-07:00', + }), + ]), + }) +# --- +# name: test_service[end2-start0-cached0-2-get_water_usage_hourly_history] + dict({ + 'history': list([ + dict({ + 'gallons': 0, + 'start': '2024-06-19T20:00:00-07:00', + }), + dict({ + 'gallons': 7.48, + 'start': '2024-06-19T21:00:00-07:00', + }), + ]), + }) +# --- +# name: test_service[end2-start0-cached1-1-get_water_usage_hourly_history] + dict({ + 'history': list([ + dict({ + 'gallons': 0, + 'start': '2024-06-19T20:00:00-07:00', + }), + dict({ + 'gallons': 7.48, + 'start': '2024-06-19T21:00:00-07:00', + }), + ]), + }) +# --- +# name: test_service[end2-start0-cached2-1-get_water_usage_hourly_history] + dict({ + 'history': list([ + dict({ + 'gallons': 0, + 'start': '2024-06-19T20:00:00-07:00', + }), + dict({ + 'gallons': 7.48, + 'start': '2024-06-19T21:00:00-07:00', + }), + ]), + }) +# --- +# name: test_service[end2-start1-cached0-2-get_water_usage_hourly_history] + dict({ + 'history': list([ + dict({ + 'gallons': 0, + 'start': '2024-06-19T20:00:00-07:00', + }), + dict({ + 'gallons': 7.48, + 'start': '2024-06-19T21:00:00-07:00', + }), + ]), + }) +# --- +# name: test_service[end2-start1-cached1-1-get_water_usage_hourly_history] + dict({ + 'history': list([ + dict({ + 'gallons': 0, + 'start': '2024-06-19T20:00:00-07:00', + }), + dict({ + 'gallons': 7.48, + 'start': '2024-06-19T21:00:00-07:00', + }), + ]), + }) +# --- +# name: test_service[end2-start1-cached2-1-get_water_usage_hourly_history] + dict({ + 'history': list([ + dict({ + 'gallons': 0, + 'start': '2024-06-19T20:00:00-07:00', + }), + dict({ + 'gallons': 7.48, + 'start': '2024-06-19T21:00:00-07:00', + }), + ]), + }) +# --- +# name: test_service[end2-start2-cached0-2-get_water_usage_hourly_history] + dict({ + 'history': list([ + dict({ + 'gallons': 0, + 'start': '2024-06-19T20:00:00-07:00', + }), + dict({ + 'gallons': 7.48, + 'start': '2024-06-19T21:00:00-07:00', + }), + ]), + }) +# --- +# name: test_service[end2-start2-cached1-1-get_water_usage_hourly_history] + dict({ + 'history': list([ + dict({ + 'gallons': 0, + 'start': '2024-06-19T20:00:00-07:00', + }), + dict({ + 'gallons': 7.48, + 'start': '2024-06-19T21:00:00-07:00', + }), + ]), + }) +# --- +# name: test_service[end2-start2-cached2-1-get_water_usage_hourly_history] + dict({ + 'history': list([ + dict({ + 'gallons': 0, + 'start': '2024-06-19T20:00:00-07:00', + }), + dict({ + 'gallons': 7.48, + 'start': '2024-06-19T21:00:00-07:00', + }), + ]), + }) +# --- +# name: test_service[end2-start3-cached0-2-get_water_usage_hourly_history] + dict({ + 'history': list([ + dict({ + 'gallons': 0, + 'start': '2024-06-19T20:00:00-07:00', + }), + dict({ + 'gallons': 7.48, + 'start': '2024-06-19T21:00:00-07:00', + }), + ]), + }) +# --- +# name: test_service[end2-start3-cached1-1-get_water_usage_hourly_history] + dict({ + 'history': list([ + dict({ + 'gallons': 0, + 'start': '2024-06-19T20:00:00-07:00', + }), + dict({ + 'gallons': 7.48, + 'start': '2024-06-19T21:00:00-07:00', + }), + ]), + }) +# --- +# name: test_service[end2-start3-cached2-1-get_water_usage_hourly_history] + dict({ + 'history': list([ + dict({ + 'gallons': 0, + 'start': '2024-06-19T20:00:00-07:00', + }), + dict({ + 'gallons': 7.48, + 'start': '2024-06-19T21:00:00-07:00', + }), + ]), + }) +# --- +# name: test_service[end2-start4-cached0-2-get_water_usage_hourly_history] + dict({ + 'history': list([ + dict({ + 'gallons': 7.48, + 'start': '2024-06-19T19:00:00-07:00', + }), + dict({ + 'gallons': 0, + 'start': '2024-06-19T20:00:00-07:00', + }), + dict({ + 'gallons': 7.48, + 'start': '2024-06-19T21:00:00-07:00', + }), + ]), + }) +# --- +# name: test_service[end2-start4-cached1-1-get_water_usage_hourly_history] + dict({ + 'history': list([ + dict({ + 'gallons': 7.48, + 'start': '2024-06-19T19:00:00-07:00', + }), + dict({ + 'gallons': 0, + 'start': '2024-06-19T20:00:00-07:00', + }), + dict({ + 'gallons': 7.48, + 'start': '2024-06-19T21:00:00-07:00', + }), + ]), + }) +# --- +# name: test_service[end2-start4-cached2-1-get_water_usage_hourly_history] + dict({ + 'history': list([ + dict({ + 'gallons': 7.48, + 'start': '2024-06-19T19:00:00-07:00', + }), + dict({ + 'gallons': 0, + 'start': '2024-06-19T20:00:00-07:00', + }), + dict({ + 'gallons': 7.48, + 'start': '2024-06-19T21:00:00-07:00', + }), + ]), + }) +# --- +# name: test_service[end3-start0-cached0-2-get_water_usage_hourly_history] + dict({ + 'history': list([ + dict({ + 'gallons': 0, + 'start': '2024-06-19T20:00:00-07:00', + }), + dict({ + 'gallons': 7.48, + 'start': '2024-06-19T21:00:00-07:00', + }), + ]), + }) +# --- +# name: test_service[end3-start0-cached1-1-get_water_usage_hourly_history] + dict({ + 'history': list([ + dict({ + 'gallons': 0, + 'start': '2024-06-19T20:00:00-07:00', + }), + dict({ + 'gallons': 7.48, + 'start': '2024-06-19T21:00:00-07:00', + }), + ]), + }) +# --- +# name: test_service[end3-start0-cached2-1-get_water_usage_hourly_history] + dict({ + 'history': list([ + dict({ + 'gallons': 0, + 'start': '2024-06-19T20:00:00-07:00', + }), + dict({ + 'gallons': 7.48, + 'start': '2024-06-19T21:00:00-07:00', + }), + ]), + }) +# --- +# name: test_service[end3-start1-cached0-2-get_water_usage_hourly_history] + dict({ + 'history': list([ + dict({ + 'gallons': 0, + 'start': '2024-06-19T20:00:00-07:00', + }), + dict({ + 'gallons': 7.48, + 'start': '2024-06-19T21:00:00-07:00', + }), + ]), + }) +# --- +# name: test_service[end3-start1-cached1-1-get_water_usage_hourly_history] + dict({ + 'history': list([ + dict({ + 'gallons': 0, + 'start': '2024-06-19T20:00:00-07:00', + }), + dict({ + 'gallons': 7.48, + 'start': '2024-06-19T21:00:00-07:00', + }), + ]), + }) +# --- +# name: test_service[end3-start1-cached2-1-get_water_usage_hourly_history] + dict({ + 'history': list([ + dict({ + 'gallons': 0, + 'start': '2024-06-19T20:00:00-07:00', + }), + dict({ + 'gallons': 7.48, + 'start': '2024-06-19T21:00:00-07:00', + }), + ]), + }) +# --- +# name: test_service[end3-start2-cached0-2-get_water_usage_hourly_history] + dict({ + 'history': list([ + dict({ + 'gallons': 0, + 'start': '2024-06-19T20:00:00-07:00', + }), + dict({ + 'gallons': 7.48, + 'start': '2024-06-19T21:00:00-07:00', + }), + ]), + }) +# --- +# name: test_service[end3-start2-cached1-1-get_water_usage_hourly_history] + dict({ + 'history': list([ + dict({ + 'gallons': 0, + 'start': '2024-06-19T20:00:00-07:00', + }), + dict({ + 'gallons': 7.48, + 'start': '2024-06-19T21:00:00-07:00', + }), + ]), + }) +# --- +# name: test_service[end3-start2-cached2-1-get_water_usage_hourly_history] + dict({ + 'history': list([ + dict({ + 'gallons': 0, + 'start': '2024-06-19T20:00:00-07:00', + }), + dict({ + 'gallons': 7.48, + 'start': '2024-06-19T21:00:00-07:00', + }), + ]), + }) +# --- +# name: test_service[end3-start3-cached0-2-get_water_usage_hourly_history] + dict({ + 'history': list([ + dict({ + 'gallons': 0, + 'start': '2024-06-19T20:00:00-07:00', + }), + dict({ + 'gallons': 7.48, + 'start': '2024-06-19T21:00:00-07:00', + }), + ]), + }) +# --- +# name: test_service[end3-start3-cached1-1-get_water_usage_hourly_history] + dict({ + 'history': list([ + dict({ + 'gallons': 0, + 'start': '2024-06-19T20:00:00-07:00', + }), + dict({ + 'gallons': 7.48, + 'start': '2024-06-19T21:00:00-07:00', + }), + ]), + }) +# --- +# name: test_service[end3-start3-cached2-1-get_water_usage_hourly_history] + dict({ + 'history': list([ + dict({ + 'gallons': 0, + 'start': '2024-06-19T20:00:00-07:00', + }), + dict({ + 'gallons': 7.48, + 'start': '2024-06-19T21:00:00-07:00', + }), + ]), + }) +# --- +# name: test_service[end3-start4-cached0-2-get_water_usage_hourly_history] + dict({ + 'history': list([ + dict({ + 'gallons': 7.48, + 'start': '2024-06-19T19:00:00-07:00', + }), + dict({ + 'gallons': 0, + 'start': '2024-06-19T20:00:00-07:00', + }), + dict({ + 'gallons': 7.48, + 'start': '2024-06-19T21:00:00-07:00', + }), + ]), + }) +# --- +# name: test_service[end3-start4-cached1-1-get_water_usage_hourly_history] + dict({ + 'history': list([ + dict({ + 'gallons': 7.48, + 'start': '2024-06-19T19:00:00-07:00', + }), + dict({ + 'gallons': 0, + 'start': '2024-06-19T20:00:00-07:00', + }), + dict({ + 'gallons': 7.48, + 'start': '2024-06-19T21:00:00-07:00', + }), + ]), + }) +# --- +# name: test_service[end3-start4-cached2-1-get_water_usage_hourly_history] + dict({ + 'history': list([ + dict({ + 'gallons': 7.48, + 'start': '2024-06-19T19:00:00-07:00', + }), + dict({ + 'gallons': 0, + 'start': '2024-06-19T20:00:00-07:00', + }), + dict({ + 'gallons': 7.48, + 'start': '2024-06-19T21:00:00-07:00', + }), + ]), + }) +# --- +# name: test_service[end4-start0-cached0-2-get_water_usage_hourly_history] + dict({ + 'history': list([ + dict({ + 'gallons': 0, + 'start': '2024-06-19T20:00:00-07:00', + }), + dict({ + 'gallons': 7.48, + 'start': '2024-06-19T21:00:00-07:00', + }), + dict({ + 'gallons': 0, + 'start': '2024-06-19T22:00:00-07:00', + }), + ]), + }) +# --- +# name: test_service[end4-start0-cached1-1-get_water_usage_hourly_history] + dict({ + 'history': list([ + dict({ + 'gallons': 0, + 'start': '2024-06-19T20:00:00-07:00', + }), + dict({ + 'gallons': 7.48, + 'start': '2024-06-19T21:00:00-07:00', + }), + dict({ + 'gallons': 0, + 'start': '2024-06-19T22:00:00-07:00', + }), + ]), + }) +# --- +# name: test_service[end4-start0-cached2-1-get_water_usage_hourly_history] + dict({ + 'history': list([ + dict({ + 'gallons': 0, + 'start': '2024-06-19T20:00:00-07:00', + }), + dict({ + 'gallons': 7.48, + 'start': '2024-06-19T21:00:00-07:00', + }), + dict({ + 'gallons': 0, + 'start': '2024-06-19T22:00:00-07:00', + }), + ]), + }) +# --- +# name: test_service[end4-start1-cached0-2-get_water_usage_hourly_history] + dict({ + 'history': list([ + dict({ + 'gallons': 0, + 'start': '2024-06-19T20:00:00-07:00', + }), + dict({ + 'gallons': 7.48, + 'start': '2024-06-19T21:00:00-07:00', + }), + dict({ + 'gallons': 0, + 'start': '2024-06-19T22:00:00-07:00', + }), + ]), + }) +# --- +# name: test_service[end4-start1-cached1-1-get_water_usage_hourly_history] + dict({ + 'history': list([ + dict({ + 'gallons': 0, + 'start': '2024-06-19T20:00:00-07:00', + }), + dict({ + 'gallons': 7.48, + 'start': '2024-06-19T21:00:00-07:00', + }), + dict({ + 'gallons': 0, + 'start': '2024-06-19T22:00:00-07:00', + }), + ]), + }) +# --- +# name: test_service[end4-start1-cached2-1-get_water_usage_hourly_history] + dict({ + 'history': list([ + dict({ + 'gallons': 0, + 'start': '2024-06-19T20:00:00-07:00', + }), + dict({ + 'gallons': 7.48, + 'start': '2024-06-19T21:00:00-07:00', + }), + dict({ + 'gallons': 0, + 'start': '2024-06-19T22:00:00-07:00', + }), + ]), + }) +# --- +# name: test_service[end4-start2-cached0-2-get_water_usage_hourly_history] + dict({ + 'history': list([ + dict({ + 'gallons': 0, + 'start': '2024-06-19T20:00:00-07:00', + }), + dict({ + 'gallons': 7.48, + 'start': '2024-06-19T21:00:00-07:00', + }), + dict({ + 'gallons': 0, + 'start': '2024-06-19T22:00:00-07:00', + }), + ]), + }) +# --- +# name: test_service[end4-start2-cached1-1-get_water_usage_hourly_history] + dict({ + 'history': list([ + dict({ + 'gallons': 0, + 'start': '2024-06-19T20:00:00-07:00', + }), + dict({ + 'gallons': 7.48, + 'start': '2024-06-19T21:00:00-07:00', + }), + dict({ + 'gallons': 0, + 'start': '2024-06-19T22:00:00-07:00', + }), + ]), + }) +# --- +# name: test_service[end4-start2-cached2-1-get_water_usage_hourly_history] + dict({ + 'history': list([ + dict({ + 'gallons': 0, + 'start': '2024-06-19T20:00:00-07:00', + }), + dict({ + 'gallons': 7.48, + 'start': '2024-06-19T21:00:00-07:00', + }), + dict({ + 'gallons': 0, + 'start': '2024-06-19T22:00:00-07:00', + }), + ]), + }) +# --- +# name: test_service[end4-start3-cached0-2-get_water_usage_hourly_history] + dict({ + 'history': list([ + dict({ + 'gallons': 0, + 'start': '2024-06-19T20:00:00-07:00', + }), + dict({ + 'gallons': 7.48, + 'start': '2024-06-19T21:00:00-07:00', + }), + dict({ + 'gallons': 0, + 'start': '2024-06-19T22:00:00-07:00', + }), + ]), + }) +# --- +# name: test_service[end4-start3-cached1-1-get_water_usage_hourly_history] + dict({ + 'history': list([ + dict({ + 'gallons': 0, + 'start': '2024-06-19T20:00:00-07:00', + }), + dict({ + 'gallons': 7.48, + 'start': '2024-06-19T21:00:00-07:00', + }), + dict({ + 'gallons': 0, + 'start': '2024-06-19T22:00:00-07:00', + }), + ]), + }) +# --- +# name: test_service[end4-start3-cached2-1-get_water_usage_hourly_history] + dict({ + 'history': list([ + dict({ + 'gallons': 0, + 'start': '2024-06-19T20:00:00-07:00', + }), + dict({ + 'gallons': 7.48, + 'start': '2024-06-19T21:00:00-07:00', + }), + dict({ + 'gallons': 0, + 'start': '2024-06-19T22:00:00-07:00', + }), + ]), + }) +# --- +# name: test_service[end4-start4-cached0-2-get_water_usage_hourly_history] + dict({ + 'history': list([ + dict({ + 'gallons': 7.48, + 'start': '2024-06-19T19:00:00-07:00', + }), + dict({ + 'gallons': 0, + 'start': '2024-06-19T20:00:00-07:00', + }), + dict({ + 'gallons': 7.48, + 'start': '2024-06-19T21:00:00-07:00', + }), + dict({ + 'gallons': 0, + 'start': '2024-06-19T22:00:00-07:00', + }), + ]), + }) +# --- +# name: test_service[end4-start4-cached1-1-get_water_usage_hourly_history] + dict({ + 'history': list([ + dict({ + 'gallons': 7.48, + 'start': '2024-06-19T19:00:00-07:00', + }), + dict({ + 'gallons': 0, + 'start': '2024-06-19T20:00:00-07:00', + }), + dict({ + 'gallons': 7.48, + 'start': '2024-06-19T21:00:00-07:00', + }), + dict({ + 'gallons': 0, + 'start': '2024-06-19T22:00:00-07:00', + }), + ]), + }) +# --- +# name: test_service[end4-start4-cached2-1-get_water_usage_hourly_history] + dict({ + 'history': list([ + dict({ + 'gallons': 7.48, + 'start': '2024-06-19T19:00:00-07:00', + }), + dict({ + 'gallons': 0, + 'start': '2024-06-19T20:00:00-07:00', + }), + dict({ + 'gallons': 7.48, + 'start': '2024-06-19T21:00:00-07:00', + }), + dict({ + 'gallons': 0, + 'start': '2024-06-19T22:00:00-07:00', + }), + ]), + }) +# --- +# name: test_service[get_water_usage_hourly_history] + dict({ + 'history': list([ + dict({ + 'gallons': 0, + 'start': '2024-06-19T22:00:00-07:00', + }), + dict({ + 'gallons': 7.48, + 'start': '2024-06-19T21:00:00-07:00', + }), + dict({ + 'gallons': 0, + 'start': '2024-06-19T20:00:00-07:00', + }), + dict({ + 'gallons': 7.48, + 'start': '2024-06-19T19:00:00-07:00', + }), + ]), + }) +# --- diff --git a/tests/test_services.py b/tests/test_services.py new file mode 100644 index 0000000..b69e765 --- /dev/null +++ b/tests/test_services.py @@ -0,0 +1,170 @@ +"""Test services for WaterSmart integration.""" + +import re + +import pytest + +from syrupy.assertion import SnapshotAssertion +import voluptuous as vol + +from homeassistant.core import HomeAssistant +from homeassistant.exceptions import ServiceValidationError + +from custom_components.watersmart.const import DOMAIN +from custom_components.watersmart.services import ( + ATTR_CONFIG_ENTRY, + HOURLY_HISTORY_SERVICE_NAME, +) + +from .conftest import MockConfigEntry + + +@pytest.mark.usefixtures("init_integration") +async def test_has_services( + hass: HomeAssistant, +) -> None: + """Test the existence of the WaterSmart Service.""" + assert hass.services.has_service(DOMAIN, HOURLY_HISTORY_SERVICE_NAME) + + +@pytest.mark.usefixtures("init_integration") +@pytest.mark.parametrize("service", [HOURLY_HISTORY_SERVICE_NAME]) +@pytest.mark.parametrize( + ("cached", "update_call_count"), + [({"cached": False}, 2), ({"cached": True}, 1), ({}, 1)], +) +@pytest.mark.parametrize( + "start", + [ + {"start": "2024-06-19T19:30:00-07:00"}, + {"start": "2024-06-19 19:30:00"}, + {"start": "2024-06-20T02:30:00.000Z"}, + {"start": 1718850600}, + {}, + ], +) +@pytest.mark.parametrize( + "end", + [ + {"end": "2024-06-19T21:30:00-07:00"}, + {"end": "2024-06-19 21:30:00"}, + {"end": "2024-06-20T04:30:00.000Z"}, + {"end": 1718857800}, + {}, + ], +) +async def test_service( + hass: HomeAssistant, + mock_config_entry: MockConfigEntry, + mock_watersmart_client, + snapshot: SnapshotAssertion, + service: str, + cached: dict[str, bool], + update_call_count: int, + start: dict[str, str], + end: dict[str, str], +): + entry = {ATTR_CONFIG_ENTRY: mock_config_entry.entry_id} + + data = entry | cached | start | end + + assert snapshot == await hass.services.async_call( + DOMAIN, + service, + data, + blocking=True, + return_response=True, + ) + + assert mock_watersmart_client.async_get_hourly_data.call_count == update_call_count + + +@pytest.fixture +def config_entry_data( + mock_config_entry: MockConfigEntry, request: pytest.FixtureRequest +) -> dict[str, str]: + """Fixture for the config entry.""" + if "config_entry" in request.param and request.param["config_entry"] is True: + return {"config_entry": mock_config_entry.entry_id} + + return request.param + + +@pytest.mark.usefixtures("init_integration") +@pytest.mark.parametrize("service", [HOURLY_HISTORY_SERVICE_NAME]) +@pytest.mark.parametrize( + ("config_entry_data", "service_data", "error", "error_message"), + [ + ({}, {}, vol.er.Error, "required key not provided .+"), + ( + {"config_entry": True}, + {"cached": "incorrect cache value"}, + vol.er.Error, + "expected bool for dictionary value .+", + ), + ( + {"config_entry": "incorrect entry"}, + {}, + ServiceValidationError, + "Invalid config entry.+", + ), + ( + {"config_entry": True}, + { + "start": "incorrect date", + }, + ServiceValidationError, + "Invalid date provided. Got incorrect date", + ), + ( + {"config_entry": True}, + { + "end": "incorrect date", + }, + ServiceValidationError, + "Invalid date provided. Got incorrect date", + ), + ], + indirect=["config_entry_data"], +) +async def test_service_validation( + hass: HomeAssistant, + service: str, + config_entry_data: dict[str, str], + service_data: dict[str, str], + error: type[Exception], + error_message: str, +) -> None: + """Test the WaterSmart Service validation.""" + + with pytest.raises(error) as exc: + await hass.services.async_call( + DOMAIN, + service, + config_entry_data | service_data, + blocking=True, + return_response=True, + ) + assert re.match(error_message, str(exc.value)) + + +@pytest.mark.usefixtures("init_integration") +@pytest.mark.parametrize("service", [HOURLY_HISTORY_SERVICE_NAME]) +async def test_service_called_with_unloaded_entry( + hass: HomeAssistant, + mock_config_entry: MockConfigEntry, + service: str, +) -> None: + """Test service calls with unloaded config entry.""" + await hass.config_entries.async_unload(mock_config_entry.entry_id) + + data = {"config_entry": mock_config_entry.entry_id} + + with pytest.raises(ServiceValidationError): + await hass.services.async_call( + DOMAIN, + service, + data, + blocking=True, + return_response=True, + )