Add external Tibber statistics (#62249)
* Tibber, add external statistics Signed-off-by: Daniel Hjelseth Høyer <github@dahoiv.net> * Tibber, add external statistics Signed-off-by: Daniel Hjelseth Høyer <github@dahoiv.net> * Tibber ext stats Signed-off-by: Daniel Hjelseth Høyer <github@dahoiv.net> * Add tests Signed-off-by: Daniel Hjelseth Høyer <github@dahoiv.net> * name Signed-off-by: Daniel Hjelseth Høyer <github@dahoiv.net>
This commit is contained in:
parent
56520b69ac
commit
ba1b09a3a5
6 changed files with 180 additions and 11 deletions
|
@ -1,8 +1,9 @@
|
||||||
{
|
{
|
||||||
|
"dependencies": ["recorder"],
|
||||||
"domain": "tibber",
|
"domain": "tibber",
|
||||||
"name": "Tibber",
|
"name": "Tibber",
|
||||||
"documentation": "https://www.home-assistant.io/integrations/tibber",
|
"documentation": "https://www.home-assistant.io/integrations/tibber",
|
||||||
"requirements": ["pyTibber==0.21.4"],
|
"requirements": ["pyTibber==0.21.6"],
|
||||||
"codeowners": ["@danielhiversen"],
|
"codeowners": ["@danielhiversen"],
|
||||||
"quality_scale": "silver",
|
"quality_scale": "silver",
|
||||||
"config_flow": true,
|
"config_flow": true,
|
||||||
|
|
|
@ -8,6 +8,12 @@ from random import randrange
|
||||||
|
|
||||||
import aiohttp
|
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 (
|
from homeassistant.components.sensor import (
|
||||||
SensorDeviceClass,
|
SensorDeviceClass,
|
||||||
SensorEntity,
|
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 home.has_active_subscription and not home.has_real_time_consumption:
|
||||||
if coordinator is None:
|
if coordinator is None:
|
||||||
coordinator = update_coordinator.DataUpdateCoordinator(
|
coordinator = TibberDataCoordinator(hass, tibber_connection)
|
||||||
hass,
|
|
||||||
_LOGGER,
|
|
||||||
name=f"Tibber {tibber_connection.name}",
|
|
||||||
update_method=tibber_connection.fetch_consumption_data_active_homes,
|
|
||||||
update_interval=timedelta(hours=1),
|
|
||||||
)
|
|
||||||
for entity_description in SENSORS:
|
for entity_description in SENSORS:
|
||||||
entities.append(TibberDataSensor(home, coordinator, entity_description))
|
entities.append(TibberDataSensor(home, coordinator, entity_description))
|
||||||
|
|
||||||
|
@ -514,3 +514,92 @@ class TibberRtDataCoordinator(update_coordinator.DataUpdateCoordinator):
|
||||||
_LOGGER.error(errors[0])
|
_LOGGER.error(errors[0])
|
||||||
return None
|
return None
|
||||||
return self.data.get("data", {}).get("liveMeasurement")
|
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)
|
||||||
|
|
|
@ -1344,7 +1344,7 @@ pyRFXtrx==0.27.0
|
||||||
# pySwitchmate==0.4.6
|
# pySwitchmate==0.4.6
|
||||||
|
|
||||||
# homeassistant.components.tibber
|
# homeassistant.components.tibber
|
||||||
pyTibber==0.21.4
|
pyTibber==0.21.6
|
||||||
|
|
||||||
# homeassistant.components.dlink
|
# homeassistant.components.dlink
|
||||||
pyW215==0.7.0
|
pyW215==0.7.0
|
||||||
|
|
|
@ -830,7 +830,7 @@ pyMetno==0.9.0
|
||||||
pyRFXtrx==0.27.0
|
pyRFXtrx==0.27.0
|
||||||
|
|
||||||
# homeassistant.components.tibber
|
# homeassistant.components.tibber
|
||||||
pyTibber==0.21.4
|
pyTibber==0.21.6
|
||||||
|
|
||||||
# homeassistant.components.nextbus
|
# homeassistant.components.nextbus
|
||||||
py_nextbusnext==0.1.5
|
py_nextbusnext==0.1.5
|
||||||
|
|
|
@ -7,7 +7,7 @@ from homeassistant import config_entries
|
||||||
from homeassistant.components.tibber.const import DOMAIN
|
from homeassistant.components.tibber.const import DOMAIN
|
||||||
from homeassistant.const import CONF_ACCESS_TOKEN
|
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)
|
@pytest.fixture(name="tibber_setup", autouse=True)
|
||||||
|
@ -19,6 +19,8 @@ def tibber_setup_fixture():
|
||||||
|
|
||||||
async def test_show_config_form(hass):
|
async def test_show_config_form(hass):
|
||||||
"""Test show configuration form."""
|
"""Test show configuration form."""
|
||||||
|
await async_init_recorder_component(hass)
|
||||||
|
|
||||||
result = await hass.config_entries.flow.async_init(
|
result = await hass.config_entries.flow.async_init(
|
||||||
DOMAIN, context={"source": config_entries.SOURCE_USER}
|
DOMAIN, context={"source": config_entries.SOURCE_USER}
|
||||||
)
|
)
|
||||||
|
@ -29,6 +31,8 @@ async def test_show_config_form(hass):
|
||||||
|
|
||||||
async def test_create_entry(hass):
|
async def test_create_entry(hass):
|
||||||
"""Test create entry from user input."""
|
"""Test create entry from user input."""
|
||||||
|
await async_init_recorder_component(hass)
|
||||||
|
|
||||||
test_data = {
|
test_data = {
|
||||||
CONF_ACCESS_TOKEN: "valid",
|
CONF_ACCESS_TOKEN: "valid",
|
||||||
}
|
}
|
||||||
|
@ -53,6 +57,8 @@ async def test_create_entry(hass):
|
||||||
|
|
||||||
async def test_flow_entry_already_exists(hass):
|
async def test_flow_entry_already_exists(hass):
|
||||||
"""Test user input for config_entry that already exists."""
|
"""Test user input for config_entry that already exists."""
|
||||||
|
await async_init_recorder_component(hass)
|
||||||
|
|
||||||
first_entry = MockConfigEntry(
|
first_entry = MockConfigEntry(
|
||||||
domain="tibber",
|
domain="tibber",
|
||||||
data={CONF_ACCESS_TOKEN: "valid"},
|
data={CONF_ACCESS_TOKEN: "valid"},
|
||||||
|
|
73
tests/components/tibber/test_statistics.py
Normal file
73
tests/components/tibber/test_statistics.py
Normal file
|
@ -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
|
Loading…
Add table
Add a link
Reference in a new issue