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
a4859a9d2e
commit
52cfc4a87f
8 changed files with 329 additions and 128 deletions
|
|
@ -2,7 +2,7 @@
|
|||
|
||||
from __future__ import annotations
|
||||
|
||||
from datetime import datetime
|
||||
from datetime import datetime, timedelta
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from homeassistant.components.binary_sensor import (
|
||||
|
|
@ -10,7 +10,7 @@ from homeassistant.components.binary_sensor import (
|
|||
BinarySensorEntity,
|
||||
BinarySensorEntityDescription,
|
||||
)
|
||||
from homeassistant.const import EntityCategory
|
||||
from homeassistant.const import PERCENTAGE, EntityCategory
|
||||
from homeassistant.util import dt as dt_util
|
||||
|
||||
from .entity import TibberPricesEntity
|
||||
|
|
@ -25,16 +25,23 @@ if TYPE_CHECKING:
|
|||
from .coordinator import TibberPricesDataUpdateCoordinator
|
||||
from .data import TibberPricesConfigEntry
|
||||
|
||||
from .const import (
|
||||
CONF_BEST_PRICE_FLEX,
|
||||
CONF_PEAK_PRICE_FLEX,
|
||||
DEFAULT_BEST_PRICE_FLEX,
|
||||
DEFAULT_PEAK_PRICE_FLEX,
|
||||
)
|
||||
|
||||
ENTITY_DESCRIPTIONS = (
|
||||
BinarySensorEntityDescription(
|
||||
key="peak_interval",
|
||||
translation_key="peak_interval",
|
||||
key="peak_price_period",
|
||||
translation_key="peak_price_period",
|
||||
name="Peak Price Interval",
|
||||
icon="mdi:clock-alert",
|
||||
),
|
||||
BinarySensorEntityDescription(
|
||||
key="best_price_interval",
|
||||
translation_key="best_price_interval",
|
||||
key="best_price_period",
|
||||
translation_key="best_price_period",
|
||||
name="Best Price Interval",
|
||||
icon="mdi:clock-check",
|
||||
),
|
||||
|
|
@ -82,25 +89,70 @@ class TibberPricesBinarySensor(TibberPricesEntity, BinarySensorEntity):
|
|||
"""Return the appropriate state getter method based on the sensor type."""
|
||||
key = self.entity_description.key
|
||||
|
||||
if key == "peak_interval":
|
||||
return lambda: self._get_price_threshold_state(threshold_percentage=0.8, high_is_active=True)
|
||||
if key == "best_price_interval":
|
||||
return lambda: self._get_price_threshold_state(threshold_percentage=0.2, high_is_active=False)
|
||||
if key == "peak_price_period":
|
||||
return self._peak_price_state
|
||||
if key == "best_price_period":
|
||||
return self._best_price_state
|
||||
if key == "connection":
|
||||
return lambda: True if self.coordinator.data else None
|
||||
|
||||
return None
|
||||
|
||||
def _get_flex_option(self, option_key: str, default: float) -> float:
|
||||
"""Get a float option from config entry options or fallback to default. Accepts 0-100."""
|
||||
options = self.coordinator.config_entry.options
|
||||
data = self.coordinator.config_entry.data
|
||||
value = options.get(option_key, data.get(option_key, default))
|
||||
try:
|
||||
value = float(value) / 100
|
||||
except (TypeError, ValueError):
|
||||
value = default
|
||||
return value
|
||||
|
||||
def _best_price_state(self) -> bool | None:
|
||||
"""Return True if current price is within +flex% of the day's minimum price."""
|
||||
price_data = self._get_current_price_data()
|
||||
if not price_data:
|
||||
return None
|
||||
prices, current_price = price_data
|
||||
min_price = min(prices)
|
||||
flex = self._get_flex_option(CONF_BEST_PRICE_FLEX, DEFAULT_BEST_PRICE_FLEX)
|
||||
threshold = min_price * (1 + flex)
|
||||
return current_price <= threshold
|
||||
|
||||
def _peak_price_state(self) -> bool | None:
|
||||
"""Return True if current price is within -flex% of the day's maximum price."""
|
||||
price_data = self._get_current_price_data()
|
||||
if not price_data:
|
||||
return None
|
||||
prices, current_price = price_data
|
||||
max_price = max(prices)
|
||||
flex = self._get_flex_option(CONF_PEAK_PRICE_FLEX, DEFAULT_PEAK_PRICE_FLEX)
|
||||
threshold = max_price * (1 - flex)
|
||||
return current_price >= threshold
|
||||
|
||||
def _get_price_threshold_state(self, *, threshold_percentage: float, high_is_active: bool) -> bool | None:
|
||||
"""Deprecate: use _best_price_state or _peak_price_state for those sensors."""
|
||||
price_data = self._get_current_price_data()
|
||||
if not price_data:
|
||||
return None
|
||||
|
||||
prices, current_price = price_data
|
||||
threshold_index = int(len(prices) * threshold_percentage)
|
||||
|
||||
if high_is_active:
|
||||
return current_price >= prices[threshold_index]
|
||||
|
||||
return current_price <= prices[threshold_index]
|
||||
|
||||
def _get_attribute_getter(self) -> Callable | None:
|
||||
"""Return the appropriate attribute getter method based on the sensor type."""
|
||||
key = self.entity_description.key
|
||||
|
||||
if key == "peak_interval":
|
||||
return lambda: self._get_price_intervals_attributes(attribute_name="peak_intervals", reverse_sort=True)
|
||||
if key == "best_price_interval":
|
||||
return lambda: self._get_price_intervals_attributes(
|
||||
attribute_name="best_price_intervals", reverse_sort=False
|
||||
)
|
||||
if key == "peak_price_period":
|
||||
return lambda: self._get_price_intervals_attributes(reverse_sort=True)
|
||||
if key == "best_price_period":
|
||||
return lambda: self._get_price_intervals_attributes(reverse_sort=False)
|
||||
|
||||
return None
|
||||
|
||||
|
|
@ -130,33 +182,93 @@ class TibberPricesBinarySensor(TibberPricesEntity, BinarySensorEntity):
|
|||
prices.sort()
|
||||
return prices, float(current_interval_data["total"])
|
||||
|
||||
def _get_price_threshold_state(self, *, threshold_percentage: float, high_is_active: bool) -> bool | None:
|
||||
def _annotate_period_intervals(
|
||||
self,
|
||||
periods: list[list[dict]],
|
||||
ref_price: float,
|
||||
avg_price: float,
|
||||
interval_minutes: int,
|
||||
) -> list[dict]:
|
||||
"""Return flattened and annotated intervals with period info and requested properties."""
|
||||
# Determine reference type for naming
|
||||
reference_type = None
|
||||
if self.entity_description.key == "best_price_period":
|
||||
reference_type = "min"
|
||||
elif self.entity_description.key == "peak_price_period":
|
||||
reference_type = "max"
|
||||
else:
|
||||
reference_type = "ref"
|
||||
# Set attribute name suffixes
|
||||
if reference_type == "min":
|
||||
diff_key = "price_diff_from_min"
|
||||
diff_ct_key = "price_diff_from_min_ct"
|
||||
diff_pct_key = "price_diff_from_min_" + PERCENTAGE
|
||||
elif reference_type == "max":
|
||||
diff_key = "price_diff_from_max"
|
||||
diff_ct_key = "price_diff_from_max_ct"
|
||||
diff_pct_key = "price_diff_from_max_" + PERCENTAGE
|
||||
else:
|
||||
diff_key = "price_diff"
|
||||
diff_ct_key = "price_diff_ct"
|
||||
diff_pct_key = "price_diff_" + PERCENTAGE
|
||||
result = []
|
||||
period_count = len(periods)
|
||||
for idx, period in enumerate(periods, 1):
|
||||
period_start = period[0]["interval_start"] if period else None
|
||||
period_start_hour = period_start.hour if period_start else None
|
||||
period_start_minute = period_start.minute if period_start else None
|
||||
period_start_time = f"{period_start_hour:02d}:{period_start_minute:02d}" if period_start else None
|
||||
period_end = period[-1]["interval_end"] if period else None
|
||||
interval_count = len(period)
|
||||
period_length = interval_count * interval_minutes
|
||||
periods_remaining = len(periods) - idx
|
||||
for interval_idx, interval in enumerate(period, 1):
|
||||
interval_copy = interval.copy()
|
||||
interval_remaining = interval_count - interval_idx
|
||||
# Compose new dict with period-related keys first, then interval timing, then price info
|
||||
new_interval = {
|
||||
"period_start": period_start,
|
||||
"period_end": period_end,
|
||||
"hour": period_start_hour,
|
||||
"minute": period_start_minute,
|
||||
"time": period_start_time,
|
||||
"period_length_minute": period_length,
|
||||
"period_remaining_minute_after_interval": interval_remaining * interval_minutes,
|
||||
"periods_total": period_count,
|
||||
"periods_remaining": periods_remaining,
|
||||
"interval_total": interval_count,
|
||||
"interval_remaining": interval_remaining,
|
||||
"interval_position": interval_idx,
|
||||
}
|
||||
# Add interval timing
|
||||
new_interval["interval_start"] = interval_copy.pop("interval_start", None)
|
||||
new_interval["interval_end"] = interval_copy.pop("interval_end", None)
|
||||
# Add hour, minute, time, price if present in interval_copy
|
||||
for k in ("interval_hour", "interval_minute", "interval_time", "price"):
|
||||
if k in interval_copy:
|
||||
new_interval[k] = interval_copy.pop(k)
|
||||
# Add the rest of the interval info (e.g. price_ct, price_difference_*, etc.)
|
||||
new_interval.update(interval_copy)
|
||||
new_interval["price_ct"] = round(new_interval["price"] * 100, 2)
|
||||
price_diff = new_interval["price"] - ref_price
|
||||
new_interval[diff_key] = round(price_diff, 4)
|
||||
new_interval[diff_ct_key] = round(price_diff * 100, 2)
|
||||
price_diff_percent = ((new_interval["price"] - ref_price) / ref_price) * 100 if ref_price != 0 else 0.0
|
||||
new_interval[diff_pct_key] = round(price_diff_percent, 2)
|
||||
# Add difference to average price of the day (avg_price is now passed in)
|
||||
avg_diff = new_interval["price"] - avg_price
|
||||
new_interval["price_diff_from_avg"] = round(avg_diff, 4)
|
||||
new_interval["price_diff_from_avg_ct"] = round(avg_diff * 100, 2)
|
||||
avg_diff_percent = ((new_interval["price"] - avg_price) / avg_price) * 100 if avg_price != 0 else 0.0
|
||||
new_interval["price_diff_from_avg_" + PERCENTAGE] = round(avg_diff_percent, 2)
|
||||
result.append(new_interval)
|
||||
return result
|
||||
|
||||
def _get_price_intervals_attributes(self, *, reverse_sort: bool) -> dict | None:
|
||||
"""
|
||||
Determine if current price is above/below threshold.
|
||||
Get price interval attributes with support for 15-minute intervals and period grouping.
|
||||
|
||||
Args:
|
||||
threshold_percentage: The percentage point in the sorted list (0.0-1.0)
|
||||
high_is_active: If True, value >= threshold is active, otherwise value <= threshold is active
|
||||
|
||||
"""
|
||||
price_data = self._get_current_price_data()
|
||||
if not price_data:
|
||||
return None
|
||||
|
||||
prices, current_price = price_data
|
||||
threshold_index = int(len(prices) * threshold_percentage)
|
||||
|
||||
if high_is_active:
|
||||
return current_price >= prices[threshold_index]
|
||||
|
||||
return current_price <= prices[threshold_index]
|
||||
|
||||
def _get_price_intervals_attributes(self, *, attribute_name: str, reverse_sort: bool) -> dict | None:
|
||||
"""
|
||||
Get price interval attributes with support for 15-minute intervals.
|
||||
|
||||
Args:
|
||||
attribute_name: The attribute name to use in the result dictionary
|
||||
reverse_sort: Whether to sort prices in reverse (high to low)
|
||||
|
||||
Returns:
|
||||
|
|
@ -172,51 +284,91 @@ class TibberPricesBinarySensor(TibberPricesEntity, BinarySensorEntity):
|
|||
if not today_prices:
|
||||
return None
|
||||
|
||||
# Detect the granularity of the data
|
||||
interval_minutes = detect_interval_granularity(today_prices)
|
||||
|
||||
# Build a list of price data with timestamps and values
|
||||
price_intervals = []
|
||||
# Use entity type to determine flex and logic, but always use 'price_intervals' as attribute name
|
||||
if reverse_sort is False: # best_price_period entity
|
||||
flex = self._get_flex_option(CONF_BEST_PRICE_FLEX, DEFAULT_BEST_PRICE_FLEX)
|
||||
prices = [float(p["total"]) for p in today_prices]
|
||||
min_price = min(prices)
|
||||
|
||||
def in_range(price: float) -> bool:
|
||||
return price <= min_price * (1 + flex)
|
||||
|
||||
ref_price = min_price
|
||||
elif reverse_sort is True: # peak_price_period entity
|
||||
flex = self._get_flex_option(CONF_PEAK_PRICE_FLEX, DEFAULT_PEAK_PRICE_FLEX)
|
||||
prices = [float(p["total"]) for p in today_prices]
|
||||
max_price = max(prices)
|
||||
|
||||
def in_range(price: float) -> bool:
|
||||
return price >= max_price * (1 - flex)
|
||||
|
||||
ref_price = max_price
|
||||
else:
|
||||
return None
|
||||
|
||||
# Calculate average price for the day (all intervals, not just periods)
|
||||
all_prices = [float(p["total"]) for p in today_prices]
|
||||
avg_price = sum(all_prices) / len(all_prices) if all_prices else 0.0
|
||||
|
||||
# Build intervals with period grouping
|
||||
periods = []
|
||||
current_period = []
|
||||
for price_data in today_prices:
|
||||
starts_at = dt_util.parse_datetime(price_data["startsAt"])
|
||||
if starts_at is None:
|
||||
continue
|
||||
|
||||
starts_at = dt_util.as_local(starts_at)
|
||||
price_intervals.append(
|
||||
{
|
||||
"starts_at": starts_at,
|
||||
"price": float(price_data["total"]),
|
||||
"hour": starts_at.hour,
|
||||
"minute": starts_at.minute,
|
||||
}
|
||||
)
|
||||
|
||||
# Sort by price (high to low for peak, low to high for best)
|
||||
sorted_intervals = sorted(price_intervals, key=lambda x: x["price"], reverse=reverse_sort)[:5]
|
||||
|
||||
# Format the result based on granularity
|
||||
hourly_interval_minutes = 60
|
||||
result = []
|
||||
for interval in sorted_intervals:
|
||||
if interval_minutes < hourly_interval_minutes: # More granular than hourly
|
||||
result.append(
|
||||
price = float(price_data["total"])
|
||||
if in_range(price):
|
||||
current_period.append(
|
||||
{
|
||||
"hour": interval["hour"],
|
||||
"minute": interval["minute"],
|
||||
"time": f"{interval['hour']:02d}:{interval['minute']:02d}",
|
||||
"price": interval["price"],
|
||||
}
|
||||
)
|
||||
else: # Hourly data (for backward compatibility)
|
||||
result.append(
|
||||
{
|
||||
"hour": interval["hour"],
|
||||
"price": interval["price"],
|
||||
"interval_hour": starts_at.hour,
|
||||
"interval_minute": starts_at.minute,
|
||||
"interval_time": f"{starts_at.hour:02d}:{starts_at.minute:02d}",
|
||||
"price": price,
|
||||
"interval_start": starts_at,
|
||||
# interval_end will be filled later
|
||||
}
|
||||
)
|
||||
elif current_period:
|
||||
periods.append(current_period)
|
||||
current_period = []
|
||||
if current_period:
|
||||
periods.append(current_period)
|
||||
|
||||
return {attribute_name: result}
|
||||
# Add interval_end to each interval (next interval's start or None)
|
||||
for period in periods:
|
||||
for idx, interval in enumerate(period):
|
||||
if idx + 1 < len(period):
|
||||
interval["interval_end"] = period[idx + 1]["interval_start"]
|
||||
else:
|
||||
# Try to estimate end as start + interval_minutes
|
||||
interval["interval_end"] = interval["interval_start"] + timedelta(minutes=interval_minutes)
|
||||
|
||||
result = self._annotate_period_intervals(periods, ref_price, avg_price, interval_minutes)
|
||||
|
||||
# Find the current or next interval (by time) from the annotated result
|
||||
now = dt_util.now()
|
||||
current_interval = None
|
||||
for interval in result:
|
||||
start = interval.get("interval_start")
|
||||
end = interval.get("interval_end")
|
||||
if start and end and start <= now < end:
|
||||
current_interval = interval.copy()
|
||||
break
|
||||
else:
|
||||
# If no current interval, show the next period's first interval (if available)
|
||||
for interval in result:
|
||||
start = interval.get("interval_start")
|
||||
if start and start > now:
|
||||
current_interval = interval.copy()
|
||||
break
|
||||
|
||||
attributes = {**current_interval} if current_interval else {}
|
||||
attributes["intervals"] = result
|
||||
return attributes
|
||||
|
||||
def _get_price_hours_attributes(self, *, attribute_name: str, reverse_sort: bool) -> dict | None:
|
||||
"""Get price hours attributes."""
|
||||
|
|
|
|||
|
|
@ -17,8 +17,12 @@ from .api import (
|
|||
TibberPricesApiClientError,
|
||||
)
|
||||
from .const import (
|
||||
CONF_BEST_PRICE_FLEX,
|
||||
CONF_EXTENDED_DESCRIPTIONS,
|
||||
CONF_PEAK_PRICE_FLEX,
|
||||
DEFAULT_BEST_PRICE_FLEX,
|
||||
DEFAULT_EXTENDED_DESCRIPTIONS,
|
||||
DEFAULT_PEAK_PRICE_FLEX,
|
||||
DOMAIN,
|
||||
LOGGER,
|
||||
)
|
||||
|
|
@ -87,6 +91,28 @@ class TibberPricesFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
|
|||
CONF_EXTENDED_DESCRIPTIONS,
|
||||
default=(user_input or {}).get(CONF_EXTENDED_DESCRIPTIONS, DEFAULT_EXTENDED_DESCRIPTIONS),
|
||||
): selector.BooleanSelector(),
|
||||
vol.Optional(
|
||||
CONF_BEST_PRICE_FLEX,
|
||||
default=(user_input or {}).get(CONF_BEST_PRICE_FLEX, DEFAULT_BEST_PRICE_FLEX),
|
||||
): selector.NumberSelector(
|
||||
selector.NumberSelectorConfig(
|
||||
min=0,
|
||||
max=20,
|
||||
step=1,
|
||||
mode=selector.NumberSelectorMode.SLIDER,
|
||||
),
|
||||
),
|
||||
vol.Optional(
|
||||
CONF_PEAK_PRICE_FLEX,
|
||||
default=(user_input or {}).get(CONF_PEAK_PRICE_FLEX, DEFAULT_PEAK_PRICE_FLEX),
|
||||
): selector.NumberSelector(
|
||||
selector.NumberSelectorConfig(
|
||||
min=0,
|
||||
max=20,
|
||||
step=1,
|
||||
mode=selector.NumberSelectorMode.SLIDER,
|
||||
),
|
||||
),
|
||||
},
|
||||
),
|
||||
errors=_errors,
|
||||
|
|
@ -105,10 +131,9 @@ class TibberPricesFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
|
|||
class TibberPricesOptionsFlowHandler(config_entries.OptionsFlow):
|
||||
"""Tibber Prices config flow options handler."""
|
||||
|
||||
def __init__(self, config_entry: config_entries.ConfigEntry) -> None:
|
||||
def __init__(self, _: config_entries.ConfigEntry) -> None:
|
||||
"""Initialize options flow."""
|
||||
super().__init__()
|
||||
self.config_entry = config_entry
|
||||
|
||||
async def async_step_init(self, user_input: dict | None = None) -> config_entries.ConfigFlowResult:
|
||||
"""Manage the options."""
|
||||
|
|
@ -162,6 +187,38 @@ class TibberPricesOptionsFlowHandler(config_entries.OptionsFlow):
|
|||
self.config_entry.data.get(CONF_EXTENDED_DESCRIPTIONS, DEFAULT_EXTENDED_DESCRIPTIONS),
|
||||
),
|
||||
): selector.BooleanSelector(),
|
||||
vol.Optional(
|
||||
CONF_BEST_PRICE_FLEX,
|
||||
default=int(
|
||||
self.config_entry.options.get(
|
||||
CONF_BEST_PRICE_FLEX,
|
||||
self.config_entry.data.get(CONF_BEST_PRICE_FLEX, DEFAULT_BEST_PRICE_FLEX),
|
||||
)
|
||||
),
|
||||
): selector.NumberSelector(
|
||||
selector.NumberSelectorConfig(
|
||||
min=0,
|
||||
max=100,
|
||||
step=1,
|
||||
mode=selector.NumberSelectorMode.SLIDER,
|
||||
),
|
||||
),
|
||||
vol.Optional(
|
||||
CONF_PEAK_PRICE_FLEX,
|
||||
default=int(
|
||||
self.config_entry.options.get(
|
||||
CONF_PEAK_PRICE_FLEX,
|
||||
self.config_entry.data.get(CONF_PEAK_PRICE_FLEX, DEFAULT_PEAK_PRICE_FLEX),
|
||||
)
|
||||
),
|
||||
): selector.NumberSelector(
|
||||
selector.NumberSelectorConfig(
|
||||
min=0,
|
||||
max=100,
|
||||
step=1,
|
||||
mode=selector.NumberSelectorMode.SLIDER,
|
||||
),
|
||||
),
|
||||
}
|
||||
|
||||
return self.async_show_form(
|
||||
|
|
|
|||
|
|
@ -16,6 +16,8 @@ VERSION = "0.1.0"
|
|||
DOMAIN = "tibber_prices"
|
||||
CONF_ACCESS_TOKEN = "access_token" # noqa: S105
|
||||
CONF_EXTENDED_DESCRIPTIONS = "extended_descriptions"
|
||||
CONF_BEST_PRICE_FLEX = "best_price_flex"
|
||||
CONF_PEAK_PRICE_FLEX = "peak_price_flex"
|
||||
|
||||
ATTRIBUTION = "Data provided by Tibber"
|
||||
|
||||
|
|
@ -25,6 +27,8 @@ SCAN_INTERVAL = 60 * 5 # 5 minutes
|
|||
# Integration name should match manifest.json
|
||||
DEFAULT_NAME = "Tibber Price Information & Ratings"
|
||||
DEFAULT_EXTENDED_DESCRIPTIONS = False
|
||||
DEFAULT_BEST_PRICE_FLEX = 5 # 5% flexibility for best price (user-facing, percent)
|
||||
DEFAULT_PEAK_PRICE_FLEX = 5 # 5% flexibility for peak price (user-facing, percent)
|
||||
|
||||
# Price level constants
|
||||
PRICE_LEVEL_NORMAL = "NORMAL"
|
||||
|
|
|
|||
|
|
@ -116,30 +116,18 @@
|
|||
}
|
||||
},
|
||||
"binary_sensor": {
|
||||
"peak_interval": {
|
||||
"name": "Spitzenpreis-Intervall",
|
||||
"peak_price_period": {
|
||||
"name": "Spitzenpreis-Periode",
|
||||
"description": "Ob das aktuelle Intervall zu den teuersten des Tages gehört",
|
||||
"long_description": "Wird aktiviert, wenn der aktuelle Preis in den oberen 20% der heutigen Preise liegt",
|
||||
"usage_tips": "Nutze dies, um den Betrieb von Geräten mit hohem Verbrauch während teurer Intervalle zu vermeiden"
|
||||
},
|
||||
"best_price_interval": {
|
||||
"name": "Bestpreis-Intervall",
|
||||
"best_price_period": {
|
||||
"name": "Bestpreis-Periode",
|
||||
"description": "Ob das aktuelle Intervall zu den günstigsten des Tages gehört",
|
||||
"long_description": "Wird aktiviert, wenn der aktuelle Preis in den unteren 20% der heutigen Preise liegt",
|
||||
"usage_tips": "Nutze dies, um Geräte mit hohem Verbrauch während der günstigsten Intervalle zu betreiben"
|
||||
},
|
||||
"peak_hour": {
|
||||
"name": "Spitzenstunde",
|
||||
"description": "Ob die aktuelle Stunde zu den teuersten des Tages gehört",
|
||||
"long_description": "Wird aktiviert, wenn der aktuelle Preis in den oberen 20% der heutigen Preise liegt",
|
||||
"usage_tips": "Nutze dies, um den Betrieb von Geräten mit hohem Verbrauch während teurer Stunden zu vermeiden"
|
||||
},
|
||||
"best_price_hour": {
|
||||
"name": "Beste-Preis-Stunde",
|
||||
"description": "Ob die aktuelle Stunde zu den günstigsten des Tages gehört",
|
||||
"long_description": "Wird aktiviert, wenn der aktuelle Preis in den unteren 20% der heutigen Preise liegt",
|
||||
"usage_tips": "Nutze dies, um Geräte mit hohem Verbrauch während der günstigsten Stunden zu betreiben"
|
||||
},
|
||||
"connection": {
|
||||
"name": "Tibber API-Verbindung",
|
||||
"description": "Ob die Verbindung zur Tibber API funktioniert",
|
||||
|
|
@ -147,4 +135,4 @@
|
|||
"usage_tips": "Nutze dies, um den Verbindungsstatus zur Tibber API zu überwachen"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -116,30 +116,18 @@
|
|||
}
|
||||
},
|
||||
"binary_sensor": {
|
||||
"peak_interval": {
|
||||
"name": "Peak Price Interval",
|
||||
"peak_price_period": {
|
||||
"name": "Peak Price Periode",
|
||||
"description": "Whether the current interval is among the most expensive of the day",
|
||||
"long_description": "Turns on when the current price is in the top 20% of today's prices",
|
||||
"usage_tips": "Use this to avoid running high-consumption appliances during expensive intervals"
|
||||
},
|
||||
"best_price_interval": {
|
||||
"name": "Best Price Interval",
|
||||
"best_price_period": {
|
||||
"name": "Best Price Periode",
|
||||
"description": "Whether the current interval is among the cheapest of the day",
|
||||
"long_description": "Turns on when the current price is in the bottom 20% of today's prices",
|
||||
"usage_tips": "Use this to run high-consumption appliances during the cheapest intervals"
|
||||
},
|
||||
"peak_hour": {
|
||||
"name": "Peak Hour",
|
||||
"description": "Whether the current hour is among the most expensive of the day",
|
||||
"long_description": "Turns on when the current price is in the top 20% of today's prices",
|
||||
"usage_tips": "Use this to avoid running high-consumption appliances during expensive hours"
|
||||
},
|
||||
"best_price_hour": {
|
||||
"name": "Best Price Hour",
|
||||
"description": "Whether the current hour is among the cheapest of the day",
|
||||
"long_description": "Turns on when the current price is in the bottom 20% of today's prices",
|
||||
"usage_tips": "Use this to run high-consumption appliances during the cheapest hours"
|
||||
},
|
||||
"connection": {
|
||||
"name": "Tibber API Connection",
|
||||
"description": "Whether the connection to the Tibber API is working",
|
||||
|
|
@ -147,4 +135,4 @@
|
|||
"usage_tips": "Use this to monitor the connection status to the Tibber API"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -13,7 +13,7 @@ from homeassistant.components.sensor import (
|
|||
SensorEntity,
|
||||
SensorEntityDescription,
|
||||
)
|
||||
from homeassistant.const import CURRENCY_CENT, CURRENCY_EURO, PERCENTAGE, EntityCategory, UnitOfPower, UnitOfTime
|
||||
from homeassistant.const import CURRENCY_EURO, PERCENTAGE, EntityCategory, UnitOfPower, UnitOfTime
|
||||
from homeassistant.util import dt as dt_util
|
||||
|
||||
from .const import (
|
||||
|
|
@ -36,7 +36,7 @@ if TYPE_CHECKING:
|
|||
from .coordinator import TibberPricesDataUpdateCoordinator
|
||||
from .data import TibberPricesConfigEntry
|
||||
|
||||
PRICE_UNIT_CENT = CURRENCY_CENT + "/" + UnitOfPower.KILO_WATT + UnitOfTime.HOURS
|
||||
PRICE_UNIT_CENT = "ct/" + UnitOfPower.KILO_WATT + UnitOfTime.HOURS
|
||||
PRICE_UNIT_EURO = CURRENCY_EURO + "/" + UnitOfPower.KILO_WATT + UnitOfTime.HOURS
|
||||
HOURS_IN_DAY = 24
|
||||
LAST_HOUR_OF_DAY = 23
|
||||
|
|
|
|||
|
|
@ -5,7 +5,9 @@
|
|||
"description": "Richte Tibber Preisinformationen & Bewertungen ein. Um ein API-Zugriffstoken zu generieren, besuche developer.tibber.com.",
|
||||
"data": {
|
||||
"access_token": "API-Zugriffstoken",
|
||||
"extended_descriptions": "Erweiterte Beschreibungen in Entitätsattributen anzeigen"
|
||||
"extended_descriptions": "Erweiterte Beschreibungen in Entitätsattributen anzeigen",
|
||||
"best_price_flex": "Flexibilität für Bestpreis (%)",
|
||||
"peak_price_flex": "Flexibilität für Spitzenpreis (%)"
|
||||
},
|
||||
"title": "Tibber Preisinformationen & Bewertungen"
|
||||
}
|
||||
|
|
@ -29,7 +31,9 @@
|
|||
"description": "Konfiguriere Optionen für Tibber Preisinformationen & Bewertungen",
|
||||
"data": {
|
||||
"access_token": "Tibber Zugangstoken",
|
||||
"extended_descriptions": "Erweiterte Beschreibungen in Entitätsattributen anzeigen"
|
||||
"extended_descriptions": "Erweiterte Beschreibungen in Entitätsattributen anzeigen",
|
||||
"best_price_flex": "Flexibilität für Bestpreis (%)",
|
||||
"peak_price_flex": "Flexibilität für Spitzenpreis (%)"
|
||||
}
|
||||
}
|
||||
},
|
||||
|
|
@ -83,15 +87,15 @@
|
|||
}
|
||||
},
|
||||
"binary_sensor": {
|
||||
"peak_hour": {
|
||||
"name": "Spitzenstunde"
|
||||
"peak_price_period": {
|
||||
"name": "Spitzenperiode"
|
||||
},
|
||||
"best_price_hour": {
|
||||
"name": "Beste-Preis-Stunde"
|
||||
"best_price_period": {
|
||||
"name": "Best-Preis-Periode"
|
||||
},
|
||||
"connection": {
|
||||
"name": "Tibber API-Verbindung"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -5,7 +5,9 @@
|
|||
"description": "Set up Tibber Price Information & Ratings. To generate an API access token, visit developer.tibber.com.",
|
||||
"data": {
|
||||
"access_token": "API access token",
|
||||
"extended_descriptions": "Show extended descriptions in entity attributes"
|
||||
"extended_descriptions": "Show extended descriptions in entity attributes",
|
||||
"best_price_flex": "Best Price Flexibility (%)",
|
||||
"peak_price_flex": "Peak Price Flexibility (%)"
|
||||
},
|
||||
"title": "Tibber Price Information & Ratings"
|
||||
}
|
||||
|
|
@ -20,7 +22,9 @@
|
|||
"abort": {
|
||||
"already_configured": "Integration is already configured",
|
||||
"entry_not_found": "Tibber configuration entry not found."
|
||||
}
|
||||
},
|
||||
"best_price_flex": "Best Price Flexibility (%)",
|
||||
"peak_price_flex": "Peak Price Flexibility (%)"
|
||||
},
|
||||
"options": {
|
||||
"step": {
|
||||
|
|
@ -29,7 +33,9 @@
|
|||
"description": "Configure options for Tibber Price Information & Ratings",
|
||||
"data": {
|
||||
"access_token": "Tibber Access Token",
|
||||
"extended_descriptions": "Show extended descriptions in entity attributes"
|
||||
"extended_descriptions": "Show extended descriptions in entity attributes",
|
||||
"best_price_flex": "Best Price Flexibility (%)",
|
||||
"peak_price_flex": "Peak Price Flexibility (%)"
|
||||
}
|
||||
}
|
||||
},
|
||||
|
|
@ -41,7 +47,9 @@
|
|||
},
|
||||
"abort": {
|
||||
"entry_not_found": "Tibber configuration entry not found."
|
||||
}
|
||||
},
|
||||
"best_price_flex": "Best Price Flexibility (%)",
|
||||
"peak_price_flex": "Peak Price Flexibility (%)"
|
||||
},
|
||||
"entity": {
|
||||
"sensor": {
|
||||
|
|
@ -98,15 +106,15 @@
|
|||
}
|
||||
},
|
||||
"binary_sensor": {
|
||||
"peak_hour": {
|
||||
"name": "Peak Hour"
|
||||
"peak_price_period": {
|
||||
"name": "Peak Price Period"
|
||||
},
|
||||
"best_price_hour": {
|
||||
"name": "Best Price Hour"
|
||||
"best_price_period": {
|
||||
"name": "Best Price Period"
|
||||
},
|
||||
"connection": {
|
||||
"name": "Tibber API Connection"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
Reference in a new issue