mirror of
https://github.com/home-assistant/core.git
synced 2025-07-22 20:57:21 +00:00
Suez water: fetch historical data in statistics (#131166)
* Suez water: fetch historical data in statistics * test review * wip: fix few things * Python is smarter than me * use snapshots for statistics and add hard limit for historical stats * refactor refresh + handle missing price * No more auth error raised * fix after rebase * Review - much cleaner <3 * fix changes * test without snapshots * fix imports
This commit is contained in:
parent
4160ed190c
commit
4737091722
@ -1,18 +1,35 @@
|
||||
"""Suez water update coordinator."""
|
||||
|
||||
from dataclasses import dataclass
|
||||
from datetime import date
|
||||
from datetime import date, datetime
|
||||
import logging
|
||||
|
||||
from pysuez import PySuezError, SuezClient
|
||||
from pysuez import PySuezError, SuezClient, TelemetryMeasure
|
||||
|
||||
from homeassistant.components.recorder import get_instance
|
||||
from homeassistant.components.recorder.models import StatisticData, StatisticMetaData
|
||||
from homeassistant.components.recorder.statistics import (
|
||||
StatisticMeanType,
|
||||
StatisticsRow,
|
||||
async_add_external_statistics,
|
||||
get_last_statistics,
|
||||
)
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import CONF_PASSWORD, CONF_USERNAME
|
||||
from homeassistant.core import _LOGGER, HomeAssistant
|
||||
from homeassistant.const import (
|
||||
CONF_PASSWORD,
|
||||
CONF_USERNAME,
|
||||
CURRENCY_EURO,
|
||||
UnitOfVolume,
|
||||
)
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.exceptions import ConfigEntryError
|
||||
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
|
||||
import homeassistant.util.dt as dt_util
|
||||
|
||||
from .const import CONF_COUNTER_ID, DATA_REFRESH_INTERVAL, DOMAIN
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@dataclass
|
||||
class SuezWaterAggregatedAttributes:
|
||||
@ -32,7 +49,7 @@ class SuezWaterData:
|
||||
|
||||
aggregated_value: float
|
||||
aggregated_attr: SuezWaterAggregatedAttributes
|
||||
price: float
|
||||
price: float | None
|
||||
|
||||
|
||||
type SuezWaterConfigEntry = ConfigEntry[SuezWaterCoordinator]
|
||||
@ -54,6 +71,11 @@ class SuezWaterCoordinator(DataUpdateCoordinator[SuezWaterData]):
|
||||
always_update=True,
|
||||
config_entry=config_entry,
|
||||
)
|
||||
self._counter_id = self.config_entry.data[CONF_COUNTER_ID]
|
||||
self._cost_statistic_id = f"{DOMAIN}:{self._counter_id}_water_cost_statistics"
|
||||
self._water_statistic_id = (
|
||||
f"{DOMAIN}:{self._counter_id}_water_consumption_statistics"
|
||||
)
|
||||
|
||||
async def _async_setup(self) -> None:
|
||||
self._suez_client = SuezClient(
|
||||
@ -72,19 +94,165 @@ class SuezWaterCoordinator(DataUpdateCoordinator[SuezWaterData]):
|
||||
|
||||
try:
|
||||
aggregated = await self._suez_client.fetch_aggregated_data()
|
||||
data = SuezWaterData(
|
||||
aggregated_value=aggregated.value,
|
||||
aggregated_attr=SuezWaterAggregatedAttributes(
|
||||
this_month_consumption=map_dict(aggregated.current_month),
|
||||
previous_month_consumption=map_dict(aggregated.previous_month),
|
||||
highest_monthly_consumption=aggregated.highest_monthly_consumption,
|
||||
last_year_overall=aggregated.previous_year,
|
||||
this_year_overall=aggregated.current_year,
|
||||
history=map_dict(aggregated.history),
|
||||
),
|
||||
price=(await self._suez_client.get_price()).price,
|
||||
)
|
||||
except PySuezError as err:
|
||||
raise UpdateFailed(f"Suez data update failed: {err}") from err
|
||||
raise UpdateFailed("Suez coordinator error communicating with API") from err
|
||||
|
||||
price = None
|
||||
try:
|
||||
price = (await self._suez_client.get_price()).price
|
||||
except PySuezError:
|
||||
_LOGGER.debug("Failed to fetch water price", stack_info=True)
|
||||
|
||||
try:
|
||||
await self._update_statistics(price)
|
||||
except PySuezError as err:
|
||||
raise UpdateFailed("Failed to update suez water statistics") from err
|
||||
|
||||
_LOGGER.debug("Successfully fetched suez data")
|
||||
return data
|
||||
return SuezWaterData(
|
||||
aggregated_value=aggregated.value,
|
||||
aggregated_attr=SuezWaterAggregatedAttributes(
|
||||
this_month_consumption=map_dict(aggregated.current_month),
|
||||
previous_month_consumption=map_dict(aggregated.previous_month),
|
||||
highest_monthly_consumption=aggregated.highest_monthly_consumption,
|
||||
last_year_overall=aggregated.previous_year,
|
||||
this_year_overall=aggregated.current_year,
|
||||
history=map_dict(aggregated.history),
|
||||
),
|
||||
price=price,
|
||||
)
|
||||
|
||||
async def _update_statistics(self, current_price: float | None) -> None:
|
||||
"""Update daily statistics."""
|
||||
_LOGGER.debug("Updating statistics for %s", self._water_statistic_id)
|
||||
|
||||
water_last_stat = await self._get_last_stat(self._water_statistic_id)
|
||||
cost_last_stat = await self._get_last_stat(self._cost_statistic_id)
|
||||
consumption_sum = (
|
||||
water_last_stat["sum"]
|
||||
if water_last_stat and water_last_stat["sum"]
|
||||
else 0.0
|
||||
)
|
||||
cost_sum = (
|
||||
cost_last_stat["sum"] if cost_last_stat and cost_last_stat["sum"] else 0.0
|
||||
)
|
||||
last_stats = (
|
||||
datetime.fromtimestamp(water_last_stat["start"]).date()
|
||||
if water_last_stat
|
||||
else None
|
||||
)
|
||||
|
||||
_LOGGER.debug(
|
||||
"Updating suez stat since %s for %s",
|
||||
str(last_stats),
|
||||
water_last_stat,
|
||||
)
|
||||
if not (
|
||||
usage := await self._suez_client.fetch_all_daily_data(
|
||||
since=last_stats,
|
||||
)
|
||||
):
|
||||
_LOGGER.debug("No recent usage data. Skipping update")
|
||||
return
|
||||
_LOGGER.debug("fetched data: %s", len(usage))
|
||||
|
||||
consumption_statistics, cost_statistics = self._build_statistics(
|
||||
current_price, consumption_sum, cost_sum, last_stats, usage
|
||||
)
|
||||
|
||||
self._persist_statistics(consumption_statistics, cost_statistics)
|
||||
|
||||
def _build_statistics(
|
||||
self,
|
||||
current_price: float | None,
|
||||
consumption_sum: float,
|
||||
cost_sum: float,
|
||||
last_stats: date | None,
|
||||
usage: list[TelemetryMeasure],
|
||||
) -> tuple[list[StatisticData], list[StatisticData]]:
|
||||
"""Build statistics data from fetched data."""
|
||||
consumption_statistics = []
|
||||
cost_statistics = []
|
||||
|
||||
for data in usage:
|
||||
if (
|
||||
(last_stats is not None and data.date <= last_stats)
|
||||
or not data.index
|
||||
or data.volume is None
|
||||
):
|
||||
continue
|
||||
consumption_date = dt_util.start_of_local_day(data.date)
|
||||
|
||||
consumption_sum += data.volume
|
||||
consumption_statistics.append(
|
||||
StatisticData(
|
||||
start=consumption_date,
|
||||
state=data.volume,
|
||||
sum=consumption_sum,
|
||||
)
|
||||
)
|
||||
if current_price is not None:
|
||||
day_cost = (data.volume / 1000) * current_price
|
||||
cost_sum += day_cost
|
||||
cost_statistics.append(
|
||||
StatisticData(
|
||||
start=consumption_date,
|
||||
state=day_cost,
|
||||
sum=cost_sum,
|
||||
)
|
||||
)
|
||||
|
||||
return consumption_statistics, cost_statistics
|
||||
|
||||
def _persist_statistics(
|
||||
self,
|
||||
consumption_statistics: list[StatisticData],
|
||||
cost_statistics: list[StatisticData],
|
||||
) -> None:
|
||||
"""Persist given statistics in recorder."""
|
||||
consumption_metadata = self._get_statistics_metadata(
|
||||
id=self._water_statistic_id, name="Consumption", unit=UnitOfVolume.LITERS
|
||||
)
|
||||
|
||||
_LOGGER.debug(
|
||||
"Adding %s statistics for %s",
|
||||
len(consumption_statistics),
|
||||
self._water_statistic_id,
|
||||
)
|
||||
async_add_external_statistics(
|
||||
self.hass, consumption_metadata, consumption_statistics
|
||||
)
|
||||
|
||||
if len(cost_statistics) > 0:
|
||||
_LOGGER.debug(
|
||||
"Adding %s statistics for %s",
|
||||
len(cost_statistics),
|
||||
self._cost_statistic_id,
|
||||
)
|
||||
cost_metadata = self._get_statistics_metadata(
|
||||
id=self._cost_statistic_id, name="Cost", unit=CURRENCY_EURO
|
||||
)
|
||||
async_add_external_statistics(self.hass, cost_metadata, cost_statistics)
|
||||
|
||||
_LOGGER.debug("Updated statistics for %s", self._water_statistic_id)
|
||||
|
||||
def _get_statistics_metadata(
|
||||
self, id: str, name: str, unit: str
|
||||
) -> StatisticMetaData:
|
||||
"""Build statistics metadata for requested configuration."""
|
||||
return StatisticMetaData(
|
||||
has_mean=False,
|
||||
mean_type=StatisticMeanType.NONE,
|
||||
has_sum=True,
|
||||
name=f"Suez water {name} {self._counter_id}",
|
||||
source=DOMAIN,
|
||||
statistic_id=id,
|
||||
unit_of_measurement=unit,
|
||||
)
|
||||
|
||||
async def _get_last_stat(self, id: str) -> StatisticsRow | None:
|
||||
"""Find last registered statistics of given id."""
|
||||
last_stat = await get_instance(self.hass).async_add_executor_job(
|
||||
get_last_statistics, self.hass, 1, id, True, {"sum"}
|
||||
)
|
||||
return last_stat[id][0] if last_stat else None
|
||||
|
@ -1,6 +1,7 @@
|
||||
{
|
||||
"domain": "suez_water",
|
||||
"name": "Suez Water",
|
||||
"after_dependencies": ["recorder"],
|
||||
"codeowners": ["@ooii", "@jb101010-2"],
|
||||
"config_flow": true,
|
||||
"documentation": "https://www.home-assistant.io/integrations/suez_water",
|
||||
|
@ -87,6 +87,14 @@ class SuezWaterSensor(CoordinatorEntity[SuezWaterCoordinator], SensorEntity):
|
||||
)
|
||||
self.entity_description = entity_description
|
||||
|
||||
@property
|
||||
def available(self) -> bool:
|
||||
"""Return if entity is available."""
|
||||
return (
|
||||
self.coordinator.last_update_success
|
||||
and self.entity_description.value_fn(self.coordinator.data) is not None
|
||||
)
|
||||
|
||||
@property
|
||||
def native_value(self) -> float | str | None:
|
||||
"""Return the state of the sensor."""
|
||||
|
@ -8,17 +8,26 @@ from pysuez import AggregatedData, PriceResult
|
||||
from pysuez.const import ATTRIBUTION
|
||||
import pytest
|
||||
|
||||
from homeassistant.components.recorder import Recorder
|
||||
from homeassistant.components.suez_water.const import CONF_COUNTER_ID, DOMAIN
|
||||
|
||||
from tests.common import MockConfigEntry
|
||||
from tests.conftest import RecorderInstanceContextManager
|
||||
|
||||
MOCK_DATA = {
|
||||
"username": "test-username",
|
||||
"password": "test-password",
|
||||
CONF_COUNTER_ID: "test-counter",
|
||||
CONF_COUNTER_ID: "123456",
|
||||
}
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
async def mock_recorder_before_hass(
|
||||
async_test_recorder: RecorderInstanceContextManager,
|
||||
) -> None:
|
||||
"""Set up recorder."""
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def mock_config_entry() -> MockConfigEntry:
|
||||
"""Create mock config_entry needed by suez_water integration."""
|
||||
@ -32,7 +41,7 @@ def mock_config_entry() -> MockConfigEntry:
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def mock_setup_entry() -> Generator[AsyncMock]:
|
||||
def mock_setup_entry(recorder_mock: Recorder) -> Generator[AsyncMock]:
|
||||
"""Override async_setup_entry."""
|
||||
with patch(
|
||||
"homeassistant.components.suez_water.async_setup_entry", return_value=True
|
||||
@ -41,7 +50,7 @@ def mock_setup_entry() -> Generator[AsyncMock]:
|
||||
|
||||
|
||||
@pytest.fixture(name="suez_client")
|
||||
def mock_suez_client() -> Generator[AsyncMock]:
|
||||
def mock_suez_client(recorder_mock: Recorder) -> Generator[AsyncMock]:
|
||||
"""Create mock for suez_water external api."""
|
||||
with (
|
||||
patch(
|
||||
|
231
tests/components/suez_water/snapshots/test_init.ambr
Normal file
231
tests/components/suez_water/snapshots/test_init.ambr
Normal file
@ -0,0 +1,231 @@
|
||||
# serializer version: 1
|
||||
# name: test_statistics[water_consumption_statistics][test_statistics_call1]
|
||||
defaultdict({
|
||||
'suez_water:123456_water_consumption_statistics': list([
|
||||
dict({
|
||||
'end': 1733043600.0,
|
||||
'last_reset': None,
|
||||
'start': 1733040000.0,
|
||||
'state': 500.0,
|
||||
'sum': 500.0,
|
||||
}),
|
||||
dict({
|
||||
'end': 1733130000.0,
|
||||
'last_reset': None,
|
||||
'start': 1733126400.0,
|
||||
'state': 500.0,
|
||||
'sum': 1000.0,
|
||||
}),
|
||||
dict({
|
||||
'end': 1733216400.0,
|
||||
'last_reset': None,
|
||||
'start': 1733212800.0,
|
||||
'state': 500.0,
|
||||
'sum': 1500.0,
|
||||
}),
|
||||
]),
|
||||
})
|
||||
# ---
|
||||
# name: test_statistics[water_consumption_statistics][test_statistics_call2]
|
||||
defaultdict({
|
||||
'suez_water:123456_water_consumption_statistics': list([
|
||||
dict({
|
||||
'end': 1733043600.0,
|
||||
'last_reset': None,
|
||||
'start': 1733040000.0,
|
||||
'state': 500.0,
|
||||
'sum': 500.0,
|
||||
}),
|
||||
dict({
|
||||
'end': 1733130000.0,
|
||||
'last_reset': None,
|
||||
'start': 1733126400.0,
|
||||
'state': 500.0,
|
||||
'sum': 1000.0,
|
||||
}),
|
||||
dict({
|
||||
'end': 1733216400.0,
|
||||
'last_reset': None,
|
||||
'start': 1733212800.0,
|
||||
'state': 500.0,
|
||||
'sum': 1500.0,
|
||||
}),
|
||||
]),
|
||||
})
|
||||
# ---
|
||||
# name: test_statistics[water_consumption_statistics][test_statistics_call3]
|
||||
defaultdict({
|
||||
'suez_water:123456_water_consumption_statistics': list([
|
||||
dict({
|
||||
'end': 1733043600.0,
|
||||
'last_reset': None,
|
||||
'start': 1733040000.0,
|
||||
'state': 500.0,
|
||||
'sum': 500.0,
|
||||
}),
|
||||
dict({
|
||||
'end': 1733130000.0,
|
||||
'last_reset': None,
|
||||
'start': 1733126400.0,
|
||||
'state': 500.0,
|
||||
'sum': 1000.0,
|
||||
}),
|
||||
dict({
|
||||
'end': 1733216400.0,
|
||||
'last_reset': None,
|
||||
'start': 1733212800.0,
|
||||
'state': 500.0,
|
||||
'sum': 1500.0,
|
||||
}),
|
||||
]),
|
||||
})
|
||||
# ---
|
||||
# name: test_statistics[water_consumption_statistics][test_statistics_call4]
|
||||
defaultdict({
|
||||
'suez_water:123456_water_consumption_statistics': list([
|
||||
dict({
|
||||
'end': 1733043600.0,
|
||||
'last_reset': None,
|
||||
'start': 1733040000.0,
|
||||
'state': 500.0,
|
||||
'sum': 500.0,
|
||||
}),
|
||||
dict({
|
||||
'end': 1733130000.0,
|
||||
'last_reset': None,
|
||||
'start': 1733126400.0,
|
||||
'state': 500.0,
|
||||
'sum': 1000.0,
|
||||
}),
|
||||
dict({
|
||||
'end': 1733216400.0,
|
||||
'last_reset': None,
|
||||
'start': 1733212800.0,
|
||||
'state': 500.0,
|
||||
'sum': 1500.0,
|
||||
}),
|
||||
dict({
|
||||
'end': 1733389200.0,
|
||||
'last_reset': None,
|
||||
'start': 1733385600.0,
|
||||
'state': 500.0,
|
||||
'sum': 2000.0,
|
||||
}),
|
||||
]),
|
||||
})
|
||||
# ---
|
||||
# name: test_statistics[water_cost_statistics][test_statistics_call1]
|
||||
defaultdict({
|
||||
'suez_water:123456_water_cost_statistics': list([
|
||||
dict({
|
||||
'end': 1733043600.0,
|
||||
'last_reset': None,
|
||||
'start': 1733040000.0,
|
||||
'state': 2.37,
|
||||
'sum': 2.37,
|
||||
}),
|
||||
dict({
|
||||
'end': 1733130000.0,
|
||||
'last_reset': None,
|
||||
'start': 1733126400.0,
|
||||
'state': 2.37,
|
||||
'sum': 4.74,
|
||||
}),
|
||||
dict({
|
||||
'end': 1733216400.0,
|
||||
'last_reset': None,
|
||||
'start': 1733212800.0,
|
||||
'state': 2.37,
|
||||
'sum': 7.11,
|
||||
}),
|
||||
]),
|
||||
})
|
||||
# ---
|
||||
# name: test_statistics[water_cost_statistics][test_statistics_call2]
|
||||
defaultdict({
|
||||
'suez_water:123456_water_cost_statistics': list([
|
||||
dict({
|
||||
'end': 1733043600.0,
|
||||
'last_reset': None,
|
||||
'start': 1733040000.0,
|
||||
'state': 2.37,
|
||||
'sum': 2.37,
|
||||
}),
|
||||
dict({
|
||||
'end': 1733130000.0,
|
||||
'last_reset': None,
|
||||
'start': 1733126400.0,
|
||||
'state': 2.37,
|
||||
'sum': 4.74,
|
||||
}),
|
||||
dict({
|
||||
'end': 1733216400.0,
|
||||
'last_reset': None,
|
||||
'start': 1733212800.0,
|
||||
'state': 2.37,
|
||||
'sum': 7.11,
|
||||
}),
|
||||
]),
|
||||
})
|
||||
# ---
|
||||
# name: test_statistics[water_cost_statistics][test_statistics_call3]
|
||||
defaultdict({
|
||||
'suez_water:123456_water_cost_statistics': list([
|
||||
dict({
|
||||
'end': 1733043600.0,
|
||||
'last_reset': None,
|
||||
'start': 1733040000.0,
|
||||
'state': 2.37,
|
||||
'sum': 2.37,
|
||||
}),
|
||||
dict({
|
||||
'end': 1733130000.0,
|
||||
'last_reset': None,
|
||||
'start': 1733126400.0,
|
||||
'state': 2.37,
|
||||
'sum': 4.74,
|
||||
}),
|
||||
dict({
|
||||
'end': 1733216400.0,
|
||||
'last_reset': None,
|
||||
'start': 1733212800.0,
|
||||
'state': 2.37,
|
||||
'sum': 7.11,
|
||||
}),
|
||||
]),
|
||||
})
|
||||
# ---
|
||||
# name: test_statistics[water_cost_statistics][test_statistics_call4]
|
||||
defaultdict({
|
||||
'suez_water:123456_water_cost_statistics': list([
|
||||
dict({
|
||||
'end': 1733043600.0,
|
||||
'last_reset': None,
|
||||
'start': 1733040000.0,
|
||||
'state': 2.37,
|
||||
'sum': 2.37,
|
||||
}),
|
||||
dict({
|
||||
'end': 1733130000.0,
|
||||
'last_reset': None,
|
||||
'start': 1733126400.0,
|
||||
'state': 2.37,
|
||||
'sum': 4.74,
|
||||
}),
|
||||
dict({
|
||||
'end': 1733216400.0,
|
||||
'last_reset': None,
|
||||
'start': 1733212800.0,
|
||||
'state': 2.37,
|
||||
'sum': 7.11,
|
||||
}),
|
||||
dict({
|
||||
'end': 1733389200.0,
|
||||
'last_reset': None,
|
||||
'start': 1733385600.0,
|
||||
'state': 2.37,
|
||||
'sum': 9.48,
|
||||
}),
|
||||
]),
|
||||
})
|
||||
# ---
|
@ -29,7 +29,7 @@
|
||||
'previous_unique_id': None,
|
||||
'supported_features': 0,
|
||||
'translation_key': 'water_price',
|
||||
'unique_id': 'test-counter_water_price',
|
||||
'unique_id': '123456_water_price',
|
||||
'unit_of_measurement': '€',
|
||||
})
|
||||
# ---
|
||||
@ -79,7 +79,7 @@
|
||||
'previous_unique_id': None,
|
||||
'supported_features': 0,
|
||||
'translation_key': 'water_usage_yesterday',
|
||||
'unique_id': 'test-counter_water_usage_yesterday',
|
||||
'unique_id': '123456_water_usage_yesterday',
|
||||
'unit_of_measurement': <UnitOfVolume.LITERS: 'L'>,
|
||||
})
|
||||
# ---
|
||||
|
@ -6,6 +6,7 @@ from pysuez.exception import PySuezError
|
||||
import pytest
|
||||
|
||||
from homeassistant import config_entries
|
||||
from homeassistant.components.recorder import Recorder
|
||||
from homeassistant.components.suez_water.const import CONF_COUNTER_ID, DOMAIN
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.data_entry_flow import FlowResultType
|
||||
@ -70,7 +71,7 @@ async def test_form_invalid_auth(
|
||||
|
||||
|
||||
async def test_form_already_configured(
|
||||
hass: HomeAssistant, suez_client: AsyncMock
|
||||
hass: HomeAssistant, recorder_mock: Recorder, suez_client: AsyncMock
|
||||
) -> None:
|
||||
"""Test we abort when entry is already configured."""
|
||||
|
||||
|
@ -1,30 +1,32 @@
|
||||
"""Test Suez_water integration initialization."""
|
||||
|
||||
from datetime import datetime, timedelta
|
||||
from unittest.mock import AsyncMock
|
||||
|
||||
from homeassistant.components.suez_water.const import CONF_COUNTER_ID, DOMAIN
|
||||
from homeassistant.components.suez_water.coordinator import PySuezError
|
||||
from freezegun.api import FrozenDateTimeFactory
|
||||
import pytest
|
||||
from syrupy.assertion import SnapshotAssertion
|
||||
|
||||
from homeassistant.components.recorder.statistics import statistics_during_period
|
||||
from homeassistant.components.suez_water.const import (
|
||||
CONF_COUNTER_ID,
|
||||
DATA_REFRESH_INTERVAL,
|
||||
DOMAIN,
|
||||
)
|
||||
from homeassistant.components.suez_water.coordinator import (
|
||||
PySuezError,
|
||||
TelemetryMeasure,
|
||||
)
|
||||
from homeassistant.config_entries import ConfigEntryState
|
||||
from homeassistant.const import CONF_USERNAME
|
||||
from homeassistant.core import HomeAssistant
|
||||
import homeassistant.util.dt as dt_util
|
||||
|
||||
from . import setup_integration
|
||||
from .conftest import MOCK_DATA
|
||||
|
||||
from tests.common import MockConfigEntry
|
||||
|
||||
|
||||
async def test_initialization_invalid_credentials(
|
||||
hass: HomeAssistant,
|
||||
suez_client: AsyncMock,
|
||||
mock_config_entry: MockConfigEntry,
|
||||
) -> None:
|
||||
"""Test that suez_water can't be loaded with invalid credentials."""
|
||||
|
||||
suez_client.check_credentials.return_value = False
|
||||
await setup_integration(hass, mock_config_entry)
|
||||
|
||||
assert mock_config_entry.state is ConfigEntryState.SETUP_ERROR
|
||||
from tests.common import MockConfigEntry, async_fire_time_changed
|
||||
from tests.components.recorder.common import async_wait_recording_done
|
||||
|
||||
|
||||
async def test_initialization_setup_api_error(
|
||||
@ -40,6 +42,210 @@ async def test_initialization_setup_api_error(
|
||||
assert mock_config_entry.state is ConfigEntryState.SETUP_RETRY
|
||||
|
||||
|
||||
async def test_init_auth_failed(
|
||||
hass: HomeAssistant,
|
||||
suez_client: AsyncMock,
|
||||
mock_config_entry: MockConfigEntry,
|
||||
) -> None:
|
||||
"""Test that suez_water reflect authentication failure."""
|
||||
suez_client.check_credentials.return_value = False
|
||||
await setup_integration(hass, mock_config_entry)
|
||||
|
||||
assert mock_config_entry.state is ConfigEntryState.SETUP_ERROR
|
||||
|
||||
|
||||
async def test_init_refresh_failed(
|
||||
hass: HomeAssistant,
|
||||
suez_client: AsyncMock,
|
||||
mock_config_entry: MockConfigEntry,
|
||||
) -> None:
|
||||
"""Test that suez_water reflect authentication failure."""
|
||||
suez_client.fetch_aggregated_data.side_effect = PySuezError("Update failed")
|
||||
await setup_integration(hass, mock_config_entry)
|
||||
|
||||
assert mock_config_entry.state is ConfigEntryState.SETUP_RETRY
|
||||
|
||||
|
||||
async def test_init_statistics_failed(
|
||||
hass: HomeAssistant,
|
||||
suez_client: AsyncMock,
|
||||
mock_config_entry: MockConfigEntry,
|
||||
) -> None:
|
||||
"""Test that suez_water reflect authentication failure."""
|
||||
suez_client.fetch_all_daily_data.side_effect = PySuezError("Update failed")
|
||||
await setup_integration(hass, mock_config_entry)
|
||||
|
||||
assert mock_config_entry.state is ConfigEntryState.SETUP_RETRY
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("recorder_mock")
|
||||
async def test_statistics_no_price(
|
||||
hass: HomeAssistant,
|
||||
suez_client: AsyncMock,
|
||||
mock_config_entry: MockConfigEntry,
|
||||
) -> None:
|
||||
"""Test that suez_water statistics does not register when no price."""
|
||||
# New data retrieved but no price
|
||||
suez_client.get_price.side_effect = PySuezError("will fail")
|
||||
suez_client.fetch_all_daily_data.return_value = [
|
||||
TelemetryMeasure(
|
||||
(datetime.now().date()).strftime("%Y-%m-%d %H:%M:%S"), 0.5, 0.5
|
||||
)
|
||||
]
|
||||
|
||||
await setup_integration(hass, mock_config_entry)
|
||||
|
||||
assert mock_config_entry.state is ConfigEntryState.LOADED
|
||||
|
||||
statistic_id = (
|
||||
f"{DOMAIN}:{mock_config_entry.data[CONF_COUNTER_ID]}_water_cost_statistics"
|
||||
)
|
||||
stats = await hass.async_add_executor_job(
|
||||
statistics_during_period,
|
||||
hass,
|
||||
datetime.now() - timedelta(days=1),
|
||||
None,
|
||||
[statistic_id],
|
||||
"hour",
|
||||
None,
|
||||
{"start", "state", "mean", "min", "max", "last_reset", "sum"},
|
||||
)
|
||||
|
||||
assert stats.get(statistic_id) is None
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("recorder_mock")
|
||||
@pytest.mark.parametrize(
|
||||
"statistic",
|
||||
[
|
||||
"water_cost_statistics",
|
||||
"water_consumption_statistics",
|
||||
],
|
||||
)
|
||||
async def test_statistics(
|
||||
hass: HomeAssistant,
|
||||
suez_client: AsyncMock,
|
||||
mock_config_entry: MockConfigEntry,
|
||||
freezer: FrozenDateTimeFactory,
|
||||
snapshot: SnapshotAssertion,
|
||||
statistic: str,
|
||||
) -> None:
|
||||
"""Test that suez_water statistics are working."""
|
||||
nb_samples = 3
|
||||
|
||||
start = datetime.fromisoformat("2024-12-04T02:00:00.0")
|
||||
freezer.move_to(start)
|
||||
|
||||
origin = dt_util.start_of_local_day(start.date()) - timedelta(days=nb_samples)
|
||||
result = [
|
||||
TelemetryMeasure(
|
||||
date=((origin + timedelta(days=d)).date()).strftime("%Y-%m-%d %H:%M:%S"),
|
||||
volume=0.5,
|
||||
index=0.5 * (d + 1),
|
||||
)
|
||||
for d in range(nb_samples)
|
||||
]
|
||||
suez_client.fetch_all_daily_data.return_value = result
|
||||
|
||||
await setup_integration(hass, mock_config_entry)
|
||||
|
||||
assert mock_config_entry.state is ConfigEntryState.LOADED
|
||||
|
||||
# Init data retrieved
|
||||
await _test_for_data(
|
||||
hass,
|
||||
suez_client,
|
||||
snapshot,
|
||||
statistic,
|
||||
origin,
|
||||
mock_config_entry.data[CONF_COUNTER_ID],
|
||||
1,
|
||||
)
|
||||
|
||||
# No new data retrieved
|
||||
suez_client.fetch_all_daily_data.return_value = []
|
||||
freezer.tick(DATA_REFRESH_INTERVAL)
|
||||
async_fire_time_changed(hass)
|
||||
|
||||
await _test_for_data(
|
||||
hass,
|
||||
suez_client,
|
||||
snapshot,
|
||||
statistic,
|
||||
origin,
|
||||
mock_config_entry.data[CONF_COUNTER_ID],
|
||||
2,
|
||||
)
|
||||
# Old data retrieved
|
||||
suez_client.fetch_all_daily_data.return_value = [
|
||||
TelemetryMeasure(
|
||||
date=(origin.date() - timedelta(days=1)).strftime("%Y-%m-%d %H:%M:%S"),
|
||||
volume=0.5,
|
||||
index=0.5 * (121 + 1),
|
||||
)
|
||||
]
|
||||
freezer.tick(DATA_REFRESH_INTERVAL)
|
||||
async_fire_time_changed(hass)
|
||||
|
||||
await _test_for_data(
|
||||
hass,
|
||||
suez_client,
|
||||
snapshot,
|
||||
statistic,
|
||||
origin,
|
||||
mock_config_entry.data[CONF_COUNTER_ID],
|
||||
3,
|
||||
)
|
||||
|
||||
# New daily data retrieved
|
||||
suez_client.fetch_all_daily_data.return_value = [
|
||||
TelemetryMeasure(
|
||||
date=(datetime.now().date()).strftime("%Y-%m-%d %H:%M:%S"),
|
||||
volume=0.5,
|
||||
index=0.5 * (121 + 1),
|
||||
)
|
||||
]
|
||||
freezer.tick(DATA_REFRESH_INTERVAL)
|
||||
async_fire_time_changed(hass)
|
||||
|
||||
await _test_for_data(
|
||||
hass,
|
||||
suez_client,
|
||||
snapshot,
|
||||
statistic,
|
||||
origin,
|
||||
mock_config_entry.data[CONF_COUNTER_ID],
|
||||
4,
|
||||
)
|
||||
|
||||
|
||||
async def _test_for_data(
|
||||
hass: HomeAssistant,
|
||||
suez_client: AsyncMock,
|
||||
snapshot: SnapshotAssertion,
|
||||
statistic: str,
|
||||
origin: datetime,
|
||||
counter_id: str,
|
||||
nb_calls: int,
|
||||
) -> None:
|
||||
await hass.async_block_till_done(True)
|
||||
await async_wait_recording_done(hass)
|
||||
|
||||
assert suez_client.fetch_all_daily_data.call_count == nb_calls
|
||||
statistic_id = f"{DOMAIN}:{counter_id}_{statistic}"
|
||||
stats = await hass.async_add_executor_job(
|
||||
statistics_during_period,
|
||||
hass,
|
||||
origin - timedelta(days=1),
|
||||
None,
|
||||
[statistic_id],
|
||||
"hour",
|
||||
None,
|
||||
{"start", "state", "mean", "min", "max", "last_reset", "sum"},
|
||||
)
|
||||
assert stats == snapshot(name=f"test_statistics_call{nb_calls}")
|
||||
|
||||
|
||||
async def test_migration_version_rollback(
|
||||
hass: HomeAssistant,
|
||||
suez_client: AsyncMock,
|
||||
|
@ -41,16 +41,23 @@ async def test_sensors_valid_state(
|
||||
assert previous.get(str(date.fromisoformat("2024-12-01"))) == 154
|
||||
|
||||
|
||||
@pytest.mark.parametrize("method", [("fetch_aggregated_data"), ("get_price")])
|
||||
@pytest.mark.parametrize(
|
||||
("method", "price_on_error", "consumption_on_error"),
|
||||
[
|
||||
("fetch_aggregated_data", STATE_UNAVAILABLE, STATE_UNAVAILABLE),
|
||||
("get_price", STATE_UNAVAILABLE, "160"),
|
||||
],
|
||||
)
|
||||
async def test_sensors_failed_update(
|
||||
hass: HomeAssistant,
|
||||
suez_client: AsyncMock,
|
||||
mock_config_entry: MockConfigEntry,
|
||||
freezer: FrozenDateTimeFactory,
|
||||
method: str,
|
||||
price_on_error: str,
|
||||
consumption_on_error: str,
|
||||
) -> None:
|
||||
"""Test that suez_water sensor reflect failure when api fails."""
|
||||
|
||||
await setup_integration(hass, mock_config_entry)
|
||||
|
||||
assert mock_config_entry.state is ConfigEntryState.LOADED
|
||||
@ -58,10 +65,10 @@ async def test_sensors_failed_update(
|
||||
entity_ids = await hass.async_add_executor_job(hass.states.entity_ids)
|
||||
assert len(entity_ids) == 2
|
||||
|
||||
for entity in entity_ids:
|
||||
state = hass.states.get(entity)
|
||||
assert entity
|
||||
assert state.state != STATE_UNAVAILABLE
|
||||
state = hass.states.get("sensor.suez_mock_device_water_price")
|
||||
assert state.state == "4.74"
|
||||
state = hass.states.get("sensor.suez_mock_device_water_usage_yesterday")
|
||||
assert state.state == "160"
|
||||
|
||||
getattr(suez_client, method).side_effect = PySuezError("Should fail to update")
|
||||
|
||||
@ -69,7 +76,7 @@ async def test_sensors_failed_update(
|
||||
async_fire_time_changed(hass)
|
||||
await hass.async_block_till_done(True)
|
||||
|
||||
for entity in entity_ids:
|
||||
state = hass.states.get(entity)
|
||||
assert entity
|
||||
assert state.state == STATE_UNAVAILABLE
|
||||
state = hass.states.get("sensor.suez_mock_device_water_price")
|
||||
assert state.state == price_on_error
|
||||
state = hass.states.get("sensor.suez_mock_device_water_usage_yesterday")
|
||||
assert state.state == consumption_on_error
|
||||
|
Loading…
x
Reference in New Issue
Block a user