Add statistics import to Ista EcoTrend integration (#118788)

* Add statistics import to Ista EcoTrend integration

* Use decorator for fixtures

* define recorder as after_dependency

* Increase test coverage

* remember initial statistic_id

* fix type checking
pull/124063/head
Mr. Bubbles 2024-08-16 16:12:15 +02:00 committed by GitHub
parent cb8a6af12d
commit ea4443f79e
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 837 additions and 2 deletions

View File

@ -1,6 +1,7 @@
{
"domain": "ista_ecotrend",
"name": "ista EcoTrend",
"after_dependencies": ["recorder"],
"codeowners": ["@tr4nt0r"],
"config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/ista_ecotrend",

View File

@ -2,10 +2,21 @@
from __future__ import annotations
import asyncio
from dataclasses import dataclass
import datetime
from enum import StrEnum
import logging
from homeassistant.components.recorder.models.statistics import (
StatisticData,
StatisticMetaData,
)
from homeassistant.components.recorder.statistics import (
async_add_external_statistics,
get_instance,
get_last_statistics,
)
from homeassistant.components.sensor import (
SensorDeviceClass,
SensorEntity,
@ -14,7 +25,11 @@ from homeassistant.components.sensor import (
)
from homeassistant.const import UnitOfEnergy, UnitOfVolume
from homeassistant.core import HomeAssistant
from homeassistant.helpers.device_registry import DeviceEntryType, DeviceInfo
from homeassistant.helpers.device_registry import (
DeviceEntry,
DeviceEntryType,
DeviceInfo,
)
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.helpers.typing import StateType
from homeassistant.helpers.update_coordinator import CoordinatorEntity
@ -22,7 +37,7 @@ from homeassistant.helpers.update_coordinator import CoordinatorEntity
from . import IstaConfigEntry
from .const import DOMAIN
from .coordinator import IstaCoordinator
from .util import IstaConsumptionType, IstaValueType, get_native_value
from .util import IstaConsumptionType, IstaValueType, get_native_value, get_statistics
_LOGGER = logging.getLogger(__name__)
@ -155,6 +170,7 @@ class IstaSensor(CoordinatorEntity[IstaCoordinator], SensorEntity):
entity_description: IstaSensorEntityDescription
_attr_has_entity_name = True
device_entry: DeviceEntry
def __init__(
self,
@ -186,3 +202,81 @@ class IstaSensor(CoordinatorEntity[IstaCoordinator], SensorEntity):
consumption_type=self.entity_description.consumption_type,
value_type=self.entity_description.value_type,
)
async def async_added_to_hass(self) -> None:
"""When added to hass."""
# perform initial statistics import when sensor is added, otherwise it would take
# 1 day when _handle_coordinator_update is triggered for the first time.
await self.update_statistics()
await super().async_added_to_hass()
def _handle_coordinator_update(self) -> None:
"""Handle coordinator update."""
asyncio.run_coroutine_threadsafe(self.update_statistics(), self.hass.loop)
async def update_statistics(self) -> None:
"""Import ista EcoTrend historical statistics."""
# Remember the statistic_id that was initially created
# in case the entity gets renamed, because we cannot
# change the statistic_id
name = self.coordinator.config_entry.options.get(
f"lts_{self.entity_description.key}_{self.consumption_unit}"
)
if not name:
name = self.entity_id.removeprefix("sensor.")
self.hass.config_entries.async_update_entry(
entry=self.coordinator.config_entry,
options={
**self.coordinator.config_entry.options,
f"lts_{self.entity_description.key}_{self.consumption_unit}": name,
},
)
statistic_id = f"{DOMAIN}:{name}"
statistics_sum = 0.0
statistics_since = None
last_stats = await get_instance(self.hass).async_add_executor_job(
get_last_statistics,
self.hass,
1,
statistic_id,
False,
{"sum"},
)
_LOGGER.debug("Last statistics: %s", last_stats)
if last_stats:
statistics_sum = last_stats[statistic_id][0].get("sum") or 0.0
statistics_since = datetime.datetime.fromtimestamp(
last_stats[statistic_id][0].get("end") or 0, tz=datetime.UTC
) + datetime.timedelta(days=1)
if monthly_consumptions := get_statistics(
self.coordinator.data[self.consumption_unit],
self.entity_description.consumption_type,
self.entity_description.value_type,
):
statistics: list[StatisticData] = [
{
"start": consumptions["date"],
"state": consumptions["value"],
"sum": (statistics_sum := statistics_sum + consumptions["value"]),
}
for consumptions in monthly_consumptions
if statistics_since is None or consumptions["date"] > statistics_since
]
metadata: StatisticMetaData = {
"has_mean": False,
"has_sum": True,
"name": f"{self.device_entry.name} {self.name}",
"source": DOMAIN,
"statistic_id": statistic_id,
"unit_of_measurement": self.entity_description.native_unit_of_measurement,
}
if statistics:
_LOGGER.debug("Insert statistics: %s %s", metadata, statistics)
async_add_external_statistics(self.hass, metadata, statistics)

View File

@ -166,3 +166,52 @@ def get_consumption_data(obj_uuid: str | None = None) -> dict[str, Any]:
},
],
}
def extend_statistics(obj_uuid: str | None = None) -> dict[str, Any]:
"""Extend statistics data with new values."""
stats = get_consumption_data(obj_uuid)
stats["costs"].insert(
0,
{
"date": {"month": 6, "year": 2024},
"costsByEnergyType": [
{
"type": "heating",
"value": 9000,
},
{
"type": "warmwater",
"value": 9000,
},
{
"type": "water",
"value": 9000,
},
],
},
)
stats["consumptions"].insert(
0,
{
"date": {"month": 6, "year": 2024},
"readings": [
{
"type": "heating",
"value": "9000",
"additionalValue": "9000,0",
},
{
"type": "warmwater",
"value": "9999,0",
"additionalValue": "90000,0",
},
{
"type": "water",
"value": "9000,0",
},
],
},
)
return stats

View File

@ -0,0 +1,609 @@
# serializer version: 1
# name: test_statistics_import[ista_ecotrend:bahnhofsstr_1a_heating_2months]
list([
dict({
'end': 1714546800.0,
'start': 1711954800.0,
'state': 104.0,
'sum': 104.0,
}),
dict({
'end': 1717225200.0,
'start': 1714546800.0,
'state': 35.0,
'sum': 139.0,
}),
])
# ---
# name: test_statistics_import[ista_ecotrend:bahnhofsstr_1a_heating_3months]
list([
dict({
'end': 1714546800.0,
'start': 1711954800.0,
'state': 104.0,
'sum': 104.0,
}),
dict({
'end': 1717225200.0,
'start': 1714546800.0,
'state': 35.0,
'sum': 139.0,
}),
dict({
'end': 1719817200.0,
'start': 1717225200.0,
'state': 9000.0,
'sum': 9139.0,
}),
])
# ---
# name: test_statistics_import[ista_ecotrend:bahnhofsstr_1a_heating_cost_2months]
list([
dict({
'end': 1714546800.0,
'start': 1711954800.0,
'state': 62.0,
'sum': 62.0,
}),
dict({
'end': 1717225200.0,
'start': 1714546800.0,
'state': 21.0,
'sum': 83.0,
}),
])
# ---
# name: test_statistics_import[ista_ecotrend:bahnhofsstr_1a_heating_cost_3months]
list([
dict({
'end': 1714546800.0,
'start': 1711954800.0,
'state': 62.0,
'sum': 62.0,
}),
dict({
'end': 1717225200.0,
'start': 1714546800.0,
'state': 21.0,
'sum': 83.0,
}),
dict({
'end': 1719817200.0,
'start': 1717225200.0,
'state': 9000.0,
'sum': 9083.0,
}),
])
# ---
# name: test_statistics_import[ista_ecotrend:bahnhofsstr_1a_heating_energy_2months]
list([
dict({
'end': 1714546800.0,
'start': 1711954800.0,
'state': 113.0,
'sum': 113.0,
}),
dict({
'end': 1717225200.0,
'start': 1714546800.0,
'state': 38.0,
'sum': 151.0,
}),
])
# ---
# name: test_statistics_import[ista_ecotrend:bahnhofsstr_1a_heating_energy_3months]
list([
dict({
'end': 1714546800.0,
'start': 1711954800.0,
'state': 113.0,
'sum': 113.0,
}),
dict({
'end': 1717225200.0,
'start': 1714546800.0,
'state': 38.0,
'sum': 151.0,
}),
dict({
'end': 1719817200.0,
'start': 1717225200.0,
'state': 9000.0,
'sum': 9151.0,
}),
])
# ---
# name: test_statistics_import[ista_ecotrend:bahnhofsstr_1a_hot_water_2months]
list([
dict({
'end': 1714546800.0,
'start': 1711954800.0,
'state': 1.1,
'sum': 1.1,
}),
dict({
'end': 1717225200.0,
'start': 1714546800.0,
'state': 1.0,
'sum': 2.1,
}),
])
# ---
# name: test_statistics_import[ista_ecotrend:bahnhofsstr_1a_hot_water_3months]
list([
dict({
'end': 1714546800.0,
'start': 1711954800.0,
'state': 1.1,
'sum': 1.1,
}),
dict({
'end': 1717225200.0,
'start': 1714546800.0,
'state': 1.0,
'sum': 2.1,
}),
dict({
'end': 1719817200.0,
'start': 1717225200.0,
'state': 9999.0,
'sum': 10001.1,
}),
])
# ---
# name: test_statistics_import[ista_ecotrend:bahnhofsstr_1a_hot_water_cost_2months]
list([
dict({
'end': 1714546800.0,
'start': 1711954800.0,
'state': 7.0,
'sum': 7.0,
}),
dict({
'end': 1717225200.0,
'start': 1714546800.0,
'state': 7.0,
'sum': 14.0,
}),
])
# ---
# name: test_statistics_import[ista_ecotrend:bahnhofsstr_1a_hot_water_cost_3months]
list([
dict({
'end': 1714546800.0,
'start': 1711954800.0,
'state': 7.0,
'sum': 7.0,
}),
dict({
'end': 1717225200.0,
'start': 1714546800.0,
'state': 7.0,
'sum': 14.0,
}),
dict({
'end': 1719817200.0,
'start': 1717225200.0,
'state': 9000.0,
'sum': 9014.0,
}),
])
# ---
# name: test_statistics_import[ista_ecotrend:bahnhofsstr_1a_hot_water_energy_2months]
list([
dict({
'end': 1714546800.0,
'start': 1711954800.0,
'state': 61.1,
'sum': 61.1,
}),
dict({
'end': 1717225200.0,
'start': 1714546800.0,
'state': 57.0,
'sum': 118.1,
}),
])
# ---
# name: test_statistics_import[ista_ecotrend:bahnhofsstr_1a_hot_water_energy_3months]
list([
dict({
'end': 1714546800.0,
'start': 1711954800.0,
'state': 61.1,
'sum': 61.1,
}),
dict({
'end': 1717225200.0,
'start': 1714546800.0,
'state': 57.0,
'sum': 118.1,
}),
dict({
'end': 1719817200.0,
'start': 1717225200.0,
'state': 90000.0,
'sum': 90118.1,
}),
])
# ---
# name: test_statistics_import[ista_ecotrend:bahnhofsstr_1a_water_2months]
list([
dict({
'end': 1714546800.0,
'start': 1711954800.0,
'state': 6.8,
'sum': 6.8,
}),
dict({
'end': 1717225200.0,
'start': 1714546800.0,
'state': 5.0,
'sum': 11.8,
}),
])
# ---
# name: test_statistics_import[ista_ecotrend:bahnhofsstr_1a_water_3months]
list([
dict({
'end': 1714546800.0,
'start': 1711954800.0,
'state': 6.8,
'sum': 6.8,
}),
dict({
'end': 1717225200.0,
'start': 1714546800.0,
'state': 5.0,
'sum': 11.8,
}),
dict({
'end': 1719817200.0,
'start': 1717225200.0,
'state': 9000.0,
'sum': 9011.8,
}),
])
# ---
# name: test_statistics_import[ista_ecotrend:bahnhofsstr_1a_water_cost_2months]
list([
dict({
'end': 1714546800.0,
'start': 1711954800.0,
'state': 2.0,
'sum': 2.0,
}),
dict({
'end': 1717225200.0,
'start': 1714546800.0,
'state': 3.0,
'sum': 5.0,
}),
])
# ---
# name: test_statistics_import[ista_ecotrend:bahnhofsstr_1a_water_cost_3months]
list([
dict({
'end': 1714546800.0,
'start': 1711954800.0,
'state': 2.0,
'sum': 2.0,
}),
dict({
'end': 1717225200.0,
'start': 1714546800.0,
'state': 3.0,
'sum': 5.0,
}),
dict({
'end': 1719817200.0,
'start': 1717225200.0,
'state': 9000.0,
'sum': 9005.0,
}),
])
# ---
# name: test_statistics_import[ista_ecotrend:luxemburger_str_1_heating_2months]
list([
dict({
'end': 1714546800.0,
'start': 1711954800.0,
'state': 104.0,
'sum': 104.0,
}),
dict({
'end': 1717225200.0,
'start': 1714546800.0,
'state': 35.0,
'sum': 139.0,
}),
])
# ---
# name: test_statistics_import[ista_ecotrend:luxemburger_str_1_heating_3months]
list([
dict({
'end': 1714546800.0,
'start': 1711954800.0,
'state': 104.0,
'sum': 104.0,
}),
dict({
'end': 1717225200.0,
'start': 1714546800.0,
'state': 35.0,
'sum': 139.0,
}),
dict({
'end': 1719817200.0,
'start': 1717225200.0,
'state': 9000.0,
'sum': 9139.0,
}),
])
# ---
# name: test_statistics_import[ista_ecotrend:luxemburger_str_1_heating_cost_2months]
list([
dict({
'end': 1714546800.0,
'start': 1711954800.0,
'state': 62.0,
'sum': 62.0,
}),
dict({
'end': 1717225200.0,
'start': 1714546800.0,
'state': 21.0,
'sum': 83.0,
}),
])
# ---
# name: test_statistics_import[ista_ecotrend:luxemburger_str_1_heating_cost_3months]
list([
dict({
'end': 1714546800.0,
'start': 1711954800.0,
'state': 62.0,
'sum': 62.0,
}),
dict({
'end': 1717225200.0,
'start': 1714546800.0,
'state': 21.0,
'sum': 83.0,
}),
dict({
'end': 1719817200.0,
'start': 1717225200.0,
'state': 9000.0,
'sum': 9083.0,
}),
])
# ---
# name: test_statistics_import[ista_ecotrend:luxemburger_str_1_heating_energy_2months]
list([
dict({
'end': 1714546800.0,
'start': 1711954800.0,
'state': 113.0,
'sum': 113.0,
}),
dict({
'end': 1717225200.0,
'start': 1714546800.0,
'state': 38.0,
'sum': 151.0,
}),
])
# ---
# name: test_statistics_import[ista_ecotrend:luxemburger_str_1_heating_energy_3months]
list([
dict({
'end': 1714546800.0,
'start': 1711954800.0,
'state': 113.0,
'sum': 113.0,
}),
dict({
'end': 1717225200.0,
'start': 1714546800.0,
'state': 38.0,
'sum': 151.0,
}),
dict({
'end': 1719817200.0,
'start': 1717225200.0,
'state': 9000.0,
'sum': 9151.0,
}),
])
# ---
# name: test_statistics_import[ista_ecotrend:luxemburger_str_1_hot_water_2months]
list([
dict({
'end': 1714546800.0,
'start': 1711954800.0,
'state': 1.1,
'sum': 1.1,
}),
dict({
'end': 1717225200.0,
'start': 1714546800.0,
'state': 1.0,
'sum': 2.1,
}),
])
# ---
# name: test_statistics_import[ista_ecotrend:luxemburger_str_1_hot_water_3months]
list([
dict({
'end': 1714546800.0,
'start': 1711954800.0,
'state': 1.1,
'sum': 1.1,
}),
dict({
'end': 1717225200.0,
'start': 1714546800.0,
'state': 1.0,
'sum': 2.1,
}),
dict({
'end': 1719817200.0,
'start': 1717225200.0,
'state': 9999.0,
'sum': 10001.1,
}),
])
# ---
# name: test_statistics_import[ista_ecotrend:luxemburger_str_1_hot_water_cost_2months]
list([
dict({
'end': 1714546800.0,
'start': 1711954800.0,
'state': 7.0,
'sum': 7.0,
}),
dict({
'end': 1717225200.0,
'start': 1714546800.0,
'state': 7.0,
'sum': 14.0,
}),
])
# ---
# name: test_statistics_import[ista_ecotrend:luxemburger_str_1_hot_water_cost_3months]
list([
dict({
'end': 1714546800.0,
'start': 1711954800.0,
'state': 7.0,
'sum': 7.0,
}),
dict({
'end': 1717225200.0,
'start': 1714546800.0,
'state': 7.0,
'sum': 14.0,
}),
dict({
'end': 1719817200.0,
'start': 1717225200.0,
'state': 9000.0,
'sum': 9014.0,
}),
])
# ---
# name: test_statistics_import[ista_ecotrend:luxemburger_str_1_hot_water_energy_2months]
list([
dict({
'end': 1714546800.0,
'start': 1711954800.0,
'state': 61.1,
'sum': 61.1,
}),
dict({
'end': 1717225200.0,
'start': 1714546800.0,
'state': 57.0,
'sum': 118.1,
}),
])
# ---
# name: test_statistics_import[ista_ecotrend:luxemburger_str_1_hot_water_energy_3months]
list([
dict({
'end': 1714546800.0,
'start': 1711954800.0,
'state': 61.1,
'sum': 61.1,
}),
dict({
'end': 1717225200.0,
'start': 1714546800.0,
'state': 57.0,
'sum': 118.1,
}),
dict({
'end': 1719817200.0,
'start': 1717225200.0,
'state': 90000.0,
'sum': 90118.1,
}),
])
# ---
# name: test_statistics_import[ista_ecotrend:luxemburger_str_1_water_2months]
list([
dict({
'end': 1714546800.0,
'start': 1711954800.0,
'state': 6.8,
'sum': 6.8,
}),
dict({
'end': 1717225200.0,
'start': 1714546800.0,
'state': 5.0,
'sum': 11.8,
}),
])
# ---
# name: test_statistics_import[ista_ecotrend:luxemburger_str_1_water_3months]
list([
dict({
'end': 1714546800.0,
'start': 1711954800.0,
'state': 6.8,
'sum': 6.8,
}),
dict({
'end': 1717225200.0,
'start': 1714546800.0,
'state': 5.0,
'sum': 11.8,
}),
dict({
'end': 1719817200.0,
'start': 1717225200.0,
'state': 9000.0,
'sum': 9011.8,
}),
])
# ---
# name: test_statistics_import[ista_ecotrend:luxemburger_str_1_water_cost_2months]
list([
dict({
'end': 1714546800.0,
'start': 1711954800.0,
'state': 2.0,
'sum': 2.0,
}),
dict({
'end': 1717225200.0,
'start': 1714546800.0,
'state': 3.0,
'sum': 5.0,
}),
])
# ---
# name: test_statistics_import[ista_ecotrend:luxemburger_str_1_water_cost_3months]
list([
dict({
'end': 1714546800.0,
'start': 1711954800.0,
'state': 2.0,
'sum': 2.0,
}),
dict({
'end': 1717225200.0,
'start': 1714546800.0,
'state': 3.0,
'sum': 5.0,
}),
dict({
'end': 1719817200.0,
'start': 1717225200.0,
'state': 9000.0,
'sum': 9005.0,
}),
])
# ---

View File

@ -0,0 +1,82 @@
"""Tests for the ista EcoTrend Statistics import."""
import datetime
from unittest.mock import MagicMock
from freezegun.api import FrozenDateTimeFactory
import pytest
from syrupy.assertion import SnapshotAssertion
from homeassistant.components.recorder.statistics import statistics_during_period
from homeassistant.config_entries import ConfigEntryState
from homeassistant.core import HomeAssistant
from homeassistant.helpers import entity_registry as er
from .conftest import extend_statistics
from tests.common import MockConfigEntry
from tests.components.recorder.common import async_wait_recording_done
@pytest.mark.usefixtures("recorder_mock", "entity_registry_enabled_by_default")
async def test_statistics_import(
hass: HomeAssistant,
ista_config_entry: MockConfigEntry,
entity_registry: er.EntityRegistry,
mock_ista: MagicMock,
snapshot: SnapshotAssertion,
freezer: FrozenDateTimeFactory,
) -> None:
"""Test setup of ista EcoTrend sensor platform."""
ista_config_entry.add_to_hass(hass)
await hass.config_entries.async_setup(ista_config_entry.entry_id)
await hass.async_block_till_done()
assert ista_config_entry.state is ConfigEntryState.LOADED
entities = er.async_entries_for_config_entry(
entity_registry, ista_config_entry.entry_id
)
await async_wait_recording_done(hass)
# Test that consumption statistics for 2 months have been added
for entity in entities:
statistic_id = f"ista_ecotrend:{entity.entity_id.removeprefix("sensor.")}"
stats = await hass.async_add_executor_job(
statistics_during_period,
hass,
datetime.datetime.fromtimestamp(0, tz=datetime.UTC),
None,
{statistic_id},
"month",
None,
{"state", "sum"},
)
assert stats[statistic_id] == snapshot(name=f"{statistic_id}_2months")
assert len(stats[statistic_id]) == 2
# Add another monthly consumption and forward
# 1 day and test if the new values have been
# appended to the statistics
mock_ista.get_consumption_data = extend_statistics
freezer.tick(datetime.timedelta(days=1))
await async_wait_recording_done(hass)
freezer.tick(datetime.timedelta(days=1))
await async_wait_recording_done(hass)
for entity in entities:
statistic_id = f"ista_ecotrend:{entity.entity_id.removeprefix("sensor.")}"
stats = await hass.async_add_executor_job(
statistics_during_period,
hass,
datetime.datetime.fromtimestamp(0, tz=datetime.UTC),
None,
{statistic_id},
"month",
None,
{"state", "sum"},
)
assert stats[statistic_id] == snapshot(name=f"{statistic_id}_3months")
assert len(stats[statistic_id]) == 3