mirror of
https://github.com/home-assistant/core.git
synced 2026-02-14 11:16:08 +01:00
Compare commits
1 Commits
frontend-d
...
tibber_ref
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c199391cde |
@@ -23,7 +23,7 @@ from homeassistant.helpers.typing import ConfigType
|
||||
from homeassistant.util import dt as dt_util, ssl as ssl_util
|
||||
|
||||
from .const import AUTH_IMPLEMENTATION, DATA_HASS_CONFIG, DOMAIN, TibberConfigEntry
|
||||
from .coordinator import TibberDataAPICoordinator
|
||||
from .coordinator import TibberDataAPICoordinator, TibberDataCoordinator
|
||||
from .services import async_setup_services
|
||||
|
||||
PLATFORMS = [Platform.BINARY_SENSOR, Platform.NOTIFY, Platform.SENSOR]
|
||||
@@ -39,6 +39,7 @@ class TibberRuntimeData:
|
||||
|
||||
session: OAuth2Session
|
||||
data_api_coordinator: TibberDataAPICoordinator | None = field(default=None)
|
||||
data_coordinator: TibberDataCoordinator | None = field(default=None)
|
||||
_client: tibber.Tibber | None = None
|
||||
|
||||
async def async_get_client(self, hass: HomeAssistant) -> tibber.Tibber:
|
||||
@@ -124,9 +125,13 @@ async def async_setup_entry(hass: HomeAssistant, entry: TibberConfigEntry) -> bo
|
||||
except tibber.FatalHttpExceptionError as err:
|
||||
raise ConfigEntryNotReady("Fatal HTTP error from Tibber API") from err
|
||||
|
||||
coordinator = TibberDataAPICoordinator(hass, entry)
|
||||
await coordinator.async_config_entry_first_refresh()
|
||||
entry.runtime_data.data_api_coordinator = coordinator
|
||||
data_api_coordinator = TibberDataAPICoordinator(hass, entry)
|
||||
await data_api_coordinator.async_config_entry_first_refresh()
|
||||
entry.runtime_data.data_api_coordinator = data_api_coordinator
|
||||
|
||||
data_coordinator = TibberDataCoordinator(hass, entry, entry.runtime_data)
|
||||
await data_coordinator.async_config_entry_first_refresh()
|
||||
entry.runtime_data.data_coordinator = data_coordinator
|
||||
|
||||
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
|
||||
return True
|
||||
|
||||
@@ -2,9 +2,11 @@
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from datetime import timedelta
|
||||
from collections.abc import Callable
|
||||
from dataclasses import dataclass
|
||||
from datetime import datetime, timedelta
|
||||
import logging
|
||||
from typing import TYPE_CHECKING, cast
|
||||
from typing import TYPE_CHECKING, Any, cast
|
||||
|
||||
from aiohttp.client_exceptions import ClientError
|
||||
import tibber
|
||||
@@ -21,8 +23,9 @@ from homeassistant.components.recorder.statistics import (
|
||||
get_last_statistics,
|
||||
statistics_during_period,
|
||||
)
|
||||
from homeassistant.const import UnitOfEnergy
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import EVENT_HOMEASSISTANT_STOP, UnitOfEnergy
|
||||
from homeassistant.core import Event, HomeAssistant, callback
|
||||
from homeassistant.exceptions import ConfigEntryAuthFailed
|
||||
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
|
||||
from homeassistant.util import dt as dt_util
|
||||
@@ -31,6 +34,9 @@ from homeassistant.util.unit_conversion import EnergyConverter
|
||||
from .const import DOMAIN
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from tibber import TibberHome
|
||||
|
||||
from . import TibberRuntimeData
|
||||
from .const import TibberConfigEntry
|
||||
|
||||
FIVE_YEARS = 5 * 365 * 24
|
||||
@@ -38,8 +44,52 @@ FIVE_YEARS = 5 * 365 * 24
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class TibberDataCoordinator(DataUpdateCoordinator[None]):
|
||||
"""Handle Tibber data and insert statistics."""
|
||||
@dataclass
|
||||
class TibberHomeData:
|
||||
"""Structured data per Tibber home from GraphQL and price API."""
|
||||
|
||||
currency: str
|
||||
price_unit: str
|
||||
current_price: float | None
|
||||
current_price_time: datetime | None
|
||||
intraday_price_ranking: float | None
|
||||
max_price: float
|
||||
avg_price: float
|
||||
min_price: float
|
||||
off_peak_1: float
|
||||
peak: float
|
||||
off_peak_2: float
|
||||
month_cost: float | None
|
||||
peak_hour: float | None
|
||||
peak_hour_time: datetime | None
|
||||
month_cons: float | None
|
||||
|
||||
|
||||
def _build_home_data(home: TibberHome) -> TibberHomeData:
|
||||
"""Build TibberHomeData from a TibberHome after price info has been fetched."""
|
||||
price_value, price_time, price_rank = home.current_price_data()
|
||||
attrs = home.current_attributes()
|
||||
return TibberHomeData(
|
||||
currency=home.currency,
|
||||
price_unit=home.price_unit,
|
||||
current_price=price_value,
|
||||
current_price_time=price_time,
|
||||
intraday_price_ranking=price_rank,
|
||||
max_price=attrs.get("max_price", 0.0),
|
||||
avg_price=attrs.get("avg_price", 0.0),
|
||||
min_price=attrs.get("min_price", 0.0),
|
||||
off_peak_1=attrs.get("off_peak_1", 0.0),
|
||||
peak=attrs.get("peak", 0.0),
|
||||
off_peak_2=attrs.get("off_peak_2", 0.0),
|
||||
month_cost=getattr(home, "month_cost", None),
|
||||
peak_hour=getattr(home, "peak_hour", None),
|
||||
peak_hour_time=getattr(home, "peak_hour_time", None),
|
||||
month_cons=getattr(home, "month_cons", None),
|
||||
)
|
||||
|
||||
|
||||
class TibberDataCoordinator(DataUpdateCoordinator[dict[str, TibberHomeData]]):
|
||||
"""Handle Tibber data, insert statistics, and expose per-home data for sensors."""
|
||||
|
||||
config_entry: TibberConfigEntry
|
||||
|
||||
@@ -47,24 +97,39 @@ class TibberDataCoordinator(DataUpdateCoordinator[None]):
|
||||
self,
|
||||
hass: HomeAssistant,
|
||||
config_entry: TibberConfigEntry,
|
||||
tibber_connection: tibber.Tibber,
|
||||
runtime_data: TibberRuntimeData,
|
||||
) -> None:
|
||||
"""Initialize the data handler."""
|
||||
super().__init__(
|
||||
hass,
|
||||
_LOGGER,
|
||||
config_entry=config_entry,
|
||||
name=f"Tibber {tibber_connection.name}",
|
||||
name="Tibber",
|
||||
update_interval=timedelta(minutes=20),
|
||||
)
|
||||
self._tibber_connection = tibber_connection
|
||||
self._runtime_data = runtime_data
|
||||
|
||||
async def _async_update_data(self) -> None:
|
||||
"""Update data via API."""
|
||||
async def _async_update_data(self) -> dict[str, TibberHomeData]:
|
||||
"""Update data via API and return per-home data for sensors."""
|
||||
tibber_connection = await self._runtime_data.async_get_client(self.hass)
|
||||
try:
|
||||
await self._tibber_connection.fetch_consumption_data_active_homes()
|
||||
await self._tibber_connection.fetch_production_data_active_homes()
|
||||
await self._insert_statistics()
|
||||
await tibber_connection.fetch_consumption_data_active_homes()
|
||||
await tibber_connection.fetch_production_data_active_homes()
|
||||
now = dt_util.now()
|
||||
for home in tibber_connection.get_homes(only_active=True):
|
||||
update_needed = False
|
||||
last_data_timestamp = home.last_data_timestamp
|
||||
|
||||
if last_data_timestamp is None:
|
||||
update_needed = True
|
||||
else:
|
||||
remaining_seconds = (last_data_timestamp - now).total_seconds()
|
||||
if remaining_seconds < 11 * 3600:
|
||||
update_needed = True
|
||||
|
||||
if update_needed:
|
||||
await home.update_info_and_price_info()
|
||||
await self._insert_statistics(tibber_connection)
|
||||
except tibber.RetryableHttpExceptionError as err:
|
||||
raise UpdateFailed(f"Error communicating with API ({err.status})") from err
|
||||
except tibber.FatalHttpExceptionError:
|
||||
@@ -72,10 +137,16 @@ class TibberDataCoordinator(DataUpdateCoordinator[None]):
|
||||
self.hass.async_create_task(
|
||||
self.hass.config_entries.async_reload(self.config_entry.entry_id)
|
||||
)
|
||||
return self.data if self.data is not None else {}
|
||||
|
||||
async def _insert_statistics(self) -> None:
|
||||
result: dict[str, TibberHomeData] = {}
|
||||
for home in tibber_connection.get_homes(only_active=True):
|
||||
result[home.home_id] = _build_home_data(home)
|
||||
return result
|
||||
|
||||
async def _insert_statistics(self, tibber_connection: tibber.Tibber) -> None:
|
||||
"""Insert Tibber statistics."""
|
||||
for home in self._tibber_connection.get_homes():
|
||||
for home in tibber_connection.get_homes():
|
||||
sensors: list[tuple[str, bool, str | None, str]] = []
|
||||
if home.hourly_consumption_data:
|
||||
sensors.append(
|
||||
@@ -257,3 +328,48 @@ class TibberDataAPICoordinator(DataUpdateCoordinator[dict[str, TibberDevice]]):
|
||||
) from err
|
||||
self._build_sensor_lookup(devices)
|
||||
return devices
|
||||
|
||||
|
||||
class TibberRtDataCoordinator(DataUpdateCoordinator[dict[str, Any]]):
|
||||
"""Handle Tibber realtime data."""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
hass: HomeAssistant,
|
||||
config_entry: ConfigEntry,
|
||||
add_sensor_callback: Callable[[TibberRtDataCoordinator, Any], None],
|
||||
tibber_home: TibberHome,
|
||||
) -> None:
|
||||
"""Initialize the data handler."""
|
||||
self._add_sensor_callback = add_sensor_callback
|
||||
super().__init__(
|
||||
hass,
|
||||
_LOGGER,
|
||||
config_entry=config_entry,
|
||||
name=tibber_home.info["viewer"]["home"]["address"].get(
|
||||
"address1", "Tibber"
|
||||
),
|
||||
)
|
||||
|
||||
self._async_remove_device_updates_handler = self.async_add_listener(
|
||||
self._data_updated
|
||||
)
|
||||
hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, self._handle_ha_stop)
|
||||
|
||||
@callback
|
||||
def _handle_ha_stop(self, _event: Event) -> None:
|
||||
"""Handle Home Assistant stopping."""
|
||||
self._async_remove_device_updates_handler()
|
||||
|
||||
@callback
|
||||
def _data_updated(self) -> None:
|
||||
"""Triggered when data is updated."""
|
||||
if live_measurement := self.get_live_measurement():
|
||||
self._add_sensor_callback(self, live_measurement)
|
||||
|
||||
def get_live_measurement(self) -> Any:
|
||||
"""Get live measurement data."""
|
||||
if errors := self.data.get("errors"):
|
||||
_LOGGER.error(errors[0])
|
||||
return None
|
||||
return self.data.get("data", {}).get("liveMeasurement")
|
||||
|
||||
140
homeassistant/components/tibber/entity.py
Normal file
140
homeassistant/components/tibber/entity.py
Normal file
@@ -0,0 +1,140 @@
|
||||
"""Shared entity base for Tibber sensors."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from datetime import datetime, timedelta
|
||||
from typing import TYPE_CHECKING, cast
|
||||
|
||||
from homeassistant.components.sensor import SensorEntityDescription
|
||||
from homeassistant.core import callback
|
||||
from homeassistant.helpers.device_registry import DeviceInfo
|
||||
from homeassistant.helpers.update_coordinator import CoordinatorEntity
|
||||
from homeassistant.util import dt as dt_util
|
||||
|
||||
from .const import DOMAIN
|
||||
from .coordinator import TibberDataCoordinator, TibberHomeData, TibberRtDataCoordinator
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from tibber import TibberHome
|
||||
|
||||
|
||||
class TibberDataCoordinatorEntity(CoordinatorEntity[TibberDataCoordinator]):
|
||||
"""Base entity for Tibber sensors using TibberDataCoordinator."""
|
||||
|
||||
_attr_has_entity_name = True
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
coordinator: TibberDataCoordinator,
|
||||
tibber_home: TibberHome,
|
||||
) -> None:
|
||||
"""Initialize the entity."""
|
||||
super().__init__(coordinator)
|
||||
self._tibber_home = tibber_home
|
||||
self._home_name: str = tibber_home.name or tibber_home.home_id
|
||||
self._model: str | None = None
|
||||
self._device_name: str = self._home_name
|
||||
|
||||
def _get_home_data(self) -> TibberHomeData | None:
|
||||
"""Return cached home data from the coordinator."""
|
||||
data = cast(dict[str, TibberHomeData] | None, self.coordinator.data)
|
||||
if data is None:
|
||||
return None
|
||||
return data.get(self._tibber_home.home_id)
|
||||
|
||||
@property
|
||||
def device_info(self) -> DeviceInfo:
|
||||
"""Return device information."""
|
||||
return DeviceInfo(
|
||||
identifiers={(DOMAIN, self._tibber_home.home_id)},
|
||||
name=self._device_name,
|
||||
model=self._model,
|
||||
)
|
||||
|
||||
|
||||
class TibberSensor:
|
||||
"""Mixin for Tibber sensors that have a Tibber home and device info.
|
||||
|
||||
Used as the first base for real-time sensors (TibberSensorRT with
|
||||
CoordinatorEntity["TibberRtDataCoordinator"]). Provides _tibber_home,
|
||||
_home_name, _model, _device_name and device_info; does not inherit
|
||||
CoordinatorEntity so the second base can be the coordinator entity.
|
||||
"""
|
||||
|
||||
def __init__(self, coordinator: object, tibber_home: TibberHome) -> None:
|
||||
"""Initialize the mixin."""
|
||||
super().__init__(coordinator) # type: ignore[call-arg]
|
||||
self._tibber_home = tibber_home
|
||||
self._home_name: str = tibber_home.name or tibber_home.home_id
|
||||
self._model: str | None = None
|
||||
self._device_name: str = self._home_name
|
||||
|
||||
@property
|
||||
def device_info(self) -> DeviceInfo:
|
||||
"""Return device information."""
|
||||
return DeviceInfo(
|
||||
identifiers={(DOMAIN, self._tibber_home.home_id)},
|
||||
name=self._device_name,
|
||||
model=self._model,
|
||||
)
|
||||
|
||||
|
||||
class TibberSensorRT(TibberSensor, CoordinatorEntity[TibberRtDataCoordinator]):
|
||||
"""Representation of a Tibber sensor for real time consumption."""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
tibber_home: TibberHome,
|
||||
description: SensorEntityDescription,
|
||||
initial_state: float,
|
||||
coordinator: TibberRtDataCoordinator,
|
||||
) -> None:
|
||||
"""Initialize the sensor."""
|
||||
super().__init__(coordinator=coordinator, tibber_home=tibber_home)
|
||||
self.entity_description = description
|
||||
self._model = "Tibber Pulse"
|
||||
self._device_name = f"{self._model} {self._home_name}"
|
||||
|
||||
self._attr_native_value = initial_state
|
||||
self._attr_last_reset: datetime | None = None
|
||||
self._attr_unique_id = f"{self._tibber_home.home_id}_rt_{description.key}"
|
||||
|
||||
if description.key in ("accumulatedCost", "accumulatedReward"):
|
||||
self._attr_native_unit_of_measurement = tibber_home.currency
|
||||
|
||||
@property
|
||||
def available(self) -> bool:
|
||||
"""Return True if entity is available."""
|
||||
return self._tibber_home.rt_subscription_running
|
||||
|
||||
@callback
|
||||
def _handle_coordinator_update(self) -> None:
|
||||
if not (live_measurement := self.coordinator.get_live_measurement()):
|
||||
return
|
||||
state = live_measurement.get(self.entity_description.key)
|
||||
if state is None:
|
||||
return
|
||||
if self.entity_description.key in (
|
||||
"accumulatedConsumption",
|
||||
"accumulatedProduction",
|
||||
):
|
||||
# Value is reset to 0 at midnight, but not always strictly increasing
|
||||
# due to hourly corrections.
|
||||
# If device is offline, last_reset should be updated when it comes
|
||||
# back online if the value has decreased
|
||||
ts_local = dt_util.parse_datetime(live_measurement["timestamp"])
|
||||
if ts_local is not None:
|
||||
if self._attr_last_reset is None or (
|
||||
state < 0.5 * self._attr_native_value
|
||||
and (
|
||||
ts_local.hour == 0
|
||||
or (ts_local - self._attr_last_reset) > timedelta(hours=24)
|
||||
)
|
||||
):
|
||||
self._attr_last_reset = dt_util.as_utc(
|
||||
ts_local.replace(hour=0, minute=0, second=0, microsecond=0)
|
||||
)
|
||||
if self.entity_description.key == "powerFactor":
|
||||
state *= 100.0
|
||||
self._attr_native_value = state
|
||||
self.async_write_ha_state()
|
||||
@@ -2,12 +2,8 @@
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from collections.abc import Callable
|
||||
import datetime
|
||||
from datetime import timedelta
|
||||
import logging
|
||||
from random import randrange
|
||||
from typing import Any
|
||||
from typing import Any, cast
|
||||
|
||||
import aiohttp
|
||||
from tibber import FatalHttpExceptionError, RetryableHttpExceptionError, TibberHome
|
||||
@@ -19,9 +15,7 @@ from homeassistant.components.sensor import (
|
||||
SensorEntityDescription,
|
||||
SensorStateClass,
|
||||
)
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import (
|
||||
EVENT_HOMEASSISTANT_STOP,
|
||||
PERCENTAGE,
|
||||
SIGNAL_STRENGTH_DECIBELS,
|
||||
EntityCategory,
|
||||
@@ -32,28 +26,26 @@ from homeassistant.const import (
|
||||
UnitOfPower,
|
||||
UnitOfTemperature,
|
||||
)
|
||||
from homeassistant.core import Event, HomeAssistant, callback
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
from homeassistant.exceptions import PlatformNotReady
|
||||
from homeassistant.helpers import entity_registry as er
|
||||
from homeassistant.helpers.device_registry import DeviceInfo
|
||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
from homeassistant.helpers.typing import StateType
|
||||
from homeassistant.helpers.update_coordinator import (
|
||||
CoordinatorEntity,
|
||||
DataUpdateCoordinator,
|
||||
)
|
||||
from homeassistant.util import Throttle, dt as dt_util
|
||||
from homeassistant.helpers.update_coordinator import CoordinatorEntity
|
||||
|
||||
from .const import DOMAIN, MANUFACTURER, TibberConfigEntry
|
||||
from .coordinator import TibberDataAPICoordinator, TibberDataCoordinator
|
||||
from .const import DOMAIN, TibberConfigEntry
|
||||
from .coordinator import (
|
||||
TibberDataAPICoordinator,
|
||||
TibberDataCoordinator,
|
||||
TibberRtDataCoordinator,
|
||||
)
|
||||
from .entity import TibberDataCoordinatorEntity, TibberSensorRT
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
ICON = "mdi:currency-usd"
|
||||
SCAN_INTERVAL = timedelta(minutes=1)
|
||||
MIN_TIME_BETWEEN_UPDATES = timedelta(minutes=5)
|
||||
PARALLEL_UPDATES = 0
|
||||
TWENTY_MINUTES = 20 * 60
|
||||
|
||||
RT_SENSORS_UNIQUE_ID_MIGRATION = {
|
||||
"accumulated_consumption_last_hour": "accumulated consumption current hour",
|
||||
@@ -262,6 +254,48 @@ SENSORS: tuple[SensorEntityDescription, ...] = (
|
||||
),
|
||||
)
|
||||
|
||||
PRICE_SENSORS: tuple[SensorEntityDescription, ...] = (
|
||||
SensorEntityDescription(
|
||||
key="current_price",
|
||||
translation_key="electricity_price",
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
),
|
||||
SensorEntityDescription(
|
||||
key="max_price",
|
||||
translation_key="max_price",
|
||||
device_class=SensorDeviceClass.MONETARY,
|
||||
),
|
||||
SensorEntityDescription(
|
||||
key="avg_price",
|
||||
translation_key="avg_price",
|
||||
device_class=SensorDeviceClass.MONETARY,
|
||||
),
|
||||
SensorEntityDescription(
|
||||
key="min_price",
|
||||
translation_key="min_price",
|
||||
device_class=SensorDeviceClass.MONETARY,
|
||||
),
|
||||
SensorEntityDescription(
|
||||
key="off_peak_1",
|
||||
translation_key="off_peak_1",
|
||||
device_class=SensorDeviceClass.MONETARY,
|
||||
),
|
||||
SensorEntityDescription(
|
||||
key="peak",
|
||||
translation_key="peak",
|
||||
device_class=SensorDeviceClass.MONETARY,
|
||||
),
|
||||
SensorEntityDescription(
|
||||
key="off_peak_2",
|
||||
translation_key="off_peak_2",
|
||||
device_class=SensorDeviceClass.MONETARY,
|
||||
),
|
||||
SensorEntityDescription(
|
||||
key="intraday_price_ranking",
|
||||
translation_key="intraday_price_ranking",
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
),
|
||||
)
|
||||
|
||||
DATA_API_SENSORS: tuple[SensorEntityDescription, ...] = (
|
||||
SensorEntityDescription(
|
||||
@@ -603,14 +637,13 @@ async def _async_setup_graphql_sensors(
|
||||
entry: TibberConfigEntry,
|
||||
async_add_entities: AddConfigEntryEntitiesCallback,
|
||||
) -> None:
|
||||
"""Set up the Tibber sensor."""
|
||||
"""Set up the Tibber GraphQL-based sensors."""
|
||||
|
||||
tibber_connection = await entry.runtime_data.async_get_client(hass)
|
||||
|
||||
entity_registry = er.async_get(hass)
|
||||
|
||||
coordinator: TibberDataCoordinator | None = None
|
||||
entities: list[TibberSensor] = []
|
||||
active_homes: list[TibberHome] = []
|
||||
for home in tibber_connection.get_homes(only_active=False):
|
||||
try:
|
||||
await home.update_info()
|
||||
@@ -626,13 +659,7 @@ async def _async_setup_graphql_sensors(
|
||||
raise PlatformNotReady from err
|
||||
|
||||
if home.has_active_subscription:
|
||||
entities.append(TibberSensorElPrice(home))
|
||||
if coordinator is None:
|
||||
coordinator = TibberDataCoordinator(hass, entry, tibber_connection)
|
||||
entities.extend(
|
||||
TibberDataSensor(home, coordinator, entity_description)
|
||||
for entity_description in SENSORS
|
||||
)
|
||||
active_homes.append(home)
|
||||
|
||||
if home.has_real_time_consumption:
|
||||
entity_creator = TibberRtEntityCreator(
|
||||
@@ -647,6 +674,18 @@ async def _async_setup_graphql_sensors(
|
||||
).async_set_updated_data
|
||||
)
|
||||
|
||||
entities: list[TibberDataSensor] = []
|
||||
coordinator = entry.runtime_data.data_coordinator
|
||||
if coordinator is not None and active_homes:
|
||||
for home in active_homes:
|
||||
entities.extend(
|
||||
TibberDataSensor(home, coordinator, desc, model="Price Sensor")
|
||||
for desc in PRICE_SENSORS
|
||||
)
|
||||
entities.extend(
|
||||
TibberDataSensor(home, coordinator, desc) for desc in SENSORS
|
||||
)
|
||||
|
||||
async_add_entities(entities)
|
||||
|
||||
|
||||
@@ -707,200 +746,70 @@ class TibberDataAPISensor(CoordinatorEntity[TibberDataAPICoordinator], SensorEnt
|
||||
return sensor.value if sensor else None
|
||||
|
||||
|
||||
class TibberSensor(SensorEntity):
|
||||
"""Representation of a generic Tibber sensor."""
|
||||
|
||||
_attr_has_entity_name = True
|
||||
|
||||
def __init__(self, *args: Any, tibber_home: TibberHome, **kwargs: Any) -> None:
|
||||
"""Initialize the sensor."""
|
||||
super().__init__(*args, **kwargs)
|
||||
self._tibber_home = tibber_home
|
||||
self._home_name = tibber_home.info["viewer"]["home"]["appNickname"]
|
||||
if self._home_name is None:
|
||||
self._home_name = tibber_home.info["viewer"]["home"]["address"].get(
|
||||
"address1", ""
|
||||
)
|
||||
self._device_name: str | None = None
|
||||
self._model: str | None = None
|
||||
|
||||
@property
|
||||
def device_info(self) -> DeviceInfo:
|
||||
"""Return the device_info of the device."""
|
||||
device_info = DeviceInfo(
|
||||
identifiers={(DOMAIN, self._tibber_home.home_id)},
|
||||
name=self._device_name,
|
||||
manufacturer=MANUFACTURER,
|
||||
)
|
||||
if self._model is not None:
|
||||
device_info["model"] = self._model
|
||||
return device_info
|
||||
|
||||
|
||||
class TibberSensorElPrice(TibberSensor):
|
||||
"""Representation of a Tibber sensor for el price."""
|
||||
|
||||
_attr_state_class = SensorStateClass.MEASUREMENT
|
||||
_attr_translation_key = "electricity_price"
|
||||
|
||||
def __init__(self, tibber_home: TibberHome) -> None:
|
||||
"""Initialize the sensor."""
|
||||
super().__init__(tibber_home=tibber_home)
|
||||
self._last_updated: datetime.datetime | None = None
|
||||
self._spread_load_constant = randrange(TWENTY_MINUTES)
|
||||
|
||||
self._attr_available = False
|
||||
self._attr_extra_state_attributes = {
|
||||
"app_nickname": None,
|
||||
"grid_company": None,
|
||||
"estimated_annual_consumption": None,
|
||||
"max_price": None,
|
||||
"avg_price": None,
|
||||
"min_price": None,
|
||||
"off_peak_1": None,
|
||||
"peak": None,
|
||||
"off_peak_2": None,
|
||||
"intraday_price_ranking": None,
|
||||
}
|
||||
self._attr_icon = ICON
|
||||
self._attr_unique_id = self._tibber_home.home_id
|
||||
self._model = "Price Sensor"
|
||||
|
||||
self._device_name = self._home_name
|
||||
|
||||
async def async_update(self) -> None:
|
||||
"""Get the latest data and updates the states."""
|
||||
now = dt_util.now()
|
||||
if (
|
||||
not self._tibber_home.last_data_timestamp
|
||||
or (self._tibber_home.last_data_timestamp - now).total_seconds()
|
||||
< 10 * 3600 - self._spread_load_constant
|
||||
or not self.available
|
||||
):
|
||||
_LOGGER.debug("Asking for new data")
|
||||
await self._fetch_data()
|
||||
|
||||
elif (
|
||||
self._tibber_home.price_total
|
||||
and self._last_updated
|
||||
and self._last_updated.hour == now.hour
|
||||
and now - self._last_updated < timedelta(minutes=15)
|
||||
and self._tibber_home.last_data_timestamp
|
||||
):
|
||||
return
|
||||
|
||||
res = self._tibber_home.current_price_data()
|
||||
self._attr_native_value, self._last_updated, price_rank = res
|
||||
self._attr_extra_state_attributes["intraday_price_ranking"] = price_rank
|
||||
|
||||
attrs = self._tibber_home.current_attributes()
|
||||
self._attr_extra_state_attributes.update(attrs)
|
||||
self._attr_available = self._attr_native_value is not None
|
||||
self._attr_native_unit_of_measurement = self._tibber_home.price_unit
|
||||
|
||||
@Throttle(MIN_TIME_BETWEEN_UPDATES)
|
||||
async def _fetch_data(self) -> None:
|
||||
_LOGGER.debug("Fetching data")
|
||||
try:
|
||||
await self._tibber_home.update_info_and_price_info()
|
||||
except TimeoutError, aiohttp.ClientError:
|
||||
return
|
||||
data = self._tibber_home.info["viewer"]["home"]
|
||||
self._attr_extra_state_attributes["app_nickname"] = data["appNickname"]
|
||||
self._attr_extra_state_attributes["grid_company"] = data["meteringPointData"][
|
||||
"gridCompany"
|
||||
]
|
||||
self._attr_extra_state_attributes["estimated_annual_consumption"] = data[
|
||||
"meteringPointData"
|
||||
]["estimatedAnnualConsumption"]
|
||||
|
||||
|
||||
class TibberDataSensor(TibberSensor, CoordinatorEntity[TibberDataCoordinator]):
|
||||
"""Representation of a Tibber sensor."""
|
||||
class TibberDataSensor(TibberDataCoordinatorEntity):
|
||||
"""Representation of a Tibber sensor reading from coordinator data."""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
tibber_home: TibberHome,
|
||||
coordinator: TibberDataCoordinator,
|
||||
entity_description: SensorEntityDescription,
|
||||
*,
|
||||
model: str | None = None,
|
||||
) -> None:
|
||||
"""Initialize the sensor."""
|
||||
super().__init__(coordinator=coordinator, tibber_home=tibber_home)
|
||||
self.entity_description = entity_description
|
||||
|
||||
self._attr_unique_id = (
|
||||
f"{self._tibber_home.home_id}_{self.entity_description.key}"
|
||||
)
|
||||
if entity_description.key == "month_cost":
|
||||
self._attr_native_unit_of_measurement = self._tibber_home.currency
|
||||
|
||||
if self.entity_description.key == "current_price":
|
||||
# Preserve the existing unique ID for the electricity price
|
||||
# entity to avoid breaking user setups.
|
||||
self._attr_unique_id = self._tibber_home.home_id
|
||||
else:
|
||||
self._attr_unique_id = (
|
||||
f"{self._tibber_home.home_id}_{self.entity_description.key}"
|
||||
)
|
||||
self._device_name = self._home_name
|
||||
if model is not None:
|
||||
self._model = model
|
||||
|
||||
@property
|
||||
def native_value(self) -> StateType:
|
||||
"""Return the value of the sensor."""
|
||||
return getattr(self._tibber_home, self.entity_description.key) # type: ignore[no-any-return]
|
||||
|
||||
|
||||
class TibberSensorRT(TibberSensor, CoordinatorEntity["TibberRtDataCoordinator"]):
|
||||
"""Representation of a Tibber sensor for real time consumption."""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
tibber_home: TibberHome,
|
||||
description: SensorEntityDescription,
|
||||
initial_state: float,
|
||||
coordinator: TibberRtDataCoordinator,
|
||||
) -> None:
|
||||
"""Initialize the sensor."""
|
||||
super().__init__(coordinator=coordinator, tibber_home=tibber_home)
|
||||
self.entity_description = description
|
||||
self._model = "Tibber Pulse"
|
||||
self._device_name = f"{self._model} {self._home_name}"
|
||||
|
||||
self._attr_native_value = initial_state
|
||||
self._attr_unique_id = f"{self._tibber_home.home_id}_rt_{description.key}"
|
||||
|
||||
if description.key in ("accumulatedCost", "accumulatedReward"):
|
||||
self._attr_native_unit_of_measurement = tibber_home.currency
|
||||
"""Return the value of the sensor from coordinator data."""
|
||||
home_data = self._get_home_data()
|
||||
if home_data is None:
|
||||
return None
|
||||
return cast(
|
||||
StateType,
|
||||
getattr(home_data, self.entity_description.key, None),
|
||||
)
|
||||
|
||||
@property
|
||||
def available(self) -> bool:
|
||||
"""Return True if entity is available."""
|
||||
return self._tibber_home.rt_subscription_running
|
||||
def native_unit_of_measurement(self) -> str | None:
|
||||
"""Return the unit from coordinator data for monetary sensors."""
|
||||
if self.entity_description.key == "current_price":
|
||||
home_data = self._get_home_data()
|
||||
if home_data is None:
|
||||
return None
|
||||
return home_data.price_unit
|
||||
|
||||
@callback
|
||||
def _handle_coordinator_update(self) -> None:
|
||||
if not (live_measurement := self.coordinator.get_live_measurement()):
|
||||
return
|
||||
state = live_measurement.get(self.entity_description.key)
|
||||
if state is None:
|
||||
return
|
||||
if self.entity_description.key in (
|
||||
"accumulatedConsumption",
|
||||
"accumulatedProduction",
|
||||
):
|
||||
# Value is reset to 0 at midnight, but not always strictly increasing
|
||||
# due to hourly corrections.
|
||||
# If device is offline, last_reset should be updated when it comes
|
||||
# back online if the value has decreased
|
||||
ts_local = dt_util.parse_datetime(live_measurement["timestamp"])
|
||||
if ts_local is not None:
|
||||
if self.last_reset is None or (
|
||||
# native_value is float
|
||||
state < 0.5 * self.native_value # type: ignore[operator]
|
||||
and (
|
||||
ts_local.hour == 0
|
||||
or (ts_local - self.last_reset) > timedelta(hours=24)
|
||||
)
|
||||
):
|
||||
self._attr_last_reset = dt_util.as_utc(
|
||||
ts_local.replace(hour=0, minute=0, second=0, microsecond=0)
|
||||
)
|
||||
if self.entity_description.key == "powerFactor":
|
||||
state *= 100.0
|
||||
self._attr_native_value = state
|
||||
self.async_write_ha_state()
|
||||
if self.entity_description.device_class == SensorDeviceClass.MONETARY:
|
||||
home_data = self._get_home_data()
|
||||
if home_data is None:
|
||||
return None
|
||||
|
||||
if self.entity_description.key in {
|
||||
"max_price",
|
||||
"avg_price",
|
||||
"min_price",
|
||||
"off_peak_1",
|
||||
"peak",
|
||||
"off_peak_2",
|
||||
}:
|
||||
return home_data.price_unit
|
||||
|
||||
return home_data.currency
|
||||
desc = cast(SensorEntityDescription, self.entity_description)
|
||||
return desc.native_unit_of_measurement
|
||||
|
||||
|
||||
class TibberRtEntityCreator:
|
||||
@@ -985,48 +894,3 @@ class TibberRtEntityCreator:
|
||||
self._added_sensors.add(sensor_description.key)
|
||||
if new_entities:
|
||||
self._async_add_entities(new_entities)
|
||||
|
||||
|
||||
class TibberRtDataCoordinator(DataUpdateCoordinator): # pylint: disable=hass-enforce-class-module
|
||||
"""Handle Tibber realtime data."""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
hass: HomeAssistant,
|
||||
config_entry: ConfigEntry,
|
||||
add_sensor_callback: Callable[[TibberRtDataCoordinator, Any], None],
|
||||
tibber_home: TibberHome,
|
||||
) -> None:
|
||||
"""Initialize the data handler."""
|
||||
self._add_sensor_callback = add_sensor_callback
|
||||
super().__init__(
|
||||
hass,
|
||||
_LOGGER,
|
||||
config_entry=config_entry,
|
||||
name=tibber_home.info["viewer"]["home"]["address"].get(
|
||||
"address1", "Tibber"
|
||||
),
|
||||
)
|
||||
|
||||
self._async_remove_device_updates_handler = self.async_add_listener(
|
||||
self._data_updated
|
||||
)
|
||||
hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, self._handle_ha_stop)
|
||||
|
||||
@callback
|
||||
def _handle_ha_stop(self, _event: Event) -> None:
|
||||
"""Handle Home Assistant stopping."""
|
||||
self._async_remove_device_updates_handler()
|
||||
|
||||
@callback
|
||||
def _data_updated(self) -> None:
|
||||
"""Triggered when data is updated."""
|
||||
if live_measurement := self.get_live_measurement():
|
||||
self._add_sensor_callback(self, live_measurement)
|
||||
|
||||
def get_live_measurement(self) -> Any:
|
||||
"""Get live measurement data."""
|
||||
if errors := self.data.get("errors"):
|
||||
_LOGGER.error(errors[0])
|
||||
return None
|
||||
return self.data.get("data", {}).get("liveMeasurement")
|
||||
|
||||
@@ -43,6 +43,9 @@
|
||||
"average_power": {
|
||||
"name": "Average power"
|
||||
},
|
||||
"avg_price": {
|
||||
"name": "Average price today"
|
||||
},
|
||||
"cellular_rssi": {
|
||||
"name": "Cellular signal strength"
|
||||
},
|
||||
@@ -136,6 +139,9 @@
|
||||
"grid_phase_count": {
|
||||
"name": "Number of grid phases"
|
||||
},
|
||||
"intraday_price_ranking": {
|
||||
"name": "Intraday price ranking"
|
||||
},
|
||||
"last_meter_consumption": {
|
||||
"name": "Last meter consumption"
|
||||
},
|
||||
@@ -145,15 +151,30 @@
|
||||
"max_power": {
|
||||
"name": "Max power"
|
||||
},
|
||||
"max_price": {
|
||||
"name": "Max price today"
|
||||
},
|
||||
"min_power": {
|
||||
"name": "Min power"
|
||||
},
|
||||
"min_price": {
|
||||
"name": "Min price today"
|
||||
},
|
||||
"month_cons": {
|
||||
"name": "Monthly net consumption"
|
||||
},
|
||||
"month_cost": {
|
||||
"name": "Monthly cost"
|
||||
},
|
||||
"off_peak_1": {
|
||||
"name": "Off-peak 1 average"
|
||||
},
|
||||
"off_peak_2": {
|
||||
"name": "Off-peak 2 average"
|
||||
},
|
||||
"peak": {
|
||||
"name": "Peak average"
|
||||
},
|
||||
"peak_hour": {
|
||||
"name": "Monthly peak hour consumption"
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user