mirror of
https://github.com/jpawlowski/hass.tibber_prices.git
synced 2026-03-30 05:13:40 +00:00
refactoring
This commit is contained in:
parent
da4ef5675a
commit
adc11b0e4d
4 changed files with 245 additions and 375 deletions
|
|
@ -269,6 +269,27 @@ def _transform_price_info(data: dict) -> dict:
|
||||||
return data
|
return data
|
||||||
|
|
||||||
|
|
||||||
|
def _flatten_price_info(subscription: dict) -> dict:
|
||||||
|
"""Extract and flatten priceInfo from subscription."""
|
||||||
|
price_info = subscription.get("priceInfo", {})
|
||||||
|
return {
|
||||||
|
"yesterday": price_info.get("yesterday", []),
|
||||||
|
"today": price_info.get("today", []),
|
||||||
|
"tomorrow": price_info.get("tomorrow", []),
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def _flatten_price_rating(subscription: dict) -> dict:
|
||||||
|
"""Extract and flatten priceRating from subscription."""
|
||||||
|
price_rating = subscription.get("priceRating", {})
|
||||||
|
return {
|
||||||
|
"hourly": price_rating.get("hourly", {}).get("entries", []),
|
||||||
|
"daily": price_rating.get("daily", {}).get("entries", []),
|
||||||
|
"monthly": price_rating.get("monthly", {}).get("entries", []),
|
||||||
|
"thresholdPercentages": price_rating.get("thresholdPercentages"),
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
class TibberPricesApiClient:
|
class TibberPricesApiClient:
|
||||||
"""Tibber API Client."""
|
"""Tibber API Client."""
|
||||||
|
|
||||||
|
|
@ -314,9 +335,9 @@ class TibberPricesApiClient:
|
||||||
query_type=QueryType.VIEWER,
|
query_type=QueryType.VIEWER,
|
||||||
)
|
)
|
||||||
|
|
||||||
async def async_get_price_info(self) -> Any:
|
async def async_get_price_info(self) -> dict:
|
||||||
"""Get price info data including today, tomorrow and last 48 hours."""
|
"""Get price info data in flat format."""
|
||||||
return await self._api_wrapper(
|
response = await self._api_wrapper(
|
||||||
data={
|
data={
|
||||||
"query": """
|
"query": """
|
||||||
{viewer{homes{id,currentSubscription{priceInfo{
|
{viewer{homes{id,currentSubscription{priceInfo{
|
||||||
|
|
@ -329,10 +350,16 @@ class TibberPricesApiClient:
|
||||||
},
|
},
|
||||||
query_type=QueryType.PRICE_INFO,
|
query_type=QueryType.PRICE_INFO,
|
||||||
)
|
)
|
||||||
|
# response is already transformed, but we want flat
|
||||||
|
try:
|
||||||
|
subscription = response["viewer"]["homes"][0]["currentSubscription"]
|
||||||
|
except KeyError:
|
||||||
|
subscription = response["data"]["viewer"]["homes"][0]["currentSubscription"]
|
||||||
|
return {"priceInfo": _flatten_price_info(subscription)}
|
||||||
|
|
||||||
async def async_get_daily_price_rating(self) -> Any:
|
async def async_get_daily_price_rating(self) -> dict:
|
||||||
"""Get daily price rating data."""
|
"""Get daily price rating data in flat format."""
|
||||||
return await self._api_wrapper(
|
response = await self._api_wrapper(
|
||||||
data={
|
data={
|
||||||
"query": """
|
"query": """
|
||||||
{viewer{homes{id,currentSubscription{priceRating{
|
{viewer{homes{id,currentSubscription{priceRating{
|
||||||
|
|
@ -345,10 +372,20 @@ class TibberPricesApiClient:
|
||||||
},
|
},
|
||||||
query_type=QueryType.DAILY_RATING,
|
query_type=QueryType.DAILY_RATING,
|
||||||
)
|
)
|
||||||
|
try:
|
||||||
|
subscription = response["viewer"]["homes"][0]["currentSubscription"]
|
||||||
|
except KeyError:
|
||||||
|
subscription = response["data"]["viewer"]["homes"][0]["currentSubscription"]
|
||||||
|
return {
|
||||||
|
"priceRating": {
|
||||||
|
"daily": _flatten_price_rating(subscription)["daily"],
|
||||||
|
"thresholdPercentages": _flatten_price_rating(subscription)["thresholdPercentages"],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async def async_get_hourly_price_rating(self) -> Any:
|
async def async_get_hourly_price_rating(self) -> dict:
|
||||||
"""Get hourly price rating data."""
|
"""Get hourly price rating data in flat format."""
|
||||||
return await self._api_wrapper(
|
response = await self._api_wrapper(
|
||||||
data={
|
data={
|
||||||
"query": """
|
"query": """
|
||||||
{viewer{homes{id,currentSubscription{priceRating{
|
{viewer{homes{id,currentSubscription{priceRating{
|
||||||
|
|
@ -361,10 +398,20 @@ class TibberPricesApiClient:
|
||||||
},
|
},
|
||||||
query_type=QueryType.HOURLY_RATING,
|
query_type=QueryType.HOURLY_RATING,
|
||||||
)
|
)
|
||||||
|
try:
|
||||||
|
subscription = response["viewer"]["homes"][0]["currentSubscription"]
|
||||||
|
except KeyError:
|
||||||
|
subscription = response["data"]["viewer"]["homes"][0]["currentSubscription"]
|
||||||
|
return {
|
||||||
|
"priceRating": {
|
||||||
|
"hourly": _flatten_price_rating(subscription)["hourly"],
|
||||||
|
"thresholdPercentages": _flatten_price_rating(subscription)["thresholdPercentages"],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async def async_get_monthly_price_rating(self) -> Any:
|
async def async_get_monthly_price_rating(self) -> dict:
|
||||||
"""Get monthly price rating data."""
|
"""Get monthly price rating data in flat format."""
|
||||||
return await self._api_wrapper(
|
response = await self._api_wrapper(
|
||||||
data={
|
data={
|
||||||
"query": """
|
"query": """
|
||||||
{viewer{homes{id,currentSubscription{priceRating{
|
{viewer{homes{id,currentSubscription{priceRating{
|
||||||
|
|
@ -377,45 +424,33 @@ class TibberPricesApiClient:
|
||||||
},
|
},
|
||||||
query_type=QueryType.MONTHLY_RATING,
|
query_type=QueryType.MONTHLY_RATING,
|
||||||
)
|
)
|
||||||
|
try:
|
||||||
|
subscription = response["viewer"]["homes"][0]["currentSubscription"]
|
||||||
|
except KeyError:
|
||||||
|
subscription = response["data"]["viewer"]["homes"][0]["currentSubscription"]
|
||||||
|
return {
|
||||||
|
"priceRating": {
|
||||||
|
"monthly": _flatten_price_rating(subscription)["monthly"],
|
||||||
|
"thresholdPercentages": _flatten_price_rating(subscription)["thresholdPercentages"],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async def async_get_data(self) -> Any:
|
async def async_get_data(self) -> dict:
|
||||||
"""Get all data from the API by combining multiple queries."""
|
"""Get all data from the API by combining multiple queries in flat format."""
|
||||||
# Get all data concurrently
|
|
||||||
price_info = await self.async_get_price_info()
|
price_info = await self.async_get_price_info()
|
||||||
daily_rating = await self.async_get_daily_price_rating()
|
daily_rating = await self.async_get_daily_price_rating()
|
||||||
hourly_rating = await self.async_get_hourly_price_rating()
|
hourly_rating = await self.async_get_hourly_price_rating()
|
||||||
monthly_rating = await self.async_get_monthly_price_rating()
|
monthly_rating = await self.async_get_monthly_price_rating()
|
||||||
|
# Merge all into one flat dict
|
||||||
# Extract the base paths to make the code more readable
|
price_rating = {
|
||||||
def get_base_path(response: dict) -> dict:
|
"thresholdPercentages": daily_rating["priceRating"].get("thresholdPercentages"),
|
||||||
"""Get the base subscription path from the response."""
|
"daily": daily_rating["priceRating"].get("daily", []),
|
||||||
return response["viewer"]["homes"][0]["currentSubscription"]
|
"hourly": hourly_rating["priceRating"].get("hourly", []),
|
||||||
|
"monthly": monthly_rating["priceRating"].get("monthly", []),
|
||||||
def get_rating_data(response: dict) -> dict:
|
}
|
||||||
"""Get the price rating data from the response."""
|
|
||||||
return get_base_path(response)["priceRating"]
|
|
||||||
|
|
||||||
price_info_data = get_base_path(price_info)["priceInfo"]
|
|
||||||
|
|
||||||
# Combine the results
|
|
||||||
return {
|
return {
|
||||||
"data": {
|
"priceInfo": price_info["priceInfo"],
|
||||||
"viewer": {
|
"priceRating": price_rating,
|
||||||
"homes": [
|
|
||||||
{
|
|
||||||
"currentSubscription": {
|
|
||||||
"priceInfo": price_info_data,
|
|
||||||
"priceRating": {
|
|
||||||
"thresholdPercentages": get_rating_data(daily_rating)["thresholdPercentages"],
|
|
||||||
"daily": get_rating_data(daily_rating)["daily"],
|
|
||||||
"hourly": get_rating_data(hourly_rating)["hourly"],
|
|
||||||
"monthly": get_rating_data(monthly_rating)["monthly"],
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async def async_set_title(self, value: str) -> Any:
|
async def async_set_title(self, value: str) -> Any:
|
||||||
|
|
|
||||||
|
|
@ -150,7 +150,7 @@ class TibberPricesBinarySensor(TibberPricesEntity, BinarySensorEntity):
|
||||||
"""Return True if tomorrow's data is fully available, False if not, None if unknown."""
|
"""Return True if tomorrow's data is fully available, False if not, None if unknown."""
|
||||||
if not self.coordinator.data:
|
if not self.coordinator.data:
|
||||||
return None
|
return None
|
||||||
price_info = self.coordinator.data["data"]["viewer"]["homes"][0]["currentSubscription"]["priceInfo"]
|
price_info = self.coordinator.data["priceInfo"]
|
||||||
tomorrow_prices = price_info.get("tomorrow", [])
|
tomorrow_prices = price_info.get("tomorrow", [])
|
||||||
interval_count = len(tomorrow_prices)
|
interval_count = len(tomorrow_prices)
|
||||||
if interval_count in TOMORROW_INTERVAL_COUNTS:
|
if interval_count in TOMORROW_INTERVAL_COUNTS:
|
||||||
|
|
@ -163,7 +163,7 @@ class TibberPricesBinarySensor(TibberPricesEntity, BinarySensorEntity):
|
||||||
"""Return attributes for tomorrow_data_available binary sensor."""
|
"""Return attributes for tomorrow_data_available binary sensor."""
|
||||||
if not self.coordinator.data:
|
if not self.coordinator.data:
|
||||||
return None
|
return None
|
||||||
price_info = self.coordinator.data["data"]["viewer"]["homes"][0]["currentSubscription"]["priceInfo"]
|
price_info = self.coordinator.data["priceInfo"]
|
||||||
tomorrow_prices = price_info.get("tomorrow", [])
|
tomorrow_prices = price_info.get("tomorrow", [])
|
||||||
interval_count = len(tomorrow_prices)
|
interval_count = len(tomorrow_prices)
|
||||||
if interval_count == 0:
|
if interval_count == 0:
|
||||||
|
|
@ -195,7 +195,7 @@ class TibberPricesBinarySensor(TibberPricesEntity, BinarySensorEntity):
|
||||||
if not self.coordinator.data:
|
if not self.coordinator.data:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
price_info = self.coordinator.data["data"]["viewer"]["homes"][0]["currentSubscription"]["priceInfo"]
|
price_info = self.coordinator.data["priceInfo"]
|
||||||
today_prices = price_info.get("today", [])
|
today_prices = price_info.get("today", [])
|
||||||
|
|
||||||
if not today_prices:
|
if not today_prices:
|
||||||
|
|
@ -488,7 +488,7 @@ class TibberPricesBinarySensor(TibberPricesEntity, BinarySensorEntity):
|
||||||
"""Get price interval attributes with support for 15-minute intervals and period grouping."""
|
"""Get price interval attributes with support for 15-minute intervals and period grouping."""
|
||||||
if not self.coordinator.data:
|
if not self.coordinator.data:
|
||||||
return None
|
return None
|
||||||
price_info = self.coordinator.data["data"]["viewer"]["homes"][0]["currentSubscription"]["priceInfo"]
|
price_info = self.coordinator.data["priceInfo"]
|
||||||
yesterday_prices = price_info.get("yesterday", [])
|
yesterday_prices = price_info.get("yesterday", [])
|
||||||
today_prices = price_info.get("today", [])
|
today_prices = price_info.get("today", [])
|
||||||
tomorrow_prices = price_info.get("tomorrow", [])
|
tomorrow_prices = price_info.get("tomorrow", [])
|
||||||
|
|
@ -530,7 +530,7 @@ class TibberPricesBinarySensor(TibberPricesEntity, BinarySensorEntity):
|
||||||
if not self.coordinator.data:
|
if not self.coordinator.data:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
price_info = self.coordinator.data["data"]["viewer"]["homes"][0]["currentSubscription"]["priceInfo"]
|
price_info = self.coordinator.data["priceInfo"]
|
||||||
|
|
||||||
today_prices = price_info.get("today", [])
|
today_prices = price_info.get("today", [])
|
||||||
if not today_prices:
|
if not today_prices:
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,7 @@ from __future__ import annotations
|
||||||
import asyncio
|
import asyncio
|
||||||
import logging
|
import logging
|
||||||
from datetime import datetime, timedelta
|
from datetime import datetime, timedelta
|
||||||
from typing import TYPE_CHECKING, Any, Final, TypedDict, cast
|
from typing import TYPE_CHECKING, Any, Final, cast
|
||||||
|
|
||||||
import homeassistant.util.dt as dt_util
|
import homeassistant.util.dt as dt_util
|
||||||
from homeassistant.core import HomeAssistant, callback
|
from homeassistant.core import HomeAssistant, callback
|
||||||
|
|
@ -22,8 +22,6 @@ from .api import (
|
||||||
from .const import DOMAIN, LOGGER
|
from .const import DOMAIN, LOGGER
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from collections.abc import Callable
|
|
||||||
|
|
||||||
from .data import TibberPricesConfigEntry
|
from .data import TibberPricesConfigEntry
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
@ -38,36 +36,6 @@ UPDATE_FAILED_MSG: Final = "Update failed"
|
||||||
AUTH_FAILED_MSG: Final = "Authentication failed"
|
AUTH_FAILED_MSG: Final = "Authentication failed"
|
||||||
|
|
||||||
|
|
||||||
class TibberPricesPriceInfo(TypedDict):
|
|
||||||
"""Type for price info data structure."""
|
|
||||||
|
|
||||||
today: list[dict[str, Any]]
|
|
||||||
tomorrow: list[dict[str, Any]]
|
|
||||||
yesterday: list[dict[str, Any]]
|
|
||||||
|
|
||||||
|
|
||||||
class TibberPricesPriceRating(TypedDict):
|
|
||||||
"""Type for price rating data structure."""
|
|
||||||
|
|
||||||
thresholdPercentages: dict[str, float] | None
|
|
||||||
hourly: dict[str, Any] | None
|
|
||||||
daily: dict[str, Any] | None
|
|
||||||
monthly: dict[str, Any] | None
|
|
||||||
|
|
||||||
|
|
||||||
class TibberPricesSubscriptionData(TypedDict):
|
|
||||||
"""Type for price info data structure."""
|
|
||||||
|
|
||||||
priceInfo: TibberPricesPriceInfo
|
|
||||||
priceRating: TibberPricesPriceRating
|
|
||||||
|
|
||||||
|
|
||||||
class TibberPricesData(TypedDict):
|
|
||||||
"""Type for Tibber API response data structure."""
|
|
||||||
|
|
||||||
data: dict[str, dict[str, list[dict[str, TibberPricesSubscriptionData]]]]
|
|
||||||
|
|
||||||
|
|
||||||
@callback
|
@callback
|
||||||
def _raise_no_data() -> None:
|
def _raise_no_data() -> None:
|
||||||
"""Raise error when no data is available."""
|
"""Raise error when no data is available."""
|
||||||
|
|
@ -76,15 +44,14 @@ def _raise_no_data() -> None:
|
||||||
|
|
||||||
@callback
|
@callback
|
||||||
def _get_latest_timestamp_from_prices(
|
def _get_latest_timestamp_from_prices(
|
||||||
price_data: TibberPricesData | None,
|
price_data: dict | None,
|
||||||
) -> datetime | None:
|
) -> datetime | None:
|
||||||
"""Get the latest timestamp from price data."""
|
"""Get the latest timestamp from price data."""
|
||||||
if not price_data or "data" not in price_data:
|
if not price_data or "priceInfo" not in price_data:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
try:
|
try:
|
||||||
subscription = price_data["data"]["viewer"]["homes"][0]["currentSubscription"]
|
price_info = price_data["priceInfo"]
|
||||||
price_info = subscription["priceInfo"]
|
|
||||||
latest_timestamp = None
|
latest_timestamp = None
|
||||||
|
|
||||||
# Check today's prices
|
# Check today's prices
|
||||||
|
|
@ -111,20 +78,19 @@ def _get_latest_timestamp_from_prices(
|
||||||
|
|
||||||
@callback
|
@callback
|
||||||
def _get_latest_timestamp_from_rating(
|
def _get_latest_timestamp_from_rating(
|
||||||
rating_data: TibberPricesData | None,
|
rating_data: dict | None,
|
||||||
) -> datetime | None:
|
) -> datetime | None:
|
||||||
"""Get the latest timestamp from rating data."""
|
"""Get the latest timestamp from rating data."""
|
||||||
if not rating_data or "data" not in rating_data:
|
if not rating_data or "priceRating" not in rating_data:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
try:
|
try:
|
||||||
subscription = rating_data["data"]["viewer"]["homes"][0]["currentSubscription"]
|
price_rating = rating_data["priceRating"]
|
||||||
price_rating = subscription["priceRating"]
|
|
||||||
latest_timestamp = None
|
latest_timestamp = None
|
||||||
|
|
||||||
# Check all rating types (hourly, daily, monthly)
|
# Check all rating types (hourly, daily, monthly)
|
||||||
for rating_type in ["hourly", "daily", "monthly"]:
|
for rating_type in ["hourly", "daily", "monthly"]:
|
||||||
if rating_entries := price_rating.get(rating_type, {}).get("entries", []):
|
if rating_entries := price_rating.get(rating_type, []):
|
||||||
for entry in rating_entries:
|
for entry in rating_entries:
|
||||||
if time := entry.get("time"):
|
if time := entry.get("time"):
|
||||||
timestamp = dt_util.parse_datetime(time)
|
timestamp = dt_util.parse_datetime(time)
|
||||||
|
|
@ -137,7 +103,7 @@ def _get_latest_timestamp_from_rating(
|
||||||
|
|
||||||
|
|
||||||
# https://developers.home-assistant.io/docs/integration_fetching_data#coordinated-single-api-poll-for-data-for-all-entities
|
# https://developers.home-assistant.io/docs/integration_fetching_data#coordinated-single-api-poll-for-data-for-all-entities
|
||||||
class TibberPricesDataUpdateCoordinator(DataUpdateCoordinator[TibberPricesData]):
|
class TibberPricesDataUpdateCoordinator(DataUpdateCoordinator[dict]):
|
||||||
"""Class to manage fetching data from the API."""
|
"""Class to manage fetching data from the API."""
|
||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
|
|
@ -152,10 +118,10 @@ class TibberPricesDataUpdateCoordinator(DataUpdateCoordinator[TibberPricesData])
|
||||||
self.config_entry = entry
|
self.config_entry = entry
|
||||||
storage_key = f"{DOMAIN}.{entry.entry_id}"
|
storage_key = f"{DOMAIN}.{entry.entry_id}"
|
||||||
self._store = Store(hass, STORAGE_VERSION, storage_key)
|
self._store = Store(hass, STORAGE_VERSION, storage_key)
|
||||||
self._cached_price_data: TibberPricesData | None = None
|
self._cached_price_data: dict | None = None
|
||||||
self._cached_rating_data_hourly: TibberPricesData | None = None
|
self._cached_rating_data_hourly: dict | None = None
|
||||||
self._cached_rating_data_daily: TibberPricesData | None = None
|
self._cached_rating_data_daily: dict | None = None
|
||||||
self._cached_rating_data_monthly: TibberPricesData | None = None
|
self._cached_rating_data_monthly: dict | None = None
|
||||||
self._last_price_update: datetime | None = None
|
self._last_price_update: datetime | None = None
|
||||||
self._last_rating_update_hourly: datetime | None = None
|
self._last_rating_update_hourly: datetime | None = None
|
||||||
self._last_rating_update_daily: datetime | None = None
|
self._last_rating_update_daily: datetime | None = None
|
||||||
|
|
@ -191,7 +157,7 @@ class TibberPricesDataUpdateCoordinator(DataUpdateCoordinator[TibberPricesData])
|
||||||
# Then do a regular refresh
|
# Then do a regular refresh
|
||||||
await self.async_refresh()
|
await self.async_refresh()
|
||||||
|
|
||||||
async def _async_update_data(self) -> TibberPricesData:
|
async def _async_update_data(self) -> dict:
|
||||||
"""
|
"""
|
||||||
Fetch new state data for the coordinator.
|
Fetch new state data for the coordinator.
|
||||||
|
|
||||||
|
|
@ -265,7 +231,7 @@ class TibberPricesDataUpdateCoordinator(DataUpdateCoordinator[TibberPricesData])
|
||||||
else:
|
else:
|
||||||
return result
|
return result
|
||||||
|
|
||||||
async def _handle_conditional_update(self, current_time: datetime) -> TibberPricesData:
|
async def _handle_conditional_update(self, current_time: datetime) -> dict:
|
||||||
"""Handle conditional update based on update conditions."""
|
"""Handle conditional update based on update conditions."""
|
||||||
# Simplified conditional update checking
|
# Simplified conditional update checking
|
||||||
update_conditions = self._check_update_conditions(current_time)
|
update_conditions = self._check_update_conditions(current_time)
|
||||||
|
|
@ -309,7 +275,7 @@ class TibberPricesDataUpdateCoordinator(DataUpdateCoordinator[TibberPricesData])
|
||||||
),
|
),
|
||||||
}
|
}
|
||||||
|
|
||||||
async def _fetch_all_data(self) -> TibberPricesData:
|
async def _fetch_all_data(self) -> dict:
|
||||||
"""
|
"""
|
||||||
Fetch all data from the API without checking update conditions.
|
Fetch all data from the API without checking update conditions.
|
||||||
|
|
||||||
|
|
@ -355,7 +321,7 @@ class TibberPricesDataUpdateCoordinator(DataUpdateCoordinator[TibberPricesData])
|
||||||
_raise_no_data()
|
_raise_no_data()
|
||||||
|
|
||||||
# Only update cache if we have valid data
|
# Only update cache if we have valid data
|
||||||
self._cached_price_data = cast("TibberPricesData", new_data["price_data"])
|
self._cached_price_data = cast("dict", new_data["price_data"])
|
||||||
self._last_price_update = current_time
|
self._last_price_update = current_time
|
||||||
|
|
||||||
# Update rating data cache only for types that were successfully fetched
|
# Update rating data cache only for types that were successfully fetched
|
||||||
|
|
@ -371,61 +337,21 @@ class TibberPricesDataUpdateCoordinator(DataUpdateCoordinator[TibberPricesData])
|
||||||
return self._merge_all_cached_data()
|
return self._merge_all_cached_data()
|
||||||
|
|
||||||
@callback
|
@callback
|
||||||
def _update_rating_cache(self, rating_type: str, rating_data: TibberPricesData, current_time: datetime) -> None:
|
def _update_rating_cache(self, rating_type: str, rating_data: dict, current_time: datetime) -> None:
|
||||||
"""Update the rating cache for a specific rating type."""
|
"""Update the rating cache for a specific rating type."""
|
||||||
if rating_type == "hourly":
|
if rating_type == "hourly":
|
||||||
self._cached_rating_data_hourly = cast("TibberPricesData", rating_data)
|
self._cached_rating_data_hourly = cast("dict", rating_data)
|
||||||
self._last_rating_update_hourly = current_time
|
self._last_rating_update_hourly = current_time
|
||||||
elif rating_type == "daily":
|
elif rating_type == "daily":
|
||||||
self._cached_rating_data_daily = cast("TibberPricesData", rating_data)
|
self._cached_rating_data_daily = cast("dict", rating_data)
|
||||||
self._last_rating_update_daily = current_time
|
self._last_rating_update_daily = current_time
|
||||||
else: # monthly
|
else: # monthly
|
||||||
self._cached_rating_data_monthly = cast("TibberPricesData", rating_data)
|
self._cached_rating_data_monthly = cast("dict", rating_data)
|
||||||
self._last_rating_update_monthly = current_time
|
self._last_rating_update_monthly = current_time
|
||||||
LOGGER.debug("Updated %s rating data cache at %s", rating_type, current_time)
|
LOGGER.debug("Updated %s rating data cache at %s", rating_type, current_time)
|
||||||
|
|
||||||
async def _store_cache(self) -> None:
|
async def _store_cache(self) -> None:
|
||||||
"""Store cache data."""
|
"""Store cache data in flat format."""
|
||||||
|
|
||||||
def _recover_and_log_timestamp(
|
|
||||||
data: TibberPricesData | None,
|
|
||||||
last_update: datetime | None,
|
|
||||||
get_latest_fn: Callable[[TibberPricesData], datetime | None],
|
|
||||||
label: str,
|
|
||||||
) -> datetime | None:
|
|
||||||
if data and not last_update:
|
|
||||||
latest_timestamp = get_latest_fn(data)
|
|
||||||
if latest_timestamp:
|
|
||||||
LOGGER.debug("Setting missing %s timestamp to: %s", label, latest_timestamp)
|
|
||||||
return latest_timestamp
|
|
||||||
LOGGER.warning("Could not recover %s timestamp from data!", label)
|
|
||||||
return last_update
|
|
||||||
|
|
||||||
self._last_price_update = _recover_and_log_timestamp(
|
|
||||||
self._cached_price_data,
|
|
||||||
self._last_price_update,
|
|
||||||
_get_latest_timestamp_from_prices,
|
|
||||||
"price update",
|
|
||||||
)
|
|
||||||
self._last_rating_update_hourly = _recover_and_log_timestamp(
|
|
||||||
self._cached_rating_data_hourly,
|
|
||||||
self._last_rating_update_hourly,
|
|
||||||
lambda d: self._get_latest_timestamp_from_rating_type(d, "hourly"),
|
|
||||||
"hourly rating",
|
|
||||||
)
|
|
||||||
self._last_rating_update_daily = _recover_and_log_timestamp(
|
|
||||||
self._cached_rating_data_daily,
|
|
||||||
self._last_rating_update_daily,
|
|
||||||
lambda d: self._get_latest_timestamp_from_rating_type(d, "daily"),
|
|
||||||
"daily rating",
|
|
||||||
)
|
|
||||||
self._last_rating_update_monthly = _recover_and_log_timestamp(
|
|
||||||
self._cached_rating_data_monthly,
|
|
||||||
self._last_rating_update_monthly,
|
|
||||||
lambda d: self._get_latest_timestamp_from_rating_type(d, "monthly"),
|
|
||||||
"monthly rating",
|
|
||||||
)
|
|
||||||
|
|
||||||
data = {
|
data = {
|
||||||
"price_data": self._cached_price_data,
|
"price_data": self._cached_price_data,
|
||||||
"rating_data_hourly": self._cached_rating_data_hourly,
|
"rating_data_hourly": self._cached_rating_data_hourly,
|
||||||
|
|
@ -446,7 +372,6 @@ class TibberPricesDataUpdateCoordinator(DataUpdateCoordinator[TibberPricesData])
|
||||||
"Storing cache data with timestamps: %s",
|
"Storing cache data with timestamps: %s",
|
||||||
{k: v for k, v in data.items() if k.startswith("last_")},
|
{k: v for k, v in data.items() if k.startswith("last_")},
|
||||||
)
|
)
|
||||||
# Defensive: warn if any required data is missing before saving
|
|
||||||
if data["price_data"] is None:
|
if data["price_data"] is None:
|
||||||
LOGGER.warning("Attempting to store cache with missing price_data!")
|
LOGGER.warning("Attempting to store cache with missing price_data!")
|
||||||
if data["last_price_update"] is None:
|
if data["last_price_update"] is None:
|
||||||
|
|
@ -514,7 +439,7 @@ class TibberPricesDataUpdateCoordinator(DataUpdateCoordinator[TibberPricesData])
|
||||||
def _should_update_rating_type(
|
def _should_update_rating_type(
|
||||||
self,
|
self,
|
||||||
current_time: datetime,
|
current_time: datetime,
|
||||||
cached_data: TibberPricesData | None,
|
cached_data: dict | None,
|
||||||
last_update: datetime | None,
|
last_update: datetime | None,
|
||||||
rating_type: str,
|
rating_type: str,
|
||||||
) -> bool:
|
) -> bool:
|
||||||
|
|
@ -589,56 +514,30 @@ class TibberPricesDataUpdateCoordinator(DataUpdateCoordinator[TibberPricesData])
|
||||||
|
|
||||||
@callback
|
@callback
|
||||||
def _extract_price_data(self, data: dict) -> dict:
|
def _extract_price_data(self, data: dict) -> dict:
|
||||||
"""Extract price data for caching."""
|
"""Extract price data for caching in flat format."""
|
||||||
try:
|
try:
|
||||||
# Try to access data in the transformed structure first
|
price_info = data["priceInfo"]
|
||||||
try:
|
|
||||||
price_info = data["viewer"]["homes"][0]["currentSubscription"]["priceInfo"]
|
|
||||||
except KeyError:
|
|
||||||
# If that fails, try the raw data structure
|
|
||||||
price_info = data["data"]["viewer"]["homes"][0]["currentSubscription"]["priceInfo"]
|
|
||||||
|
|
||||||
# Ensure we have all required fields
|
|
||||||
extracted_price_info = {
|
extracted_price_info = {
|
||||||
|
"yesterday": price_info.get("yesterday", []),
|
||||||
"today": price_info.get("today", []),
|
"today": price_info.get("today", []),
|
||||||
"tomorrow": price_info.get("tomorrow", []),
|
"tomorrow": price_info.get("tomorrow", []),
|
||||||
"yesterday": price_info.get("yesterday", []),
|
|
||||||
}
|
}
|
||||||
except (KeyError, IndexError) as ex:
|
except (KeyError, IndexError, TypeError) as ex:
|
||||||
LOGGER.error("Error extracting price data: %s", ex)
|
LOGGER.error("Error extracting price data: %s", ex)
|
||||||
return {
|
extracted_price_info = {"yesterday": [], "today": [], "tomorrow": []}
|
||||||
"data": {
|
return extracted_price_info
|
||||||
"viewer": {
|
|
||||||
"homes": [
|
|
||||||
{
|
|
||||||
"currentSubscription": {
|
|
||||||
"priceInfo": {
|
|
||||||
"today": [],
|
|
||||||
"tomorrow": [],
|
|
||||||
"yesterday": [],
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return {"data": {"viewer": {"homes": [{"currentSubscription": {"priceInfo": extracted_price_info}}]}}}
|
|
||||||
|
|
||||||
@callback
|
@callback
|
||||||
def _get_latest_timestamp_from_rating_type(
|
def _get_latest_timestamp_from_rating_type(self, rating_data: dict | None, rating_type: str) -> datetime | None:
|
||||||
self, rating_data: TibberPricesData | None, rating_type: str
|
|
||||||
) -> datetime | None:
|
|
||||||
"""Get the latest timestamp from a specific rating type."""
|
"""Get the latest timestamp from a specific rating type."""
|
||||||
if not rating_data or "data" not in rating_data:
|
if not rating_data or "priceRating" not in rating_data:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
try:
|
try:
|
||||||
subscription = rating_data["data"]["viewer"]["homes"][0]["currentSubscription"]
|
price_rating = rating_data["priceRating"]
|
||||||
price_rating = subscription["priceRating"]
|
|
||||||
result = None
|
result = None
|
||||||
|
|
||||||
if rating_entries := price_rating.get(rating_type, {}).get("entries", []):
|
if rating_entries := price_rating.get(rating_type, []):
|
||||||
for entry in rating_entries:
|
for entry in rating_entries:
|
||||||
if time := entry.get("time"):
|
if time := entry.get("time"):
|
||||||
timestamp = dt_util.parse_datetime(time)
|
timestamp = dt_util.parse_datetime(time)
|
||||||
|
|
@ -649,99 +548,135 @@ class TibberPricesDataUpdateCoordinator(DataUpdateCoordinator[TibberPricesData])
|
||||||
return result
|
return result
|
||||||
|
|
||||||
async def _get_rating_data_for_type(self, rating_type: str) -> dict:
|
async def _get_rating_data_for_type(self, rating_type: str) -> dict:
|
||||||
"""Get fresh rating data for a specific type."""
|
"""Get fresh rating data for a specific type in flat format."""
|
||||||
client = self.config_entry.runtime_data.client
|
client = self.config_entry.runtime_data.client
|
||||||
|
|
||||||
if rating_type == "hourly":
|
if rating_type == "hourly":
|
||||||
data = await client.async_get_hourly_price_rating()
|
data = await client.async_get_hourly_price_rating()
|
||||||
elif rating_type == "daily":
|
elif rating_type == "daily":
|
||||||
data = await client.async_get_daily_price_rating()
|
data = await client.async_get_daily_price_rating()
|
||||||
else: # monthly
|
else:
|
||||||
data = await client.async_get_monthly_price_rating()
|
data = await client.async_get_monthly_price_rating()
|
||||||
|
|
||||||
|
# Accept both {"priceRating": {...}} and flat {rating_type: [...], ...} dicts
|
||||||
try:
|
try:
|
||||||
# Try to access data in the transformed structure first
|
price_rating = data.get("priceRating", data)
|
||||||
rating = data["viewer"]["homes"][0]["currentSubscription"]["priceRating"]
|
threshold = price_rating.get("thresholdPercentages")
|
||||||
except KeyError:
|
entries = price_rating.get(rating_type, [])
|
||||||
try:
|
except KeyError as ex:
|
||||||
# If that fails, try the raw data structure
|
LOGGER.error("Failed to extract rating data (flat format): %s", ex)
|
||||||
rating = data["data"]["viewer"]["homes"][0]["currentSubscription"]["priceRating"]
|
raise TibberPricesApiClientError(
|
||||||
except KeyError as ex:
|
TibberPricesApiClientError.EMPTY_DATA_ERROR.format(query_type=rating_type)
|
||||||
LOGGER.error("Failed to extract rating data: %s", ex)
|
) from ex
|
||||||
raise TibberPricesApiClientError(
|
return {"priceRating": {rating_type: entries, "thresholdPercentages": threshold}}
|
||||||
TibberPricesApiClientError.EMPTY_DATA_ERROR.format(query_type=rating_type)
|
|
||||||
) from ex
|
|
||||||
else:
|
|
||||||
return {
|
|
||||||
"data": {
|
|
||||||
"viewer": {
|
|
||||||
"homes": [
|
|
||||||
{
|
|
||||||
"currentSubscription": {
|
|
||||||
"priceRating": {
|
|
||||||
"thresholdPercentages": rating["thresholdPercentages"],
|
|
||||||
rating_type: rating[rating_type],
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else:
|
|
||||||
return {
|
|
||||||
"data": {
|
|
||||||
"viewer": {
|
|
||||||
"homes": [
|
|
||||||
{
|
|
||||||
"currentSubscription": {
|
|
||||||
"priceRating": {
|
|
||||||
"thresholdPercentages": rating["thresholdPercentages"],
|
|
||||||
rating_type: rating[rating_type],
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@callback
|
@callback
|
||||||
def _merge_all_cached_data(self) -> TibberPricesData:
|
def _merge_all_cached_data(self) -> dict:
|
||||||
"""Merge all cached data."""
|
"""Merge all cached data into flat format."""
|
||||||
if not self._cached_price_data:
|
if not self._cached_price_data:
|
||||||
return cast("TibberPricesData", {})
|
return {}
|
||||||
|
# Wrap cached price data in 'priceInfo' only if not already wrapped
|
||||||
|
if "priceInfo" in self._cached_price_data:
|
||||||
|
merged = {"priceInfo": self._cached_price_data["priceInfo"]}
|
||||||
|
else:
|
||||||
|
merged = {"priceInfo": self._cached_price_data}
|
||||||
|
price_rating = {"hourly": [], "daily": [], "monthly": [], "thresholdPercentages": None}
|
||||||
|
for rating_type, cached in zip(
|
||||||
|
["hourly", "daily", "monthly"],
|
||||||
|
[self._cached_rating_data_hourly, self._cached_rating_data_daily, self._cached_rating_data_monthly],
|
||||||
|
strict=True,
|
||||||
|
):
|
||||||
|
if cached and "priceRating" in cached:
|
||||||
|
entries = cached["priceRating"].get(rating_type, [])
|
||||||
|
price_rating[rating_type] = entries
|
||||||
|
if not price_rating["thresholdPercentages"]:
|
||||||
|
price_rating["thresholdPercentages"] = cached["priceRating"].get("thresholdPercentages")
|
||||||
|
merged["priceRating"] = price_rating
|
||||||
|
return merged
|
||||||
|
|
||||||
# Start with price info
|
async def _async_initialize(self) -> None:
|
||||||
subscription = {
|
"""Load stored data in flat format."""
|
||||||
"priceInfo": self._cached_price_data["data"]["viewer"]["homes"][0]["currentSubscription"]["priceInfo"],
|
stored = await self._store.async_load()
|
||||||
"priceRating": {
|
if stored is None:
|
||||||
"thresholdPercentages": None,
|
LOGGER.warning("No cache file found or cache is empty on startup.")
|
||||||
},
|
else:
|
||||||
}
|
LOGGER.debug("Loading stored data: %s", stored)
|
||||||
|
if stored:
|
||||||
|
self._cached_price_data = stored.get("price_data")
|
||||||
|
self._cached_rating_data_hourly = stored.get("rating_data_hourly")
|
||||||
|
self._cached_rating_data_daily = stored.get("rating_data_daily")
|
||||||
|
self._cached_rating_data_monthly = stored.get("rating_data_monthly")
|
||||||
|
self._last_price_update = self._recover_timestamp(self._cached_price_data, stored.get("last_price_update"))
|
||||||
|
self._last_rating_update_hourly = self._recover_timestamp(
|
||||||
|
self._cached_rating_data_hourly,
|
||||||
|
stored.get("last_rating_update_hourly"),
|
||||||
|
"hourly",
|
||||||
|
)
|
||||||
|
self._last_rating_update_daily = self._recover_timestamp(
|
||||||
|
self._cached_rating_data_daily,
|
||||||
|
stored.get("last_rating_update_daily"),
|
||||||
|
"daily",
|
||||||
|
)
|
||||||
|
self._last_rating_update_monthly = self._recover_timestamp(
|
||||||
|
self._cached_rating_data_monthly,
|
||||||
|
stored.get("last_rating_update_monthly"),
|
||||||
|
"monthly",
|
||||||
|
)
|
||||||
|
LOGGER.debug(
|
||||||
|
"Loaded stored cache data - Price update: %s, Rating hourly: %s, daily: %s, monthly: %s",
|
||||||
|
self._last_price_update,
|
||||||
|
self._last_rating_update_hourly,
|
||||||
|
self._last_rating_update_daily,
|
||||||
|
self._last_rating_update_monthly,
|
||||||
|
)
|
||||||
|
if self._cached_price_data is None:
|
||||||
|
LOGGER.warning("Cached price data missing after cache load!")
|
||||||
|
if self._last_price_update is None:
|
||||||
|
LOGGER.warning("Price update timestamp missing after cache load!")
|
||||||
|
else:
|
||||||
|
LOGGER.info("No cache loaded; will fetch fresh data on first update.")
|
||||||
|
|
||||||
# Add rating data if available
|
def get_all_intervals(self) -> list[dict]:
|
||||||
rating_data = {
|
"""Return a combined, sorted list of all price intervals for yesterday, today, and tomorrow."""
|
||||||
"hourly": self._cached_rating_data_hourly,
|
if not self.data or "priceInfo" not in self.data:
|
||||||
"daily": self._cached_rating_data_daily,
|
return []
|
||||||
"monthly": self._cached_rating_data_monthly,
|
price_info = self.data["priceInfo"]
|
||||||
}
|
all_prices = price_info.get("yesterday", []) + price_info.get("today", []) + price_info.get("tomorrow", [])
|
||||||
|
return sorted(all_prices, key=lambda p: p["startsAt"])
|
||||||
|
|
||||||
for rating_type, data in rating_data.items():
|
def get_interval_granularity(self) -> int | None:
|
||||||
if data and "data" in data:
|
"""Return the interval granularity in minutes (e.g., 15 or 60) for today's data."""
|
||||||
rating = data["data"]["viewer"]["homes"][0]["currentSubscription"]["priceRating"]
|
if not self.data or "priceInfo" not in self.data:
|
||||||
|
return None
|
||||||
|
today_prices = self.data["priceInfo"].get("today", [])
|
||||||
|
from .sensor import detect_interval_granularity
|
||||||
|
|
||||||
# Set thresholdPercentages from any available rating data
|
return detect_interval_granularity(today_prices) if today_prices else None
|
||||||
if not subscription["priceRating"]["thresholdPercentages"]:
|
|
||||||
subscription["priceRating"]["thresholdPercentages"] = rating["thresholdPercentages"]
|
|
||||||
|
|
||||||
# Add the specific rating type data
|
def get_current_interval_data(self) -> dict | None:
|
||||||
subscription["priceRating"][rating_type] = rating[rating_type]
|
"""Return the price data for the current interval."""
|
||||||
|
if not self.data or "priceInfo" not in self.data:
|
||||||
|
return None
|
||||||
|
price_info = self.data["priceInfo"]
|
||||||
|
now = dt_util.now()
|
||||||
|
interval_length = self.get_interval_granularity()
|
||||||
|
from .sensor import find_price_data_for_interval
|
||||||
|
|
||||||
return cast(
|
return find_price_data_for_interval(price_info, now, interval_length)
|
||||||
"TibberPricesData",
|
|
||||||
{"data": {"viewer": {"homes": [{"currentSubscription": subscription}]}}},
|
def get_combined_price_info(self) -> dict:
|
||||||
)
|
"""Return a dict with all intervals under a single key 'all'."""
|
||||||
|
return {"all": self.get_all_intervals()}
|
||||||
|
|
||||||
|
def is_tomorrow_data_available(self) -> bool | None:
|
||||||
|
"""Return True if tomorrow's data is fully available, False if not, None if unknown."""
|
||||||
|
if not self.data or "priceInfo" not in self.data:
|
||||||
|
return None
|
||||||
|
tomorrow_prices = self.data["priceInfo"].get("tomorrow", [])
|
||||||
|
interval_count = len(tomorrow_prices)
|
||||||
|
min_tomorrow_intervals_hourly = 24
|
||||||
|
min_tomorrow_intervals_15min = 96
|
||||||
|
tomorrow_interval_counts = {min_tomorrow_intervals_hourly, min_tomorrow_intervals_15min}
|
||||||
|
return interval_count in tomorrow_interval_counts
|
||||||
|
|
||||||
async def async_request_refresh(self) -> None:
|
async def async_request_refresh(self) -> None:
|
||||||
"""
|
"""
|
||||||
|
|
@ -756,9 +691,9 @@ class TibberPricesDataUpdateCoordinator(DataUpdateCoordinator[TibberPricesData])
|
||||||
self._force_update = True
|
self._force_update = True
|
||||||
await self.async_refresh()
|
await self.async_refresh()
|
||||||
|
|
||||||
def _transform_api_response(self, data: dict[str, Any]) -> TibberPricesData:
|
def _transform_api_response(self, data: dict[str, Any]) -> dict:
|
||||||
"""Transform API response to coordinator data format."""
|
"""Transform API response to coordinator data format."""
|
||||||
return cast("TibberPricesData", data)
|
return cast("dict", data)
|
||||||
|
|
||||||
async def _perform_midnight_rotation(self) -> None:
|
async def _perform_midnight_rotation(self) -> None:
|
||||||
"""
|
"""
|
||||||
|
|
@ -775,8 +710,7 @@ class TibberPricesDataUpdateCoordinator(DataUpdateCoordinator[TibberPricesData])
|
||||||
|
|
||||||
async with self._rotation_lock:
|
async with self._rotation_lock:
|
||||||
try:
|
try:
|
||||||
subscription = self._cached_price_data["data"]["viewer"]["homes"][0]["currentSubscription"]
|
price_info = self._cached_price_data["priceInfo"]
|
||||||
price_info = subscription["priceInfo"]
|
|
||||||
|
|
||||||
# Save current data state for logging
|
# Save current data state for logging
|
||||||
today_count = len(price_info.get("today", []))
|
today_count = len(price_info.get("today", []))
|
||||||
|
|
@ -823,105 +757,10 @@ class TibberPricesDataUpdateCoordinator(DataUpdateCoordinator[TibberPricesData])
|
||||||
except (KeyError, TypeError, ValueError) as ex:
|
except (KeyError, TypeError, ValueError) as ex:
|
||||||
LOGGER.error("Error during midnight data rotation in hourly update: %s", ex)
|
LOGGER.error("Error during midnight data rotation in hourly update: %s", ex)
|
||||||
|
|
||||||
def get_all_intervals(self) -> list[dict]:
|
|
||||||
"""Return a combined, sorted list of all price intervals for yesterday, today, and tomorrow."""
|
|
||||||
if not self.data:
|
|
||||||
return []
|
|
||||||
price_info = self.data["data"]["viewer"]["homes"][0]["currentSubscription"]["priceInfo"]
|
|
||||||
all_prices = price_info.get("yesterday", []) + price_info.get("today", []) + price_info.get("tomorrow", [])
|
|
||||||
return sorted(all_prices, key=lambda p: p["startsAt"])
|
|
||||||
|
|
||||||
def get_interval_granularity(self) -> int | None:
|
|
||||||
"""Return the interval granularity in minutes (e.g., 15 or 60) for today's data."""
|
|
||||||
if not self.data:
|
|
||||||
return None
|
|
||||||
price_info = self.data["data"]["viewer"]["homes"][0]["currentSubscription"]["priceInfo"]
|
|
||||||
today_prices = price_info.get("today", [])
|
|
||||||
from .sensor import detect_interval_granularity
|
|
||||||
|
|
||||||
return detect_interval_granularity(today_prices) if today_prices else None
|
|
||||||
|
|
||||||
def get_current_interval_data(self) -> dict | None:
|
|
||||||
"""Return the price data for the current interval."""
|
|
||||||
if not self.data:
|
|
||||||
return None
|
|
||||||
price_info = self.data["data"]["viewer"]["homes"][0]["currentSubscription"]["priceInfo"]
|
|
||||||
now = dt_util.now()
|
|
||||||
interval_length = self.get_interval_granularity()
|
|
||||||
from .sensor import find_price_data_for_interval
|
|
||||||
|
|
||||||
return find_price_data_for_interval(price_info, now, interval_length)
|
|
||||||
|
|
||||||
def get_combined_price_info(self) -> dict:
|
|
||||||
"""Return a dict with all intervals under a single key 'all'."""
|
|
||||||
return {"all": self.get_all_intervals()}
|
|
||||||
|
|
||||||
def is_tomorrow_data_available(self) -> bool | None:
|
|
||||||
"""Return True if tomorrow's data is fully available, False if not, None if unknown."""
|
|
||||||
if not self.data:
|
|
||||||
return None
|
|
||||||
price_info = self.data["data"]["viewer"]["homes"][0]["currentSubscription"]["priceInfo"]
|
|
||||||
tomorrow_prices = price_info.get("tomorrow", [])
|
|
||||||
interval_count = len(tomorrow_prices)
|
|
||||||
# Use the same logic as in binary_sensor.py
|
|
||||||
min_tomorrow_intervals_hourly = 24
|
|
||||||
min_tomorrow_intervals_15min = 96
|
|
||||||
tomorrow_interval_counts = {min_tomorrow_intervals_hourly, min_tomorrow_intervals_15min}
|
|
||||||
return interval_count in tomorrow_interval_counts
|
|
||||||
|
|
||||||
async def _async_initialize(self) -> None:
|
|
||||||
"""Load stored data."""
|
|
||||||
stored = await self._store.async_load()
|
|
||||||
if stored is None:
|
|
||||||
LOGGER.warning("No cache file found or cache is empty on startup.")
|
|
||||||
else:
|
|
||||||
LOGGER.debug("Loading stored data: %s", stored)
|
|
||||||
|
|
||||||
if stored:
|
|
||||||
# Load cached data
|
|
||||||
self._cached_price_data = cast("TibberPricesData", stored.get("price_data"))
|
|
||||||
self._cached_rating_data_hourly = cast("TibberPricesData", stored.get("rating_data_hourly"))
|
|
||||||
self._cached_rating_data_daily = cast("TibberPricesData", stored.get("rating_data_daily"))
|
|
||||||
self._cached_rating_data_monthly = cast("TibberPricesData", stored.get("rating_data_monthly"))
|
|
||||||
|
|
||||||
# Recover timestamps
|
|
||||||
self._last_price_update = self._recover_timestamp(self._cached_price_data, stored.get("last_price_update"))
|
|
||||||
self._last_rating_update_hourly = self._recover_timestamp(
|
|
||||||
self._cached_rating_data_hourly,
|
|
||||||
stored.get("last_rating_update_hourly"),
|
|
||||||
"hourly",
|
|
||||||
)
|
|
||||||
self._last_rating_update_daily = self._recover_timestamp(
|
|
||||||
self._cached_rating_data_daily,
|
|
||||||
stored.get("last_rating_update_daily"),
|
|
||||||
"daily",
|
|
||||||
)
|
|
||||||
self._last_rating_update_monthly = self._recover_timestamp(
|
|
||||||
self._cached_rating_data_monthly,
|
|
||||||
stored.get("last_rating_update_monthly"),
|
|
||||||
"monthly",
|
|
||||||
)
|
|
||||||
|
|
||||||
LOGGER.debug(
|
|
||||||
"Loaded stored cache data - Price update: %s, Rating hourly: %s, daily: %s, monthly: %s",
|
|
||||||
self._last_price_update,
|
|
||||||
self._last_rating_update_hourly,
|
|
||||||
self._last_rating_update_daily,
|
|
||||||
self._last_rating_update_monthly,
|
|
||||||
)
|
|
||||||
|
|
||||||
# Defensive: warn if any required data is missing
|
|
||||||
if self._cached_price_data is None:
|
|
||||||
LOGGER.warning("Cached price data missing after cache load!")
|
|
||||||
if self._last_price_update is None:
|
|
||||||
LOGGER.warning("Price update timestamp missing after cache load!")
|
|
||||||
else:
|
|
||||||
LOGGER.info("No cache loaded; will fetch fresh data on first update.")
|
|
||||||
|
|
||||||
@callback
|
@callback
|
||||||
def _recover_timestamp(
|
def _recover_timestamp(
|
||||||
self,
|
self,
|
||||||
data: TibberPricesData | None,
|
data: dict | None,
|
||||||
stored_timestamp: str | None,
|
stored_timestamp: str | None,
|
||||||
rating_type: str | None = None,
|
rating_type: str | None = None,
|
||||||
) -> datetime | None:
|
) -> datetime | None:
|
||||||
|
|
|
||||||
|
|
@ -310,8 +310,7 @@ class TibberPricesSensor(TibberPricesEntity, SensorEntity):
|
||||||
"""Get price for current hour or with offset."""
|
"""Get price for current hour or with offset."""
|
||||||
if not self.coordinator.data:
|
if not self.coordinator.data:
|
||||||
return None
|
return None
|
||||||
|
price_info = self.coordinator.data["priceInfo"]
|
||||||
price_info = self.coordinator.data["data"]["viewer"]["homes"][0]["currentSubscription"]["priceInfo"]
|
|
||||||
|
|
||||||
# Use HomeAssistant's dt_util to get the current time in the user's timezone
|
# Use HomeAssistant's dt_util to get the current time in the user's timezone
|
||||||
now = dt_util.now()
|
now = dt_util.now()
|
||||||
|
|
@ -408,7 +407,7 @@ class TibberPricesSensor(TibberPricesEntity, SensorEntity):
|
||||||
if not self.coordinator.data:
|
if not self.coordinator.data:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
price_info = self.coordinator.data["data"]["viewer"]["homes"][0]["currentSubscription"]["priceInfo"]
|
price_info = self.coordinator.data["priceInfo"]
|
||||||
today_prices = price_info.get("today", [])
|
today_prices = price_info.get("today", [])
|
||||||
if not today_prices:
|
if not today_prices:
|
||||||
return None
|
return None
|
||||||
|
|
@ -509,12 +508,11 @@ class TibberPricesSensor(TibberPricesEntity, SensorEntity):
|
||||||
self._last_rating_difference = None
|
self._last_rating_difference = None
|
||||||
self._last_rating_level = None
|
self._last_rating_level = None
|
||||||
return None
|
return None
|
||||||
subscription = self.coordinator.data["data"]["viewer"]["homes"][0]["currentSubscription"]
|
price_rating = self.coordinator.data.get("priceRating", {})
|
||||||
price_rating = subscription.get("priceRating", {}) or {}
|
|
||||||
now = dt_util.now()
|
now = dt_util.now()
|
||||||
rating_data = price_rating.get(rating_type, {})
|
# In the new flat format, price_rating[rating_type] is a list of entries
|
||||||
entries = rating_data.get("entries", []) if rating_data else []
|
entries = price_rating.get(rating_type, [])
|
||||||
entry = self._find_rating_entry(entries, now, rating_type, dict(subscription))
|
entry = self._find_rating_entry(entries, now, rating_type, dict(self.coordinator.data))
|
||||||
if entry:
|
if entry:
|
||||||
difference = entry.get("difference")
|
difference = entry.get("difference")
|
||||||
level = entry.get("level")
|
level = entry.get("level")
|
||||||
|
|
@ -530,7 +528,7 @@ class TibberPricesSensor(TibberPricesEntity, SensorEntity):
|
||||||
if not self.coordinator.data:
|
if not self.coordinator.data:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
price_info = self.coordinator.data["data"]["viewer"]["homes"][0]["currentSubscription"]["priceInfo"]
|
price_info = self.coordinator.data["priceInfo"]
|
||||||
latest_timestamp = None
|
latest_timestamp = None
|
||||||
|
|
||||||
for day in ["today", "tomorrow"]:
|
for day in ["today", "tomorrow"]:
|
||||||
|
|
@ -565,10 +563,8 @@ class TibberPricesSensor(TibberPricesEntity, SensorEntity):
|
||||||
if not self.coordinator.data:
|
if not self.coordinator.data:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
price_info = self.coordinator.data["data"]["viewer"]["homes"][0]["currentSubscription"]["priceInfo"]
|
price_info = self.coordinator.data["priceInfo"]
|
||||||
price_rating = (
|
price_rating = self.coordinator.data.get("priceRating", {})
|
||||||
self.coordinator.data["data"]["viewer"]["homes"][0]["currentSubscription"].get("priceRating", {}) or {}
|
|
||||||
)
|
|
||||||
|
|
||||||
# Determine data granularity from the current price data
|
# Determine data granularity from the current price data
|
||||||
today_prices = price_info.get("today", [])
|
today_prices = price_info.get("today", [])
|
||||||
|
|
@ -878,7 +874,7 @@ class TibberPricesSensor(TibberPricesEntity, SensorEntity):
|
||||||
# Add timestamp for next interval price sensors
|
# Add timestamp for next interval price sensors
|
||||||
if self.entity_description.key in ["next_interval_price", "next_interval_price_eur"]:
|
if self.entity_description.key in ["next_interval_price", "next_interval_price_eur"]:
|
||||||
# Get the next interval's data
|
# Get the next interval's data
|
||||||
price_info = self.coordinator.data["data"]["viewer"]["homes"][0]["currentSubscription"]["priceInfo"]
|
price_info = self.coordinator.data["priceInfo"]
|
||||||
today_prices = price_info.get("today", [])
|
today_prices = price_info.get("today", [])
|
||||||
data_granularity = detect_interval_granularity(today_prices) if today_prices else MINUTES_PER_INTERVAL
|
data_granularity = detect_interval_granularity(today_prices) if today_prices else MINUTES_PER_INTERVAL
|
||||||
now = dt_util.now()
|
now = dt_util.now()
|
||||||
|
|
@ -916,7 +912,7 @@ class TibberPricesSensor(TibberPricesEntity, SensorEntity):
|
||||||
def _add_statistics_attributes(self, attributes: dict) -> None:
|
def _add_statistics_attributes(self, attributes: dict) -> None:
|
||||||
"""Add attributes for statistics, rating, and diagnostic sensors."""
|
"""Add attributes for statistics, rating, and diagnostic sensors."""
|
||||||
key = self.entity_description.key
|
key = self.entity_description.key
|
||||||
price_info = self.coordinator.data["data"]["viewer"]["homes"][0]["currentSubscription"]["priceInfo"]
|
price_info = self.coordinator.data["priceInfo"]
|
||||||
now = dt_util.now()
|
now = dt_util.now()
|
||||||
if key == "price_rating":
|
if key == "price_rating":
|
||||||
today_prices = price_info.get("today", [])
|
today_prices = price_info.get("today", [])
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue