mirror of
https://github.com/jpawlowski/hass.tibber_prices.git
synced 2026-03-30 05:13:40 +00:00
add some entities
This commit is contained in:
parent
5150ae3f48
commit
23a46faecc
10 changed files with 723 additions and 95 deletions
|
|
@ -28,7 +28,6 @@ if TYPE_CHECKING:
|
||||||
PLATFORMS: list[Platform] = [
|
PLATFORMS: list[Platform] = [
|
||||||
Platform.SENSOR,
|
Platform.SENSOR,
|
||||||
Platform.BINARY_SENSOR,
|
Platform.BINARY_SENSOR,
|
||||||
Platform.SWITCH,
|
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,7 @@
|
||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from datetime import datetime
|
||||||
from typing import TYPE_CHECKING
|
from typing import TYPE_CHECKING
|
||||||
|
|
||||||
from homeassistant.components.binary_sensor import (
|
from homeassistant.components.binary_sensor import (
|
||||||
|
|
@ -10,6 +11,7 @@ from homeassistant.components.binary_sensor import (
|
||||||
BinarySensorEntityDescription,
|
BinarySensorEntityDescription,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
from .const import NAME
|
||||||
from .entity import TibberPricesEntity
|
from .entity import TibberPricesEntity
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
|
|
@ -21,15 +23,30 @@ if TYPE_CHECKING:
|
||||||
|
|
||||||
ENTITY_DESCRIPTIONS = (
|
ENTITY_DESCRIPTIONS = (
|
||||||
BinarySensorEntityDescription(
|
BinarySensorEntityDescription(
|
||||||
key="tibber_prices",
|
key="peak_hour",
|
||||||
name="Tibber Prices Binary Sensor",
|
translation_key="peak_hour",
|
||||||
|
name="Electricity Peak Hour",
|
||||||
|
device_class=BinarySensorDeviceClass.POWER,
|
||||||
|
icon="mdi:flash-alert",
|
||||||
|
),
|
||||||
|
BinarySensorEntityDescription(
|
||||||
|
key="best_price_hour",
|
||||||
|
translation_key="best_price_hour",
|
||||||
|
name="Best Electricity Price Hour",
|
||||||
|
device_class=BinarySensorDeviceClass.POWER,
|
||||||
|
icon="mdi:flash-outline",
|
||||||
|
),
|
||||||
|
BinarySensorEntityDescription(
|
||||||
|
key="connection",
|
||||||
|
translation_key="connection",
|
||||||
|
name="Tibber API Connection",
|
||||||
device_class=BinarySensorDeviceClass.CONNECTIVITY,
|
device_class=BinarySensorDeviceClass.CONNECTIVITY,
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
async def async_setup_entry(
|
async def async_setup_entry(
|
||||||
hass: HomeAssistant, # noqa: ARG001 Unused function argument: `hass`
|
hass: HomeAssistant,
|
||||||
entry: TibberPricesConfigEntry,
|
entry: TibberPricesConfigEntry,
|
||||||
async_add_entities: AddEntitiesCallback,
|
async_add_entities: AddEntitiesCallback,
|
||||||
) -> None:
|
) -> None:
|
||||||
|
|
@ -54,8 +71,106 @@ class TibberPricesBinarySensor(TibberPricesEntity, BinarySensorEntity):
|
||||||
"""Initialize the binary_sensor class."""
|
"""Initialize the binary_sensor class."""
|
||||||
super().__init__(coordinator)
|
super().__init__(coordinator)
|
||||||
self.entity_description = entity_description
|
self.entity_description = entity_description
|
||||||
|
self._attr_unique_id = f"{coordinator.config_entry.entry_id}_{entity_description.key}"
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def is_on(self) -> bool:
|
def is_on(self) -> bool | None:
|
||||||
"""Return true if the binary_sensor is on."""
|
"""Return true if the binary_sensor is on."""
|
||||||
return self.coordinator.data.get("title", "") == "foo"
|
try:
|
||||||
|
if not self.coordinator.data:
|
||||||
|
return None
|
||||||
|
|
||||||
|
subscription = self.coordinator.data["data"]["viewer"]["homes"][0]["currentSubscription"]
|
||||||
|
price_info = subscription["priceInfo"]
|
||||||
|
|
||||||
|
now = datetime.now()
|
||||||
|
current_hour_data = None
|
||||||
|
today_prices = price_info.get("today", [])
|
||||||
|
|
||||||
|
if not today_prices:
|
||||||
|
return None
|
||||||
|
|
||||||
|
# Find current hour's data
|
||||||
|
for price_data in today_prices:
|
||||||
|
starts_at = datetime.fromisoformat(price_data["startsAt"])
|
||||||
|
if starts_at.hour == now.hour:
|
||||||
|
current_hour_data = price_data
|
||||||
|
break
|
||||||
|
|
||||||
|
if not current_hour_data:
|
||||||
|
return None
|
||||||
|
|
||||||
|
if self.entity_description.key == "peak_hour":
|
||||||
|
# Consider it a peak hour if the price is in the top 20% of today's prices
|
||||||
|
prices = [float(price["total"]) for price in today_prices]
|
||||||
|
prices.sort()
|
||||||
|
threshold_index = int(len(prices) * 0.8)
|
||||||
|
peak_threshold = prices[threshold_index]
|
||||||
|
return float(current_hour_data["total"]) >= peak_threshold
|
||||||
|
|
||||||
|
elif self.entity_description.key == "best_price_hour":
|
||||||
|
# Consider it a best price hour if the price is in the bottom 20% of today's prices
|
||||||
|
prices = [float(price["total"]) for price in today_prices]
|
||||||
|
prices.sort()
|
||||||
|
threshold_index = int(len(prices) * 0.2)
|
||||||
|
best_threshold = prices[threshold_index]
|
||||||
|
return float(current_hour_data["total"]) <= best_threshold
|
||||||
|
|
||||||
|
elif self.entity_description.key == "connection":
|
||||||
|
# Check if we have valid current data
|
||||||
|
return bool(current_hour_data)
|
||||||
|
|
||||||
|
return None
|
||||||
|
|
||||||
|
except (KeyError, ValueError, TypeError) as ex:
|
||||||
|
self.coordinator.logger.error(
|
||||||
|
"Error getting binary sensor state",
|
||||||
|
extra={
|
||||||
|
"error": str(ex),
|
||||||
|
"entity": self.entity_description.key,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
return None
|
||||||
|
|
||||||
|
@property
|
||||||
|
def extra_state_attributes(self) -> dict | None:
|
||||||
|
"""Return additional state attributes."""
|
||||||
|
try:
|
||||||
|
if not self.coordinator.data:
|
||||||
|
return None
|
||||||
|
|
||||||
|
subscription = self.coordinator.data["data"]["viewer"]["homes"][0]["currentSubscription"]
|
||||||
|
price_info = subscription["priceInfo"]
|
||||||
|
|
||||||
|
attributes = {}
|
||||||
|
|
||||||
|
if self.entity_description.key in ["peak_hour", "best_price_hour"]:
|
||||||
|
today_prices = price_info.get("today", [])
|
||||||
|
if today_prices:
|
||||||
|
prices = [(datetime.fromisoformat(price["startsAt"]).hour, float(price["total"]))
|
||||||
|
for price in today_prices]
|
||||||
|
|
||||||
|
if self.entity_description.key == "peak_hour":
|
||||||
|
# Get top 5 peak hours
|
||||||
|
peak_hours = sorted(prices, key=lambda x: x[1], reverse=True)[:5]
|
||||||
|
attributes["peak_hours"] = [
|
||||||
|
{"hour": hour, "price": price} for hour, price in peak_hours
|
||||||
|
]
|
||||||
|
else:
|
||||||
|
# Get top 5 best price hours
|
||||||
|
best_hours = sorted(prices, key=lambda x: x[1])[:5]
|
||||||
|
attributes["best_price_hours"] = [
|
||||||
|
{"hour": hour, "price": price} for hour, price in best_hours
|
||||||
|
]
|
||||||
|
|
||||||
|
return attributes if attributes else None
|
||||||
|
|
||||||
|
except (KeyError, ValueError, TypeError) as ex:
|
||||||
|
self.coordinator.logger.error(
|
||||||
|
"Error getting binary sensor attributes",
|
||||||
|
extra={
|
||||||
|
"error": str(ex),
|
||||||
|
"entity": self.entity_description.key,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
return None
|
||||||
|
|
|
||||||
|
|
@ -23,6 +23,18 @@ class TibberPricesFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
|
||||||
|
|
||||||
VERSION = 1
|
VERSION = 1
|
||||||
|
|
||||||
|
def __init__(self) -> None:
|
||||||
|
"""Initialize the config flow."""
|
||||||
|
super().__init__()
|
||||||
|
self._reauth_entry: config_entries.ConfigEntry | None = None
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def async_get_options_flow(
|
||||||
|
config_entry: config_entries.ConfigEntry,
|
||||||
|
) -> config_entries.OptionsFlow:
|
||||||
|
"""Get the options flow for this handler."""
|
||||||
|
return TibberPricesOptionsFlowHandler(config_entry)
|
||||||
|
|
||||||
async def async_step_user(
|
async def async_step_user(
|
||||||
self,
|
self,
|
||||||
user_input: dict | None = None,
|
user_input: dict | None = None,
|
||||||
|
|
@ -78,3 +90,78 @@ class TibberPricesFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
|
||||||
)
|
)
|
||||||
result = await client.async_test_connection()
|
result = await client.async_test_connection()
|
||||||
return result["viewer"]["name"]
|
return result["viewer"]["name"]
|
||||||
|
|
||||||
|
|
||||||
|
class TibberPricesOptionsFlowHandler(config_entries.OptionsFlow):
|
||||||
|
"""Tibber Prices config flow options handler."""
|
||||||
|
|
||||||
|
def __init__(self, config_entry: config_entries.ConfigEntry) -> None:
|
||||||
|
"""Initialize options flow."""
|
||||||
|
super().__init__()
|
||||||
|
# Store the entry_id instead of the whole config_entry
|
||||||
|
self._entry_id = config_entry.entry_id
|
||||||
|
|
||||||
|
async def async_step_init(
|
||||||
|
self, user_input: dict | None = None
|
||||||
|
) -> config_entries.ConfigFlowResult:
|
||||||
|
"""Manage the options."""
|
||||||
|
errors: dict[str, str] = {}
|
||||||
|
|
||||||
|
if user_input is not None:
|
||||||
|
try:
|
||||||
|
# Test the new access token and get account name
|
||||||
|
client = TibberPricesApiClient(
|
||||||
|
access_token=user_input[CONF_ACCESS_TOKEN],
|
||||||
|
session=async_create_clientsession(self.hass),
|
||||||
|
)
|
||||||
|
result = await client.async_test_connection()
|
||||||
|
new_account_name = result["viewer"]["name"]
|
||||||
|
|
||||||
|
# Get the config entry using the entry_id
|
||||||
|
config_entry = self.hass.config_entries.async_get_entry(self._entry_id)
|
||||||
|
if not config_entry:
|
||||||
|
return self.async_abort(reason="entry_not_found")
|
||||||
|
|
||||||
|
# Check if this token is for the same account
|
||||||
|
current_unique_id = config_entry.unique_id
|
||||||
|
new_unique_id = slugify(new_account_name)
|
||||||
|
|
||||||
|
if current_unique_id != new_unique_id:
|
||||||
|
# Token is for a different account
|
||||||
|
errors["base"] = "different_account"
|
||||||
|
else:
|
||||||
|
# Update the config entry with the new access token
|
||||||
|
return self.async_create_entry(title="", data=user_input)
|
||||||
|
|
||||||
|
except TibberPricesApiClientAuthenticationError as exception:
|
||||||
|
LOGGER.warning(exception)
|
||||||
|
errors["base"] = "auth"
|
||||||
|
except TibberPricesApiClientCommunicationError as exception:
|
||||||
|
LOGGER.error(exception)
|
||||||
|
errors["base"] = "connection"
|
||||||
|
except TibberPricesApiClientError as exception:
|
||||||
|
LOGGER.exception(exception)
|
||||||
|
errors["base"] = "unknown"
|
||||||
|
|
||||||
|
# Get current config entry to get the current access token
|
||||||
|
config_entry = self.hass.config_entries.async_get_entry(self._entry_id)
|
||||||
|
if not config_entry:
|
||||||
|
return self.async_abort(reason="entry_not_found")
|
||||||
|
|
||||||
|
# If there's no user input or if there were errors, show the form
|
||||||
|
schema = {
|
||||||
|
vol.Required(
|
||||||
|
CONF_ACCESS_TOKEN,
|
||||||
|
default=config_entry.data.get(CONF_ACCESS_TOKEN, ""),
|
||||||
|
): selector.TextSelector(
|
||||||
|
selector.TextSelectorConfig(
|
||||||
|
type=selector.TextSelectorType.TEXT,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
}
|
||||||
|
|
||||||
|
return self.async_show_form(
|
||||||
|
step_id="init",
|
||||||
|
data_schema=vol.Schema(schema),
|
||||||
|
errors=errors,
|
||||||
|
)
|
||||||
|
|
|
||||||
|
|
@ -9,6 +9,7 @@ from typing import TYPE_CHECKING, Any, Final, TypedDict, cast
|
||||||
|
|
||||||
from homeassistant.core import HomeAssistant, callback
|
from homeassistant.core import HomeAssistant, callback
|
||||||
from homeassistant.exceptions import ConfigEntryAuthFailed
|
from homeassistant.exceptions import ConfigEntryAuthFailed
|
||||||
|
from homeassistant.helpers.event import async_track_time_change
|
||||||
from homeassistant.helpers.storage import Store
|
from homeassistant.helpers.storage import Store
|
||||||
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
|
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
|
||||||
import homeassistant.util.dt as dt_util
|
import homeassistant.util.dt as dt_util
|
||||||
|
|
@ -162,15 +163,15 @@ class TibberPricesDataUpdateCoordinator(DataUpdateCoordinator[TibberPricesData])
|
||||||
|
|
||||||
# Schedule updates at the start of every hour
|
# Schedule updates at the start of every hour
|
||||||
self._remove_update_listeners.append(
|
self._remove_update_listeners.append(
|
||||||
hass.helpers.event.async_track_time_change(
|
async_track_time_change(
|
||||||
self._async_refresh_hourly, minute=0, second=0
|
hass, self._async_refresh_hourly, minute=0, second=0
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
# Schedule data rotation at midnight
|
# Schedule data rotation at midnight
|
||||||
self._remove_update_listeners.append(
|
self._remove_update_listeners.append(
|
||||||
hass.helpers.event.async_track_time_change(
|
async_track_time_change(
|
||||||
self._async_handle_midnight_rotation, hour=0, minute=0, second=0
|
hass, self._async_handle_midnight_rotation, hour=0, minute=0, second=0
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,7 @@ from __future__ import annotations
|
||||||
from homeassistant.helpers.device_registry import DeviceInfo
|
from homeassistant.helpers.device_registry import DeviceInfo
|
||||||
from homeassistant.helpers.update_coordinator import CoordinatorEntity
|
from homeassistant.helpers.update_coordinator import CoordinatorEntity
|
||||||
|
|
||||||
from .const import ATTRIBUTION
|
from .const import ATTRIBUTION, DOMAIN, NAME
|
||||||
from .coordinator import TibberPricesDataUpdateCoordinator
|
from .coordinator import TibberPricesDataUpdateCoordinator
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -13,16 +13,27 @@ class TibberPricesEntity(CoordinatorEntity[TibberPricesDataUpdateCoordinator]):
|
||||||
"""TibberPricesEntity class."""
|
"""TibberPricesEntity class."""
|
||||||
|
|
||||||
_attr_attribution = ATTRIBUTION
|
_attr_attribution = ATTRIBUTION
|
||||||
|
_attr_has_entity_name = True
|
||||||
|
|
||||||
def __init__(self, coordinator: TibberPricesDataUpdateCoordinator) -> None:
|
def __init__(self, coordinator: TibberPricesDataUpdateCoordinator) -> None:
|
||||||
"""Initialize."""
|
"""Initialize."""
|
||||||
super().__init__(coordinator)
|
super().__init__(coordinator)
|
||||||
self._attr_unique_id = coordinator.config_entry.entry_id
|
|
||||||
|
# Get home name from Tibber API if available
|
||||||
|
home_name = None
|
||||||
|
if coordinator.data:
|
||||||
|
try:
|
||||||
|
home = coordinator.data["data"]["viewer"]["homes"][0]
|
||||||
|
home_name = home.get("address", {}).get("address1", "Tibber Home")
|
||||||
|
except (KeyError, IndexError):
|
||||||
|
home_name = "Tibber Home"
|
||||||
|
else:
|
||||||
|
home_name = "Tibber Home"
|
||||||
|
|
||||||
self._attr_device_info = DeviceInfo(
|
self._attr_device_info = DeviceInfo(
|
||||||
identifiers={
|
identifiers={(DOMAIN, coordinator.config_entry.entry_id)},
|
||||||
(
|
name=home_name,
|
||||||
coordinator.config_entry.domain,
|
manufacturer="Tibber",
|
||||||
coordinator.config_entry.entry_id,
|
model="Price API",
|
||||||
),
|
sw_version=str(coordinator.config_entry.version),
|
||||||
},
|
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -6,6 +6,7 @@
|
||||||
],
|
],
|
||||||
"config_flow": true,
|
"config_flow": true,
|
||||||
"documentation": "https://github.com/jpawlowski/hass.tibber_prices",
|
"documentation": "https://github.com/jpawlowski/hass.tibber_prices",
|
||||||
|
"icon": "mdi:chart-line",
|
||||||
"iot_class": "cloud_polling",
|
"iot_class": "cloud_polling",
|
||||||
"issue_tracker": "https://github.com/jpawlowski/hass.tibber_prices/issues",
|
"issue_tracker": "https://github.com/jpawlowski/hass.tibber_prices/issues",
|
||||||
"version": "0.1.0"
|
"version": "0.1.0"
|
||||||
|
|
|
||||||
|
|
@ -2,10 +2,19 @@
|
||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from datetime import datetime
|
||||||
from typing import TYPE_CHECKING
|
from typing import TYPE_CHECKING
|
||||||
|
|
||||||
from homeassistant.components.sensor import SensorEntity, SensorEntityDescription
|
from homeassistant.components.sensor import (
|
||||||
|
SensorDeviceClass,
|
||||||
|
SensorEntity,
|
||||||
|
SensorEntityDescription,
|
||||||
|
SensorStateClass,
|
||||||
|
)
|
||||||
|
from homeassistant.const import CURRENCY_EURO, EntityCategory
|
||||||
|
from homeassistant.util import dt as dt_util
|
||||||
|
|
||||||
|
from .const import DOMAIN
|
||||||
from .entity import TibberPricesEntity
|
from .entity import TibberPricesEntity
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
|
|
@ -15,17 +24,126 @@ if TYPE_CHECKING:
|
||||||
from .coordinator import TibberPricesDataUpdateCoordinator
|
from .coordinator import TibberPricesDataUpdateCoordinator
|
||||||
from .data import TibberPricesConfigEntry
|
from .data import TibberPricesConfigEntry
|
||||||
|
|
||||||
ENTITY_DESCRIPTIONS = (
|
# Main price sensors that users will typically use in automations
|
||||||
|
PRICE_SENSORS = (
|
||||||
SensorEntityDescription(
|
SensorEntityDescription(
|
||||||
key="tibber_prices",
|
key="current_price",
|
||||||
name="Integration Sensor",
|
translation_key="current_price",
|
||||||
icon="mdi:format-quote-close",
|
name="Current Electricity Price",
|
||||||
|
icon="mdi:currency-eur",
|
||||||
|
device_class=SensorDeviceClass.MONETARY,
|
||||||
|
state_class=SensorStateClass.TOTAL,
|
||||||
|
native_unit_of_measurement=CURRENCY_EURO,
|
||||||
),
|
),
|
||||||
|
SensorEntityDescription(
|
||||||
|
key="next_hour_price",
|
||||||
|
translation_key="next_hour_price",
|
||||||
|
name="Next Hour Electricity Price",
|
||||||
|
icon="mdi:currency-eur-off",
|
||||||
|
device_class=SensorDeviceClass.MONETARY,
|
||||||
|
state_class=SensorStateClass.TOTAL,
|
||||||
|
native_unit_of_measurement=CURRENCY_EURO,
|
||||||
|
),
|
||||||
|
SensorEntityDescription(
|
||||||
|
key="price_level",
|
||||||
|
translation_key="price_level",
|
||||||
|
name="Current Price Level",
|
||||||
|
icon="mdi:meter-electric",
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
# Statistical price sensors
|
||||||
|
STATISTICS_SENSORS = (
|
||||||
|
SensorEntityDescription(
|
||||||
|
key="lowest_price_today",
|
||||||
|
translation_key="lowest_price_today",
|
||||||
|
name="Today's Lowest Price",
|
||||||
|
icon="mdi:currency-eur",
|
||||||
|
device_class=SensorDeviceClass.MONETARY,
|
||||||
|
state_class=SensorStateClass.TOTAL,
|
||||||
|
native_unit_of_measurement=CURRENCY_EURO,
|
||||||
|
entity_category=EntityCategory.DIAGNOSTIC,
|
||||||
|
),
|
||||||
|
SensorEntityDescription(
|
||||||
|
key="highest_price_today",
|
||||||
|
translation_key="highest_price_today",
|
||||||
|
name="Today's Highest Price",
|
||||||
|
icon="mdi:currency-eur",
|
||||||
|
device_class=SensorDeviceClass.MONETARY,
|
||||||
|
state_class=SensorStateClass.TOTAL,
|
||||||
|
native_unit_of_measurement=CURRENCY_EURO,
|
||||||
|
entity_category=EntityCategory.DIAGNOSTIC,
|
||||||
|
),
|
||||||
|
SensorEntityDescription(
|
||||||
|
key="average_price_today",
|
||||||
|
translation_key="average_price_today",
|
||||||
|
name="Today's Average Price",
|
||||||
|
icon="mdi:currency-eur",
|
||||||
|
device_class=SensorDeviceClass.MONETARY,
|
||||||
|
state_class=SensorStateClass.TOTAL,
|
||||||
|
native_unit_of_measurement=CURRENCY_EURO,
|
||||||
|
entity_category=EntityCategory.DIAGNOSTIC,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
# Rating sensors
|
||||||
|
RATING_SENSORS = (
|
||||||
|
SensorEntityDescription(
|
||||||
|
key="hourly_rating",
|
||||||
|
translation_key="hourly_rating",
|
||||||
|
name="Hourly Price Rating",
|
||||||
|
icon="mdi:clock-outline",
|
||||||
|
native_unit_of_measurement="%",
|
||||||
|
entity_category=EntityCategory.DIAGNOSTIC,
|
||||||
|
),
|
||||||
|
SensorEntityDescription(
|
||||||
|
key="daily_rating",
|
||||||
|
translation_key="daily_rating",
|
||||||
|
name="Daily Price Rating",
|
||||||
|
icon="mdi:calendar-today",
|
||||||
|
native_unit_of_measurement="%",
|
||||||
|
entity_category=EntityCategory.DIAGNOSTIC,
|
||||||
|
),
|
||||||
|
SensorEntityDescription(
|
||||||
|
key="monthly_rating",
|
||||||
|
translation_key="monthly_rating",
|
||||||
|
name="Monthly Price Rating",
|
||||||
|
icon="mdi:calendar-month",
|
||||||
|
native_unit_of_measurement="%",
|
||||||
|
entity_category=EntityCategory.DIAGNOSTIC,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
# Diagnostic sensors for data availability
|
||||||
|
DIAGNOSTIC_SENSORS = (
|
||||||
|
SensorEntityDescription(
|
||||||
|
key="data_timestamp",
|
||||||
|
translation_key="data_timestamp",
|
||||||
|
name="Last Data Update",
|
||||||
|
icon="mdi:clock-check",
|
||||||
|
device_class=SensorDeviceClass.TIMESTAMP,
|
||||||
|
entity_category=EntityCategory.DIAGNOSTIC,
|
||||||
|
),
|
||||||
|
SensorEntityDescription(
|
||||||
|
key="tomorrow_data_available",
|
||||||
|
translation_key="tomorrow_data_available",
|
||||||
|
name="Tomorrow's Data Status",
|
||||||
|
icon="mdi:calendar-check",
|
||||||
|
entity_category=EntityCategory.DIAGNOSTIC,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
# Combine all sensors
|
||||||
|
ENTITY_DESCRIPTIONS = (
|
||||||
|
*PRICE_SENSORS,
|
||||||
|
*STATISTICS_SENSORS,
|
||||||
|
*RATING_SENSORS,
|
||||||
|
*DIAGNOSTIC_SENSORS,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
async def async_setup_entry(
|
async def async_setup_entry(
|
||||||
hass: HomeAssistant, # noqa: ARG001 Unused function argument: `hass`
|
hass: HomeAssistant,
|
||||||
entry: TibberPricesConfigEntry,
|
entry: TibberPricesConfigEntry,
|
||||||
async_add_entities: AddEntitiesCallback,
|
async_add_entities: AddEntitiesCallback,
|
||||||
) -> None:
|
) -> None:
|
||||||
|
|
@ -50,8 +168,183 @@ class TibberPricesSensor(TibberPricesEntity, SensorEntity):
|
||||||
"""Initialize the sensor class."""
|
"""Initialize the sensor class."""
|
||||||
super().__init__(coordinator)
|
super().__init__(coordinator)
|
||||||
self.entity_description = entity_description
|
self.entity_description = entity_description
|
||||||
|
self._attr_unique_id = f"{coordinator.config_entry.entry_id}_{entity_description.key}"
|
||||||
|
self._attr_has_entity_name = True
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def native_value(self) -> str | None:
|
def native_value(self) -> float | str | datetime | None:
|
||||||
"""Return the native value of the sensor."""
|
"""Return the native value of the sensor."""
|
||||||
return self.coordinator.data.get("body")
|
try:
|
||||||
|
if not self.coordinator.data:
|
||||||
|
return None
|
||||||
|
|
||||||
|
subscription = self.coordinator.data["data"]["viewer"]["homes"][0]["currentSubscription"]
|
||||||
|
price_info = subscription["priceInfo"]
|
||||||
|
price_rating = subscription.get("priceRating") or {}
|
||||||
|
|
||||||
|
# Get current hour's data
|
||||||
|
now = datetime.now()
|
||||||
|
current_hour_data = None
|
||||||
|
for price_data in price_info.get("today", []):
|
||||||
|
starts_at = datetime.fromisoformat(price_data["startsAt"])
|
||||||
|
if starts_at.hour == now.hour:
|
||||||
|
current_hour_data = price_data
|
||||||
|
break
|
||||||
|
|
||||||
|
if self.entity_description.key == "current_price":
|
||||||
|
return float(current_hour_data["total"]) if current_hour_data else None
|
||||||
|
|
||||||
|
elif self.entity_description.key == "next_hour_price":
|
||||||
|
next_hour = (now.hour + 1) % 24
|
||||||
|
for price_data in price_info.get("today", []):
|
||||||
|
starts_at = datetime.fromisoformat(price_data["startsAt"])
|
||||||
|
if starts_at.hour == next_hour:
|
||||||
|
return float(price_data["total"])
|
||||||
|
return None
|
||||||
|
|
||||||
|
elif self.entity_description.key == "lowest_price_today":
|
||||||
|
today_prices = price_info.get("today", [])
|
||||||
|
if not today_prices:
|
||||||
|
return None
|
||||||
|
return min(float(price["total"]) for price in today_prices)
|
||||||
|
|
||||||
|
elif self.entity_description.key == "highest_price_today":
|
||||||
|
today_prices = price_info.get("today", [])
|
||||||
|
if not today_prices:
|
||||||
|
return None
|
||||||
|
return max(float(price["total"]) for price in today_prices)
|
||||||
|
|
||||||
|
elif self.entity_description.key == "average_price_today":
|
||||||
|
today_prices = price_info.get("today", [])
|
||||||
|
if not today_prices:
|
||||||
|
return None
|
||||||
|
return sum(float(price["total"]) for price in today_prices) / len(today_prices)
|
||||||
|
|
||||||
|
elif self.entity_description.key == "price_level":
|
||||||
|
return current_hour_data["level"] if current_hour_data else None
|
||||||
|
|
||||||
|
elif self.entity_description.key == "hourly_rating":
|
||||||
|
hourly = price_rating.get("hourly", {})
|
||||||
|
entries = hourly.get("entries", []) if hourly else []
|
||||||
|
if not entries:
|
||||||
|
return None
|
||||||
|
for entry in entries:
|
||||||
|
starts_at = datetime.fromisoformat(entry["time"])
|
||||||
|
if starts_at.hour == now.hour:
|
||||||
|
return round(float(entry["difference"]) * 100, 1)
|
||||||
|
return None
|
||||||
|
|
||||||
|
elif self.entity_description.key == "daily_rating":
|
||||||
|
daily = price_rating.get("daily", {})
|
||||||
|
entries = daily.get("entries", []) if daily else []
|
||||||
|
if not entries:
|
||||||
|
return None
|
||||||
|
for entry in entries:
|
||||||
|
starts_at = datetime.fromisoformat(entry["time"])
|
||||||
|
if starts_at.date() == now.date():
|
||||||
|
return round(float(entry["difference"]) * 100, 1)
|
||||||
|
return None
|
||||||
|
|
||||||
|
elif self.entity_description.key == "monthly_rating":
|
||||||
|
monthly = price_rating.get("monthly", {})
|
||||||
|
entries = monthly.get("entries", []) if monthly else []
|
||||||
|
if not entries:
|
||||||
|
return None
|
||||||
|
for entry in entries:
|
||||||
|
starts_at = datetime.fromisoformat(entry["time"])
|
||||||
|
if starts_at.month == now.month and starts_at.year == now.year:
|
||||||
|
return round(float(entry["difference"]) * 100, 1)
|
||||||
|
return None
|
||||||
|
|
||||||
|
elif self.entity_description.key == "data_timestamp":
|
||||||
|
# Return the latest timestamp from any data we have
|
||||||
|
latest_timestamp = None
|
||||||
|
|
||||||
|
# Check today's data
|
||||||
|
for price_data in price_info.get("today", []):
|
||||||
|
timestamp = datetime.fromisoformat(price_data["startsAt"])
|
||||||
|
if not latest_timestamp or timestamp > latest_timestamp:
|
||||||
|
latest_timestamp = timestamp
|
||||||
|
|
||||||
|
# Check tomorrow's data
|
||||||
|
for price_data in price_info.get("tomorrow", []):
|
||||||
|
timestamp = datetime.fromisoformat(price_data["startsAt"])
|
||||||
|
if not latest_timestamp or timestamp > latest_timestamp:
|
||||||
|
latest_timestamp = timestamp
|
||||||
|
|
||||||
|
return dt_util.as_utc(latest_timestamp) if latest_timestamp else None
|
||||||
|
|
||||||
|
elif self.entity_description.key == "tomorrow_data_available":
|
||||||
|
tomorrow_prices = price_info.get("tomorrow", [])
|
||||||
|
if not tomorrow_prices:
|
||||||
|
return "No"
|
||||||
|
# Check if we have a full day of data (24 hours)
|
||||||
|
return "Yes" if len(tomorrow_prices) == 24 else "Partial"
|
||||||
|
|
||||||
|
return None
|
||||||
|
|
||||||
|
except (KeyError, ValueError, TypeError) as ex:
|
||||||
|
self.coordinator.logger.error(
|
||||||
|
"Error getting sensor value",
|
||||||
|
extra={
|
||||||
|
"error": str(ex),
|
||||||
|
"entity": self.entity_description.key,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
return None
|
||||||
|
|
||||||
|
@property
|
||||||
|
def extra_state_attributes(self) -> dict | None:
|
||||||
|
"""Return additional state attributes."""
|
||||||
|
try:
|
||||||
|
if not self.coordinator.data:
|
||||||
|
return None
|
||||||
|
|
||||||
|
subscription = self.coordinator.data["data"]["viewer"]["homes"][0]["currentSubscription"]
|
||||||
|
price_info = subscription["priceInfo"]
|
||||||
|
|
||||||
|
attributes = {}
|
||||||
|
|
||||||
|
if self.entity_description.key == "current_price":
|
||||||
|
attributes["timestamp"] = price_info.get("current", {}).get("startsAt")
|
||||||
|
elif self.entity_description.key == "next_hour_price":
|
||||||
|
attributes["timestamp"] = price_info.get("current", {}).get("startsAt")
|
||||||
|
elif self.entity_description.key == "price_level":
|
||||||
|
attributes["timestamp"] = price_info.get("current", {}).get("startsAt")
|
||||||
|
elif self.entity_description.key == "lowest_price_today":
|
||||||
|
attributes["timestamp"] = price_info.get("today", [{}])[0].get("startsAt")
|
||||||
|
elif self.entity_description.key == "highest_price_today":
|
||||||
|
attributes["timestamp"] = price_info.get("today", [{}])[0].get("startsAt")
|
||||||
|
elif self.entity_description.key == "average_price_today":
|
||||||
|
attributes["timestamp"] = price_info.get("today", [{}])[0].get("startsAt")
|
||||||
|
elif self.entity_description.key == "hourly_rating":
|
||||||
|
attributes["timestamp"] = price_info.get("current", {}).get("startsAt")
|
||||||
|
elif self.entity_description.key == "daily_rating":
|
||||||
|
attributes["timestamp"] = price_info.get("today", [{}])[0].get("startsAt")
|
||||||
|
elif self.entity_description.key == "monthly_rating":
|
||||||
|
attributes["timestamp"] = price_info.get("today", [{}])[0].get("startsAt")
|
||||||
|
elif self.entity_description.key == "data_timestamp":
|
||||||
|
attributes["timestamp"] = price_info.get("today", [{}])[0].get("startsAt")
|
||||||
|
elif self.entity_description.key == "tomorrow_data_available":
|
||||||
|
attributes["timestamp"] = price_info.get("today", [{}])[0].get("startsAt")
|
||||||
|
|
||||||
|
# Add translated description
|
||||||
|
if self.hass is not None:
|
||||||
|
key = f"entity.sensor.{self.entity_description.translation_key}.description"
|
||||||
|
language_config = getattr(self.hass.config, 'language', None)
|
||||||
|
if isinstance(language_config, dict):
|
||||||
|
description = language_config.get(key)
|
||||||
|
if description is not None:
|
||||||
|
attributes["description"] = description
|
||||||
|
|
||||||
|
return attributes if attributes else None
|
||||||
|
|
||||||
|
except (KeyError, ValueError, TypeError) as ex:
|
||||||
|
self.coordinator.logger.error(
|
||||||
|
"Error getting sensor attributes",
|
||||||
|
extra={
|
||||||
|
"error": str(ex),
|
||||||
|
"entity": self.entity_description.key,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
return None
|
||||||
|
|
|
||||||
|
|
@ -1,65 +0,0 @@
|
||||||
"""Switch platform for tibber_prices."""
|
|
||||||
|
|
||||||
from __future__ import annotations
|
|
||||||
|
|
||||||
from typing import TYPE_CHECKING, Any
|
|
||||||
|
|
||||||
from homeassistant.components.switch import SwitchEntity, SwitchEntityDescription
|
|
||||||
|
|
||||||
from .entity import TibberPricesEntity
|
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
|
||||||
from homeassistant.core import HomeAssistant
|
|
||||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
|
||||||
|
|
||||||
from .coordinator import TibberPricesDataUpdateCoordinator
|
|
||||||
from .data import TibberPricesConfigEntry
|
|
||||||
|
|
||||||
ENTITY_DESCRIPTIONS = (
|
|
||||||
SwitchEntityDescription(
|
|
||||||
key="tibber_prices",
|
|
||||||
name="Integration Switch",
|
|
||||||
icon="mdi:format-quote-close",
|
|
||||||
),
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
async def async_setup_entry(
|
|
||||||
hass: HomeAssistant, # noqa: ARG001 Unused function argument: `hass`
|
|
||||||
entry: TibberPricesConfigEntry,
|
|
||||||
async_add_entities: AddEntitiesCallback,
|
|
||||||
) -> None:
|
|
||||||
"""Set up the switch platform."""
|
|
||||||
async_add_entities(
|
|
||||||
TibberPricesSwitch(
|
|
||||||
coordinator=entry.runtime_data.coordinator,
|
|
||||||
entity_description=entity_description,
|
|
||||||
)
|
|
||||||
for entity_description in ENTITY_DESCRIPTIONS
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class TibberPricesSwitch(TibberPricesEntity, SwitchEntity):
|
|
||||||
"""tibber_prices switch class."""
|
|
||||||
|
|
||||||
def __init__(
|
|
||||||
self,
|
|
||||||
coordinator: TibberPricesDataUpdateCoordinator,
|
|
||||||
entity_description: SwitchEntityDescription,
|
|
||||||
) -> None:
|
|
||||||
"""Initialize the switch class."""
|
|
||||||
super().__init__(coordinator)
|
|
||||||
self.entity_description = entity_description
|
|
||||||
|
|
||||||
@property
|
|
||||||
def is_on(self) -> bool:
|
|
||||||
"""Return true if the switch is on."""
|
|
||||||
return self.coordinator.data.get("title", "") == "foo"
|
|
||||||
|
|
||||||
async def async_turn_on(self, **_: Any) -> None:
|
|
||||||
"""Turn on the switch."""
|
|
||||||
await self.coordinator.async_request_refresh()
|
|
||||||
|
|
||||||
async def async_turn_off(self, **_: Any) -> None:
|
|
||||||
"""Turn off the switch."""
|
|
||||||
await self.coordinator.async_request_refresh()
|
|
||||||
103
custom_components/tibber_prices/translations/de.json
Normal file
103
custom_components/tibber_prices/translations/de.json
Normal file
|
|
@ -0,0 +1,103 @@
|
||||||
|
{
|
||||||
|
"config": {
|
||||||
|
"step": {
|
||||||
|
"user": {
|
||||||
|
"description": "Wenn Sie Hilfe bei der Konfiguration benötigen, schauen Sie hier: https://github.com/jpawlowski/hass.tibber_prices",
|
||||||
|
"data": {
|
||||||
|
"access_token": "Tibber Zugangstoken"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"error": {
|
||||||
|
"auth": "Der Tibber Zugangstoken ist ungültig.",
|
||||||
|
"connection": "Keine Verbindung zu Tibber möglich. Bitte überprüfen Sie Ihre Internetverbindung.",
|
||||||
|
"unknown": "Ein unerwarteter Fehler ist aufgetreten. Bitte prüfen Sie die Logs für Details."
|
||||||
|
},
|
||||||
|
"abort": {
|
||||||
|
"already_configured": "Dieser Eintrag ist bereits konfiguriert.",
|
||||||
|
"entry_not_found": "Tibber-Konfigurationseintrag nicht gefunden."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"options": {
|
||||||
|
"step": {
|
||||||
|
"init": {
|
||||||
|
"title": "Tibber-Konfiguration aktualisieren",
|
||||||
|
"description": "Aktualisieren Sie Ihren Tibber-API-Zugangstoken. Wenn Sie einen neuen Token benötigen, können Sie diesen unter https://developer.tibber.com/settings/access-token generieren",
|
||||||
|
"data": {
|
||||||
|
"access_token": "Tibber Zugangstoken"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"error": {
|
||||||
|
"auth": "Der Tibber Zugangstoken ist ungültig.",
|
||||||
|
"connection": "Keine Verbindung zu Tibber möglich. Bitte überprüfen Sie Ihre Internetverbindung.",
|
||||||
|
"unknown": "Ein unerwarteter Fehler ist aufgetreten. Bitte prüfen Sie die Logs für Details.",
|
||||||
|
"different_account": "Der neue Zugangstoken gehört zu einem anderen Tibber-Konto. Bitte verwenden Sie einen Token desselben Kontos oder erstellen Sie eine neue Konfiguration für das andere Konto."
|
||||||
|
},
|
||||||
|
"abort": {
|
||||||
|
"entry_not_found": "Tibber-Konfigurationseintrag nicht gefunden."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"entity": {
|
||||||
|
"sensor": {
|
||||||
|
"current_price": {
|
||||||
|
"name": "Aktueller Preis",
|
||||||
|
"description": "Der aktuelle Strompreis dieser Stunde inklusive Steuern"
|
||||||
|
},
|
||||||
|
"next_hour_price": {
|
||||||
|
"name": "Preis nächste Stunde",
|
||||||
|
"description": "Der Strompreis der nächsten Stunde inklusive Steuern"
|
||||||
|
},
|
||||||
|
"price_level": {
|
||||||
|
"name": "Preisniveau",
|
||||||
|
"description": "Aktueller Preisindikator (SEHR_GÜNSTIG, GÜNSTIG, NORMAL, TEUER, SEHR_TEUER)"
|
||||||
|
},
|
||||||
|
"lowest_price_today": {
|
||||||
|
"name": "Niedrigster Preis heute",
|
||||||
|
"description": "Der niedrigste Strompreis des aktuellen Tages"
|
||||||
|
},
|
||||||
|
"highest_price_today": {
|
||||||
|
"name": "Höchster Preis heute",
|
||||||
|
"description": "Der höchste Strompreis des aktuellen Tages"
|
||||||
|
},
|
||||||
|
"average_price_today": {
|
||||||
|
"name": "Durchschnittspreis heute",
|
||||||
|
"description": "Der durchschnittliche Strompreis des aktuellen Tages"
|
||||||
|
},
|
||||||
|
"hourly_rating": {
|
||||||
|
"name": "Stündliche Preisbewertung",
|
||||||
|
"description": "Preisvergleich mit historischen Daten für die aktuelle Stunde (prozentuale Abweichung)"
|
||||||
|
},
|
||||||
|
"daily_rating": {
|
||||||
|
"name": "Tägliche Preisbewertung",
|
||||||
|
"description": "Preisvergleich mit historischen Daten für den aktuellen Tag (prozentuale Abweichung)"
|
||||||
|
},
|
||||||
|
"monthly_rating": {
|
||||||
|
"name": "Monatliche Preisbewertung",
|
||||||
|
"description": "Preisvergleich mit historischen Daten für den aktuellen Monat (prozentuale Abweichung)"
|
||||||
|
},
|
||||||
|
"data_timestamp": {
|
||||||
|
"name": "Letzte Datenaktualisierung",
|
||||||
|
"description": "Zeitstempel der zuletzt von Tibber empfangenen Preisdaten"
|
||||||
|
},
|
||||||
|
"tomorrow_data_available": {
|
||||||
|
"name": "Daten für morgen verfügbar",
|
||||||
|
"description": "Zeigt an, ob Preisdaten für morgen verfügbar sind (Ja/Nein/Teilweise)"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"binary_sensor": {
|
||||||
|
"peak_hour": {
|
||||||
|
"name": "Spitzenstunde",
|
||||||
|
"description": "Zeigt an, ob die aktuelle Stunde zu den 20% teuersten Stunden des Tages gehört"
|
||||||
|
},
|
||||||
|
"best_price_hour": {
|
||||||
|
"name": "Beste Preisstunde",
|
||||||
|
"description": "Zeigt an, ob die aktuelle Stunde zu den 20% günstigsten Stunden des Tages gehört"
|
||||||
|
},
|
||||||
|
"connection": {
|
||||||
|
"name": "Verbindungsstatus",
|
||||||
|
"description": "Zeigt an, ob aktuelle gültige Preisdaten von Tibber vorliegen"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -9,12 +9,95 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"error": {
|
"error": {
|
||||||
"auth": "Tibber Access Token is wrong.",
|
"auth": "The Tibber Access Token is invalid.",
|
||||||
"connection": "Unable to connect to the server.",
|
"connection": "Unable to connect to Tibber. Please check your internet connection.",
|
||||||
"unknown": "Unknown error occurred."
|
"unknown": "An unexpected error occurred. Please check the logs for details."
|
||||||
},
|
},
|
||||||
"abort": {
|
"abort": {
|
||||||
"already_configured": "This entry is already configured."
|
"already_configured": "This entry is already configured.",
|
||||||
|
"entry_not_found": "Tibber configuration entry not found."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"options": {
|
||||||
|
"step": {
|
||||||
|
"init": {
|
||||||
|
"title": "Update Tibber Configuration",
|
||||||
|
"description": "Update your Tibber API access token. If you need a new token, you can generate one at https://developer.tibber.com/settings/access-token",
|
||||||
|
"data": {
|
||||||
|
"access_token": "Tibber Access Token"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"error": {
|
||||||
|
"auth": "The Tibber Access Token is invalid.",
|
||||||
|
"connection": "Unable to connect to Tibber. Please check your internet connection.",
|
||||||
|
"unknown": "An unexpected error occurred. Please check the logs for details.",
|
||||||
|
"different_account": "The new access token belongs to a different Tibber account. Please use a token from the same account or create a new configuration for the other account."
|
||||||
|
},
|
||||||
|
"abort": {
|
||||||
|
"entry_not_found": "Tibber configuration entry not found."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"entity": {
|
||||||
|
"sensor": {
|
||||||
|
"current_price": {
|
||||||
|
"name": "Current Price",
|
||||||
|
"description": "The current hour's electricity price including taxes"
|
||||||
|
},
|
||||||
|
"next_hour_price": {
|
||||||
|
"name": "Next Hour Price",
|
||||||
|
"description": "The next hour's electricity price including taxes"
|
||||||
|
},
|
||||||
|
"price_level": {
|
||||||
|
"name": "Price Level",
|
||||||
|
"description": "Current price level indicator (VERY_CHEAP, CHEAP, NORMAL, EXPENSIVE, VERY_EXPENSIVE)"
|
||||||
|
},
|
||||||
|
"lowest_price_today": {
|
||||||
|
"name": "Lowest Price Today",
|
||||||
|
"description": "The lowest electricity price for the current day"
|
||||||
|
},
|
||||||
|
"highest_price_today": {
|
||||||
|
"name": "Highest Price Today",
|
||||||
|
"description": "The highest electricity price for the current day"
|
||||||
|
},
|
||||||
|
"average_price_today": {
|
||||||
|
"name": "Average Price Today",
|
||||||
|
"description": "The average electricity price for the current day"
|
||||||
|
},
|
||||||
|
"hourly_rating": {
|
||||||
|
"name": "Hourly Price Rating",
|
||||||
|
"description": "Price comparison with historical data for the current hour (percentage difference)"
|
||||||
|
},
|
||||||
|
"daily_rating": {
|
||||||
|
"name": "Daily Price Rating",
|
||||||
|
"description": "Price comparison with historical data for the current day (percentage difference)"
|
||||||
|
},
|
||||||
|
"monthly_rating": {
|
||||||
|
"name": "Monthly Price Rating",
|
||||||
|
"description": "Price comparison with historical data for the current month (percentage difference)"
|
||||||
|
},
|
||||||
|
"data_timestamp": {
|
||||||
|
"name": "Last Data Update",
|
||||||
|
"description": "Timestamp of the most recent price data received from Tibber"
|
||||||
|
},
|
||||||
|
"tomorrow_data_available": {
|
||||||
|
"name": "Tomorrow's Data Available",
|
||||||
|
"description": "Indicates if price data for tomorrow is available (Yes/No/Partial)"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"binary_sensor": {
|
||||||
|
"peak_hour": {
|
||||||
|
"name": "Peak Hour",
|
||||||
|
"description": "Indicates if the current hour is in the top 20% most expensive hours of the day"
|
||||||
|
},
|
||||||
|
"best_price_hour": {
|
||||||
|
"name": "Best Price Hour",
|
||||||
|
"description": "Indicates if the current hour is in the bottom 20% cheapest hours of the day"
|
||||||
|
},
|
||||||
|
"connection": {
|
||||||
|
"name": "Connection Status",
|
||||||
|
"description": "Indicates if we have valid current price data from Tibber"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Loading…
Reference in a new issue