mirror of
https://github.com/home-assistant/core.git
synced 2026-08-19 12:48:57 +01:00
Improve data updating for Tibber (#168065)
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> Co-authored-by: Martin Hjelmare <marhje52@gmail.com>
This commit is contained in:
co-authored by
Copilot
Martin Hjelmare
parent
aa7474839b
commit
dc8abff6b9
@@ -26,6 +26,7 @@ from .const import AUTH_IMPLEMENTATION, DATA_HASS_CONFIG, DOMAIN, TibberConfigEn
|
||||
from .coordinator import (
|
||||
TibberDataAPICoordinator,
|
||||
TibberDataCoordinator,
|
||||
TibberFetchPriceCoordinator,
|
||||
TibberPriceCoordinator,
|
||||
)
|
||||
from .services import async_setup_services
|
||||
@@ -44,6 +45,7 @@ class TibberRuntimeData:
|
||||
session: OAuth2Session
|
||||
data_api_coordinator: TibberDataAPICoordinator | None = field(default=None)
|
||||
data_coordinator: TibberDataCoordinator | None = field(default=None)
|
||||
fetch_price_coordinator: TibberFetchPriceCoordinator | None = field(default=None)
|
||||
price_coordinator: TibberPriceCoordinator | None = field(default=None)
|
||||
_client: tibber.Tibber | None = None
|
||||
|
||||
@@ -131,7 +133,11 @@ async def async_setup_entry(hass: HomeAssistant, entry: TibberConfigEntry) -> bo
|
||||
raise ConfigEntryNotReady("Fatal HTTP error from Tibber API") from err
|
||||
|
||||
if tibber_connection.get_homes(only_active=True):
|
||||
price_coordinator = TibberPriceCoordinator(hass, entry)
|
||||
fetch_price_coordinator = TibberFetchPriceCoordinator(hass, entry)
|
||||
await fetch_price_coordinator.async_config_entry_first_refresh()
|
||||
entry.runtime_data.fetch_price_coordinator = fetch_price_coordinator
|
||||
|
||||
price_coordinator = TibberPriceCoordinator(hass, entry, fetch_price_coordinator)
|
||||
await price_coordinator.async_config_entry_first_refresh()
|
||||
entry.runtime_data.price_coordinator = price_coordinator
|
||||
|
||||
|
||||
@@ -24,7 +24,7 @@ from homeassistant.components.recorder.statistics import (
|
||||
statistics_during_period,
|
||||
)
|
||||
from homeassistant.const import UnitOfEnergy
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.core import CALLBACK_TYPE, HomeAssistant, callback
|
||||
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
|
||||
from homeassistant.util import dt as dt_util
|
||||
from homeassistant.util.unit_conversion import EnergyConverter
|
||||
@@ -102,7 +102,7 @@ class TibberCoordinator[_DataT](DataUpdateCoordinator[_DataT]):
|
||||
config_entry: TibberConfigEntry,
|
||||
*,
|
||||
name: str,
|
||||
update_interval: timedelta,
|
||||
update_interval: timedelta | None = None,
|
||||
) -> None:
|
||||
"""Initialize the coordinator."""
|
||||
super().__init__(
|
||||
@@ -278,21 +278,54 @@ class TibberDataCoordinator(TibberCoordinator[None]):
|
||||
|
||||
|
||||
class TibberPriceCoordinator(TibberCoordinator[dict[str, TibberHomeData]]):
|
||||
"""Handle Tibber price data and insert statistics."""
|
||||
"""Handle Tibber price data."""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
hass: HomeAssistant,
|
||||
config_entry: TibberConfigEntry,
|
||||
price_fetch_coordinator: TibberFetchPriceCoordinator,
|
||||
) -> None:
|
||||
"""Initialize the price coordinator."""
|
||||
super().__init__(
|
||||
hass,
|
||||
config_entry,
|
||||
name=f"{DOMAIN} price",
|
||||
update_interval=timedelta(minutes=1),
|
||||
)
|
||||
self._tomorrow_price_poll_threshold_seconds = random.uniform(0, 3600 * 10)
|
||||
self._price_fetch_coordinator = price_fetch_coordinator
|
||||
self._unsub_price_fetch_listener: CALLBACK_TYPE | None = None
|
||||
|
||||
@callback
|
||||
def _build_price_data(self) -> dict[str, TibberHomeData]:
|
||||
"""Build derived price data from the fetched Tibber homes."""
|
||||
return {
|
||||
home_id: _build_home_data(home)
|
||||
for home_id, home in (self._price_fetch_coordinator.data or {}).items()
|
||||
}
|
||||
|
||||
@callback
|
||||
def _async_handle_price_fetch_update(self) -> None:
|
||||
"""Update derived price data when fetched prices change."""
|
||||
self.update_interval = self._time_until_next_15_minute()
|
||||
self.async_set_updated_data(self._build_price_data())
|
||||
|
||||
@callback
|
||||
def _schedule_refresh(self) -> None:
|
||||
"""Start listening to fetched price data when entities subscribe."""
|
||||
super()._schedule_refresh()
|
||||
if self._unsub_price_fetch_listener is None:
|
||||
self._unsub_price_fetch_listener = (
|
||||
self._price_fetch_coordinator.async_add_listener(
|
||||
self._async_handle_price_fetch_update
|
||||
)
|
||||
)
|
||||
|
||||
def _unschedule_refresh(self) -> None:
|
||||
"""Stop listening to fetched price data when unused."""
|
||||
super()._unschedule_refresh()
|
||||
if self._unsub_price_fetch_listener is not None:
|
||||
self._unsub_price_fetch_listener()
|
||||
self._unsub_price_fetch_listener = None
|
||||
|
||||
def _time_until_next_15_minute(self) -> timedelta:
|
||||
"""Return time until the next 15-minute boundary (0, 15, 30, 45) in UTC."""
|
||||
@@ -309,7 +342,30 @@ class TibberPriceCoordinator(TibberCoordinator[dict[str, TibberHomeData]]):
|
||||
return next_run - now
|
||||
|
||||
async def _async_update_data(self) -> dict[str, TibberHomeData]:
|
||||
"""Update data via API and return per-home data for sensors."""
|
||||
self.update_interval = self._time_until_next_15_minute()
|
||||
return self._build_price_data()
|
||||
|
||||
|
||||
class TibberFetchPriceCoordinator(TibberCoordinator[dict[str, tibber.TibberHome]]):
|
||||
"""Fetch Tibber price data from the API."""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
hass: HomeAssistant,
|
||||
config_entry: TibberConfigEntry,
|
||||
) -> None:
|
||||
"""Initialize the price coordinator."""
|
||||
super().__init__(
|
||||
hass,
|
||||
config_entry,
|
||||
name=f"{DOMAIN} price fetch",
|
||||
)
|
||||
self._tomorrow_price_poll_threshold_seconds = random.uniform(
|
||||
3600 * 14, 3600 * 22
|
||||
)
|
||||
|
||||
async def _async_update_data(self) -> dict[str, tibber.TibberHome]:
|
||||
"""Fetch latest price data via API and return per-home data."""
|
||||
tibber_connection = await self._async_get_client()
|
||||
active_homes = tibber_connection.get_homes(only_active=True)
|
||||
|
||||
@@ -341,28 +397,31 @@ class TibberPriceCoordinator(TibberCoordinator[dict[str, TibberHomeData]]):
|
||||
return True
|
||||
if _has_prices_tomorrow(home):
|
||||
return False
|
||||
if (today_end - now).total_seconds() < (
|
||||
self._tomorrow_price_poll_threshold_seconds
|
||||
if now >= today_start + timedelta(
|
||||
seconds=self._tomorrow_price_poll_threshold_seconds
|
||||
):
|
||||
return True
|
||||
return False
|
||||
|
||||
homes_to_update = [home for home in active_homes if _needs_update(home)]
|
||||
self.update_interval = timedelta(seconds=random.uniform(60, 60 * 10))
|
||||
|
||||
try:
|
||||
if homes_to_update:
|
||||
await asyncio.gather(
|
||||
*(home.update_info_and_price_info() for home in homes_to_update)
|
||||
await asyncio.gather(
|
||||
*(
|
||||
home.update_info_and_price_info()
|
||||
for home in active_homes
|
||||
if _needs_update(home)
|
||||
)
|
||||
except tibber.RetryableHttpExceptionError as err:
|
||||
raise UpdateFailed(f"Error communicating with API ({err.status})") from err
|
||||
except tibber.FatalHttpExceptionError as err:
|
||||
raise UpdateFailed(f"Error communicating with API ({err.status})") from err
|
||||
)
|
||||
except tibber.exceptions.RateLimitExceededError as err:
|
||||
raise UpdateFailed(
|
||||
f"Rate limit exceeded, retry after {err.retry_after} seconds",
|
||||
retry_after=err.retry_after,
|
||||
) from err
|
||||
except tibber.exceptions.HttpExceptionError as err:
|
||||
raise UpdateFailed(f"Error communicating with API ({err})") from err
|
||||
|
||||
result = {home.home_id: _build_home_data(home) for home in active_homes}
|
||||
|
||||
self.update_interval = self._time_until_next_15_minute()
|
||||
return result
|
||||
return {home.home_id: home for home in active_homes}
|
||||
|
||||
|
||||
class TibberDataAPICoordinator(TibberCoordinator[dict[str, TibberDevice]]):
|
||||
|
||||
@@ -750,7 +750,7 @@ class TibberSensorElPrice(TibberSensor, CoordinatorEntity[TibberPriceCoordinator
|
||||
) -> None:
|
||||
"""Initialize the sensor."""
|
||||
super().__init__(coordinator=coordinator, tibber_home=tibber_home)
|
||||
self._attr_available = False
|
||||
self._price_data_available = False
|
||||
self._attr_native_unit_of_measurement = tibber_home.price_unit
|
||||
self._attr_extra_state_attributes = {
|
||||
"app_nickname": None,
|
||||
@@ -771,6 +771,11 @@ class TibberSensorElPrice(TibberSensor, CoordinatorEntity[TibberPriceCoordinator
|
||||
self._device_name = self._home_name
|
||||
self._update_attributes()
|
||||
|
||||
@property
|
||||
def available(self) -> bool:
|
||||
"""Return if the sensor is available."""
|
||||
return super().available and self._price_data_available
|
||||
|
||||
@callback
|
||||
def _handle_coordinator_update(self) -> None:
|
||||
self._update_attributes()
|
||||
@@ -784,7 +789,8 @@ class TibberSensorElPrice(TibberSensor, CoordinatorEntity[TibberPriceCoordinator
|
||||
(home_data := data.get(self._tibber_home.home_id)) is None
|
||||
or (current_price := home_data.get("current_price")) is None
|
||||
):
|
||||
self._attr_available = False
|
||||
self._price_data_available = False
|
||||
self._attr_native_value = None
|
||||
return
|
||||
|
||||
self._attr_native_unit_of_measurement = home_data.get(
|
||||
@@ -805,7 +811,7 @@ class TibberSensorElPrice(TibberSensor, CoordinatorEntity[TibberPriceCoordinator
|
||||
self._attr_extra_state_attributes["estimated_annual_consumption"] = home_data[
|
||||
"estimated_annual_consumption"
|
||||
]
|
||||
self._attr_available = True
|
||||
self._price_data_available = True
|
||||
|
||||
|
||||
class TibberDataSensor(TibberSensor, CoordinatorEntity[TibberDataCoordinator]):
|
||||
|
||||
@@ -18,6 +18,7 @@ from homeassistant.components.tibber.const import AUTH_IMPLEMENTATION, DOMAIN
|
||||
from homeassistant.const import CONF_ACCESS_TOKEN, Platform
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.setup import async_setup_component
|
||||
from homeassistant.util import dt as dt_util
|
||||
|
||||
from tests.common import MockConfigEntry
|
||||
from tests.typing import RecorderInstanceContextManager
|
||||
@@ -145,6 +146,57 @@ def create_tibber_device(
|
||||
return tibber.data_api.TibberDevice(device_data, home_id=home_id)
|
||||
|
||||
|
||||
def create_tibber_home(
|
||||
*,
|
||||
current_price: float | None = 1.25,
|
||||
price_total: dict[str, float] | None = None,
|
||||
) -> MagicMock:
|
||||
"""Create a mocked Tibber home with an active subscription."""
|
||||
home = MagicMock()
|
||||
home.home_id = "home-id"
|
||||
home.name = "Home"
|
||||
home.currency = "NOK"
|
||||
home.price_unit = "NOK/kWh"
|
||||
home.price_total = price_total or {}
|
||||
home.has_active_subscription = True
|
||||
home.has_real_time_consumption = False
|
||||
home.last_data_timestamp = None
|
||||
home.update_info = AsyncMock(return_value=None)
|
||||
home.update_info_and_price_info = AsyncMock(return_value=None)
|
||||
home.current_price_data = MagicMock(
|
||||
return_value=(current_price, dt_util.utcnow(), 0.4)
|
||||
)
|
||||
home.current_attributes = MagicMock(
|
||||
return_value={
|
||||
"max_price": 1.8,
|
||||
"avg_price": 1.2,
|
||||
"min_price": 0.8,
|
||||
"off_peak_1": 0.9,
|
||||
"peak": 1.7,
|
||||
"off_peak_2": 1.0,
|
||||
}
|
||||
)
|
||||
home.month_cost = 111.1
|
||||
home.peak_hour = 2.5
|
||||
home.peak_hour_time = dt_util.utcnow()
|
||||
home.month_cons = 222.2
|
||||
home.hourly_consumption_data = []
|
||||
home.hourly_production_data = []
|
||||
home.info = {
|
||||
"viewer": {
|
||||
"home": {
|
||||
"appNickname": "Home",
|
||||
"address": {"address1": "Street 1"},
|
||||
"meteringPointData": {
|
||||
"gridCompany": "GridCo",
|
||||
"estimatedAnnualConsumption": 12000,
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
return home
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def config_entry(hass: HomeAssistant) -> MockConfigEntry:
|
||||
"""Tibber config entry."""
|
||||
|
||||
@@ -0,0 +1,252 @@
|
||||
"""Tests for the Tibber coordinators."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from datetime import date, datetime, timedelta
|
||||
from unittest.mock import MagicMock
|
||||
|
||||
from freezegun.api import FrozenDateTimeFactory
|
||||
import pytest
|
||||
import tibber
|
||||
|
||||
from homeassistant.components.recorder import Recorder
|
||||
from homeassistant.components.tibber.const import DOMAIN
|
||||
from homeassistant.const import STATE_UNAVAILABLE, Platform
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers import entity_registry as er
|
||||
from homeassistant.util import dt as dt_util
|
||||
|
||||
from .conftest import create_tibber_home
|
||||
|
||||
from tests.common import MockConfigEntry, async_fire_time_changed
|
||||
|
||||
|
||||
def _prices_for_days(*days: str) -> dict[str, float]:
|
||||
"""Return price data keyed by ISO timestamps for the given days."""
|
||||
return {f"{day}T12:00:00+00:00": 1.0 for day in days}
|
||||
|
||||
|
||||
async def _async_setup_price_sensor(
|
||||
hass: HomeAssistant,
|
||||
config_entry: MockConfigEntry,
|
||||
tibber_mock: MagicMock,
|
||||
entity_registry: er.EntityRegistry,
|
||||
home: MagicMock,
|
||||
) -> str:
|
||||
"""Set up the Tibber config entry and return the price sensor entity id."""
|
||||
tibber_mock.get_homes.return_value = [home]
|
||||
config_entry.data["token"]["expires_at"] = dt_util.utcnow().timestamp() + 86400
|
||||
|
||||
await hass.config_entries.async_setup(config_entry.entry_id)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
entity_id = entity_registry.async_get_entity_id("sensor", DOMAIN, home.home_id)
|
||||
assert entity_id is not None
|
||||
assert hass.states.get(entity_id) is not None
|
||||
return entity_id
|
||||
|
||||
|
||||
async def _async_fire_coordinator_update(
|
||||
hass: HomeAssistant,
|
||||
freezer: FrozenDateTimeFactory,
|
||||
delta: timedelta,
|
||||
) -> None:
|
||||
"""Move time forward and fire scheduled coordinator updates."""
|
||||
freezer.tick(delta)
|
||||
async_fire_time_changed(hass)
|
||||
await hass.async_block_till_done(wait_background_tasks=True)
|
||||
|
||||
|
||||
async def test_price_fetch_refreshes_when_today_prices_are_missing(
|
||||
recorder_mock: Recorder,
|
||||
hass: HomeAssistant,
|
||||
config_entry: MockConfigEntry,
|
||||
tibber_mock: MagicMock,
|
||||
setup_credentials: None,
|
||||
entity_registry: er.EntityRegistry,
|
||||
freezer: FrozenDateTimeFactory,
|
||||
) -> None:
|
||||
"""Test price fetching when cached prices do not include today."""
|
||||
await hass.config.async_set_time_zone("UTC")
|
||||
freezer.move_to("2026-04-26 00:10:00+00:00")
|
||||
home = create_tibber_home(price_total=_prices_for_days("2026-04-25"))
|
||||
|
||||
async def update_info_and_price_info() -> None:
|
||||
home.price_total = _prices_for_days("2026-04-26")
|
||||
|
||||
home.update_info_and_price_info.side_effect = update_info_and_price_info
|
||||
|
||||
await _async_setup_price_sensor(
|
||||
hass, config_entry, tibber_mock, entity_registry, home
|
||||
)
|
||||
|
||||
# Update immediately when no prices are available for today
|
||||
assert home.update_info_and_price_info.await_count == 1
|
||||
|
||||
await _async_fire_coordinator_update(hass, freezer, timedelta(hours=12))
|
||||
|
||||
# No update after 12 hours
|
||||
assert home.update_info_and_price_info.await_count == 1
|
||||
|
||||
|
||||
async def test_price_fetch_waits_until_tomorrow_price_polling_window(
|
||||
recorder_mock: Recorder,
|
||||
hass: HomeAssistant,
|
||||
config_entry: MockConfigEntry,
|
||||
tibber_mock: MagicMock,
|
||||
setup_credentials: None,
|
||||
entity_registry: er.EntityRegistry,
|
||||
freezer: FrozenDateTimeFactory,
|
||||
) -> None:
|
||||
"""Test price fetching waits until the tomorrow-price polling window."""
|
||||
await hass.config.async_set_time_zone("UTC")
|
||||
freezer.move_to("2026-04-26 13:00:00+00:00")
|
||||
home = create_tibber_home(price_total=_prices_for_days("2026-04-26"))
|
||||
|
||||
async def update_info_and_price_info() -> None:
|
||||
home.price_total = _prices_for_days("2026-04-26", "2026-04-27")
|
||||
|
||||
home.update_info_and_price_info.side_effect = update_info_and_price_info
|
||||
|
||||
await _async_setup_price_sensor(
|
||||
hass, config_entry, tibber_mock, entity_registry, home
|
||||
)
|
||||
|
||||
assert home.update_info_and_price_info.await_count == 0
|
||||
|
||||
await _async_fire_coordinator_update(hass, freezer, timedelta(minutes=10))
|
||||
|
||||
# No update before the price polling window has passed
|
||||
assert home.update_info_and_price_info.await_count == 0
|
||||
|
||||
await _async_fire_coordinator_update(hass, freezer, timedelta(hours=9, minutes=50))
|
||||
|
||||
# Update after the price polling window has passed
|
||||
assert home.update_info_and_price_info.await_count == 1
|
||||
|
||||
await _async_fire_coordinator_update(hass, freezer, timedelta(hours=10))
|
||||
|
||||
# Ensure we only update once
|
||||
assert home.update_info_and_price_info.await_count == 1
|
||||
|
||||
|
||||
async def test_price_fetch_skips_update_when_tomorrow_prices_exist(
|
||||
recorder_mock: Recorder,
|
||||
hass: HomeAssistant,
|
||||
config_entry: MockConfigEntry,
|
||||
tibber_mock: MagicMock,
|
||||
setup_credentials: None,
|
||||
entity_registry: er.EntityRegistry,
|
||||
freezer: FrozenDateTimeFactory,
|
||||
) -> None:
|
||||
"""Test price fetching skips homes that already have tomorrow prices."""
|
||||
await hass.config.async_set_time_zone("UTC")
|
||||
freezer.move_to("2026-04-26 23:00:00+00:00")
|
||||
home = create_tibber_home(price_total=_prices_for_days("2026-04-26", "2026-04-27"))
|
||||
|
||||
await _async_setup_price_sensor(
|
||||
hass, config_entry, tibber_mock, entity_registry, home
|
||||
)
|
||||
|
||||
await _async_fire_coordinator_update(hass, freezer, timedelta(hours=10))
|
||||
|
||||
# No update when tomorrow prices are already available
|
||||
assert home.update_info_and_price_info.await_count == 0
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("exception", "expected_message"),
|
||||
[
|
||||
pytest.param(
|
||||
tibber.exceptions.RateLimitExceededError(
|
||||
429, "Too many requests", "RATE_LIMIT", 123
|
||||
),
|
||||
"Rate limit exceeded, retry after 123 seconds",
|
||||
id="rate_limit",
|
||||
),
|
||||
pytest.param(
|
||||
tibber.exceptions.HttpExceptionError(503, "Service unavailable"),
|
||||
"Error communicating with API (Service unavailable)",
|
||||
id="http_error",
|
||||
),
|
||||
],
|
||||
)
|
||||
async def test_price_fetch_refresh_handles_update_exceptions(
|
||||
recorder_mock: Recorder,
|
||||
hass: HomeAssistant,
|
||||
config_entry: MockConfigEntry,
|
||||
tibber_mock: MagicMock,
|
||||
setup_credentials: None,
|
||||
entity_registry: er.EntityRegistry,
|
||||
freezer: FrozenDateTimeFactory,
|
||||
caplog: pytest.LogCaptureFixture,
|
||||
exception: Exception,
|
||||
expected_message: str,
|
||||
) -> None:
|
||||
"""Test handled exceptions during price fetching coordinator refresh."""
|
||||
await hass.config.async_set_time_zone("UTC")
|
||||
freezer.move_to("2026-04-26 23:00:00+00:00")
|
||||
home = create_tibber_home(price_total=_prices_for_days("2026-04-26"))
|
||||
|
||||
entity_id = await _async_setup_price_sensor(
|
||||
hass, config_entry, tibber_mock, entity_registry, home
|
||||
)
|
||||
state = hass.states.get(entity_id)
|
||||
assert state is not None
|
||||
assert state.state != STATE_UNAVAILABLE
|
||||
|
||||
initial_update_count = home.update_info_and_price_info.await_count
|
||||
home.update_info_and_price_info.side_effect = exception
|
||||
|
||||
await _async_fire_coordinator_update(hass, freezer, timedelta(hours=1))
|
||||
|
||||
assert home.update_info_and_price_info.await_count == initial_update_count + 1
|
||||
assert (
|
||||
f"Error fetching {DOMAIN} price fetch data: {expected_message}" in caplog.text
|
||||
)
|
||||
|
||||
state = hass.states.get(entity_id)
|
||||
assert state is not None
|
||||
assert state.state != STATE_UNAVAILABLE
|
||||
|
||||
|
||||
async def test_price_sensor_unavailable_when_cached_prices_run_out(
|
||||
recorder_mock: Recorder,
|
||||
hass: HomeAssistant,
|
||||
config_entry: MockConfigEntry,
|
||||
tibber_mock: MagicMock,
|
||||
setup_credentials: None,
|
||||
entity_registry: er.EntityRegistry,
|
||||
freezer: FrozenDateTimeFactory,
|
||||
) -> None:
|
||||
"""Test price sensor becomes unavailable when cached prices no longer apply."""
|
||||
await hass.config.async_set_time_zone("UTC")
|
||||
freezer.move_to("2026-04-26 23:50:00+00:00")
|
||||
home = create_tibber_home(price_total=_prices_for_days("2026-04-26"))
|
||||
|
||||
def current_price_data() -> tuple[float | None, datetime | None, float | None]:
|
||||
if dt_util.now().date() == date(2026, 4, 26):
|
||||
return (1.25, dt_util.utcnow(), 0.4)
|
||||
return (None, None, None)
|
||||
|
||||
home.current_price_data.side_effect = current_price_data
|
||||
|
||||
entity_id = await _async_setup_price_sensor(
|
||||
hass, config_entry, tibber_mock, entity_registry, home
|
||||
)
|
||||
|
||||
state = hass.states.get(entity_id)
|
||||
assert state is not None
|
||||
assert float(state.state) == 1.25
|
||||
|
||||
await _async_fire_coordinator_update(hass, freezer, timedelta(minutes=10))
|
||||
|
||||
state = hass.states.get(entity_id)
|
||||
assert state is not None
|
||||
assert state.state == STATE_UNAVAILABLE
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def platforms() -> list[Platform]:
|
||||
"""Fixture to specify platforms to test."""
|
||||
return [Platform.SENSOR]
|
||||
@@ -11,59 +11,12 @@ from homeassistant.components.tibber.const import DOMAIN
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers import entity_registry as er
|
||||
from homeassistant.helpers.entity_component import async_update_entity
|
||||
from homeassistant.util import dt as dt_util
|
||||
|
||||
from .conftest import create_tibber_device
|
||||
from .conftest import create_tibber_device, create_tibber_home
|
||||
|
||||
from tests.common import MockConfigEntry
|
||||
|
||||
|
||||
def _create_home(*, current_price: float | None = 1.25) -> MagicMock:
|
||||
"""Create a mocked Tibber home with an active subscription."""
|
||||
home = MagicMock()
|
||||
home.home_id = "home-id"
|
||||
home.name = "Home"
|
||||
home.currency = "NOK"
|
||||
home.price_unit = "NOK/kWh"
|
||||
home.has_active_subscription = True
|
||||
home.has_real_time_consumption = False
|
||||
home.last_data_timestamp = None
|
||||
home.update_info = AsyncMock(return_value=None)
|
||||
home.update_info_and_price_info = AsyncMock(return_value=None)
|
||||
home.current_price_data = MagicMock(
|
||||
return_value=(current_price, dt_util.utcnow(), 0.4)
|
||||
)
|
||||
home.current_attributes = MagicMock(
|
||||
return_value={
|
||||
"max_price": 1.8,
|
||||
"avg_price": 1.2,
|
||||
"min_price": 0.8,
|
||||
"off_peak_1": 0.9,
|
||||
"peak": 1.7,
|
||||
"off_peak_2": 1.0,
|
||||
}
|
||||
)
|
||||
home.month_cost = 111.1
|
||||
home.peak_hour = 2.5
|
||||
home.peak_hour_time = dt_util.utcnow()
|
||||
home.month_cons = 222.2
|
||||
home.hourly_consumption_data = []
|
||||
home.hourly_production_data = []
|
||||
home.info = {
|
||||
"viewer": {
|
||||
"home": {
|
||||
"appNickname": "Home",
|
||||
"address": {"address1": "Street 1"},
|
||||
"meteringPointData": {
|
||||
"gridCompany": "GridCo",
|
||||
"estimatedAnnualConsumption": 12000,
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
return home
|
||||
|
||||
|
||||
async def test_price_sensor_state_unit_and_attributes(
|
||||
recorder_mock: Recorder,
|
||||
hass: HomeAssistant,
|
||||
@@ -73,7 +26,7 @@ async def test_price_sensor_state_unit_and_attributes(
|
||||
entity_registry: er.EntityRegistry,
|
||||
) -> None:
|
||||
"""Test price sensor state and attributes."""
|
||||
home = _create_home(current_price=1.25)
|
||||
home = create_tibber_home(current_price=1.25)
|
||||
tibber_mock.get_homes.return_value = [home]
|
||||
|
||||
await hass.config_entries.async_setup(config_entry.entry_id)
|
||||
|
||||
Reference in New Issue
Block a user