diff --git a/homeassistant/components/energy/websocket_api.py b/homeassistant/components/energy/websocket_api.py index cdc7599b55b..d243faae89f 100644 --- a/homeassistant/components/energy/websocket_api.py +++ b/homeassistant/components/energy/websocket_api.py @@ -274,14 +274,16 @@ async def ws_get_fossil_energy_consumption( ) -> dict[datetime, float]: """Combine multiple statistics, returns a dict indexed by start time.""" result: defaultdict[datetime, float] = defaultdict(float) + seen: defaultdict[datetime, set[str]] = defaultdict(set) for statistics_id, stat in stats.items(): if statistics_id not in statistic_ids: continue for period in stat: - if period["sum"] is None: + if period["sum"] is None or statistics_id in seen[period["start"]]: continue result[period["start"]] += period["sum"] + seen[period["start"]].add(statistics_id) return {key: result[key] for key in sorted(result)} diff --git a/tests/components/energy/test_websocket_api.py b/tests/components/energy/test_websocket_api.py index 46c6a5c0fa6..c1dc195a63e 100644 --- a/tests/components/energy/test_websocket_api.py +++ b/tests/components/energy/test_websocket_api.py @@ -1,9 +1,10 @@ """Test the Energy websocket API.""" -from unittest.mock import AsyncMock, Mock +from unittest.mock import AsyncMock, Mock, patch import pytest from homeassistant.components.energy import data, is_configured +from homeassistant.components.recorder import statistics from homeassistant.components.recorder.statistics import async_add_external_statistics from homeassistant.setup import async_setup_component from homeassistant.util import dt as dt_util @@ -963,6 +964,220 @@ async def test_fossil_energy_consumption(hass, hass_ws_client): } +@pytest.mark.freeze_time("2021-08-01 00:00:00+00:00") +async def test_fossil_energy_consumption_duplicate(hass, hass_ws_client): + """Test fossil_energy_consumption with co2 sensor data.""" + now = dt_util.utcnow() + later = dt_util.as_utc(dt_util.parse_datetime("2022-09-01 00:00:00")) + + await hass.async_add_executor_job(init_recorder_component, hass) + await async_setup_component(hass, "history", {}) + await async_setup_component(hass, "sensor", {}) + + period1 = dt_util.as_utc(dt_util.parse_datetime("2021-09-01 00:00:00")) + period2 = dt_util.as_utc(dt_util.parse_datetime("2021-09-30 23:00:00")) + period2_day_start = dt_util.as_utc(dt_util.parse_datetime("2021-09-30 00:00:00")) + period3 = dt_util.as_utc(dt_util.parse_datetime("2021-10-01 00:00:00")) + period4 = dt_util.as_utc(dt_util.parse_datetime("2021-10-31 23:00:00")) + period4_day_start = dt_util.as_utc(dt_util.parse_datetime("2021-10-31 00:00:00")) + + external_energy_statistics_1 = ( + { + "start": period1, + "last_reset": None, + "state": 0, + "sum": 2, + }, + { + "start": period2, + "last_reset": None, + "state": 1, + "sum": 3, + }, + { + "start": period3, + "last_reset": None, + "state": 2, + "sum": 4, + }, + { + "start": period4, + "last_reset": None, + "state": 3, + "sum": 5, + }, + { + "start": period4, + "last_reset": None, + "state": 3, + "sum": 5, + }, + ) + external_energy_metadata_1 = { + "has_mean": False, + "has_sum": True, + "name": "Total imported energy", + "source": "test", + "statistic_id": "test:total_energy_import_tariff_1", + "unit_of_measurement": "kWh", + } + external_energy_statistics_2 = ( + { + "start": period1, + "last_reset": None, + "state": 0, + "sum": 20, + }, + { + "start": period2, + "last_reset": None, + "state": 1, + "sum": 30, + }, + { + "start": period3, + "last_reset": None, + "state": 2, + "sum": 40, + }, + { + "start": period4, + "last_reset": None, + "state": 3, + "sum": 50, + }, + { + "start": period4, + "last_reset": None, + "state": 3, + "sum": 50, + }, + ) + external_energy_metadata_2 = { + "has_mean": False, + "has_sum": True, + "name": "Total imported energy", + "source": "test", + "statistic_id": "test:total_energy_import_tariff_2", + "unit_of_measurement": "kWh", + } + external_co2_statistics = ( + { + "start": period1, + "last_reset": None, + "mean": 10, + }, + { + "start": period2, + "last_reset": None, + "mean": 30, + }, + { + "start": period3, + "last_reset": None, + "mean": 60, + }, + { + "start": period4, + "last_reset": None, + "mean": 90, + }, + ) + external_co2_metadata = { + "has_mean": True, + "has_sum": False, + "name": "Fossil percentage", + "source": "test", + "statistic_id": "test:fossil_percentage", + "unit_of_measurement": "%", + } + + with patch.object( + statistics, "_statistics_exists", return_value=False + ), patch.object( + statistics, "_insert_statistics", wraps=statistics._insert_statistics + ) as insert_statistics_mock: + async_add_external_statistics( + hass, external_energy_metadata_1, external_energy_statistics_1 + ) + async_add_external_statistics( + hass, external_energy_metadata_2, external_energy_statistics_2 + ) + async_add_external_statistics( + hass, external_co2_metadata, external_co2_statistics + ) + await async_wait_recording_done_without_instance(hass) + assert insert_statistics_mock.call_count == 14 + + client = await hass_ws_client() + await client.send_json( + { + "id": 1, + "type": "energy/fossil_energy_consumption", + "start_time": now.isoformat(), + "end_time": later.isoformat(), + "energy_statistic_ids": [ + "test:total_energy_import_tariff_1", + "test:total_energy_import_tariff_2", + ], + "co2_statistic_id": "test:fossil_percentage", + "period": "hour", + } + ) + response = await client.receive_json() + assert response["success"] + assert response["result"] == { + period2.isoformat(): pytest.approx((33.0 - 22.0) * 0.3), + period3.isoformat(): pytest.approx((44.0 - 33.0) * 0.6), + period4.isoformat(): pytest.approx((55.0 - 44.0) * 0.9), + } + + await client.send_json( + { + "id": 2, + "type": "energy/fossil_energy_consumption", + "start_time": now.isoformat(), + "end_time": later.isoformat(), + "energy_statistic_ids": [ + "test:total_energy_import_tariff_1", + "test:total_energy_import_tariff_2", + ], + "co2_statistic_id": "test:fossil_percentage", + "period": "day", + } + ) + response = await client.receive_json() + assert response["success"] + assert response["result"] == { + period2_day_start.isoformat(): pytest.approx((33.0 - 22.0) * 0.3), + period3.isoformat(): pytest.approx((44.0 - 33.0) * 0.6), + period4_day_start.isoformat(): pytest.approx((55.0 - 44.0) * 0.9), + } + + await client.send_json( + { + "id": 3, + "type": "energy/fossil_energy_consumption", + "start_time": now.isoformat(), + "end_time": later.isoformat(), + "energy_statistic_ids": [ + "test:total_energy_import_tariff_1", + "test:total_energy_import_tariff_2", + ], + "co2_statistic_id": "test:fossil_percentage", + "period": "month", + } + ) + response = await client.receive_json() + assert response["success"] + assert response["result"] == { + period1.isoformat(): pytest.approx((33.0 - 22.0) * 0.3), + period3.isoformat(): pytest.approx( + ((44.0 - 33.0) * 0.6) + ((55.0 - 44.0) * 0.9) + ), + } + + async def test_fossil_energy_consumption_checks(hass, hass_ws_client): """Test fossil_energy_consumption parameter validation.""" client = await hass_ws_client(hass)