feat(sensors): add price volatility analysis and period filters

Added comprehensive volatility analysis system:
- 4 new volatility sensors (today, tomorrow, next_24h, today+tomorrow)
- Volatility classification (LOW/MODERATE/HIGH/VERY HIGH) based on price spread
- Configurable thresholds in options flow (step 6 of 6)
- Best/Peak price period filters using volatility and price level
- Price spread calculation in get_price service

Volatility sensors help users decide if price-based optimization is worthwhile.
For example, battery optimization only makes sense when volatility ≥ MODERATE.

Period filters allow AND-logic combinations:
- best_price_min_volatility: Only show cheap periods on volatile days
- best_price_max_level: Only show periods when prices reach desired level
- peak_price_min_volatility: Only show peaks on volatile days
- peak_price_min_level: Only show peaks when expensive levels occur

All 5 language files updated (de, en, nb, nl, sv) with:
- Volatility sensor translations (name, states, descriptions)
- Config flow step 6 "Volatility" with threshold settings
- Step progress indicators added to all config steps
- Period filter translations with usage tips

Impact: Users can now assess daily price volatility and configure period
sensors to only activate when conditions justify battery cycling or load
shifting. Reduces unnecessary battery wear on low-volatility days.
This commit is contained in:
Julian Pawlowski 2025-11-09 14:24:34 +00:00
parent 165bbd4d88
commit f4568be34e
15 changed files with 1419 additions and 148 deletions

View file

@ -27,8 +27,29 @@ if TYPE_CHECKING:
from .data import TibberPricesConfigEntry from .data import TibberPricesConfigEntry
from .const import ( from .const import (
CONF_BEST_PRICE_MAX_LEVEL,
CONF_BEST_PRICE_MIN_VOLATILITY,
CONF_EXTENDED_DESCRIPTIONS, CONF_EXTENDED_DESCRIPTIONS,
CONF_MIN_VOLATILITY_FOR_PERIODS,
CONF_PEAK_PRICE_MIN_LEVEL,
CONF_PEAK_PRICE_MIN_VOLATILITY,
CONF_VOLATILITY_THRESHOLD_HIGH,
CONF_VOLATILITY_THRESHOLD_MODERATE,
CONF_VOLATILITY_THRESHOLD_VERY_HIGH,
DEFAULT_BEST_PRICE_MAX_LEVEL,
DEFAULT_BEST_PRICE_MIN_VOLATILITY,
DEFAULT_EXTENDED_DESCRIPTIONS, DEFAULT_EXTENDED_DESCRIPTIONS,
DEFAULT_MIN_VOLATILITY_FOR_PERIODS,
DEFAULT_PEAK_PRICE_MIN_LEVEL,
DEFAULT_PEAK_PRICE_MIN_VOLATILITY,
DEFAULT_VOLATILITY_THRESHOLD_HIGH,
DEFAULT_VOLATILITY_THRESHOLD_MODERATE,
DEFAULT_VOLATILITY_THRESHOLD_VERY_HIGH,
PRICE_LEVEL_MAPPING,
VOLATILITY_HIGH,
VOLATILITY_LOW,
VOLATILITY_MODERATE,
VOLATILITY_VERY_HIGH,
async_get_entity_description, async_get_entity_description,
get_entity_description, get_entity_description,
) )
@ -141,24 +162,28 @@ class TibberPricesBinarySensor(TibberPricesEntity, BinarySensorEntity):
if not self.coordinator.data: if not self.coordinator.data:
return None return None
attrs = self._get_price_intervals_attributes(reverse_sort=False) attrs = self._get_price_intervals_attributes(reverse_sort=False)
if not attrs or "start" not in attrs or "end" not in attrs: if not attrs:
return None return False # Should not happen, but safety fallback
now = dt_util.now()
start = attrs.get("start") start = attrs.get("start")
end = attrs.get("end") end = attrs.get("end")
return start <= now < end if start and end else None if not start or not end:
return False # No period found = sensor is off
now = dt_util.now()
return start <= now < end
def _peak_price_state(self) -> bool | None: def _peak_price_state(self) -> bool | None:
"""Return True if the current time is within a peak price period.""" """Return True if the current time is within a peak price period."""
if not self.coordinator.data: if not self.coordinator.data:
return None return None
attrs = self._get_price_intervals_attributes(reverse_sort=True) attrs = self._get_price_intervals_attributes(reverse_sort=True)
if not attrs or "start" not in attrs or "end" not in attrs: if not attrs:
return None return False # Should not happen, but safety fallback
now = dt_util.now()
start = attrs.get("start") start = attrs.get("start")
end = attrs.get("end") end = attrs.get("end")
return start <= now < end if start and end else None if not start or not end:
return False # No period found = sensor is off
now = dt_util.now()
return start <= now < end
def _tomorrow_data_available_state(self) -> bool | None: def _tomorrow_data_available_state(self) -> bool | None:
"""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."""
@ -329,20 +354,25 @@ class TibberPricesBinarySensor(TibberPricesEntity, BinarySensorEntity):
1. Gets lightweight period summaries from coordinator 1. Gets lightweight period summaries from coordinator
2. Fetches actual price data from priceInfo on-demand 2. Fetches actual price data from priceInfo on-demand
3. Builds annotations without storing data redundantly 3. Builds annotations without storing data redundantly
4. Filters periods based on volatility and level thresholds if configured
""" """
# Check if periods should be filtered based on volatility and level
if not self._should_show_periods(reverse_sort=reverse_sort):
return self._build_empty_periods_result(reverse_sort=reverse_sort)
# Get precomputed period summaries from coordinator # Get precomputed period summaries from coordinator
period_data = self._get_precomputed_period_data(reverse_sort=reverse_sort) period_data = self._get_precomputed_period_data(reverse_sort=reverse_sort)
if not period_data: if not period_data:
return None return self._build_no_periods_result()
period_summaries = period_data.get("periods", []) period_summaries = period_data.get("periods", [])
if not period_summaries: if not period_summaries:
return None return self._build_no_periods_result()
# Build full interval data from summaries + priceInfo # Build full interval data from summaries + priceInfo
intervals = self._get_period_intervals_from_price_info(period_summaries, reverse_sort=reverse_sort) intervals = self._get_period_intervals_from_price_info(period_summaries, reverse_sort=reverse_sort)
if not intervals: if not intervals:
return None return self._build_no_periods_result()
# Find current or next interval # Find current or next interval
current_interval = self._find_current_or_next_interval(intervals) current_interval = self._find_current_or_next_interval(intervals)
@ -353,6 +383,228 @@ class TibberPricesBinarySensor(TibberPricesEntity, BinarySensorEntity):
# Build final attributes # Build final attributes
return self._build_final_attributes(current_interval, periods_summary, intervals) return self._build_final_attributes(current_interval, periods_summary, intervals)
def _should_show_periods(self, *, reverse_sort: bool) -> bool:
"""
Check if periods should be shown based on volatility AND level filters (UND-Verknüpfung).
Args:
reverse_sort: If False (best_price), checks max_level filter.
If True (peak_price), checks min_level filter.
Returns:
True if periods should be displayed, False if they should be filtered out.
Both conditions must be met for periods to be shown.
"""
if not self.coordinator.data:
return True
# Check volatility filter
if not self._check_volatility_filter(reverse_sort=reverse_sort):
return False
# Check level filter (UND-Verknüpfung)
return self._check_level_filter(reverse_sort=reverse_sort)
def _check_volatility_filter(self, *, reverse_sort: bool) -> bool:
"""
Check if today's volatility meets the minimum requirement.
Args:
reverse_sort: If False (best_price), uses CONF_BEST_PRICE_MIN_VOLATILITY.
If True (peak_price), uses CONF_PEAK_PRICE_MIN_VOLATILITY.
"""
# Get appropriate volatility config based on sensor type
if reverse_sort:
# Peak price sensor
min_volatility = self.coordinator.config_entry.options.get(
CONF_PEAK_PRICE_MIN_VOLATILITY,
# Migration: fall back to old global filter, then to new default
self.coordinator.config_entry.options.get(
CONF_MIN_VOLATILITY_FOR_PERIODS,
DEFAULT_PEAK_PRICE_MIN_VOLATILITY,
),
)
else:
# Best price sensor
min_volatility = self.coordinator.config_entry.options.get(
CONF_BEST_PRICE_MIN_VOLATILITY,
# Migration: fall back to old global filter, then to new default
self.coordinator.config_entry.options.get(
CONF_MIN_VOLATILITY_FOR_PERIODS,
DEFAULT_BEST_PRICE_MIN_VOLATILITY,
),
)
# "LOW" means no filtering (show at any volatility ≥0ct)
if min_volatility == VOLATILITY_LOW:
return True
# Legacy migration: "ANY" also means no filtering
if min_volatility == "ANY":
return True
# Get today's price data to calculate volatility
price_info = self.coordinator.data.get("priceInfo", {})
today_prices = price_info.get("today", [])
prices = [p.get("total") for p in today_prices if "total" in p] if today_prices else []
if not prices:
return True # If no prices, don't filter
# Calculate today's spread (volatility metric) in minor units
spread_major = (max(prices) - min(prices)) * 100
# Get volatility thresholds from config
threshold_moderate = self.coordinator.config_entry.options.get(
CONF_VOLATILITY_THRESHOLD_MODERATE,
DEFAULT_VOLATILITY_THRESHOLD_MODERATE,
)
threshold_high = self.coordinator.config_entry.options.get(
CONF_VOLATILITY_THRESHOLD_HIGH,
DEFAULT_VOLATILITY_THRESHOLD_HIGH,
)
threshold_very_high = self.coordinator.config_entry.options.get(
CONF_VOLATILITY_THRESHOLD_VERY_HIGH,
DEFAULT_VOLATILITY_THRESHOLD_VERY_HIGH,
)
# Map min_volatility to threshold and check if spread meets requirement
threshold_map = {
VOLATILITY_MODERATE: threshold_moderate,
VOLATILITY_HIGH: threshold_high,
VOLATILITY_VERY_HIGH: threshold_very_high,
}
required_threshold = threshold_map.get(min_volatility)
return spread_major >= required_threshold if required_threshold is not None else True
def _check_level_filter(self, *, reverse_sort: bool) -> bool:
"""
Check if today has any intervals that meet the level requirement.
Args:
reverse_sort: If False (best_price), checks max_level (upper bound filter).
If True (peak_price), checks min_level (lower bound filter).
Returns:
True if ANY interval meets the level requirement, False otherwise.
"""
# Get appropriate config based on sensor type
if reverse_sort:
# Peak price: minimum level filter (lower bound)
level_config = self.coordinator.config_entry.options.get(
CONF_PEAK_PRICE_MIN_LEVEL,
DEFAULT_PEAK_PRICE_MIN_LEVEL,
)
else:
# Best price: maximum level filter (upper bound)
level_config = self.coordinator.config_entry.options.get(
CONF_BEST_PRICE_MAX_LEVEL,
DEFAULT_BEST_PRICE_MAX_LEVEL,
)
# "ANY" means no level filtering
if level_config == "ANY":
return True
# Get today's intervals
price_info = self.coordinator.data.get("priceInfo", {})
today_intervals = price_info.get("today", [])
if not today_intervals:
return True # If no data, don't filter
# Check if ANY interval today meets the level requirement
level_order = PRICE_LEVEL_MAPPING.get(level_config, 0)
if reverse_sort:
# Peak price: level >= min_level (show if ANY interval is expensive enough)
return any(
PRICE_LEVEL_MAPPING.get(interval.get("level", "NORMAL"), 0) >= level_order
for interval in today_intervals
)
# Best price: level <= max_level (show if ANY interval is cheap enough)
return any(
PRICE_LEVEL_MAPPING.get(interval.get("level", "NORMAL"), 0) <= level_order for interval in today_intervals
)
def _build_no_periods_result(self) -> dict:
"""
Build result when no periods exist (not filtered, just none available).
Returns:
A dict with empty periods and timestamp.
"""
# Calculate timestamp: current time rounded down to last quarter hour
now = dt_util.now()
current_minute = (now.minute // 15) * 15
timestamp = now.replace(minute=current_minute, second=0, microsecond=0)
return {
"timestamp": timestamp,
"start": None,
"end": None,
"periods": [],
}
def _build_empty_periods_result(self, *, reverse_sort: bool) -> dict:
"""
Build result when periods are filtered due to volatility or level constraints.
Args:
reverse_sort: If False (best_price), reports max_level filter.
If True (peak_price), reports min_level filter.
Returns:
A dict with empty periods and a reason attribute explaining why.
"""
min_volatility = self.coordinator.config_entry.options.get(
CONF_MIN_VOLATILITY_FOR_PERIODS,
DEFAULT_MIN_VOLATILITY_FOR_PERIODS,
)
# Get appropriate level config based on sensor type
if reverse_sort:
level_config = self.coordinator.config_entry.options.get(
CONF_PEAK_PRICE_MIN_LEVEL,
DEFAULT_PEAK_PRICE_MIN_LEVEL,
)
level_filter_type = "below" # Peak price: level below min threshold
else:
level_config = self.coordinator.config_entry.options.get(
CONF_BEST_PRICE_MAX_LEVEL,
DEFAULT_BEST_PRICE_MAX_LEVEL,
)
level_filter_type = "above" # Best price: level above max threshold
# Build reason string explaining which filter(s) prevented display
reasons = []
if min_volatility != "ANY" and not self._check_volatility_filter(reverse_sort=reverse_sort):
reasons.append(f"volatility_below_{min_volatility.lower()}")
if level_config != "ANY" and not self._check_level_filter(reverse_sort=reverse_sort):
reasons.append(f"level_{level_filter_type}_{level_config.lower()}")
# Join multiple reasons with "and"
reason = "_and_".join(reasons) if reasons else "filtered"
# Calculate timestamp: current time rounded down to last quarter hour
now = dt_util.now()
current_minute = (now.minute // 15) * 15
timestamp = now.replace(minute=current_minute, second=0, microsecond=0)
return {
"timestamp": timestamp,
"start": None,
"end": None,
"periods": [],
"reason": reason,
}
def _find_current_or_next_interval(self, intervals: list[dict]) -> dict | None: def _find_current_or_next_interval(self, intervals: list[dict]) -> dict | None:
"""Find the current or next interval from the filtered list.""" """Find the current or next interval from the filtered list."""
now = dt_util.now() now = dt_util.now()
@ -432,6 +684,7 @@ class TibberPricesBinarySensor(TibberPricesEntity, BinarySensorEntity):
"price_avg": round(sum(prices) / len(prices), 2) if prices else 0, "price_avg": round(sum(prices) / len(prices), 2) if prices else 0,
"price_min": round(min(prices), 2) if prices else 0, "price_min": round(min(prices), 2) if prices else 0,
"price_max": round(max(prices), 2) if prices else 0, "price_max": round(max(prices), 2) if prices else 0,
"price_spread": round(max(prices) - min(prices), 2) if prices else 0,
"hour": first.get("hour"), "hour": first.get("hour"),
"minute": first.get("minute"), "minute": first.get("minute"),
"time": first.get("time"), "time": first.get("time"),
@ -486,6 +739,7 @@ class TibberPricesBinarySensor(TibberPricesEntity, BinarySensorEntity):
"price_avg": current_period_summary.get("price_avg"), "price_avg": current_period_summary.get("price_avg"),
"price_min": current_period_summary.get("price_min"), "price_min": current_period_summary.get("price_min"),
"price_max": current_period_summary.get("price_max"), "price_max": current_period_summary.get("price_max"),
"price_spread": current_period_summary.get("price_spread"),
"hour": current_period_summary.get("hour"), "hour": current_period_summary.get("hour"),
"minute": current_period_summary.get("minute"), "minute": current_period_summary.get("minute"),
"time": current_period_summary.get("time"), "time": current_period_summary.get("time"),

View file

@ -2,7 +2,7 @@
from __future__ import annotations from __future__ import annotations
from typing import Any from typing import Any, ClassVar
import voluptuous as vol import voluptuous as vol
@ -39,30 +39,47 @@ from .api import (
TibberPricesApiClientError, TibberPricesApiClientError,
) )
from .const import ( from .const import (
BEST_PRICE_MAX_LEVEL_OPTIONS,
CONF_BEST_PRICE_FLEX, CONF_BEST_PRICE_FLEX,
CONF_BEST_PRICE_MAX_LEVEL,
CONF_BEST_PRICE_MIN_DISTANCE_FROM_AVG, CONF_BEST_PRICE_MIN_DISTANCE_FROM_AVG,
CONF_BEST_PRICE_MIN_PERIOD_LENGTH, CONF_BEST_PRICE_MIN_PERIOD_LENGTH,
CONF_BEST_PRICE_MIN_VOLATILITY,
CONF_EXTENDED_DESCRIPTIONS, CONF_EXTENDED_DESCRIPTIONS,
CONF_PEAK_PRICE_FLEX, CONF_PEAK_PRICE_FLEX,
CONF_PEAK_PRICE_MIN_DISTANCE_FROM_AVG, CONF_PEAK_PRICE_MIN_DISTANCE_FROM_AVG,
CONF_PEAK_PRICE_MIN_LEVEL,
CONF_PEAK_PRICE_MIN_PERIOD_LENGTH, CONF_PEAK_PRICE_MIN_PERIOD_LENGTH,
CONF_PEAK_PRICE_MIN_VOLATILITY,
CONF_PRICE_RATING_THRESHOLD_HIGH, CONF_PRICE_RATING_THRESHOLD_HIGH,
CONF_PRICE_RATING_THRESHOLD_LOW, CONF_PRICE_RATING_THRESHOLD_LOW,
CONF_PRICE_TREND_THRESHOLD_FALLING, CONF_PRICE_TREND_THRESHOLD_FALLING,
CONF_PRICE_TREND_THRESHOLD_RISING, CONF_PRICE_TREND_THRESHOLD_RISING,
CONF_VOLATILITY_THRESHOLD_HIGH,
CONF_VOLATILITY_THRESHOLD_MODERATE,
CONF_VOLATILITY_THRESHOLD_VERY_HIGH,
DEFAULT_BEST_PRICE_FLEX, DEFAULT_BEST_PRICE_FLEX,
DEFAULT_BEST_PRICE_MAX_LEVEL,
DEFAULT_BEST_PRICE_MIN_DISTANCE_FROM_AVG, DEFAULT_BEST_PRICE_MIN_DISTANCE_FROM_AVG,
DEFAULT_BEST_PRICE_MIN_PERIOD_LENGTH, DEFAULT_BEST_PRICE_MIN_PERIOD_LENGTH,
DEFAULT_BEST_PRICE_MIN_VOLATILITY,
DEFAULT_EXTENDED_DESCRIPTIONS, DEFAULT_EXTENDED_DESCRIPTIONS,
DEFAULT_PEAK_PRICE_FLEX, DEFAULT_PEAK_PRICE_FLEX,
DEFAULT_PEAK_PRICE_MIN_DISTANCE_FROM_AVG, DEFAULT_PEAK_PRICE_MIN_DISTANCE_FROM_AVG,
DEFAULT_PEAK_PRICE_MIN_LEVEL,
DEFAULT_PEAK_PRICE_MIN_PERIOD_LENGTH, DEFAULT_PEAK_PRICE_MIN_PERIOD_LENGTH,
DEFAULT_PEAK_PRICE_MIN_VOLATILITY,
DEFAULT_PRICE_RATING_THRESHOLD_HIGH, DEFAULT_PRICE_RATING_THRESHOLD_HIGH,
DEFAULT_PRICE_RATING_THRESHOLD_LOW, DEFAULT_PRICE_RATING_THRESHOLD_LOW,
DEFAULT_PRICE_TREND_THRESHOLD_FALLING, DEFAULT_PRICE_TREND_THRESHOLD_FALLING,
DEFAULT_PRICE_TREND_THRESHOLD_RISING, DEFAULT_PRICE_TREND_THRESHOLD_RISING,
DEFAULT_VOLATILITY_THRESHOLD_HIGH,
DEFAULT_VOLATILITY_THRESHOLD_MODERATE,
DEFAULT_VOLATILITY_THRESHOLD_VERY_HIGH,
DOMAIN, DOMAIN,
LOGGER, LOGGER,
MIN_VOLATILITY_FOR_PERIODS_OPTIONS,
PEAK_PRICE_MIN_LEVEL_OPTIONS,
) )
@ -450,10 +467,40 @@ class TibberPricesSubentryFlowHandler(ConfigSubentryFlow):
class TibberPricesOptionsFlowHandler(OptionsFlow): class TibberPricesOptionsFlowHandler(OptionsFlow):
"""Handle options for tibber_prices entries.""" """Handle options for tibber_prices entries."""
# Step progress tracking
_TOTAL_STEPS: ClassVar[int] = 6
_STEP_INFO: ClassVar[dict[str, int]] = {
"init": 1,
"price_rating": 2,
"volatility": 3,
"best_price": 4,
"peak_price": 5,
"price_trend": 6,
}
def __init__(self) -> None: def __init__(self) -> None:
"""Initialize options flow.""" """Initialize options flow."""
self._options: dict[str, Any] = {} self._options: dict[str, Any] = {}
def _get_step_description_placeholders(self, step_id: str) -> dict[str, str]:
"""Get description placeholders with step progress."""
if step_id not in self._STEP_INFO:
return {}
step_num = self._STEP_INFO[step_id]
# Get translations loaded by Home Assistant
standard_translations_key = f"{DOMAIN}_standard_translations_{self.hass.config.language}"
translations = self.hass.data.get(standard_translations_key, {})
# Get step progress text from translations with placeholders
step_progress_template = translations.get("common", {}).get("step_progress", "Step {step_num} of {total_steps}")
step_progress = step_progress_template.format(step_num=step_num, total_steps=self._TOTAL_STEPS)
return {
"step_progress": step_progress,
}
async def async_step_init(self, user_input: dict[str, Any] | None = None) -> ConfigFlowResult: async def async_step_init(self, user_input: dict[str, Any] | None = None) -> ConfigFlowResult:
"""Manage the options - General Settings.""" """Manage the options - General Settings."""
# Initialize options from config_entry on first call # Initialize options from config_entry on first call
@ -477,6 +524,7 @@ class TibberPricesOptionsFlowHandler(OptionsFlow):
} }
), ),
description_placeholders={ description_placeholders={
**self._get_step_description_placeholders("init"),
"user_login": self.config_entry.data.get("user_login", "N/A"), "user_login": self.config_entry.data.get("user_login", "N/A"),
}, },
) )
@ -485,7 +533,7 @@ class TibberPricesOptionsFlowHandler(OptionsFlow):
"""Configure price rating thresholds.""" """Configure price rating thresholds."""
if user_input is not None: if user_input is not None:
self._options.update(user_input) self._options.update(user_input)
return await self.async_step_best_price() return await self.async_step_volatility()
return self.async_show_form( return self.async_show_form(
step_id="price_rating", step_id="price_rating",
@ -503,6 +551,7 @@ class TibberPricesOptionsFlowHandler(OptionsFlow):
NumberSelectorConfig( NumberSelectorConfig(
min=-100, min=-100,
max=0, max=0,
unit_of_measurement="%",
step=1, step=1,
mode=NumberSelectorMode.SLIDER, mode=NumberSelectorMode.SLIDER,
), ),
@ -519,12 +568,14 @@ class TibberPricesOptionsFlowHandler(OptionsFlow):
NumberSelectorConfig( NumberSelectorConfig(
min=0, min=0,
max=100, max=100,
unit_of_measurement="%",
step=1, step=1,
mode=NumberSelectorMode.SLIDER, mode=NumberSelectorMode.SLIDER,
), ),
), ),
} }
), ),
description_placeholders=self._get_step_description_placeholders("price_rating"),
) )
async def async_step_best_price(self, user_input: dict[str, Any] | None = None) -> ConfigFlowResult: async def async_step_best_price(self, user_input: dict[str, Any] | None = None) -> ConfigFlowResult:
@ -567,6 +618,7 @@ class TibberPricesOptionsFlowHandler(OptionsFlow):
min=0, min=0,
max=100, max=100,
step=1, step=1,
unit_of_measurement="%",
mode=NumberSelectorMode.SLIDER, mode=NumberSelectorMode.SLIDER,
), ),
), ),
@ -583,11 +635,39 @@ class TibberPricesOptionsFlowHandler(OptionsFlow):
min=0, min=0,
max=50, max=50,
step=1, step=1,
unit_of_measurement="%",
mode=NumberSelectorMode.SLIDER, mode=NumberSelectorMode.SLIDER,
), ),
), ),
vol.Optional(
CONF_BEST_PRICE_MIN_VOLATILITY,
default=self.config_entry.options.get(
CONF_BEST_PRICE_MIN_VOLATILITY,
DEFAULT_BEST_PRICE_MIN_VOLATILITY,
),
): SelectSelector(
SelectSelectorConfig(
options=MIN_VOLATILITY_FOR_PERIODS_OPTIONS,
mode=SelectSelectorMode.DROPDOWN,
translation_key="volatility",
),
),
vol.Optional(
CONF_BEST_PRICE_MAX_LEVEL,
default=self.config_entry.options.get(
CONF_BEST_PRICE_MAX_LEVEL,
DEFAULT_BEST_PRICE_MAX_LEVEL,
),
): SelectSelector(
SelectSelectorConfig(
options=BEST_PRICE_MAX_LEVEL_OPTIONS,
mode=SelectSelectorMode.DROPDOWN,
translation_key="price_level",
),
),
} }
), ),
description_placeholders=self._get_step_description_placeholders("best_price"),
) )
async def async_step_peak_price(self, user_input: dict[str, Any] | None = None) -> ConfigFlowResult: async def async_step_peak_price(self, user_input: dict[str, Any] | None = None) -> ConfigFlowResult:
@ -630,6 +710,7 @@ class TibberPricesOptionsFlowHandler(OptionsFlow):
min=-100, min=-100,
max=0, max=0,
step=1, step=1,
unit_of_measurement="%",
mode=NumberSelectorMode.SLIDER, mode=NumberSelectorMode.SLIDER,
), ),
), ),
@ -646,11 +727,39 @@ class TibberPricesOptionsFlowHandler(OptionsFlow):
min=0, min=0,
max=50, max=50,
step=1, step=1,
unit_of_measurement="%",
mode=NumberSelectorMode.SLIDER, mode=NumberSelectorMode.SLIDER,
), ),
), ),
vol.Optional(
CONF_PEAK_PRICE_MIN_VOLATILITY,
default=self.config_entry.options.get(
CONF_PEAK_PRICE_MIN_VOLATILITY,
DEFAULT_PEAK_PRICE_MIN_VOLATILITY,
),
): SelectSelector(
SelectSelectorConfig(
options=MIN_VOLATILITY_FOR_PERIODS_OPTIONS,
mode=SelectSelectorMode.DROPDOWN,
translation_key="volatility",
),
),
vol.Optional(
CONF_PEAK_PRICE_MIN_LEVEL,
default=self.config_entry.options.get(
CONF_PEAK_PRICE_MIN_LEVEL,
DEFAULT_PEAK_PRICE_MIN_LEVEL,
),
): SelectSelector(
SelectSelectorConfig(
options=PEAK_PRICE_MIN_LEVEL_OPTIONS,
mode=SelectSelectorMode.DROPDOWN,
translation_key="price_level",
),
),
} }
), ),
description_placeholders=self._get_step_description_placeholders("peak_price"),
) )
async def async_step_price_trend(self, user_input: dict[str, Any] | None = None) -> ConfigFlowResult: async def async_step_price_trend(self, user_input: dict[str, Any] | None = None) -> ConfigFlowResult:
@ -676,6 +785,7 @@ class TibberPricesOptionsFlowHandler(OptionsFlow):
min=1, min=1,
max=50, max=50,
step=1, step=1,
unit_of_measurement="%",
mode=NumberSelectorMode.SLIDER, mode=NumberSelectorMode.SLIDER,
), ),
), ),
@ -692,9 +802,77 @@ class TibberPricesOptionsFlowHandler(OptionsFlow):
min=-50, min=-50,
max=-1, max=-1,
step=1, step=1,
unit_of_measurement="%",
mode=NumberSelectorMode.SLIDER, mode=NumberSelectorMode.SLIDER,
), ),
), ),
} }
), ),
description_placeholders=self._get_step_description_placeholders("price_trend"),
)
async def async_step_volatility(self, user_input: dict[str, Any] | None = None) -> ConfigFlowResult:
"""Configure volatility thresholds and period filtering."""
if user_input is not None:
self._options.update(user_input)
return await self.async_step_best_price()
return self.async_show_form(
step_id="volatility",
data_schema=vol.Schema(
{
vol.Optional(
CONF_VOLATILITY_THRESHOLD_MODERATE,
default=float(
self.config_entry.options.get(
CONF_VOLATILITY_THRESHOLD_MODERATE,
DEFAULT_VOLATILITY_THRESHOLD_MODERATE,
)
),
): NumberSelector(
NumberSelectorConfig(
min=0.0,
max=100.0,
step=0.1,
unit_of_measurement="ct",
mode=NumberSelectorMode.BOX,
),
),
vol.Optional(
CONF_VOLATILITY_THRESHOLD_HIGH,
default=float(
self.config_entry.options.get(
CONF_VOLATILITY_THRESHOLD_HIGH,
DEFAULT_VOLATILITY_THRESHOLD_HIGH,
)
),
): NumberSelector(
NumberSelectorConfig(
min=0.0,
max=100.0,
step=0.1,
unit_of_measurement="ct",
mode=NumberSelectorMode.BOX,
),
),
vol.Optional(
CONF_VOLATILITY_THRESHOLD_VERY_HIGH,
default=float(
self.config_entry.options.get(
CONF_VOLATILITY_THRESHOLD_VERY_HIGH,
DEFAULT_VOLATILITY_THRESHOLD_VERY_HIGH,
)
),
): NumberSelector(
NumberSelectorConfig(
min=0.0,
max=100.0,
step=0.1,
unit_of_measurement="ct",
mode=NumberSelectorMode.BOX,
),
),
}
),
description_placeholders=self._get_step_description_placeholders("volatility"),
) )

View file

@ -28,6 +28,14 @@ CONF_PRICE_RATING_THRESHOLD_LOW = "price_rating_threshold_low"
CONF_PRICE_RATING_THRESHOLD_HIGH = "price_rating_threshold_high" CONF_PRICE_RATING_THRESHOLD_HIGH = "price_rating_threshold_high"
CONF_PRICE_TREND_THRESHOLD_RISING = "price_trend_threshold_rising" CONF_PRICE_TREND_THRESHOLD_RISING = "price_trend_threshold_rising"
CONF_PRICE_TREND_THRESHOLD_FALLING = "price_trend_threshold_falling" CONF_PRICE_TREND_THRESHOLD_FALLING = "price_trend_threshold_falling"
CONF_VOLATILITY_THRESHOLD_MODERATE = "volatility_threshold_moderate"
CONF_VOLATILITY_THRESHOLD_HIGH = "volatility_threshold_high"
CONF_VOLATILITY_THRESHOLD_VERY_HIGH = "volatility_threshold_very_high"
CONF_BEST_PRICE_MIN_VOLATILITY = "best_price_min_volatility"
CONF_PEAK_PRICE_MIN_VOLATILITY = "peak_price_min_volatility"
CONF_MIN_VOLATILITY_FOR_PERIODS = "min_volatility_for_periods" # Deprecated: Use CONF_BEST_PRICE_MIN_VOLATILITY
CONF_BEST_PRICE_MAX_LEVEL = "best_price_max_level"
CONF_PEAK_PRICE_MIN_LEVEL = "peak_price_min_level"
ATTRIBUTION = "Data provided by Tibber" ATTRIBUTION = "Data provided by Tibber"
@ -44,6 +52,14 @@ DEFAULT_PRICE_RATING_THRESHOLD_LOW = -10 # Default rating threshold low percent
DEFAULT_PRICE_RATING_THRESHOLD_HIGH = 10 # Default rating threshold high percentage DEFAULT_PRICE_RATING_THRESHOLD_HIGH = 10 # Default rating threshold high percentage
DEFAULT_PRICE_TREND_THRESHOLD_RISING = 5 # Default trend threshold for rising prices (%) DEFAULT_PRICE_TREND_THRESHOLD_RISING = 5 # Default trend threshold for rising prices (%)
DEFAULT_PRICE_TREND_THRESHOLD_FALLING = -5 # Default trend threshold for falling prices (%, negative value) DEFAULT_PRICE_TREND_THRESHOLD_FALLING = -5 # Default trend threshold for falling prices (%, negative value)
DEFAULT_VOLATILITY_THRESHOLD_MODERATE = 5.0 # Default threshold for MODERATE volatility (ct/øre)
DEFAULT_VOLATILITY_THRESHOLD_HIGH = 15.0 # Default threshold for HIGH volatility (ct/øre)
DEFAULT_VOLATILITY_THRESHOLD_VERY_HIGH = 30.0 # Default threshold for VERY_HIGH volatility (ct/øre)
DEFAULT_BEST_PRICE_MIN_VOLATILITY = "LOW" # Show best price at any volatility (optimization always useful)
DEFAULT_PEAK_PRICE_MIN_VOLATILITY = "LOW" # Always show peak price (warning relevant even at low spreads)
DEFAULT_MIN_VOLATILITY_FOR_PERIODS = "LOW" # Deprecated: Use DEFAULT_BEST_PRICE_MIN_VOLATILITY
DEFAULT_BEST_PRICE_MAX_LEVEL = "ANY" # Default: show best price periods regardless of price level
DEFAULT_PEAK_PRICE_MIN_LEVEL = "ANY" # Default: show peak price periods regardless of price level
# Home types # Home types
HOME_TYPE_APARTMENT = "APARTMENT" HOME_TYPE_APARTMENT = "APARTMENT"
@ -119,6 +135,48 @@ def format_price_unit_minor(currency_code: str | None) -> str:
return f"{minor_symbol}/{UnitOfPower.KILO_WATT}{UnitOfTime.HOURS}" return f"{minor_symbol}/{UnitOfPower.KILO_WATT}{UnitOfTime.HOURS}"
def calculate_volatility_level(
spread: float,
threshold_moderate: float | None = None,
threshold_high: float | None = None,
threshold_very_high: float | None = None,
) -> str:
"""
Calculate volatility level from price spread.
Volatility indicates how much prices fluctuate during a period, which helps
determine whether active load shifting is worthwhile.
Args:
spread: Absolute price difference between max and min (in minor currency units, e.g., ct or øre)
threshold_moderate: Custom threshold for MODERATE level (default: use VOLATILITY_THRESHOLD_MODERATE)
threshold_high: Custom threshold for HIGH level (default: use VOLATILITY_THRESHOLD_HIGH)
threshold_very_high: Custom threshold for VERY_HIGH level (default: use VOLATILITY_THRESHOLD_VERY_HIGH)
Returns:
Volatility level: LOW, MODERATE, HIGH, or VERY_HIGH
Examples:
- spread < 5: LOW minimal optimization potential
- 5 spread < 15: MODERATE some optimization worthwhile
- 15 spread < 30: HIGH strong optimization recommended
- spread 30: VERY_HIGH maximum optimization potential
"""
# Use provided thresholds or fall back to constants
t_moderate = threshold_moderate if threshold_moderate is not None else VOLATILITY_THRESHOLD_MODERATE
t_high = threshold_high if threshold_high is not None else VOLATILITY_THRESHOLD_HIGH
t_very_high = threshold_very_high if threshold_very_high is not None else VOLATILITY_THRESHOLD_VERY_HIGH
if spread < t_moderate:
return VOLATILITY_LOW
if spread < t_high:
return VOLATILITY_MODERATE
if spread < t_very_high:
return VOLATILITY_HIGH
return VOLATILITY_VERY_HIGH
# Price level constants from Tibber API # Price level constants from Tibber API
PRICE_LEVEL_VERY_CHEAP = "VERY_CHEAP" PRICE_LEVEL_VERY_CHEAP = "VERY_CHEAP"
PRICE_LEVEL_CHEAP = "CHEAP" PRICE_LEVEL_CHEAP = "CHEAP"
@ -131,6 +189,17 @@ PRICE_RATING_LOW = "LOW"
PRICE_RATING_NORMAL = "NORMAL" PRICE_RATING_NORMAL = "NORMAL"
PRICE_RATING_HIGH = "HIGH" PRICE_RATING_HIGH = "HIGH"
# Price volatility levels (based on spread between min and max)
VOLATILITY_LOW = "LOW"
VOLATILITY_MODERATE = "MODERATE"
VOLATILITY_HIGH = "HIGH"
VOLATILITY_VERY_HIGH = "VERY_HIGH"
# Volatility thresholds (in minor currency units like ct or øre)
VOLATILITY_THRESHOLD_MODERATE = 5 # Below this: LOW, above: MODERATE
VOLATILITY_THRESHOLD_HIGH = 15 # Below this: MODERATE, above: HIGH
VOLATILITY_THRESHOLD_VERY_HIGH = 30 # Below this: HIGH, above: VERY_HIGH
# Sensor options (lowercase versions for ENUM device class) # Sensor options (lowercase versions for ENUM device class)
# NOTE: These constants define the valid enum options, but they are not used directly # NOTE: These constants define the valid enum options, but they are not used directly
# in sensor.py due to import timing issues. Instead, the options are defined inline # in sensor.py due to import timing issues. Instead, the options are defined inline
@ -149,6 +218,44 @@ PRICE_RATING_OPTIONS = [
PRICE_RATING_HIGH.lower(), PRICE_RATING_HIGH.lower(),
] ]
VOLATILITY_OPTIONS = [
VOLATILITY_LOW.lower(),
VOLATILITY_MODERATE.lower(),
VOLATILITY_HIGH.lower(),
VOLATILITY_VERY_HIGH.lower(),
]
# Valid options for minimum volatility filter for periods
MIN_VOLATILITY_FOR_PERIODS_OPTIONS = [
VOLATILITY_LOW, # Show at any volatility (≥0ct spread) - no filter
VOLATILITY_MODERATE, # Only show periods when volatility ≥ MODERATE (≥5ct)
VOLATILITY_HIGH, # Only show periods when volatility ≥ HIGH (≥15ct)
VOLATILITY_VERY_HIGH, # Only show periods when volatility ≥ VERY_HIGH (≥30ct)
]
# Valid options for best price maximum level filter (AND-linked with volatility filter)
# Sorted from cheap to expensive: user selects "up to how expensive"
BEST_PRICE_MAX_LEVEL_OPTIONS = [
"ANY", # No filter, allow all price levels
PRICE_LEVEL_VERY_CHEAP, # Only show if level ≤ VERY_CHEAP
PRICE_LEVEL_CHEAP, # Only show if level ≤ CHEAP
PRICE_LEVEL_NORMAL, # Only show if level ≤ NORMAL
PRICE_LEVEL_EXPENSIVE, # Only show if level ≤ EXPENSIVE
]
# Valid options for peak price minimum level filter (AND-linked with volatility filter)
# Sorted from expensive to cheap: user selects "starting from how expensive"
PEAK_PRICE_MIN_LEVEL_OPTIONS = [
"ANY", # No filter, allow all price levels
PRICE_LEVEL_EXPENSIVE, # Only show if level ≥ EXPENSIVE
PRICE_LEVEL_NORMAL, # Only show if level ≥ NORMAL
PRICE_LEVEL_CHEAP, # Only show if level ≥ CHEAP
PRICE_LEVEL_VERY_CHEAP, # Only show if level ≥ VERY_CHEAP
]
# Deprecated: Use BEST_PRICE_MAX_LEVEL_OPTIONS or PEAK_PRICE_MIN_LEVEL_OPTIONS instead
MAX_LEVEL_FOR_PERIODS_OPTIONS = BEST_PRICE_MAX_LEVEL_OPTIONS
# Mapping for comparing price levels (used for sorting) # Mapping for comparing price levels (used for sorting)
PRICE_LEVEL_MAPPING = { PRICE_LEVEL_MAPPING = {
PRICE_LEVEL_VERY_CHEAP: -2, PRICE_LEVEL_VERY_CHEAP: -2,
@ -165,6 +272,14 @@ PRICE_RATING_MAPPING = {
PRICE_RATING_HIGH: 1, PRICE_RATING_HIGH: 1,
} }
# Mapping for comparing volatility levels (used for sorting)
VOLATILITY_MAPPING = {
VOLATILITY_LOW: 0,
VOLATILITY_MODERATE: 1,
VOLATILITY_HIGH: 2,
VOLATILITY_VERY_HIGH: 3,
}
LOGGER = logging.getLogger(__package__) LOGGER = logging.getLogger(__package__)
# Path to custom translations directory # Path to custom translations directory

View file

@ -227,12 +227,32 @@
}, },
"data_timestamp": { "data_timestamp": {
"description": "Zeitstempel des letzten verfügbaren Preisintervalls", "description": "Zeitstempel des letzten verfügbaren Preisintervalls",
"long_description": "Zeigt den Zeitstempel des letzten verfügbaren Preisintervalls von deinem Tibber-Abonnement an" "long_description": "Zeigt den Zeitstempel des letzten verfügbaren Preisdatenintervalls von Ihrem Tibber-Abonnement"
},
"today_volatility": {
"description": "Preisvolatilitätsklassifizierung für heute",
"long_description": "Zeigt, wie stark die Strompreise im Laufe des heutigen Tages variieren, basierend auf der Spannweite (Differenz zwischen höchstem und niedrigstem Preis). Klassifizierung: NIEDRIG = Spannweite < 5ct, MODERAT = 5-15ct, HOCH = 15-30ct, SEHR HOCH = >30ct.",
"usage_tips": "Verwenden Sie dies, um zu entscheiden, ob preisbasierte Optimierung lohnenswert ist. Zum Beispiel lohnt sich bei einer Balkonbatterie mit 15% Effizienzverlusten die Optimierung nur, wenn die Volatilität mindestens MODERAT ist. Erstellen Sie Automatisierungen, die die Volatilität prüfen, bevor Lade-/Entladezyklen geplant werden."
},
"tomorrow_volatility": {
"description": "Preisvolatilitätsklassifizierung für morgen",
"long_description": "Zeigt, wie stark die Strompreise im Laufe des morgigen Tages variieren werden, basierend auf der Spannweite (Differenz zwischen höchstem und niedrigstem Preis). Wird nicht verfügbar, bis morgige Daten veröffentlicht sind (typischerweise 13:00-14:00 MEZ).",
"usage_tips": "Verwenden Sie dies zur Vorausplanung des morgigen Energieverbrauchs. Bei HOHER oder SEHR HOHER Volatilität morgen lohnt sich die Optimierung des Energieverbrauchs. Bei NIEDRIGER Volatilität können Sie Geräte jederzeit ohne wesentliche Kostenunterschiede betreiben."
},
"next_24h_volatility": {
"description": "Preisvolatilitätsklassifizierung für die rollierenden nächsten 24 Stunden",
"long_description": "Zeigt, wie stark die Strompreise in den nächsten 24 Stunden ab jetzt variieren (rollierendes Fenster). Dies überschreitet Tagesgrenzen und aktualisiert sich alle 15 Minuten, wodurch eine vorausschauende Volatilitätsbewertung unabhängig von Kalendertagen bereitgestellt wird.",
"usage_tips": "Bester Sensor für Echtzeitoptimierungsentscheidungen. Im Gegensatz zu Heute/Morgen-Sensoren, die um Mitternacht wechseln, bietet dies eine kontinuierliche 24h-Volatilitätsbewertung. Verwenden Sie dies für Batterielade-Strategien, die Tagesgrenzen überschreiten."
},
"today_tomorrow_volatility": {
"description": "Kombinierte Preisvolatilitätsklassifizierung für heute und morgen",
"long_description": "Zeigt die Volatilität über heute und morgen zusammen (wenn morgige Daten verfügbar sind). Bietet eine erweiterte Ansicht der Preisvariation über bis zu 48 Stunden. Fällt auf Nur-Heute zurück, wenn morgige Daten noch nicht verfügbar sind.",
"usage_tips": "Verwenden Sie dies für Mehrtagsplanung und um zu verstehen, ob Preismöglichkeiten über die Tagesgrenze hinweg bestehen. Die Attribute 'today_volatility' und 'tomorrow_volatility' zeigen individuelle Tagesbeiträge. Nützlich für die Planung von Ladesitzungen, die Mitternacht überschreiten könnten."
}, },
"price_forecast": { "price_forecast": {
"description": "Prognose der kommenden Strompreise", "description": "Prognose kommender Strompreise",
"long_description": "Zeigt kommende Strompreise für zukünftige Intervalle in einem Format an, das leicht in Dashboards verwendet werden kann", "long_description": "Zeigt kommende Strompreise für zukünftige Intervalle in einem Format, das einfach in Dashboards verwendet werden kann",
"usage_tips": "Verwende die Attribute dieser Entität, um bevorstehende Preise in Diagrammen oder benutzerdefinierten Karten anzuzeigen. Greife entweder auf 'intervals' für alle zukünftigen Intervalle oder auf 'hours' für stündliche Zusammenfassungen zu." "usage_tips": "Verwenden Sie die Attribute dieser Entität, um kommende Preise in Diagrammen oder benutzerdefinierten Karten anzuzeigen. Greifen Sie entweder auf 'intervals' für alle zukünftigen Intervalle oder auf 'hours' für stündliche Zusammenfassungen zu."
} }
}, },
"binary_sensor": { "binary_sensor": {

View file

@ -229,6 +229,26 @@
"description": "Timestamp of the latest available price data interval", "description": "Timestamp of the latest available price data interval",
"long_description": "Shows the timestamp of the latest available price data interval from your Tibber subscription" "long_description": "Shows the timestamp of the latest available price data interval from your Tibber subscription"
}, },
"today_volatility": {
"description": "Price volatility classification for today",
"long_description": "Shows how much electricity prices vary throughout today based on the spread (difference between highest and lowest price). Classification: LOW = spread < 5ct, MODERATE = 5-15ct, HIGH = 15-30ct, VERY HIGH = >30ct.",
"usage_tips": "Use this to decide if price-based optimization is worthwhile. For example, with a balcony battery that has 15% efficiency losses, optimization only makes sense when volatility is at least MODERATE. Create automations that check volatility before scheduling charging/discharging cycles."
},
"tomorrow_volatility": {
"description": "Price volatility classification for tomorrow",
"long_description": "Shows how much electricity prices will vary throughout tomorrow based on the spread (difference between highest and lowest price). Becomes unavailable until tomorrow's data is published (typically 13:00-14:00 CET).",
"usage_tips": "Use this for advance planning of tomorrow's energy usage. If tomorrow has HIGH or VERY HIGH volatility, it's worth optimizing energy consumption timing. If LOW, you can run devices anytime without significant cost differences."
},
"next_24h_volatility": {
"description": "Price volatility classification for the rolling next 24 hours",
"long_description": "Shows how much electricity prices vary in the next 24 hours from now (rolling window). This crosses day boundaries and updates every 15 minutes, providing a forward-looking volatility assessment independent of calendar days.",
"usage_tips": "Best sensor for real-time optimization decisions. Unlike today/tomorrow sensors that switch at midnight, this provides continuous 24h volatility assessment. Use for battery charging strategies that span across day boundaries."
},
"today_tomorrow_volatility": {
"description": "Combined price volatility classification for today and tomorrow",
"long_description": "Shows volatility across both today and tomorrow combined (when tomorrow's data is available). Provides an extended view of price variation spanning up to 48 hours. Falls back to today-only when tomorrow's data isn't available yet.",
"usage_tips": "Use this for multi-day planning and to understand if price opportunities exist across the day boundary. The 'today_volatility' and 'tomorrow_volatility' breakdown attributes show individual day contributions. Useful for scheduling charging sessions that might span midnight."
},
"price_forecast": { "price_forecast": {
"description": "Forecast of upcoming electricity prices", "description": "Forecast of upcoming electricity prices",
"long_description": "Shows upcoming electricity prices for future intervals in a format that's easy to use in dashboards", "long_description": "Shows upcoming electricity prices for future intervals in a format that's easy to use in dashboards",

View file

@ -229,10 +229,32 @@
"description": "Tidsstempel for siste tilgjengelige prisdataintervall", "description": "Tidsstempel for siste tilgjengelige prisdataintervall",
"long_description": "Viser tidsstempelet for siste tilgjengelige prisdataintervall fra ditt Tibber-abonnement" "long_description": "Viser tidsstempelet for siste tilgjengelige prisdataintervall fra ditt Tibber-abonnement"
}, },
"long_description": "Viser tidsstemplet for siste tilgjengelige prisdataintervall fra Tibber-abonnementet ditt"
},
"today_volatility": {
"description": "Prisvolatilitetsklassifisering for i dag",
"long_description": "Viser hvor mye strømprisene varierer gjennom dagen basert på spredningen (forskjellen mellom høyeste og laveste pris). Klassifisering: LAV = spredning < 5 øre, MODERAT = 5-15 øre, HØY = 15-30 øre, SVÆRT HØY = >30 øre.",
"usage_tips": "Bruk dette for å avgjøre om prisbasert optimalisering er verdt det. For eksempel, med et balkongbatteri som har 15% effektivitetstap, lønner optimalisering seg bare når volatiliteten er minst MODERAT. Lag automatiseringer som sjekker volatilitet før planlegging av lade-/utladingssykluser."
},
"tomorrow_volatility": {
"description": "Prisvolatilitetsklassifisering for i morgen",
"long_description": "Viser hvor mye strømprisene vil variere gjennom morgendagen basert på spredningen (forskjellen mellom høyeste og laveste pris). Blir utilgjengelig til morgendagens data publiseres (typisk 13:00-14:00 CET).",
"usage_tips": "Bruk dette for forhåndsplanlegging av morgendagens energibruk. Hvis morgendagen har HØY eller SVÆRT HØY volatilitet, er det verdt å optimalisere energiforbrukstiming. Hvis LAV, kan du kjøre enheter når som helst uten vesentlige kostnadsforskjeller."
},
"next_24h_volatility": {
"description": "Prisvolatilitetsklassifisering for de rullende neste 24 timene",
"long_description": "Viser hvor mye strømprisene varierer i de neste 24 timene fra nå (rullerende vindu). Dette krysser daggrenser og oppdateres hvert 15. minutt, og gir en fremadskuende volatilitetsvurdering uavhengig av kalenderdager.",
"usage_tips": "Best sensor for sanntidsoptimaliseringsbeslutninger. I motsetning til i dag/i morgen-sensorer som bytter ved midnatt, gir dette en kontinuerlig 24t volatilitetsvurdering. Bruk for batteriladingsstrategier som spenner over daggrenser."
},
"today_tomorrow_volatility": {
"description": "Kombinert prisvolatilitetsklassifisering for i dag og i morgen",
"long_description": "Viser volatilitet over både i dag og i morgen kombinert (når morgendagens data er tilgjengelig). Gir et utvidet syn på prisvariasjon som spenner opptil 48 timer. Faller tilbake til bare i dag når morgendagens data ikke er tilgjengelig ennå.",
"usage_tips": "Bruk dette for flerdag planlegging og for å forstå om prismuligheter eksisterer over daggrensen. 'today_volatility' og 'tomorrow_volatility' nedbrytningsattributtene viser individuelle dagbidrag. Nyttig for planlegging av ladesesjoner som kan gå over midnatt."
},
"price_forecast": { "price_forecast": {
"description": "Prognose for kommende elektrisitetspriser", "description": "Prognose for kommende strømpriser",
"long_description": "Viser kommende elektrisitetspriser for fremtidige intervaller i et format som er enkelt å bruke i dashboards", "long_description": "Viser kommende strømpriser for fremtidige intervaller i et format som er enkelt å bruke i dashboards",
"usage_tips": "Bruk denne entitetens attributter til å vise kommende priser i diagrammer eller tilpassede kort. Få tilgang til enten 'intervals' for alle fremtidige intervaller eller 'hours' for timesammendrag." "usage_tips": "Bruk denne enhetens attributter for å vise kommende priser i diagrammer eller tilpassede kort. Få tilgang til enten 'intervals' for alle fremtidige intervaller eller 'hours' for timevise sammendrag."
} }
}, },
"binary_sensor": { "binary_sensor": {

View file

@ -229,10 +229,32 @@
"description": "Tijdstempel van het laatst beschikbare prijsgegevensinterval", "description": "Tijdstempel van het laatst beschikbare prijsgegevensinterval",
"long_description": "Toont het tijdstempel van het laatst beschikbare prijsgegevensinterval van uw Tibber-abonnement" "long_description": "Toont het tijdstempel van het laatst beschikbare prijsgegevensinterval van uw Tibber-abonnement"
}, },
"long_description": "Toont de tijdstempel van het laatst beschikbare prijsgegevensinterval van uw Tibber-abonnement"
},
"today_volatility": {
"description": "Prijsvolatiliteitsclassificatie voor vandaag",
"long_description": "Toont hoeveel elektriciteitsprijzen variëren gedurende vandaag op basis van de spreiding (verschil tussen hoogste en laagste prijs). Classificatie: LAAG = spreiding < 5ct, GEMATIGD = 5-15ct, HOOG = 15-30ct, ZEER HOOG = >30ct.",
"usage_tips": "Gebruik dit om te beslissen of prijsgebaseerde optimalisatie de moeite waard is. Bijvoorbeeld, met een balkonbatterij met 15% efficiëntieverlies is optimalisatie alleen zinvol wanneer de volatiliteit ten minste GEMATIGD is. Maak automatiseringen die de volatiliteit controleren voordat laad-/ontlaadcycli worden gepland."
},
"tomorrow_volatility": {
"description": "Prijsvolatiliteitsclassificatie voor morgen",
"long_description": "Toont hoeveel elektriciteitsprijzen zullen variëren gedurende morgen op basis van de spreiding (verschil tussen hoogste en laagste prijs). Wordt niet beschikbaar totdat de gegevens van morgen zijn gepubliceerd (doorgaans 13:00-14:00 CET).",
"usage_tips": "Gebruik dit voor vooruitplanning van het energieverbruik van morgen. Als morgen EEN HOGE of ZEER HOGE volatiliteit heeft, is het de moeite waard om de timing van het energieverbruik te optimaliseren. Bij LAGE volatiliteit kunt u apparaten op elk moment gebruiken zonder significante kostenverschillen."
},
"next_24h_volatility": {
"description": "Prijsvolatiliteitsclassificatie voor de rollende volgende 24 uur",
"long_description": "Toont hoeveel elektriciteitsprijzen variëren in de volgende 24 uur vanaf nu (rollend venster). Dit overschrijdt daggrenzen en wordt elke 15 minuten bijgewerkt, waardoor een vooruitkijkende volatiliteitsbeoordeling wordt geboden onafhankelijk van kalenderdagen.",
"usage_tips": "Beste sensor voor realtime optimalisatiebeslissingen. In tegenstelling tot vandaag/morgen-sensoren die bij middernacht omschakelen, biedt dit een continue 24u volatiliteitsbeoordeling. Gebruik voor batterijlaadstrategieën die daggrenzen overschrijden."
},
"today_tomorrow_volatility": {
"description": "Gecombineerde prijsvolatiliteitsclassificatie voor vandaag en morgen",
"long_description": "Toont volatiliteit over zowel vandaag als morgen gecombineerd (wanneer gegevens van morgen beschikbaar zijn). Biedt een uitgebreid beeld van prijsvariatie over maximaal 48 uur. Valt terug op alleen vandaag wanneer gegevens van morgen nog niet beschikbaar zijn.",
"usage_tips": "Gebruik dit voor meerdaagse planning en om te begrijpen of prijsmogelijkheden bestaan over de daggrens. De 'today_volatility' en 'tomorrow_volatility' uitsplitsingsattributen tonen individuele dagbijdragen. Nuttig voor het plannen van laadsessies die middernacht kunnen overschrijden."
},
"price_forecast": { "price_forecast": {
"description": "Prognose van aanstaande elektriciteitsprijzen", "description": "Voorspelling van komende elektriciteitsprijzen",
"long_description": "Toont aanstaande elektriciteitsprijzen voor toekomstige intervallen in een formaat dat gemakkelijk te gebruiken is in dashboards", "long_description": "Toont komende elektriciteitsprijzen voor toekomstige intervallen in een formaat dat gemakkelijk te gebruiken is in dashboards",
"usage_tips": "Gebruik de attributen van deze entiteit om aanstaande prijzen weer te geven in grafieken of aangepaste kaarten. Toegang tot 'intervals' voor alle toekomstige intervallen of 'hours' voor uuroverzichten." "usage_tips": "Gebruik de attributen van deze entiteit om komende prijzen weer te geven in grafieken of aangepaste kaarten. Toegang tot 'intervals' voor alle toekomstige intervallen of 'hours' voor uurlijkse samenvattingen."
} }
}, },
"binary_sensor": { "binary_sensor": {

View file

@ -227,12 +227,32 @@
}, },
"data_timestamp": { "data_timestamp": {
"description": "Tidsstämpel för senaste tillgängliga prisdataintervall", "description": "Tidsstämpel för senaste tillgängliga prisdataintervall",
"long_description": "Visar tidsstämpeln för senaste tillgängliga prisdataintervall från ditt Tibber-abonnemang" "long_description": "Visar tidsstämpeln för det senaste tillgängliga prisdataintervallet från ditt Tibber-abonnemang"
},
"today_volatility": {
"description": "Prisvolatilitetsklassificering för idag",
"long_description": "Visar hur mycket elpriserna varierar under dagen baserat på spridningen (skillnaden mellan högsta och lägsta pris). Klassificering: LÅG = spridning < 5 öre, MÅTTLIG = 5-15 öre, HÖG = 15-30 öre, MYCKET HÖG = >30 öre.",
"usage_tips": "Använd detta för att avgöra om prisbaserad optimering är värt besväret. Till exempel, med ett balkongbatteri som har 15% effektivitetsförlust är optimering endast meningsfull när volatiliteten är åtminstone MÅTTLIG. Skapa automationer som kontrollerar volatiliteten innan laddnings-/urladdningscykler planeras."
},
"tomorrow_volatility": {
"description": "Prisvolatilitetsklassificering för imorgon",
"long_description": "Visar hur mycket elpriserna kommer att variera under morgondagen baserat på spridningen (skillnaden mellan högsta och lägsta pris). Blir otillgänglig tills morgondagens data publiceras (vanligtvis 13:00-14:00 CET).",
"usage_tips": "Använd detta för förhandsplanering av morgondagens energianvändning. Om morgondagen har HÖG eller MYCKET HÖG volatilitet är det värt att optimera energiförbrukningstiming. Vid LÅG volatilitet kan du köra enheter när som helst utan betydande kostnadsskillnader."
},
"next_24h_volatility": {
"description": "Prisvolatilitetsklassificering för rullande nästa 24 timmar",
"long_description": "Visar hur mycket elpriserna varierar under de nästa 24 timmarna från nu (rullande fönster). Detta korsar daggränser och uppdateras var 15:e minut, vilket ger en framåtblickande volatilitetsbedömning oberoende av kalenderdagar.",
"usage_tips": "Bästa sensorn för realtidsoptimeringsbeslut. Till skillnad från idag/imorgon-sensorer som växlar vid midnatt ger detta en kontinuerlig 24t volatilitetsbedömning. Använd för batteriladningsstrategier som sträcker sig över daggränser."
},
"today_tomorrow_volatility": {
"description": "Kombinerad prisvolatilitetsklassificering för idag och imorgon",
"long_description": "Visar volatilitet över både idag och imorgon kombinerat (när morgondagens data är tillgänglig). Ger en utökad vy av prisvariationen som sträcker sig upp till 48 timmar. Faller tillbaka på endast idag när morgondagens data inte är tillgänglig än.",
"usage_tips": "Använd detta för flerdagarsplanering och för att förstå om prismöjligheter finns över daggränsen. 'today_volatility' och 'tomorrow_volatility' uppdelningsattributen visar individuella dagsbidrag. Användbart för planering av laddningssessioner som kan sträcka sig över midnatt."
}, },
"price_forecast": { "price_forecast": {
"description": "Prognos för kommande elpriser", "description": "Prognos för kommande elpriser",
"long_description": "Visar kommande elpriser för framtida intervaller i ett format som är lätt att använda i instrumentpaneler", "long_description": "Visar kommande elpriser för framtida intervaller i ett format som är enkelt att använda i instrumentpaneler",
"usage_tips": "Använd denna entitets attribut för att visa kommande priser i diagram eller anpassade kort. Få tillgång till antingen 'intervals' för alla framtida intervaller eller 'hours' för timsammanfattningar." "usage_tips": "Använd denna enhets attribut för att visa kommande priser i diagram eller anpassade kort. Få åtkomst till antingen 'intervals' för alla framtida intervaller eller 'hours' för timvisa sammanfattningar."
} }
}, },
"binary_sensor": { "binary_sensor": {

View file

@ -40,6 +40,7 @@ from .const import (
PRICE_LEVEL_MAPPING, PRICE_LEVEL_MAPPING,
PRICE_RATING_MAPPING, PRICE_RATING_MAPPING,
async_get_entity_description, async_get_entity_description,
calculate_volatility_level,
format_price_unit_minor, format_price_unit_minor,
get_entity_description, get_entity_description,
get_price_level_translation, get_price_level_translation,
@ -261,6 +262,45 @@ STATISTICS_SENSORS = (
), ),
) )
# Volatility sensors (price spread analysis)
# NOTE: Enum options are defined inline (not imported from const.py) to avoid
# import timing issues with Home Assistant's entity platform initialization.
# Keep in sync with VOLATILITY_OPTIONS in const.py!
VOLATILITY_SENSORS = (
SensorEntityDescription(
key="today_volatility",
translation_key="today_volatility",
name="Today's Price Volatility",
icon="mdi:chart-bell-curve-cumulative",
device_class=SensorDeviceClass.ENUM,
options=["low", "moderate", "high", "very_high"],
),
SensorEntityDescription(
key="tomorrow_volatility",
translation_key="tomorrow_volatility",
name="Tomorrow's Price Volatility",
icon="mdi:chart-bell-curve-cumulative",
device_class=SensorDeviceClass.ENUM,
options=["low", "moderate", "high", "very_high"],
),
SensorEntityDescription(
key="next_24h_volatility",
translation_key="next_24h_volatility",
name="Next 24h Price Volatility",
icon="mdi:chart-bell-curve-cumulative",
device_class=SensorDeviceClass.ENUM,
options=["low", "moderate", "high", "very_high"],
),
SensorEntityDescription(
key="today_tomorrow_volatility",
translation_key="today_tomorrow_volatility",
name="Today + Tomorrow Price Volatility",
icon="mdi:chart-bell-curve-cumulative",
device_class=SensorDeviceClass.ENUM,
options=["low", "moderate", "high", "very_high"],
),
)
# Rating sensors # Rating sensors
# NOTE: Enum options are defined inline (not imported from const.py) to avoid # NOTE: Enum options are defined inline (not imported from const.py) to avoid
# import timing issues with Home Assistant's entity platform initialization. # import timing issues with Home Assistant's entity platform initialization.
@ -488,6 +528,7 @@ DIAGNOSTIC_SENSORS = (
ENTITY_DESCRIPTIONS = ( ENTITY_DESCRIPTIONS = (
*PRICE_SENSORS, *PRICE_SENSORS,
*STATISTICS_SENSORS, *STATISTICS_SENSORS,
*VOLATILITY_SENSORS,
*RATING_SENSORS, *RATING_SENSORS,
*FUTURE_AVERAGE_SENSORS, *FUTURE_AVERAGE_SENSORS,
*TREND_SENSORS, *TREND_SENSORS,
@ -671,6 +712,11 @@ class TibberPricesSensor(TibberPricesEntity, SensorEntity):
"data_timestamp": self._get_data_timestamp, "data_timestamp": self._get_data_timestamp,
# Price forecast sensor # Price forecast sensor
"price_forecast": self._get_price_forecast_value, "price_forecast": self._get_price_forecast_value,
# Volatility sensors
"today_volatility": lambda: self._get_volatility_value(volatility_type="today"),
"tomorrow_volatility": lambda: self._get_volatility_value(volatility_type="tomorrow"),
"next_24h_volatility": lambda: self._get_volatility_value(volatility_type="next_24h"),
"today_tomorrow_volatility": lambda: self._get_volatility_value(volatility_type="today_tomorrow"),
} }
return handlers.get(key) return handlers.get(key)
@ -1354,6 +1400,139 @@ class TibberPricesSensor(TibberPricesEntity, SensorEntity):
return dt_util.as_utc(latest_timestamp) if latest_timestamp else None return dt_util.as_utc(latest_timestamp) if latest_timestamp else None
def _get_prices_for_volatility(self, volatility_type: str, price_info: dict) -> list[float]:
"""
Get price list for volatility calculation based on type.
Args:
volatility_type: One of "today", "tomorrow", "next_24h", "today_tomorrow"
price_info: Price information dictionary from coordinator data
Returns:
List of prices to analyze
"""
if volatility_type == "today":
return [float(p["total"]) for p in price_info.get("today", []) if "total" in p]
if volatility_type == "tomorrow":
return [float(p["total"]) for p in price_info.get("tomorrow", []) if "total" in p]
if volatility_type == "next_24h":
# Rolling 24h from now
now = dt_util.now()
end_time = now + timedelta(hours=24)
prices = []
for day_key in ["today", "tomorrow"]:
for price_data in price_info.get(day_key, []):
starts_at = dt_util.parse_datetime(price_data.get("startsAt"))
if starts_at is None:
continue
starts_at = dt_util.as_local(starts_at)
if now <= starts_at < end_time and "total" in price_data:
prices.append(float(price_data["total"]))
return prices
if volatility_type == "today_tomorrow":
# Combined today + tomorrow
prices = []
for day_key in ["today", "tomorrow"]:
for price_data in price_info.get(day_key, []):
if "total" in price_data:
prices.append(float(price_data["total"]))
return prices
return []
def _add_volatility_type_attributes(
self,
volatility_type: str,
price_info: dict,
thresholds: dict,
) -> None:
"""Add type-specific attributes for volatility sensors."""
if volatility_type == "today_tomorrow":
# Add breakdown for today vs tomorrow
today_prices = [float(p["total"]) for p in price_info.get("today", []) if "total" in p]
tomorrow_prices = [float(p["total"]) for p in price_info.get("tomorrow", []) if "total" in p]
if today_prices:
today_spread = (max(today_prices) - min(today_prices)) * 100
today_vol = calculate_volatility_level(today_spread, **thresholds)
self._last_volatility_attributes["today_spread"] = round(today_spread, 2)
self._last_volatility_attributes["today_volatility"] = today_vol
self._last_volatility_attributes["interval_count_today"] = len(today_prices)
if tomorrow_prices:
tomorrow_spread = (max(tomorrow_prices) - min(tomorrow_prices)) * 100
tomorrow_vol = calculate_volatility_level(tomorrow_spread, **thresholds)
self._last_volatility_attributes["tomorrow_spread"] = round(tomorrow_spread, 2)
self._last_volatility_attributes["tomorrow_volatility"] = tomorrow_vol
self._last_volatility_attributes["interval_count_tomorrow"] = len(tomorrow_prices)
elif volatility_type == "next_24h":
# Add time window info
now = dt_util.now()
self._last_volatility_attributes["timestamp"] = now.isoformat()
def _get_volatility_value(self, *, volatility_type: str) -> str | None:
"""
Calculate price volatility (spread) for different time periods.
Args:
volatility_type: One of "today", "tomorrow", "next_24h", "today_tomorrow"
Returns:
Volatility level: "low", "moderate", "high", "very_high", or None if unavailable
"""
if not self.coordinator.data:
return None
price_info = self.coordinator.data.get("priceInfo", {})
# Get volatility thresholds from config
thresholds = {
"threshold_moderate": self.coordinator.config_entry.options.get("volatility_threshold_moderate", 5.0),
"threshold_high": self.coordinator.config_entry.options.get("volatility_threshold_high", 15.0),
"threshold_very_high": self.coordinator.config_entry.options.get("volatility_threshold_very_high", 30.0),
}
# Get prices based on volatility type
prices_to_analyze = self._get_prices_for_volatility(volatility_type, price_info)
if not prices_to_analyze:
return None
# Calculate spread
price_min = min(prices_to_analyze)
price_max = max(prices_to_analyze)
spread = price_max - price_min
# Convert to minor currency units (ct/øre) for volatility calculation
spread_minor = spread * 100
# Calculate volatility level with custom thresholds
volatility = calculate_volatility_level(spread_minor, **thresholds)
# Store attributes for this sensor
self._last_volatility_attributes = {
"price_spread": round(spread_minor, 2),
"price_volatility": volatility,
"price_min": round(price_min * 100, 2),
"price_max": round(price_max * 100, 2),
"price_avg": round((sum(prices_to_analyze) / len(prices_to_analyze)) * 100, 2),
"interval_count": len(prices_to_analyze),
}
# Add type-specific attributes
self._add_volatility_type_attributes(volatility_type, price_info, thresholds)
# Return lowercase for ENUM device class
return volatility.lower()
# Add method to get future price intervals # Add method to get future price intervals
def _get_price_forecast_value(self) -> str | None: def _get_price_forecast_value(self) -> str | None:
"""Get the highest or lowest price status for the price forecast entity.""" """Get the highest or lowest price status for the price forecast entity."""
@ -1500,6 +1679,11 @@ class TibberPricesSensor(TibberPricesEntity, SensorEntity):
# Convert to list sorted by hour # Convert to list sorted by hour
attributes["intervals_by_hour"] = [hour_data for _, hour_data in sorted(hours.items())] attributes["intervals_by_hour"] = [hour_data for _, hour_data in sorted(hours.items())]
def _add_volatility_attributes(self, attributes: dict) -> None:
"""Add attributes for volatility sensors."""
if hasattr(self, "_last_volatility_attributes") and self._last_volatility_attributes:
attributes.update(self._last_volatility_attributes)
@property @property
def native_value(self) -> float | str | datetime | None: def native_value(self) -> float | str | datetime | None:
"""Return the native value of the sensor.""" """Return the native value of the sensor."""
@ -1670,6 +1854,8 @@ class TibberPricesSensor(TibberPricesEntity, SensorEntity):
self._add_statistics_attributes(attributes) self._add_statistics_attributes(attributes)
elif key == "price_forecast": elif key == "price_forecast":
self._add_price_forecast_attributes(attributes) self._add_price_forecast_attributes(attributes)
elif key.endswith("_volatility"):
self._add_volatility_attributes(attributes)
# For price_level, add the original level as attribute # For price_level, add the original level as attribute
if key == "price_level" and hasattr(self, "_last_price_level") and self._last_price_level is not None: if key == "price_level" and hasattr(self, "_last_price_level") and self._last_price_level is not None:
attributes["level_id"] = self._last_price_level attributes["level_id"] = self._last_price_level

View file

@ -20,6 +20,12 @@ from .api import (
) )
from .average_utils import calculate_leading_24h_avg, calculate_trailing_24h_avg from .average_utils import calculate_leading_24h_avg, calculate_trailing_24h_avg
from .const import ( from .const import (
CONF_VOLATILITY_THRESHOLD_HIGH,
CONF_VOLATILITY_THRESHOLD_MODERATE,
CONF_VOLATILITY_THRESHOLD_VERY_HIGH,
DEFAULT_VOLATILITY_THRESHOLD_HIGH,
DEFAULT_VOLATILITY_THRESHOLD_MODERATE,
DEFAULT_VOLATILITY_THRESHOLD_VERY_HIGH,
DOMAIN, DOMAIN,
PRICE_LEVEL_CHEAP, PRICE_LEVEL_CHEAP,
PRICE_LEVEL_EXPENSIVE, PRICE_LEVEL_EXPENSIVE,
@ -29,6 +35,7 @@ from .const import (
PRICE_RATING_HIGH, PRICE_RATING_HIGH,
PRICE_RATING_LOW, PRICE_RATING_LOW,
PRICE_RATING_NORMAL, PRICE_RATING_NORMAL,
calculate_volatility_level,
get_price_level_translation, get_price_level_translation,
) )
@ -98,6 +105,19 @@ async def _get_price(call: ServiceCall) -> dict[str, Any]:
_, coordinator, _ = _get_entry_and_data(hass, entry_id) _, coordinator, _ = _get_entry_and_data(hass, entry_id)
price_info_data, currency = _extract_price_data(coordinator.data) price_info_data, currency = _extract_price_data(coordinator.data)
# Get volatility thresholds from config
thresholds = {
"threshold_moderate": coordinator.config_entry.options.get(
CONF_VOLATILITY_THRESHOLD_MODERATE, DEFAULT_VOLATILITY_THRESHOLD_MODERATE
),
"threshold_high": coordinator.config_entry.options.get(
CONF_VOLATILITY_THRESHOLD_HIGH, DEFAULT_VOLATILITY_THRESHOLD_HIGH
),
"threshold_very_high": coordinator.config_entry.options.get(
CONF_VOLATILITY_THRESHOLD_VERY_HIGH, DEFAULT_VOLATILITY_THRESHOLD_VERY_HIGH
),
}
# Determine which days to include # Determine which days to include
if explicit_day: if explicit_day:
day_key = day if day in ("yesterday", "today", "tomorrow") else "today" day_key = day if day in ("yesterday", "today", "tomorrow") else "today"
@ -116,7 +136,7 @@ async def _get_price(call: ServiceCall) -> dict[str, Any]:
stats_transformed = _transform_price_intervals(stats_raw) stats_transformed = _transform_price_intervals(stats_raw)
# Calculate stats # Calculate stats
price_stats = _get_price_stats(stats_transformed) price_stats = _get_price_stats(stats_transformed, thresholds)
# Determine now and simulation flag # Determine now and simulation flag
now, is_simulated = _determine_now_and_simulation(time_value, stats_transformed) now, is_simulated = _determine_now_and_simulation(time_value, stats_transformed)
@ -429,7 +449,7 @@ def _enrich_intervals_with_averages(intervals: list[dict], price_info_by_day: di
interval["leading_price_average_minor"] = round(leading_avg * 100, 2) interval["leading_price_average_minor"] = round(leading_avg * 100, 2)
def _get_price_stats(merged: list[dict]) -> PriceStats: def _get_price_stats(merged: list[dict], thresholds: dict) -> PriceStats:
"""Calculate average, min, and max price from merged data.""" """Calculate average, min, and max price from merged data."""
if merged: if merged:
price_sum = sum(float(interval.get("price", 0)) for interval in merged if "price" in interval) price_sum = sum(float(interval.get("price", 0)) for interval in merged if "price" in interval)
@ -438,6 +458,10 @@ def _get_price_stats(merged: list[dict]) -> PriceStats:
price_avg = 0 price_avg = 0
price_min, price_min_interval = _get_price_stat(merged, "min") price_min, price_min_interval = _get_price_stat(merged, "min")
price_max, price_max_interval = _get_price_stat(merged, "max") price_max, price_max_interval = _get_price_stat(merged, "max")
price_spread = round(price_max - price_min, 4) if price_min is not None and price_max is not None else 0
# Convert spread to minor currency units (ct/øre) for volatility calculation
price_spread_minor = price_spread * 100
price_volatility = calculate_volatility_level(price_spread_minor, **thresholds)
return PriceStats( return PriceStats(
price_avg=price_avg, price_avg=price_avg,
price_min=price_min, price_min=price_min,
@ -448,6 +472,8 @@ def _get_price_stats(merged: list[dict]) -> PriceStats:
price_max_end_time=price_max_interval.get("end_time") if price_max_interval else None, price_max_end_time=price_max_interval.get("end_time") if price_max_interval else None,
price_min_interval=price_min_interval, price_min_interval=price_min_interval,
price_max_interval=price_max_interval, price_max_interval=price_max_interval,
price_spread=price_spread,
price_volatility=price_volatility,
stats_merged=merged, stats_merged=merged,
) )
@ -594,6 +620,8 @@ def _build_price_response(ctx: PriceResponseContext) -> dict[str, Any]:
"next": clean_interval(ctx.next_interval), "next": clean_interval(ctx.next_interval),
"currency": ctx.currency, "currency": ctx.currency,
"interval_count": len(ctx.merged), "interval_count": len(ctx.merged),
"price_spread": round(price_stats.price_spread * 100, 2), # Convert to minor currency unit
"price_volatility": price_stats.price_volatility,
"intervals": ctx.merged, "intervals": ctx.merged,
} }
@ -626,6 +654,8 @@ class PriceStats:
price_max_start_time: str | None price_max_start_time: str | None
price_max_end_time: str | None price_max_end_time: str | None
price_max_interval: dict | None price_max_interval: dict | None
price_spread: float
price_volatility: str
stats_merged: list[dict] stats_merged: list[dict]

View file

@ -48,6 +48,9 @@
"reauth_successful": "Erneute Authentifizierung erfolgreich. Die Integration wurde mit dem neuen Zugriffstoken aktualisiert." "reauth_successful": "Erneute Authentifizierung erfolgreich. Die Integration wurde mit dem neuen Zugriffstoken aktualisiert."
} }
}, },
"common": {
"step_progress": "Schritt {step_num} von {total_steps}"
},
"config_subentries": { "config_subentries": {
"home": { "home": {
"initiate_flow": { "initiate_flow": {
@ -79,43 +82,64 @@
"step": { "step": {
"init": { "init": {
"title": "Allgemeine Einstellungen", "title": "Allgemeine Einstellungen",
"description": "Konfiguration allgemeiner Einstellungen für Tibber Preisinformationen & Bewertungen.\n\nBenutzer: {user_login}", "description": "{step_progress}\n\nKonfiguration allgemeiner Einstellungen für Tibber Preisinformationen & Bewertungen.\n\nBenutzer: {user_login}",
"data": { "data": {
"extended_descriptions": "Erweiterte Beschreibungen in Entity-Attributen anzeigen" "extended_descriptions": "Erweiterte Beschreibungen in Entity-Attributen anzeigen"
} }
}, },
"price_rating": { "price_rating": {
"title": "Preisbewertungs-Schwellwerte", "title": "Preisbewertungs-Schwellwerte",
"description": "Konfiguration der Schwellwerte für Preisbewertungsstufen (NIEDRIG/NORMAL/HOCH) basierend auf dem Vergleich mit dem gleitenden 24-Stunden-Durchschnitt.", "description": "{step_progress}\n\nKonfiguration der Schwellwerte für Preisbewertungsstufen (NIEDRIG/NORMAL/HOCH) basierend auf dem Vergleich mit dem gleitenden 24-Stunden-Durchschnitt.",
"data": { "data": {
"price_rating_threshold_low": "Schwellwert für niedrige Bewertung (% unter gleitendem Durchschnitt)", "price_rating_threshold_low": "Schwellwert für niedrige Bewertung (unter gleitendem Durchschnitt)",
"price_rating_threshold_high": "Schwellwert für hohe Bewertung (% über gleitendem Durchschnitt)" "price_rating_threshold_high": "Schwellwert für hohe Bewertung (über gleitendem Durchschnitt)"
} }
}, },
"best_price": { "best_price": {
"title": "Bestpreis-Periode Einstellungen", "title": "Bestpreis-Periode Einstellungen",
"description": "Konfiguration für den Bestpreis-Periode Binärsensor. Dieser Sensor ist während der Zeiträume mit den niedrigsten Strompreisen aktiv.", "description": "{step_progress}\n\nKonfiguration für den Bestpreis-Periode Binärsensor. Dieser Sensor ist während der Zeiträume mit den niedrigsten Strompreisen aktiv.",
"data": { "data": {
"best_price_min_period_length": "Minimale Periodenlänge", "best_price_min_period_length": "Minimale Periodenlänge",
"best_price_flex": "Flexibilität: Maximale % über dem Mindestpreis", "best_price_flex": "Flexibilität: Maximal über dem Mindestpreis",
"best_price_min_distance_from_avg": "Mindestabstand: Erforderliche % unter dem Tagesdurchschnitt" "best_price_min_distance_from_avg": "Mindestabstand: Erforderlich unter dem Tagesdurchschnitt",
"best_price_min_volatility": "Mindest-Volatilitätsfilter",
"best_price_max_level": "Preisniveau-Filter (Optional)"
},
"data_description": {
"best_price_min_volatility": "Zeigt Bestpreis-Perioden nur an, wenn die heutige Volatilität mindestens diesem Level entspricht. Standard: 'Niedrig' (zeigt unabhängig von Volatilität) - Batterie-Optimierung ist auch bei geringen Preisschwankungen nützlich. Wähle 'Moderat'/'Hoch' um Perioden nur an volatileren Tagen anzuzeigen. UND-Verknüpfung: Volatilität UND Niveaufilter müssen beide erfüllt sein.",
"best_price_max_level": "Zeigt Bestpreis-Perioden nur an, wenn mindestens ein Intervall heute ein Preisniveau ≤ dem gewählten Wert hat. UND-Verknüpfung: Volatilitätsfilter (falls gesetzt) UND Niveaufilter müssen beide erfüllt sein. Nützlich um Batterieladen an teuren Tagen zu vermeiden. Wähle 'Beliebig' um diesen Filter zu deaktivieren."
} }
}, },
"peak_price": { "peak_price": {
"title": "Spitzenpreis-Periode Einstellungen", "title": "Spitzenpreis-Periode Einstellungen",
"description": "Konfiguration für den Spitzenpreis-Periode Binärsensor. Dieser Sensor ist während der Zeiträume mit den höchsten Strompreisen aktiv.", "description": "{step_progress}\n\nKonfiguration für den Spitzenpreis-Periode Binärsensor. Dieser Sensor ist während der Zeiträume mit den höchsten Strompreisen aktiv.",
"data": { "data": {
"peak_price_min_period_length": "Minimale Periodenlänge", "peak_price_min_period_length": "Minimale Periodenlänge",
"peak_price_flex": "Flexibilität: Maximale % unter dem Höchstpreis (negativer Wert)", "peak_price_flex": "Flexibilität: Maximal unter dem Höchstpreis (negativer Wert)",
"peak_price_min_distance_from_avg": "Mindestabstand: Erforderliche % über dem Tagesdurchschnitt" "peak_price_min_distance_from_avg": "Mindestabstand: Erforderlich über dem Tagesdurchschnitt",
"peak_price_min_volatility": "Mindest-Volatilitätsfilter",
"peak_price_min_level": "Preisniveau-Filter (Optional)"
},
"data_description": {
"peak_price_min_volatility": "Zeigt Spitzenpreis-Perioden nur an, wenn die heutige Volatilität mindestens diesem Level entspricht. Standard: 'Niedrig' (zeigt unabhängig von Volatilität) - Spitzenwarnungen sind auch bei niedrigen Spannen relevant, da teure Stunden vermeiden immer wichtig ist. Wähle 'Moderat'/'Hoch' um Peaks nur an volatilen Tagen anzuzeigen. UND-Verknüpfung: Volatilität UND Niveaufilter müssen beide erfüllt sein.",
"peak_price_min_level": "Zeigt Spitzenpreis-Perioden nur an, wenn mindestens ein Intervall heute ein Preisniveau ≥ dem gewählten Wert hat. UND-Verknüpfung: Volatilitätsfilter (falls gesetzt) UND Niveaufilter müssen beide erfüllt sein. Normalerweise auf 'Beliebig' gesetzt, da Spitzenperioden relativ zum Tag sind. Wähle 'Beliebig' um diesen Filter zu deaktivieren."
} }
}, },
"price_trend": { "price_trend": {
"title": "Preistrend-Schwellenwerte", "title": "Preistrend-Schwellenwerte",
"description": "Konfiguration der Schwellenwerte für Preistrend-Sensoren. Diese Sensoren vergleichen den aktuellen Preis mit dem Durchschnitt der nächsten N Stunden, um festzustellen, ob die Preise steigen, fallen oder stabil sind.", "description": "{step_progress}\n\nKonfigurieren Sie Schwellenwerte für Preistrend-Sensoren. Diese Sensoren vergleichen den aktuellen Preis mit dem Durchschnitt der nächsten N Stunden, um festzustellen, ob die Preise steigen, fallen oder stabil sind.",
"data": { "data": {
"price_trend_threshold_rising": "Schwellenwert für Steigen (% über aktuellem Preis)", "price_trend_threshold_rising": "Steigender Schwellenwert (über dem aktuellen Preis)",
"price_trend_threshold_falling": "Schwellenwert für Fallen (% unter aktuellem Preis, negativer Wert)" "price_trend_threshold_falling": "Fallender Schwellenwert (unter dem aktuellen Preis, negativer Wert)"
}
},
"volatility": {
"title": "Preisvolatilität Schwellenwerte",
"description": "{step_progress}\n\nKonfigurieren Sie Schwellenwerte für die Volatilitätsklassifizierung. Volatilität misst Preisschwankungen (Spanne zwischen Min/Max) in kleinster Währungseinheit. Diese Schwellenwerte werden von Volatilitätssensoren und Periodenfiltern verwendet.",
"data": {
"volatility_threshold_moderate": "Moderate Schwelle (Spanne ≥ dieser Wert)",
"volatility_threshold_high": "Hohe Schwelle (Spanne ≥ dieser Wert)",
"volatility_threshold_very_high": "Sehr hohe Schwelle (Spanne ≥ dieser Wert)"
} }
} }
}, },
@ -373,7 +397,43 @@
"name": "Monatliche Preisbewertung" "name": "Monatliche Preisbewertung"
}, },
"data_timestamp": { "data_timestamp": {
"name": "Ablauf der Preisdaten" "name": "Preisdaten-Ablauf"
},
"today_volatility": {
"name": "Preisvolatilität heute",
"state": {
"low": "Niedrig",
"moderate": "Moderat",
"high": "Hoch",
"very_high": "Sehr hoch"
}
},
"tomorrow_volatility": {
"name": "Preisvolatilität morgen",
"state": {
"low": "Niedrig",
"moderate": "Moderat",
"high": "Hoch",
"very_high": "Sehr hoch"
}
},
"next_24h_volatility": {
"name": "Preisvolatilität der nächsten 24h",
"state": {
"low": "Niedrig",
"moderate": "Moderat",
"high": "Hoch",
"very_high": "Sehr hoch"
}
},
"today_tomorrow_volatility": {
"name": "Preisvolatilität heute+morgen",
"state": {
"low": "Niedrig",
"moderate": "Moderat",
"high": "Hoch",
"very_high": "Sehr hoch"
}
}, },
"price_forecast": { "price_forecast": {
"name": "Preisprognose" "name": "Preisprognose"
@ -416,5 +476,25 @@
} }
} }
}, },
"selector": {
"volatility": {
"options": {
"LOW": "Niedrig",
"MODERATE": "Moderat",
"HIGH": "Hoch",
"VERY_HIGH": "Sehr hoch"
}
},
"price_level": {
"options": {
"ANY": "Beliebig",
"VERY_CHEAP": "Sehr günstig",
"CHEAP": "Günstig",
"NORMAL": "Normal",
"EXPENSIVE": "Teuer",
"VERY_EXPENSIVE": "Sehr teuer"
}
}
},
"title": "Tibber Preisinformationen & Bewertungen" "title": "Tibber Preisinformationen & Bewertungen"
} }

View file

@ -48,6 +48,9 @@
"reauth_successful": "Reauthentication successful. The integration has been updated with the new access token." "reauth_successful": "Reauthentication successful. The integration has been updated with the new access token."
} }
}, },
"common": {
"step_progress": "Step {step_num} of {total_steps}"
},
"config_subentries": { "config_subentries": {
"home": { "home": {
"initiate_flow": { "initiate_flow": {
@ -79,43 +82,64 @@
"step": { "step": {
"init": { "init": {
"title": "General Settings", "title": "General Settings",
"description": "Configure general settings for Tibber Price Information & Ratings.\n\nUser: {user_login}", "description": "{step_progress}\n\nConfigure general settings for Tibber Price Information & Ratings.\n\nUser: {user_login}",
"data": { "data": {
"extended_descriptions": "Show extended descriptions in entity attributes" "extended_descriptions": "Show extended descriptions in entity attributes"
} }
}, },
"price_rating": { "price_rating": {
"title": "Price Rating Thresholds", "title": "Price Rating Thresholds",
"description": "Configure thresholds for price rating levels (LOW/NORMAL/HIGH) based on comparison with trailing 24-hour average.", "description": "{step_progress}\n\nConfigure thresholds for price rating levels (LOW/NORMAL/HIGH) based on comparison with trailing 24-hour average.",
"data": { "data": {
"price_rating_threshold_low": "Low Rating Threshold (% below trailing average)", "price_rating_threshold_low": "Low Rating Threshold (below trailing average)",
"price_rating_threshold_high": "High Rating Threshold (% above trailing average)" "price_rating_threshold_high": "High Rating Threshold (above trailing average)"
} }
}, },
"best_price": { "best_price": {
"title": "Best Price Period Settings", "title": "Best Price Period Settings",
"description": "Configure settings for the Best Price Period binary sensor. This sensor is active during periods with the lowest electricity prices.", "description": "{step_progress}\n\nConfigure settings for the Best Price Period binary sensor. This sensor is active during periods with the lowest electricity prices.",
"data": { "data": {
"best_price_min_period_length": "Minimum Period Length", "best_price_min_period_length": "Minimum Period Length",
"best_price_flex": "Flexibility: Maximum % above minimum price", "best_price_flex": "Flexibility: Maximum above minimum price",
"best_price_min_distance_from_avg": "Minimum Distance: Required % below daily average" "best_price_min_distance_from_avg": "Minimum Distance: Required below daily average",
"best_price_min_volatility": "Minimum Volatility Filter",
"best_price_max_level": "Price Level Filter (Optional)"
},
"data_description": {
"best_price_min_volatility": "Only show best price periods when today's volatility meets or exceeds this level. Default: 'Low' (show regardless of volatility) - battery optimization is useful even with small price variations. Select 'Moderate'/'High' to only show periods on more volatile days. Works with AND logic: volatility AND level filter must both pass.",
"best_price_max_level": "Only show best price periods if at least one interval today has a price level ≤ selected value. Works with AND logic: volatility filter (if set) AND level filter must both pass. Useful to avoid battery charging on expensive days. Select 'Any' to disable this filter."
} }
}, },
"peak_price": { "peak_price": {
"title": "Peak Price Period Settings", "title": "Peak Price Period Settings",
"description": "Configure settings for the Peak Price Period binary sensor. This sensor is active during periods with the highest electricity prices.", "description": "{step_progress}\n\nConfigure settings for the Peak Price Period binary sensor. This sensor is active during periods with the highest electricity prices.",
"data": { "data": {
"peak_price_min_period_length": "Minimum Period Length", "peak_price_min_period_length": "Minimum Period Length",
"peak_price_flex": "Flexibility: Maximum % below maximum price (negative value)", "peak_price_flex": "Flexibility: Maximum below maximum price (negative value)",
"peak_price_min_distance_from_avg": "Minimum Distance: Required % above daily average" "peak_price_min_distance_from_avg": "Minimum Distance: Required above daily average",
"peak_price_min_volatility": "Minimum Volatility Filter",
"peak_price_min_level": "Price Level Filter (Optional)"
},
"data_description": {
"peak_price_min_volatility": "Only show peak price periods when today's volatility meets or exceeds this level. Default: 'Low' (show regardless of volatility) - peak warnings are relevant even at low spreads since avoiding expensive hours always matters. Select 'Moderate'/'High' to only show peaks on volatile days. Works with AND logic: volatility AND level filter must both pass.",
"peak_price_min_level": "Only show peak price periods if at least one interval today has a price level ≥ selected value. Works with AND logic: volatility filter (if set) AND level filter must both pass. Typically set to 'Any' since peak periods are relative to the day. Select 'Any' to disable this filter."
} }
}, },
"price_trend": { "price_trend": {
"title": "Price Trend Thresholds", "title": "Price Trend Thresholds",
"description": "Configure thresholds for price trend sensors. These sensors compare the current price with the average of the next N hours to determine if prices are rising, falling, or stable.", "description": "{step_progress}\n\nConfigure thresholds for price trend sensors. These sensors compare the current price with the average of the next N hours to determine if prices are rising, falling, or stable.",
"data": { "data": {
"price_trend_threshold_rising": "Rising Threshold (% above current price)", "price_trend_threshold_rising": "Rising Threshold (above current price)",
"price_trend_threshold_falling": "Falling Threshold (% below current price, negative value)" "price_trend_threshold_falling": "Falling Threshold (below current price, negative value)"
}
},
"volatility": {
"title": "Price Volatility Thresholds",
"description": "{step_progress}\n\nConfigure thresholds for volatility classification. Volatility measures price variation (spread between min/max) in minor currency units. These thresholds are used by volatility sensors and period filters.",
"data": {
"volatility_threshold_moderate": "Moderate Threshold (spread ≥ this value)",
"volatility_threshold_high": "High Threshold (spread ≥ this value)",
"volatility_threshold_very_high": "Very High Threshold (spread ≥ this value)"
} }
} }
}, },
@ -371,6 +395,42 @@
"data_timestamp": { "data_timestamp": {
"name": "Price Data Expiration" "name": "Price Data Expiration"
}, },
"today_volatility": {
"name": "Today's Price Volatility",
"state": {
"low": "Low",
"moderate": "Moderate",
"high": "High",
"very_high": "Very High"
}
},
"tomorrow_volatility": {
"name": "Tomorrow's Price Volatility",
"state": {
"low": "Low",
"moderate": "Moderate",
"high": "High",
"very_high": "Very High"
}
},
"next_24h_volatility": {
"name": "Next 24h Price Volatility",
"state": {
"low": "Low",
"moderate": "Moderate",
"high": "High",
"very_high": "Very High"
}
},
"today_tomorrow_volatility": {
"name": "Today+Tomorrow Price Volatility",
"state": {
"low": "Low",
"moderate": "Moderate",
"high": "High",
"very_high": "Very High"
}
},
"price_forecast": { "price_forecast": {
"name": "Price Forecast" "name": "Price Forecast"
} }
@ -411,5 +471,26 @@
} }
} }
} }
},
"selector": {
"volatility": {
"options": {
"LOW": "Low",
"MODERATE": "Moderate",
"HIGH": "High",
"VERY_HIGH": "Very high"
}
},
"price_level": {
"options": {
"ANY": "Any",
"VERY_CHEAP": "Very cheap",
"CHEAP": "Cheap",
"NORMAL": "Normal",
"EXPENSIVE": "Expensive",
"VERY_EXPENSIVE": "Very expensive"
} }
} }
},
"title": "Tibber Price Information & Ratings"
}

View file

@ -48,6 +48,9 @@
"reauth_successful": "Autentisering vellykket. Integrasjonen er oppdatert med det nye tilgangstokenet." "reauth_successful": "Autentisering vellykket. Integrasjonen er oppdatert med det nye tilgangstokenet."
} }
}, },
"common": {
"step_progress": "Trinn {step_num} av {total_steps}"
},
"config_subentries": { "config_subentries": {
"home": { "home": {
"initiate_flow": { "initiate_flow": {
@ -79,14 +82,14 @@
"step": { "step": {
"init": { "init": {
"title": "Generelle innstillinger", "title": "Generelle innstillinger",
"description": "Konfigurer generelle innstillinger for Tibber Prisinformasjon & Vurderinger.\n\nBruker: {user_login}", "description": "{step_progress}\n\nKonfigurer generelle innstillinger for Tibber Prisinformasjon & Vurderinger.\n\nBruker: {user_login}",
"data": { "data": {
"extended_descriptions": "Vis utvidede beskrivelser i entitetsattributter" "extended_descriptions": "Vis utvidede beskrivelser i entitetsattributter"
} }
}, },
"price_rating": { "price_rating": {
"title": "Prisvurderingsterskler", "title": "Prisvurderingsterskler",
"description": "Konfigurer terskler for prisvurderingsnivåer (LAV/NORMAL/HØY) basert på sammenligning med 24-timers glidende gjennomsnitt.", "description": "{step_progress}\n\nKonfigurer terskler for prisvurderingsnivåer (LAV/NORMAL/HØY) basert på sammenligning med 24-timers glidende gjennomsnitt.",
"data": { "data": {
"price_rating_threshold_low": "Lav vurderingsterskel (% under glidende gjennomsnitt)", "price_rating_threshold_low": "Lav vurderingsterskel (% under glidende gjennomsnitt)",
"price_rating_threshold_high": "Høy vurderingsterskel (% over glidende gjennomsnitt)" "price_rating_threshold_high": "Høy vurderingsterskel (% over glidende gjennomsnitt)"
@ -94,28 +97,49 @@
}, },
"best_price": { "best_price": {
"title": "Innstillinger for beste prisperiode", "title": "Innstillinger for beste prisperiode",
"description": "Konfigurer innstillinger for binærsensoren Beste prisperiode. Denne sensoren er aktiv i perioder med de laveste strømprisene.", "description": "{step_progress}\n\nKonfigurer innstillinger for binærsensoren Beste prisperiode. Denne sensoren er aktiv i perioder med de laveste strømprisene.",
"data": { "data": {
"best_price_min_period_length": "Minimum periodelengde", "best_price_min_period_length": "Minimum periodelengde",
"best_price_flex": "Fleksibilitet: Maksimum % over minimumspris", "best_price_flex": "Fleksibilitet: Maksimum % over minimumspris",
"best_price_min_distance_from_avg": "Minimumsavstand: Påkrevd % under daglig gjennomsnitt" "best_price_min_distance_from_avg": "Minimumsavstand: Påkrevd % under daglig gjennomsnitt",
"best_price_min_volatility": "Minimum volatilitetsfilter",
"best_price_max_level": "Prisnivåfilter (valgfritt)"
},
"data_description": {
"best_price_min_volatility": "Vis kun beste prisperioder når dagens volatilitet oppfyller eller overskrider dette nivået. Standard: 'Lav' (vis uavhengig av volatilitet) - batterioptimalisering er nyttig selv ved små prisvariasjoner. Velg 'Moderat'/'Høy' for kun å vise perioder på mer volatile dager.",
"best_price_max_level": "Vis kun beste prisperioder hvis minst ett intervall i dag har et prisnivå ≤ valgt verdi. Fungerer med OG-logikk: volatilitetsfilter (hvis satt) OG nivåfilter må begge være oppfylt. Nyttig for å unngå batterilading på dyre dager. Velg 'Alle' for å deaktivere dette filteret."
} }
}, },
"peak_price": { "peak_price": {
"title": "Innstillinger for topprisperiode", "title": "Innstillinger for topprisperiode",
"description": "Konfigurer innstillinger for binærsensoren Topprisperiode. Denne sensoren er aktiv i perioder med de høyeste strømprisene.", "description": "{step_progress}\n\nKonfigurer innstillinger for binærsensoren Topprisperiode. Denne sensoren er aktiv i perioder med de høyeste strømprisene.",
"data": { "data": {
"peak_price_min_period_length": "Minimum periodelengde", "peak_price_min_period_length": "Minimum periodelengde",
"peak_price_flex": "Fleksibilitet: Maksimum % under maksimumspris (negativ verdi)", "peak_price_flex": "Fleksibilitet: Maksimum % under maksimumspris (negativ verdi)",
"peak_price_min_distance_from_avg": "Minimumsavstand: Påkrevd % over daglig gjennomsnitt" "peak_price_min_distance_from_avg": "Minimumsavstand: Påkrevd % over daglig gjennomsnitt",
"peak_price_min_volatility": "Minimum volatilitetsfilter",
"peak_price_min_level": "Prisnivåfilter (valgfritt)"
},
"data_description": {
"peak_price_min_volatility": "Vis kun topprisperioder når dagens volatilitet oppfyller eller overskrider dette nivået. Standard: 'Lav' (vis uavhengig av volatilitet) - toppadvarsler er relevante selv ved lav spredning siden unngåelse av dyre timer alltid er viktig. Velg 'Moderat'/'Høy' for kun å vise topper på volatile dager.",
"peak_price_min_level": "Vis kun topprisperioder hvis minst ett intervall i dag har et prisnivå ≥ valgt verdi. Fungerer med OG-logikk: volatilitetsfilter (hvis satt) OG nivåfilter må begge være oppfylt. Vanligvis satt til 'Alle' siden toppperioder er relative til dagen. Velg 'Alle' for å deaktivere dette filteret."
} }
}, },
"price_trend": { "price_trend": {
"title": "Terskelverdier for pristrend", "title": "Pristrendterskler",
"description": "Konfigurer terskelverdier for pristrendsensorer. Disse sensorene sammenligner gjeldende pris med gjennomsnittet av de neste N timene for å avgjøre om prisene stiger, faller eller er stabile.", "description": "{step_progress}\n\nKonfigurer terskler for pristrendsensorer. Disse sensorene sammenligner nåværende pris med gjennomsnittet av de neste N timene for å bestemme om prisene stiger, faller eller er stabile.",
"data": { "data": {
"price_trend_threshold_rising": "Stigende terskelverdi (% over gjeldende pris)", "price_trend_threshold_rising": "Stigende terskel (% over nåværende pris)",
"price_trend_threshold_falling": "Fallende terskelverdi (% under gjeldende pris, negativ verdi)" "price_trend_threshold_falling": "Fallende terskel (% under nåværende pris, negativ verdi)"
}
},
"volatility": {
"title": "Prisvolatilitet Terskler",
"description": "{step_progress}\n\nKonfigurer terskler for volatilitetsklassifisering. Volatilitet måler prisvariasjoner (spredning mellom min/maks) i minste valutaenhet (ct/øre). Disse tersklene brukes av volatilitetssensorer og periodefiltre.",
"data": {
"volatility_threshold_moderate": "Moderat terskel (ct/øre, spredning ≥ denne verdien)",
"volatility_threshold_high": "Høy terskel (ct/øre, spredning ≥ denne verdien)",
"volatility_threshold_very_high": "Veldig høy terskel (ct/øre, spredning ≥ denne verdien)"
} }
} }
}, },
@ -369,7 +393,43 @@
"name": "Månedlig prisvurdering" "name": "Månedlig prisvurdering"
}, },
"data_timestamp": { "data_timestamp": {
"name": "Prisdata utløpstid" "name": "Prisdata Utløp"
},
"today_volatility": {
"name": "Dagens Prisvolatilitet",
"state": {
"low": "Lav",
"moderate": "Moderat",
"high": "Høy",
"very_high": "Svært Høy"
}
},
"tomorrow_volatility": {
"name": "Morgendagens Prisvolatilitet",
"state": {
"low": "Lav",
"moderate": "Moderat",
"high": "Høy",
"very_high": "Svært Høy"
}
},
"next_24h_volatility": {
"name": "Neste 24t Prisvolatilitet",
"state": {
"low": "Lav",
"moderate": "Moderat",
"high": "Høy",
"very_high": "Svært Høy"
}
},
"today_tomorrow_volatility": {
"name": "I dag+I morgen Prisvolatilitet",
"state": {
"low": "Lav",
"moderate": "Moderat",
"high": "Høy",
"very_high": "Svært Høy"
}
}, },
"price_forecast": { "price_forecast": {
"name": "Prisprognose" "name": "Prisprognose"
@ -411,5 +471,26 @@
} }
} }
} }
},
"selector": {
"volatility": {
"options": {
"LOW": "Lav",
"MODERATE": "Moderat",
"HIGH": "Høy",
"VERY_HIGH": "Svært høy"
}
},
"price_level": {
"options": {
"ANY": "Alle",
"VERY_CHEAP": "Svært billig",
"CHEAP": "Billig",
"NORMAL": "Normal",
"EXPENSIVE": "Dyr",
"VERY_EXPENSIVE": "Svært dyr"
} }
} }
},
"title": "Tibber Prisinformasjon & Vurderinger"
}

View file

@ -48,6 +48,9 @@
"reauth_successful": "Herauthenticatie succesvol. De integratie is bijgewerkt met het nieuwe toegangstoken." "reauth_successful": "Herauthenticatie succesvol. De integratie is bijgewerkt met het nieuwe toegangstoken."
} }
}, },
"common": {
"step_progress": "Stap {step_num} van {total_steps}"
},
"config_subentries": { "config_subentries": {
"home": { "home": {
"initiate_flow": { "initiate_flow": {
@ -79,14 +82,14 @@
"step": { "step": {
"init": { "init": {
"title": "Algemene instellingen", "title": "Algemene instellingen",
"description": "Configureer algemene instellingen voor Tibber Prijsinformatie & Beoordelingen.\n\nGebruiker: {user_login}", "description": "{step_progress}\n\nConfigureer algemene instellingen voor Tibber Prijsinformatie & Beoordelingen.\n\nGebruiker: {user_login}",
"data": { "data": {
"extended_descriptions": "Uitgebreide beschrijvingen tonen in entiteitsattributen" "extended_descriptions": "Uitgebreide beschrijvingen tonen in entiteitsattributen"
} }
}, },
"price_rating": { "price_rating": {
"title": "Prijsbeoordelingsdrempels", "title": "Prijsbeoordelingsdrempels",
"description": "Configureer drempels voor prijsbeoordelingsniveaus (LAAG/NORMAAL/HOOG) op basis van vergelijking met het voortschrijdend 24-uurs gemiddelde.", "description": "{step_progress}\n\nConfigureer drempels voor prijsbeoordelingsniveaus (LAAG/NORMAAL/HOOG) op basis van vergelijking met het voortschrijdend 24-uurs gemiddelde.",
"data": { "data": {
"price_rating_threshold_low": "Lage beoordelingsdrempel (% onder voortschrijdend gemiddelde)", "price_rating_threshold_low": "Lage beoordelingsdrempel (% onder voortschrijdend gemiddelde)",
"price_rating_threshold_high": "Hoge beoordelingsdrempel (% boven voortschrijdend gemiddelde)" "price_rating_threshold_high": "Hoge beoordelingsdrempel (% boven voortschrijdend gemiddelde)"
@ -94,28 +97,49 @@
}, },
"best_price": { "best_price": {
"title": "Instellingen beste prijsperiode", "title": "Instellingen beste prijsperiode",
"description": "Configureer instellingen voor de Beste Prijsperiode binaire sensor. Deze sensor is actief tijdens perioden met de laagste elektriciteitsprijzen.", "description": "{step_progress}\n\nConfigureer instellingen voor de Beste Prijsperiode binaire sensor. Deze sensor is actief tijdens perioden met de laagste elektriciteitsprijzen.",
"data": { "data": {
"best_price_min_period_length": "Minimale periode lengte", "best_price_min_period_length": "Minimale periode lengte",
"best_price_flex": "Flexibiliteit: Maximaal % boven minimumprijs", "best_price_flex": "Flexibiliteit: Maximaal % boven minimumprijs",
"best_price_min_distance_from_avg": "Minimale afstand: Vereist % onder dagelijks gemiddelde" "best_price_min_distance_from_avg": "Minimale afstand: Vereist % onder dagelijks gemiddelde",
"best_price_min_volatility": "Minimum volatiliteitsfilter",
"best_price_max_level": "Prijsniveaufilter (Optioneel)"
},
"data_description": {
"best_price_min_volatility": "Toon alleen beste prijsperiodes wanneer de volatiliteit van vandaag dit niveau bereikt of overschrijdt. Standaard: 'Laag' (toon ongeacht volatiliteit) - batterijoptimalisatie is nuttig zelfs bij kleine prijsvariaties. Selecteer 'Matig'/'Hoog' om periodes alleen op meer volatiele dagen te tonen.",
"best_price_max_level": "Toon alleen beste prijsperiodes als minstens één interval vandaag een prijsniveau ≤ geselecteerde waarde heeft. Werkt met EN-logica: volatiliteitsfilter (indien ingesteld) EN niveaufilter moeten beide voldaan zijn. Nuttig om batterij laden op dure dagen te vermijden. Selecteer 'Alle' om dit filter uit te schakelen."
} }
}, },
"peak_price": { "peak_price": {
"title": "Instellingen piekprijsperiode", "title": "Instellingen piekprijsperiode",
"description": "Configureer instellingen voor de Piekprijsperiode binaire sensor. Deze sensor is actief tijdens perioden met de hoogste elektriciteitsprijzen.", "description": "{step_progress}\n\nConfigureer instellingen voor de Piekprijsperiode binaire sensor. Deze sensor is actief tijdens perioden met de hoogste elektriciteitsprijzen.",
"data": { "data": {
"peak_price_min_period_length": "Minimale periode lengte", "peak_price_min_period_length": "Minimale periode lengte",
"peak_price_flex": "Flexibiliteit: Maximaal % onder maximumprijs (negatieve waarde)", "peak_price_flex": "Flexibiliteit: Maximaal % onder maximumprijs (negatieve waarde)",
"peak_price_min_distance_from_avg": "Minimale afstand: Vereist % boven dagelijks gemiddelde" "peak_price_min_distance_from_avg": "Minimale afstand: Vereist % boven dagelijks gemiddelde",
"peak_price_min_volatility": "Minimum volatiliteitsfilter",
"peak_price_min_level": "Prijsniveaufilter (Optioneel)"
},
"data_description": {
"peak_price_min_volatility": "Toon alleen piekprijsperiodes wanneer de volatiliteit van vandaag dit niveau bereikt of overschrijdt. Standaard: 'Laag' (toon ongeacht volatiliteit) - piekwaarschuwingen zijn relevant zelfs bij lage spreiding omdat vermijding van dure uren altijd belangrijk is. Selecteer 'Matig'/'Hoog' om alleen pieken op volatiele dagen te tonen.",
"peak_price_min_level": "Toon alleen piekprijsperiodes als minstens één interval vandaag een prijsniveau ≥ geselecteerde waarde heeft. Werkt met EN-logica: volatiliteitsfilter (indien ingesteld) EN niveaufilter moeten beide voldaan zijn. Meestal ingesteld op 'Alle' omdat piekperiodes relatief zijn aan de dag. Selecteer 'Alle' om dit filter uit te schakelen."
} }
}, },
"price_trend": { "price_trend": {
"title": "Prijstrend drempelwaarden", "title": "Prijstrenddrempels",
"description": "Configureer drempelwaarden voor prijstrendsensoren. Deze sensoren vergelijken de huidige prijs met het gemiddelde van de volgende N uur om te bepalen of de prijzen stijgen, dalen of stabiel zijn.", "description": "{step_progress}\n\nConfigureer drempels voor prijstrendsensoren. Deze sensoren vergelijken de huidige prijs met het gemiddelde van de volgende N uur om te bepalen of prijzen stijgen, dalen of stabiel zijn.",
"data": { "data": {
"price_trend_threshold_rising": "Stijgende drempelwaarde (% boven huidige prijs)", "price_trend_threshold_rising": "Stijgende drempel (% boven huidige prijs)",
"price_trend_threshold_falling": "Dalende drempelwaarde (% onder huidige prijs, negatieve waarde)" "price_trend_threshold_falling": "Dalende drempel (% onder huidige prijs, negatieve waarde)"
}
},
"volatility": {
"title": "Prijsvolatiliteit Drempels",
"description": "{step_progress}\n\nConfigureer drempels voor volatiliteitsclassificatie. Volatiliteit meet prijsschommelingen (spreiding tussen min/max) in kleinste valuta-eenheid (ct/øre). Deze drempels worden gebruikt door volatiliteitssensoren en periodefilters.",
"data": {
"volatility_threshold_moderate": "Matige drempel (ct/øre, spreiding ≥ deze waarde)",
"volatility_threshold_high": "Hoge drempel (ct/øre, spreiding ≥ deze waarde)",
"volatility_threshold_very_high": "Zeer hoge drempel (ct/øre, spreiding ≥ deze waarde)"
} }
} }
}, },
@ -369,7 +393,43 @@
"name": "Maandelijkse prijsbeoordeling" "name": "Maandelijkse prijsbeoordeling"
}, },
"data_timestamp": { "data_timestamp": {
"name": "Prijsdata vervaltijd" "name": "Prijsgegevens Vervaldatum"
},
"today_volatility": {
"name": "Vandaag Prijsvolatiliteit",
"state": {
"low": "Laag",
"moderate": "Gematigd",
"high": "Hoog",
"very_high": "Zeer Hoog"
}
},
"tomorrow_volatility": {
"name": "Morgen Prijsvolatiliteit",
"state": {
"low": "Laag",
"moderate": "Gematigd",
"high": "Hoog",
"very_high": "Zeer Hoog"
}
},
"next_24h_volatility": {
"name": "Volgende 24u Prijsvolatiliteit",
"state": {
"low": "Laag",
"moderate": "Gematigd",
"high": "Hoog",
"very_high": "Zeer Hoog"
}
},
"today_tomorrow_volatility": {
"name": "Vandaag+Morgen Prijsvolatiliteit",
"state": {
"low": "Laag",
"moderate": "Gematigd",
"high": "Hoog",
"very_high": "Zeer Hoog"
}
}, },
"price_forecast": { "price_forecast": {
"name": "Prijsprognose" "name": "Prijsprognose"
@ -411,5 +471,26 @@
} }
} }
} }
},
"selector": {
"volatility": {
"options": {
"LOW": "Laag",
"MODERATE": "Matig",
"HIGH": "Hoog",
"VERY_HIGH": "Zeer hoog"
}
},
"price_level": {
"options": {
"ANY": "Alle",
"VERY_CHEAP": "Zeer goedkoop",
"CHEAP": "Goedkoop",
"NORMAL": "Normaal",
"EXPENSIVE": "Duur",
"VERY_EXPENSIVE": "Zeer duur"
} }
} }
},
"title": "Tibber Prijsinformatie & Beoordelingen"
}

View file

@ -48,6 +48,9 @@
"reauth_successful": "Omautentisering lyckades. Integrationen har uppdaterats med den nya åtkomsttoken." "reauth_successful": "Omautentisering lyckades. Integrationen har uppdaterats med den nya åtkomsttoken."
} }
}, },
"common": {
"step_progress": "Steg {step_num} av {total_steps}"
},
"config_subentries": { "config_subentries": {
"home": { "home": {
"initiate_flow": { "initiate_flow": {
@ -79,14 +82,14 @@
"step": { "step": {
"init": { "init": {
"title": "Allmänna inställningar", "title": "Allmänna inställningar",
"description": "Konfigurera allmänna inställningar för Tibber Prisinformation & Betyg.\n\nAnvändare: {user_login}", "description": "{step_progress}\n\nKonfigurera allmänna inställningar för Tibber Prisinformation & Betyg.\n\nAnvändare: {user_login}",
"data": { "data": {
"extended_descriptions": "Visa utökade beskrivningar i entitetsattribut" "extended_descriptions": "Visa utökade beskrivningar i entitetsattribut"
} }
}, },
"price_rating": { "price_rating": {
"title": "Prisvärderingströsklar", "title": "Prisvärderingströsklar",
"description": "Konfigurera trösklar för prisvärderingsnivåer (LÅG/NORMAL/HÖG) baserat på jämförelse med rullande 24-timmars genomsnitt.", "description": "{step_progress}\n\nKonfigurera trösklar för prisvärderingsnivåer (LÅG/NORMAL/HÖG) baserat på jämförelse med rullande 24-timmars genomsnitt.",
"data": { "data": {
"price_rating_threshold_low": "Låg värderingströskel (% under rullande genomsnitt)", "price_rating_threshold_low": "Låg värderingströskel (% under rullande genomsnitt)",
"price_rating_threshold_high": "Hög värderingströskel (% över rullande genomsnitt)" "price_rating_threshold_high": "Hög värderingströskel (% över rullande genomsnitt)"
@ -94,28 +97,49 @@
}, },
"best_price": { "best_price": {
"title": "Inställningar för bästa prisperiod", "title": "Inställningar för bästa prisperiod",
"description": "Konfigurera inställningar för Bästa Prisperiod binärsensor. Denna sensor är aktiv under perioder med de lägsta elpriserna.", "description": "{step_progress}\n\nKonfigurera inställningar för Bästa Prisperiod binärsensor. Denna sensor är aktiv under perioder med de lägsta elpriserna.",
"data": { "data": {
"best_price_min_period_length": "Minsta periodlängd", "best_price_min_period_length": "Minsta periodlängd",
"best_price_flex": "Flexibilitet: Maximalt % över minimumpris", "best_price_flex": "Flexibilitet: Maximalt % över minimumpris",
"best_price_min_distance_from_avg": "Minimiavstånd: Krävd % under dagligt genomsnitt" "best_price_min_distance_from_avg": "Minimiavstånd: Krävd % under dagligt genomsnitt",
"best_price_min_volatility": "Minimum volatilitetsfilter",
"best_price_max_level": "Prisnivåfilter (Valfritt)"
},
"data_description": {
"best_price_min_volatility": "Visa endast bästa prisperioder när dagens volatilitet uppfyller eller överskrider denna nivå. Standard: 'Låg' (visa oavsett volatilitet) - batterioptimering är användbart även vid små prisvariationer. Välj 'Måttlig'/'Hög' för att endast visa perioder på mer volatila dagar.",
"best_price_max_level": "Visa endast bästa prisperioder om minst ett intervall idag har en prisnivå ≤ valt värde. Fungerar med OCH-logik: volatilitetsfilter (om inställt) OCH nivåfilter måste båda vara uppfyllda. Användbart för att undvika batteriladdning på dyra dagar. Välj 'Alla' för att inaktivera detta filter."
} }
}, },
"peak_price": { "peak_price": {
"title": "Inställningar för topprisperiod", "title": "Inställningar för topprisperiod",
"description": "Konfigurera inställningar för Topprisperiod binärsensor. Denna sensor är aktiv under perioder med de högsta elpriserna.", "description": "{step_progress}\n\nKonfigurera inställningar för Topprisperiod binärsensor. Denna sensor är aktiv under perioder med de högsta elpriserna.",
"data": { "data": {
"peak_price_min_period_length": "Minsta periodlängd", "peak_price_min_period_length": "Minsta periodlängd",
"peak_price_flex": "Flexibilitet: Maximalt % under maximumpris (negativt värde)", "peak_price_flex": "Flexibilitet: Maximalt % under maximumpris (negativt värde)",
"peak_price_min_distance_from_avg": "Minimiavstånd: Krävd % över dagligt genomsnitt" "peak_price_min_distance_from_avg": "Minimiavstånd: Krävd % över dagligt genomsnitt",
"peak_price_min_volatility": "Minimum volatilitetsfilter",
"peak_price_min_level": "Prisnivåfilter (Valfritt)"
},
"data_description": {
"peak_price_min_volatility": "Visa endast topprisperioder när dagens volatilitet uppfyller eller överskrider denna nivå. Standard: 'Låg' (visa oavsett volatilitet) - toppvarningar är relevanta även vid låg spridning eftersom undvikande av dyra timmar alltid är viktigt. Välj 'Måttlig'/'Hög' för att endast visa toppar på volatila dagar.",
"peak_price_min_level": "Visa endast topprisperioder om minst ett intervall idag har en prisnivå ≥ valt värde. Fungerar med OCH-logik: volatilitetsfilter (om inställt) OCH nivåfilter måste båda vara uppfyllda. Vanligtvis inställt på 'Alla' eftersom toppperioder är relativa till dagen. Välj 'Alla' för att inaktivera detta filter."
} }
}, },
"price_trend": { "price_trend": {
"title": "Pristrend tröskelvärden", "title": "Pristrendtrösklar",
"description": "Konfigurera tröskelvärden för pristrendsensorer. Dessa sensorer jämför det aktuella priset med genomsnittet av de nästa N timmarna för att avgöra om priserna stiger, faller eller är stabila.", "description": "{step_progress}\n\nKonfigurera trösklar för pristrendsensorer. Dessa sensorer jämför det aktuella priset med genomsnittet av de nästa N timmarna för att avgöra om priserna stiger, faller eller är stabila.",
"data": { "data": {
"price_trend_threshold_rising": "Stigande tröskelvärde (% över aktuellt pris)", "price_trend_threshold_rising": "Stigande tröskel (% över aktuellt pris)",
"price_trend_threshold_falling": "Fallande tröskelvärde (% under aktuellt pris, negativt värde)" "price_trend_threshold_falling": "Fallande tröskel (% under aktuellt pris, negativt värde)"
}
},
"volatility": {
"title": "Prisvolatilitet Trösklar",
"description": "{step_progress}\n\nKonfigurera trösklar för volatilitetsklassificering. Volatilitet mäter prisvariationer (spridning mellan min/max) i minsta valutaenhet (ct/øre). Dessa trösklar används av volatilitetssensorer och periodfilter.",
"data": {
"volatility_threshold_moderate": "Måttlig tröskel (ct/øre, spridning ≥ detta värde)",
"volatility_threshold_high": "Hög tröskel (ct/øre, spridning ≥ detta värde)",
"volatility_threshold_very_high": "Mycket hög tröskel (ct/øre, spridning ≥ detta värde)"
} }
} }
}, },
@ -369,7 +393,43 @@
"name": "Månatlig prisvärdering" "name": "Månatlig prisvärdering"
}, },
"data_timestamp": { "data_timestamp": {
"name": "Prisdata utgångstid" "name": "Prisdata Utgångsdatum"
},
"today_volatility": {
"name": "Dagens Prisvolatilitet",
"state": {
"low": "Låg",
"moderate": "Måttlig",
"high": "Hög",
"very_high": "Mycket Hög"
}
},
"tomorrow_volatility": {
"name": "Morgondagens Prisvolatilitet",
"state": {
"low": "Låg",
"moderate": "Måttlig",
"high": "Hög",
"very_high": "Mycket Hög"
}
},
"next_24h_volatility": {
"name": "Nästa 24t Prisvolatilitet",
"state": {
"low": "Låg",
"moderate": "Måttlig",
"high": "Hög",
"very_high": "Mycket Hög"
}
},
"today_tomorrow_volatility": {
"name": "Idag+Imorgon Prisvolatilitet",
"state": {
"low": "Låg",
"moderate": "Måttlig",
"high": "Hög",
"very_high": "Mycket Hög"
}
}, },
"price_forecast": { "price_forecast": {
"name": "Prisprognos" "name": "Prisprognos"
@ -411,5 +471,26 @@
} }
} }
} }
},
"selector": {
"volatility": {
"options": {
"LOW": "Låg",
"MODERATE": "Måttlig",
"HIGH": "Hög",
"VERY_HIGH": "Mycket hög"
}
},
"price_level": {
"options": {
"ANY": "Alla",
"VERY_CHEAP": "Mycket billigt",
"CHEAP": "Billigt",
"NORMAL": "Normalt",
"EXPENSIVE": "Dyrt",
"VERY_EXPENSIVE": "Mycket dyrt"
} }
} }
},
"title": "Tibber Prisinformation & Betyg"
}