diff --git a/homeassistant/components/tibber/manifest.json b/homeassistant/components/tibber/manifest.json index 8c7a49256df..b32c74fb5b0 100644 --- a/homeassistant/components/tibber/manifest.json +++ b/homeassistant/components/tibber/manifest.json @@ -1,8 +1,9 @@ { + "dependencies": ["recorder"], "domain": "tibber", "name": "Tibber", "documentation": "https://www.home-assistant.io/integrations/tibber", - "requirements": ["pyTibber==0.21.4"], + "requirements": ["pyTibber==0.21.6"], "codeowners": ["@danielhiversen"], "quality_scale": "silver", "config_flow": true, diff --git a/homeassistant/components/tibber/sensor.py b/homeassistant/components/tibber/sensor.py index 58e9a7c5e6f..bd853e10a21 100644 --- a/homeassistant/components/tibber/sensor.py +++ b/homeassistant/components/tibber/sensor.py @@ -8,6 +8,12 @@ from random import randrange import aiohttp +from homeassistant.components.recorder.models import StatisticData, StatisticMetaData +from homeassistant.components.recorder.statistics import ( + async_add_external_statistics, + get_last_statistics, + statistics_during_period, +) from homeassistant.components.sensor import ( SensorDeviceClass, SensorEntity, @@ -251,13 +257,7 @@ async def async_setup_entry(hass, entry, async_add_entities): ) if home.has_active_subscription and not home.has_real_time_consumption: if coordinator is None: - coordinator = update_coordinator.DataUpdateCoordinator( - hass, - _LOGGER, - name=f"Tibber {tibber_connection.name}", - update_method=tibber_connection.fetch_consumption_data_active_homes, - update_interval=timedelta(hours=1), - ) + coordinator = TibberDataCoordinator(hass, tibber_connection) for entity_description in SENSORS: entities.append(TibberDataSensor(home, coordinator, entity_description)) @@ -514,3 +514,92 @@ class TibberRtDataCoordinator(update_coordinator.DataUpdateCoordinator): _LOGGER.error(errors[0]) return None return self.data.get("data", {}).get("liveMeasurement") + + +class TibberDataCoordinator(update_coordinator.DataUpdateCoordinator): + """Handle Tibber data and insert statistics.""" + + def __init__(self, hass, tibber_connection): + """Initialize the data handler.""" + super().__init__( + hass, + _LOGGER, + name=f"Tibber {tibber_connection.name}", + update_interval=timedelta(hours=1), + ) + self._tibber_connection = tibber_connection + + async def _async_update_data(self): + """Update data via API.""" + await self._tibber_connection.fetch_consumption_data_active_homes() + await self._insert_statistics() + + async def _insert_statistics(self): + """Insert Tibber statistics.""" + for home in self._tibber_connection.get_homes(): + if not home.hourly_consumption_data: + continue + + statistic_id = ( + f"{TIBBER_DOMAIN}:energy_consumption_{home.home_id.replace('-', '')}" + ) + + last_stats = await self.hass.async_add_executor_job( + get_last_statistics, self.hass, 1, statistic_id, True + ) + + if not last_stats: + # First time we insert 5 years of data (if available) + hourly_consumption_data = await home.get_historic_data(5 * 365 * 24) + + _sum = 0 + last_stats_time = None + else: + # hourly_consumption_data contains the last 30 days of consumption data. + # We update the statistics with the last 30 days of data to handle corrections in the data. + hourly_consumption_data = home.hourly_consumption_data + + start = dt_util.parse_datetime( + hourly_consumption_data[0]["from"] + ) - timedelta(hours=1) + stat = await self.hass.async_add_executor_job( + statistics_during_period, + self.hass, + start, + None, + [statistic_id], + "hour", + True, + ) + _sum = stat[statistic_id][0]["sum"] + last_stats_time = stat[statistic_id][0]["start"] + + statistics = [] + + for data in hourly_consumption_data: + if data.get("consumption") is None: + continue + + start = dt_util.parse_datetime(data["from"]) + if last_stats_time is not None and start <= last_stats_time: + continue + + _sum += data["consumption"] + + statistics.append( + StatisticData( + start=start, + state=data["consumption"], + sum=_sum, + ) + ) + + metadata = StatisticMetaData( + has_mean=False, + has_sum=True, + name=f"{home.name} consumption", + source=TIBBER_DOMAIN, + statistic_id=statistic_id, + unit_of_measurement=ENERGY_KILO_WATT_HOUR, + ) + async_add_external_statistics(self.hass, metadata, statistics) diff --git a/requirements_all.txt b/requirements_all.txt index 8598b8c9ad7..28edcb65df5 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1344,7 +1344,7 @@ pyRFXtrx==0.27.0 # pySwitchmate==0.4.6 # homeassistant.components.tibber -pyTibber==0.21.4 +pyTibber==0.21.6 # homeassistant.components.dlink pyW215==0.7.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index a3345536d12..c58d664e7af 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -830,7 +830,7 @@ pyMetno==0.9.0 pyRFXtrx==0.27.0 # homeassistant.components.tibber -pyTibber==0.21.4 +pyTibber==0.21.6 # homeassistant.components.nextbus py_nextbusnext==0.1.5 diff --git a/tests/components/tibber/test_config_flow.py b/tests/components/tibber/test_config_flow.py index 6eaa52ac103..3081323d9b6 100644 --- a/tests/components/tibber/test_config_flow.py +++ b/tests/components/tibber/test_config_flow.py @@ -7,7 +7,7 @@ from homeassistant import config_entries from homeassistant.components.tibber.const import DOMAIN from homeassistant.const import CONF_ACCESS_TOKEN -from tests.common import MockConfigEntry +from tests.common import MockConfigEntry, async_init_recorder_component @pytest.fixture(name="tibber_setup", autouse=True) @@ -19,6 +19,8 @@ def tibber_setup_fixture(): async def test_show_config_form(hass): """Test show configuration form.""" + await async_init_recorder_component(hass) + result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) @@ -29,6 +31,8 @@ async def test_show_config_form(hass): async def test_create_entry(hass): """Test create entry from user input.""" + await async_init_recorder_component(hass) + test_data = { CONF_ACCESS_TOKEN: "valid", } @@ -53,6 +57,8 @@ async def test_create_entry(hass): async def test_flow_entry_already_exists(hass): """Test user input for config_entry that already exists.""" + await async_init_recorder_component(hass) + first_entry = MockConfigEntry( domain="tibber", data={CONF_ACCESS_TOKEN: "valid"}, diff --git a/tests/components/tibber/test_statistics.py b/tests/components/tibber/test_statistics.py new file mode 100644 index 00000000000..d4b994b2ef7 --- /dev/null +++ b/tests/components/tibber/test_statistics.py @@ -0,0 +1,73 @@ +"""Test adding external statistics from Tibber.""" +from unittest.mock import AsyncMock + +from homeassistant.components.recorder.statistics import statistics_during_period +from homeassistant.components.tibber.sensor import TibberDataCoordinator +from homeassistant.util import dt as dt_util + +from tests.common import async_init_recorder_component +from tests.components.recorder.common import async_wait_recording_done_without_instance + +_CONSUMPTION_DATA_1 = [ + { + "from": "2022-01-03T00:00:00.000+01:00", + "totalCost": 1.1, + "consumption": 2.1, + }, + { + "from": "2022-01-03T01:00:00.000+01:00", + "totalCost": 1.2, + "consumption": 2.2, + }, + { + "from": "2022-01-03T02:00:00.000+01:00", + "totalCost": 1.3, + "consumption": 2.3, + }, +] + + +async def test_async_setup_entry(hass): + """Test setup Tibber.""" + await async_init_recorder_component(hass) + + def _get_homes(): + tibber_home = AsyncMock() + tibber_home.home_id = "home_id" + tibber_home.get_historic_data.return_value = _CONSUMPTION_DATA_1 + return [tibber_home] + + tibber_connection = AsyncMock() + tibber_connection.name = "tibber" + tibber_connection.fetch_consumption_data_active_homes.return_value = None + tibber_connection.get_homes = _get_homes + + coordinator = TibberDataCoordinator(hass, tibber_connection) + await coordinator._async_update_data() + await async_wait_recording_done_without_instance(hass) + + statistic_id = "tibber:energy_consumption_home_id" + + stats = await hass.async_add_executor_job( + statistics_during_period, + hass, + dt_util.parse_datetime(_CONSUMPTION_DATA_1[0]["from"]), + None, + [statistic_id], + "hour", + True, + ) + + assert len(stats) == 1 + assert len(stats[statistic_id]) == 3 + _sum = 0 + for k, stat in enumerate(stats[statistic_id]): + assert stat["start"] == dt_util.parse_datetime(_CONSUMPTION_DATA_1[k]["from"]) + assert stat["state"] == _CONSUMPTION_DATA_1[k]["consumption"] + assert stat["mean"] is None + assert stat["min"] is None + assert stat["max"] is None + assert stat["last_reset"] is None + + _sum += _CONSUMPTION_DATA_1[k]["consumption"] + assert stat["sum"] == _sum