diff --git a/homeassistant/components/suez_water/coordinator.py b/homeassistant/components/suez_water/coordinator.py index 10d4d3cdbcb..83283ae8ec5 100644 --- a/homeassistant/components/suez_water/coordinator.py +++ b/homeassistant/components/suez_water/coordinator.py @@ -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 diff --git a/homeassistant/components/suez_water/manifest.json b/homeassistant/components/suez_water/manifest.json index 128f7aa4d8d..9149f216563 100644 --- a/homeassistant/components/suez_water/manifest.json +++ b/homeassistant/components/suez_water/manifest.json @@ -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", diff --git a/homeassistant/components/suez_water/sensor.py b/homeassistant/components/suez_water/sensor.py index a162cc6168d..9bbe24abb59 100644 --- a/homeassistant/components/suez_water/sensor.py +++ b/homeassistant/components/suez_water/sensor.py @@ -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.""" diff --git a/tests/components/suez_water/conftest.py b/tests/components/suez_water/conftest.py index 73557fd3bde..9d29191289e 100644 --- a/tests/components/suez_water/conftest.py +++ b/tests/components/suez_water/conftest.py @@ -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( diff --git a/tests/components/suez_water/snapshots/test_init.ambr b/tests/components/suez_water/snapshots/test_init.ambr new file mode 100644 index 00000000000..24e11654cd0 --- /dev/null +++ b/tests/components/suez_water/snapshots/test_init.ambr @@ -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, + }), + ]), + }) +# --- diff --git a/tests/components/suez_water/snapshots/test_sensor.ambr b/tests/components/suez_water/snapshots/test_sensor.ambr index 536e79df606..0ce631bf1b3 100644 --- a/tests/components/suez_water/snapshots/test_sensor.ambr +++ b/tests/components/suez_water/snapshots/test_sensor.ambr @@ -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': , }) # --- diff --git a/tests/components/suez_water/test_config_flow.py b/tests/components/suez_water/test_config_flow.py index bebb4fd72ac..656c804e4d9 100644 --- a/tests/components/suez_water/test_config_flow.py +++ b/tests/components/suez_water/test_config_flow.py @@ -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.""" diff --git a/tests/components/suez_water/test_init.py b/tests/components/suez_water/test_init.py index 16d32b61dee..ce010f50153 100644 --- a/tests/components/suez_water/test_init.py +++ b/tests/components/suez_water/test_init.py @@ -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, diff --git a/tests/components/suez_water/test_sensor.py b/tests/components/suez_water/test_sensor.py index f9e7ff1f9e6..3ed0d8f0bed 100644 --- a/tests/components/suez_water/test_sensor.py +++ b/tests/components/suez_water/test_sensor.py @@ -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