Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We鈥檒l occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add external Tibber statistics #62249

Merged
merged 5 commits into from
Jan 5, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
3 changes: 2 additions & 1 deletion homeassistant/components/tibber/manifest.json
Original file line number Diff line number Diff line change
@@ -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,
Expand Down
103 changes: 96 additions & 7 deletions homeassistant/components/tibber/sensor.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,12 @@

import aiohttp

from homeassistant.components.recorder.models import StatisticData, StatisticMetaData
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we need to add the recorder to dependencies of tibber now?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It is added to after_dependencies
Is that correct?

Built-in integrations shall only specify other built-in integrations in after_dependencies.

https://developers.home-assistant.io/docs/creating_integration_manifest/#after-dependencies

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If the dependency isn't always required by the integration or if we need to handle a missing dependency gracefully in a config flow we use after_dependencies. If the dependency is always required we use dependencies .

from homeassistant.components.recorder.statistics import (
async_add_external_statistics,
get_last_statistics,
statistics_during_period,
)
from homeassistant.components.sensor import (
SensorDeviceClass,
SensorEntity,
Expand Down Expand Up @@ -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))

Expand Down Expand Up @@ -514,3 +514,92 @@ def get_live_measurement(self):
_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)
emontnemery marked this conversation as resolved.
Show resolved Hide resolved

_sum = 0
last_stats_time = None
else:
emontnemery marked this conversation as resolved.
Show resolved Hide resolved
# 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)
2 changes: 1 addition & 1 deletion requirements_all.txt
Original file line number Diff line number Diff line change
Expand Up @@ -1339,7 +1339,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
Expand Down
2 changes: 1 addition & 1 deletion requirements_test_all.txt
Original file line number Diff line number Diff line change
Expand Up @@ -820,7 +820,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
Expand Down
8 changes: 7 additions & 1 deletion tests/components/tibber/test_config_flow.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
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)
Expand All @@ -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}
)
Expand All @@ -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",
}
Expand All @@ -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"},
Expand Down
73 changes: 73 additions & 0 deletions tests/components/tibber/test_statistics.py
Original file line number Diff line number Diff line change
@@ -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