fix(config_flow): restructure options flow to menu-based navigation and fix settings persistence

Fixes configuration wizard not saving settings (#59):

Root cause was twofold:
1. Linear multi-step flow pattern didn't properly persist changes between steps
2. Best/peak price settings used nested sections format - values were saved
   in sections (period_settings, flexibility_settings, etc.) but read from
   flat structure, causing configured values to be ignored on subsequent runs

Solution:
- Replaced linear step-through flow with menu-based navigation system
- Each configuration area now has dedicated "Save & Back" buttons
- Removed nested sections from all steps except best/peak price (where they
  provide better UX for grouping related settings)
- Fixed best/peak price steps to correctly extract values from sections:
  period_settings, flexibility_settings, relaxation_and_target_periods
- Added reset-to-defaults functionality with confirmation dialog

UI/UX improvements:
- Menu structure: General Settings, Currency Display, Price Rating Thresholds,
  Volatility, Best Price Period, Peak Price Period, Price Trend,
  Chart Data Export, Reset to Defaults, Back
- Removed confusing step progress indicators ("{step_num} / {total_steps}")
- Changed all submit buttons from "Continue →" to "↩ Save & Back"
- Clear grouping of settings by functional area

Translation updates (nl.json + sv.json):
- Refined volatility threshold descriptions with CV formula explanations
- Clarified price trend thresholds (compares current vs. future N-hour average,
  not "per hour increase")
- Standardized terminology (e.g., "entry" → "item", compound word consistency)
- Consistently formatted all sensor names and descriptions
- Added new data lifecycle status sensor names

Technical changes:
- Options flow refactored from linear to menu pattern with menu_options dict
- New reset_to_defaults step with confirmation and abort handlers
- Section extraction logic in best_price/peak_price steps now correctly reads
  from nested structure (period_settings.*, flexibility_settings.*, etc.)
- Removed sections from general_settings, display_settings, volatility, etc.
  (simpler flat structure via menu navigation)

Impact: Configuration wizard now reliably saves all settings. Users can
navigate between setting areas without restarting the flow. Reset function
enables quick recovery when experimenting with thresholds. Previously
configured best/peak price settings are now correctly applied.
This commit is contained in:
Julian Pawlowski 2025-12-13 13:33:31 +00:00
parent 1c19cebff5
commit 6c741e8392
16 changed files with 1404 additions and 1127 deletions

View file

@ -126,14 +126,15 @@ async def _migrate_config_options(hass: HomeAssistant, entry: ConfigEntry) -> No
migration_performed = False migration_performed = False
migrated = dict(entry.options) migrated = dict(entry.options)
# Migration: Set currency_display_mode to minor for existing configs # Migration: Set currency_display_mode to subunit for legacy configs
# New configs get currency-appropriate defaults from schema. # New configs (created after v1.1.0) get currency-appropriate defaults via get_default_options().
# This preserves legacy behavior where all prices were in subunit currency. # This migration preserves legacy behavior where all prices were in subunit currency (cents/øre).
# Only runs for old config entries that don't have this option explicitly set.
if CONF_CURRENCY_DISPLAY_MODE not in migrated: if CONF_CURRENCY_DISPLAY_MODE not in migrated:
migrated[CONF_CURRENCY_DISPLAY_MODE] = DISPLAY_MODE_SUBUNIT migrated[CONF_CURRENCY_DISPLAY_MODE] = DISPLAY_MODE_SUBUNIT
migration_performed = True migration_performed = True
LOGGER.info( LOGGER.info(
"[%s] Migrated config: Set currency_display_mode=%s (legacy default for existing configs)", "[%s] Migrated legacy config: Set currency_display_mode=%s (preserves pre-v1.1.0 behavior)",
entry.title, entry.title,
DISPLAY_MODE_SUBUNIT, DISPLAY_MODE_SUBUNIT,
) )
@ -276,7 +277,8 @@ async def async_setup_entry(
# https://developers.home-assistant.io/docs/integration_fetching_data#coordinated-single-api-poll-for-data-for-all-entities # https://developers.home-assistant.io/docs/integration_fetching_data#coordinated-single-api-poll-for-data-for-all-entities
if entry.state == ConfigEntryState.SETUP_IN_PROGRESS: if entry.state == ConfigEntryState.SETUP_IN_PROGRESS:
await coordinator.async_config_entry_first_refresh() await coordinator.async_config_entry_first_refresh()
entry.async_on_unload(entry.add_update_listener(async_reload_entry)) # Note: Options update listener is registered in coordinator.__init__
# (handles cache invalidation + refresh without full reload)
else: else:
await coordinator.async_refresh() await coordinator.async_refresh()

View file

@ -3,7 +3,8 @@
from __future__ import annotations from __future__ import annotations
import logging import logging
from typing import TYPE_CHECKING, Any, ClassVar from copy import deepcopy
from typing import TYPE_CHECKING, Any
if TYPE_CHECKING: if TYPE_CHECKING:
from collections.abc import Mapping from collections.abc import Mapping
@ -16,6 +17,7 @@ from custom_components.tibber_prices.config_flow_handlers.schemas import (
get_peak_price_schema, get_peak_price_schema,
get_price_rating_schema, get_price_rating_schema,
get_price_trend_schema, get_price_trend_schema,
get_reset_to_defaults_schema,
get_volatility_schema, get_volatility_schema,
) )
from custom_components.tibber_prices.config_flow_handlers.validators import ( from custom_components.tibber_prices.config_flow_handlers.validators import (
@ -60,6 +62,7 @@ from custom_components.tibber_prices.const import (
DEFAULT_VOLATILITY_THRESHOLD_MODERATE, DEFAULT_VOLATILITY_THRESHOLD_MODERATE,
DEFAULT_VOLATILITY_THRESHOLD_VERY_HIGH, DEFAULT_VOLATILITY_THRESHOLD_VERY_HIGH,
DOMAIN, DOMAIN,
get_default_options,
) )
from homeassistant.config_entries import ConfigFlowResult, OptionsFlow from homeassistant.config_entries import ConfigFlowResult, OptionsFlow
@ -69,23 +72,34 @@ _LOGGER = logging.getLogger(__name__)
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] = 8
_STEP_INFO: ClassVar[dict[str, int]] = {
"init": 1,
"display_settings": 2,
"current_interval_price_rating": 3,
"volatility": 4,
"best_price": 5,
"peak_price": 6,
"price_trend": 7,
"chart_data_export": 8,
}
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 _merge_section_data(self, user_input: dict[str, Any]) -> None:
"""
Merge section data from form input into options.
Home Assistant forms with section() return nested dicts like:
{"section_name": {"setting1": value1, "setting2": value2}}
We need to preserve this structure in config_entry.options.
Args:
user_input: Nested user input from form with sections
"""
for section_key, section_data in user_input.items():
if isinstance(section_data, dict):
# This is a section - ensure the section exists in options
if section_key not in self._options:
self._options[section_key] = {}
# Update the section with new values
self._options[section_key].update(section_data)
else:
# This is a direct value - keep it as is
self._options[section_key] = section_data
def _migrate_config_options(self, options: Mapping[str, Any]) -> dict[str, Any]: def _migrate_config_options(self, options: Mapping[str, Any]) -> dict[str, Any]:
""" """
Migrate deprecated config options to current format. Migrate deprecated config options to current format.
@ -100,7 +114,10 @@ class TibberPricesOptionsFlowHandler(OptionsFlow):
Migrated options dict with deprecated keys removed/renamed Migrated options dict with deprecated keys removed/renamed
""" """
migrated = dict(options) # CRITICAL: Use deepcopy to avoid modifying the original config_entry.options
# If we use dict(options), nested dicts are still referenced, causing
# self._options modifications to leak into config_entry.options
migrated = deepcopy(dict(options))
migration_performed = False migration_performed = False
# Migration 1: Rename relaxation_step_* to relaxation_attempts_* # Migration 1: Rename relaxation_step_* to relaxation_attempts_*
@ -144,41 +161,98 @@ class TibberPricesOptionsFlowHandler(OptionsFlow):
return migrated return migrated
def _get_step_description_placeholders(self, step_id: str) -> dict[str, str]: def _save_options_if_changed(self) -> bool:
"""Get description placeholders with step progress.""" """
if step_id not in self._STEP_INFO: Save options only if they actually changed.
return {}
step_num = self._STEP_INFO[step_id] Returns:
True if options were updated, False if no changes detected
# Get translations loaded by Home Assistant """
standard_translations_key = f"{DOMAIN}_standard_translations_{self.hass.config.language}" # Compare old and new options
translations = self.hass.data.get(standard_translations_key, {}) if self.config_entry.options != self._options:
self.hass.config_entries.async_update_entry(
self.config_entry,
options=self._options,
)
return True
return False
# Get step progress text from translations with placeholders async def async_step_init(self, _user_input: dict[str, Any] | None = None) -> ConfigFlowResult:
step_progress_template = translations.get("common", {}).get("step_progress", "Step {step_num} of {total_steps}") """Manage the options - show menu."""
step_progress = step_progress_template.format(step_num=step_num, total_steps=self._TOTAL_STEPS) # Always reload options from config_entry to get latest saved state
# This ensures changes from previous steps are visible
return {
"step_progress": step_progress,
}
async def async_step_init(self, user_input: dict[str, Any] | None = None) -> ConfigFlowResult:
"""Manage the options - General Settings."""
# Initialize options from config_entry on first call
if not self._options:
# Migrate deprecated config options before processing
self._options = self._migrate_config_options(self.config_entry.options) self._options = self._migrate_config_options(self.config_entry.options)
# Show menu with all configuration categories
return self.async_show_menu(
step_id="init",
menu_options=[
"general_settings",
"display_settings",
"current_interval_price_rating",
"volatility",
"best_price",
"peak_price",
"price_trend",
"chart_data_export",
"reset_to_defaults",
"finish",
],
)
async def async_step_reset_to_defaults(self, user_input: dict[str, Any] | None = None) -> ConfigFlowResult:
"""Reset all settings to factory defaults."""
if user_input is not None: if user_input is not None:
# Check if user confirmed the reset
if user_input.get("confirm_reset", False):
# Get currency from config_entry.data (this is immutable and safe)
currency_code = self.config_entry.data.get("currency", None)
# Completely replace options with fresh defaults (factory reset)
# This discards ALL old data including legacy structures
self._options = get_default_options(currency_code)
# Force save the new options
self._save_options_if_changed()
_LOGGER.info(
"Factory reset performed for config entry '%s' - all settings restored to defaults",
self.config_entry.title,
)
# Show success message and return to menu
return self.async_abort(reason="reset_successful")
# User didn't check the box - they want to cancel
# Show info message (not error) and return to menu
return self.async_abort(reason="reset_cancelled")
# Show confirmation form with checkbox
return self.async_show_form(
step_id="reset_to_defaults",
data_schema=get_reset_to_defaults_schema(),
)
async def async_step_finish(self, _user_input: dict[str, Any] | None = None) -> ConfigFlowResult:
"""Close the options flow."""
# Use empty reason to close without any message
return self.async_abort(reason="finished")
async def async_step_general_settings(self, user_input: dict[str, Any] | None = None) -> ConfigFlowResult:
"""Configure general settings."""
if user_input is not None:
# Update options with new values
self._options.update(user_input) self._options.update(user_input)
return await self.async_step_display_settings() # Save options only if changed (triggers listeners automatically)
self._save_options_if_changed()
# Return to menu for more changes
return await self.async_step_init()
return self.async_show_form( return self.async_show_form(
step_id="init", step_id="general_settings",
data_schema=get_options_init_schema(self.config_entry.options), data_schema=get_options_init_schema(self.config_entry.options),
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"),
}, },
) )
@ -194,13 +268,16 @@ class TibberPricesOptionsFlowHandler(OptionsFlow):
currency_code = tibber_data.coordinator.data.get("currency") currency_code = tibber_data.coordinator.data.get("currency")
if user_input is not None: if user_input is not None:
# Update options with new values
self._options.update(user_input) self._options.update(user_input)
return await self.async_step_current_interval_price_rating() # async_create_entry automatically handles change detection and listener triggering
self._save_options_if_changed()
# Return to menu for more changes
return await self.async_step_init()
return self.async_show_form( return self.async_show_form(
step_id="display_settings", step_id="display_settings",
data_schema=get_display_settings_schema(self.config_entry.options, currency_code), data_schema=get_display_settings_schema(self.config_entry.options, currency_code),
description_placeholders=self._get_step_description_placeholders("display_settings"),
) )
async def async_step_current_interval_price_rating( async def async_step_current_interval_price_rating(
@ -210,6 +287,9 @@ class TibberPricesOptionsFlowHandler(OptionsFlow):
errors: dict[str, str] = {} errors: dict[str, str] = {}
if user_input is not None: if user_input is not None:
# Schema is now flattened - fields come directly in user_input
# But we still need to store them in nested structure for coordinator
# Validate low price rating threshold # Validate low price rating threshold
if CONF_PRICE_RATING_THRESHOLD_LOW in user_input and not validate_price_rating_threshold_low( if CONF_PRICE_RATING_THRESHOLD_LOW in user_input and not validate_price_rating_threshold_low(
user_input[CONF_PRICE_RATING_THRESHOLD_LOW] user_input[CONF_PRICE_RATING_THRESHOLD_LOW]
@ -223,25 +303,29 @@ class TibberPricesOptionsFlowHandler(OptionsFlow):
errors[CONF_PRICE_RATING_THRESHOLD_HIGH] = "invalid_price_rating_high" errors[CONF_PRICE_RATING_THRESHOLD_HIGH] = "invalid_price_rating_high"
# Cross-validate both thresholds together (LOW must be < HIGH) # Cross-validate both thresholds together (LOW must be < HIGH)
if not errors and not validate_price_rating_thresholds( if not errors:
user_input.get( # Get current values directly from options (now flat)
low_val = user_input.get(
CONF_PRICE_RATING_THRESHOLD_LOW, self._options.get(CONF_PRICE_RATING_THRESHOLD_LOW, -10) CONF_PRICE_RATING_THRESHOLD_LOW, self._options.get(CONF_PRICE_RATING_THRESHOLD_LOW, -10)
), )
user_input.get( high_val = user_input.get(
CONF_PRICE_RATING_THRESHOLD_HIGH, self._options.get(CONF_PRICE_RATING_THRESHOLD_HIGH, 10) CONF_PRICE_RATING_THRESHOLD_HIGH, self._options.get(CONF_PRICE_RATING_THRESHOLD_HIGH, 10)
), )
): if not validate_price_rating_thresholds(low_val, high_val):
# This should never happen given the range constraints, but add error for safety # This should never happen given the range constraints, but add error for safety
errors["base"] = "invalid_price_rating_thresholds" errors["base"] = "invalid_price_rating_thresholds"
if not errors: if not errors:
# Store flat data directly in options (no section wrapping)
self._options.update(user_input) self._options.update(user_input)
return await self.async_step_volatility() # async_create_entry automatically handles change detection and listener triggering
self._save_options_if_changed()
# Return to menu for more changes
return await self.async_step_init()
return self.async_show_form( return self.async_show_form(
step_id="current_interval_price_rating", step_id="current_interval_price_rating",
data_schema=get_price_rating_schema(self.config_entry.options), data_schema=get_price_rating_schema(self.config_entry.options),
description_placeholders=self._get_step_description_placeholders("current_interval_price_rating"),
errors=errors, errors=errors,
) )
@ -250,46 +334,61 @@ class TibberPricesOptionsFlowHandler(OptionsFlow):
errors: dict[str, str] = {} errors: dict[str, str] = {}
if user_input is not None: if user_input is not None:
# Extract settings from sections
period_settings = user_input.get("period_settings", {})
flexibility_settings = user_input.get("flexibility_settings", {})
relaxation_settings = user_input.get("relaxation_and_target_periods", {})
# Validate period length # Validate period length
if CONF_BEST_PRICE_MIN_PERIOD_LENGTH in user_input and not validate_period_length( if CONF_BEST_PRICE_MIN_PERIOD_LENGTH in period_settings and not validate_period_length(
user_input[CONF_BEST_PRICE_MIN_PERIOD_LENGTH] period_settings[CONF_BEST_PRICE_MIN_PERIOD_LENGTH]
): ):
errors[CONF_BEST_PRICE_MIN_PERIOD_LENGTH] = "invalid_period_length" errors[CONF_BEST_PRICE_MIN_PERIOD_LENGTH] = "invalid_period_length"
# Validate flex percentage # Validate flex percentage
if CONF_BEST_PRICE_FLEX in user_input and not validate_flex_percentage(user_input[CONF_BEST_PRICE_FLEX]): if CONF_BEST_PRICE_FLEX in flexibility_settings and not validate_flex_percentage(
flexibility_settings[CONF_BEST_PRICE_FLEX]
):
errors[CONF_BEST_PRICE_FLEX] = "invalid_flex" errors[CONF_BEST_PRICE_FLEX] = "invalid_flex"
# Validate distance from average (Best Price uses negative values) # Validate distance from average (Best Price uses negative values)
if CONF_BEST_PRICE_MIN_DISTANCE_FROM_AVG in user_input and not validate_best_price_distance_percentage( if (
user_input[CONF_BEST_PRICE_MIN_DISTANCE_FROM_AVG] CONF_BEST_PRICE_MIN_DISTANCE_FROM_AVG in flexibility_settings
and not validate_best_price_distance_percentage(
flexibility_settings[CONF_BEST_PRICE_MIN_DISTANCE_FROM_AVG]
)
): ):
errors[CONF_BEST_PRICE_MIN_DISTANCE_FROM_AVG] = "invalid_best_price_distance" errors[CONF_BEST_PRICE_MIN_DISTANCE_FROM_AVG] = "invalid_best_price_distance"
# Validate minimum periods count # Validate minimum periods count
if CONF_MIN_PERIODS_BEST in user_input and not validate_min_periods(user_input[CONF_MIN_PERIODS_BEST]): if CONF_MIN_PERIODS_BEST in relaxation_settings and not validate_min_periods(
relaxation_settings[CONF_MIN_PERIODS_BEST]
):
errors[CONF_MIN_PERIODS_BEST] = "invalid_min_periods" errors[CONF_MIN_PERIODS_BEST] = "invalid_min_periods"
# Validate gap count # Validate gap count
if CONF_BEST_PRICE_MAX_LEVEL_GAP_COUNT in user_input and not validate_gap_count( if CONF_BEST_PRICE_MAX_LEVEL_GAP_COUNT in period_settings and not validate_gap_count(
user_input[CONF_BEST_PRICE_MAX_LEVEL_GAP_COUNT] period_settings[CONF_BEST_PRICE_MAX_LEVEL_GAP_COUNT]
): ):
errors[CONF_BEST_PRICE_MAX_LEVEL_GAP_COUNT] = "invalid_gap_count" errors[CONF_BEST_PRICE_MAX_LEVEL_GAP_COUNT] = "invalid_gap_count"
# Validate relaxation attempts # Validate relaxation attempts
if CONF_RELAXATION_ATTEMPTS_BEST in user_input and not validate_relaxation_attempts( if CONF_RELAXATION_ATTEMPTS_BEST in relaxation_settings and not validate_relaxation_attempts(
user_input[CONF_RELAXATION_ATTEMPTS_BEST] relaxation_settings[CONF_RELAXATION_ATTEMPTS_BEST]
): ):
errors[CONF_RELAXATION_ATTEMPTS_BEST] = "invalid_relaxation_attempts" errors[CONF_RELAXATION_ATTEMPTS_BEST] = "invalid_relaxation_attempts"
if not errors: if not errors:
self._options.update(user_input) # Merge section data into options
return await self.async_step_peak_price() self._merge_section_data(user_input)
# async_create_entry automatically handles change detection and listener triggering
self._save_options_if_changed()
# Return to menu for more changes
return await self.async_step_init()
return self.async_show_form( return self.async_show_form(
step_id="best_price", step_id="best_price",
data_schema=get_best_price_schema(self.config_entry.options), data_schema=get_best_price_schema(self.config_entry.options),
description_placeholders=self._get_step_description_placeholders("best_price"),
errors=errors, errors=errors,
) )
@ -298,46 +397,58 @@ class TibberPricesOptionsFlowHandler(OptionsFlow):
errors: dict[str, str] = {} errors: dict[str, str] = {}
if user_input is not None: if user_input is not None:
# Extract settings from sections
period_settings = user_input.get("period_settings", {})
flexibility_settings = user_input.get("flexibility_settings", {})
relaxation_settings = user_input.get("relaxation_and_target_periods", {})
# Validate period length # Validate period length
if CONF_PEAK_PRICE_MIN_PERIOD_LENGTH in user_input and not validate_period_length( if CONF_PEAK_PRICE_MIN_PERIOD_LENGTH in period_settings and not validate_period_length(
user_input[CONF_PEAK_PRICE_MIN_PERIOD_LENGTH] period_settings[CONF_PEAK_PRICE_MIN_PERIOD_LENGTH]
): ):
errors[CONF_PEAK_PRICE_MIN_PERIOD_LENGTH] = "invalid_period_length" errors[CONF_PEAK_PRICE_MIN_PERIOD_LENGTH] = "invalid_period_length"
# Validate flex percentage (peak uses negative values) # Validate flex percentage (peak uses negative values)
if CONF_PEAK_PRICE_FLEX in user_input and not validate_flex_percentage(user_input[CONF_PEAK_PRICE_FLEX]): if CONF_PEAK_PRICE_FLEX in flexibility_settings and not validate_flex_percentage(
flexibility_settings[CONF_PEAK_PRICE_FLEX]
):
errors[CONF_PEAK_PRICE_FLEX] = "invalid_flex" errors[CONF_PEAK_PRICE_FLEX] = "invalid_flex"
# Validate distance from average (Peak Price uses positive values) # Validate distance from average (Peak Price uses positive values)
if CONF_PEAK_PRICE_MIN_DISTANCE_FROM_AVG in user_input and not validate_distance_percentage( if CONF_PEAK_PRICE_MIN_DISTANCE_FROM_AVG in flexibility_settings and not validate_distance_percentage(
user_input[CONF_PEAK_PRICE_MIN_DISTANCE_FROM_AVG] flexibility_settings[CONF_PEAK_PRICE_MIN_DISTANCE_FROM_AVG]
): ):
errors[CONF_PEAK_PRICE_MIN_DISTANCE_FROM_AVG] = "invalid_peak_price_distance" errors[CONF_PEAK_PRICE_MIN_DISTANCE_FROM_AVG] = "invalid_peak_price_distance"
# Validate minimum periods count # Validate minimum periods count
if CONF_MIN_PERIODS_PEAK in user_input and not validate_min_periods(user_input[CONF_MIN_PERIODS_PEAK]): if CONF_MIN_PERIODS_PEAK in relaxation_settings and not validate_min_periods(
relaxation_settings[CONF_MIN_PERIODS_PEAK]
):
errors[CONF_MIN_PERIODS_PEAK] = "invalid_min_periods" errors[CONF_MIN_PERIODS_PEAK] = "invalid_min_periods"
# Validate gap count # Validate gap count
if CONF_PEAK_PRICE_MAX_LEVEL_GAP_COUNT in user_input and not validate_gap_count( if CONF_PEAK_PRICE_MAX_LEVEL_GAP_COUNT in period_settings and not validate_gap_count(
user_input[CONF_PEAK_PRICE_MAX_LEVEL_GAP_COUNT] period_settings[CONF_PEAK_PRICE_MAX_LEVEL_GAP_COUNT]
): ):
errors[CONF_PEAK_PRICE_MAX_LEVEL_GAP_COUNT] = "invalid_gap_count" errors[CONF_PEAK_PRICE_MAX_LEVEL_GAP_COUNT] = "invalid_gap_count"
# Validate relaxation attempts # Validate relaxation attempts
if CONF_RELAXATION_ATTEMPTS_PEAK in user_input and not validate_relaxation_attempts( if CONF_RELAXATION_ATTEMPTS_PEAK in relaxation_settings and not validate_relaxation_attempts(
user_input[CONF_RELAXATION_ATTEMPTS_PEAK] relaxation_settings[CONF_RELAXATION_ATTEMPTS_PEAK]
): ):
errors[CONF_RELAXATION_ATTEMPTS_PEAK] = "invalid_relaxation_attempts" errors[CONF_RELAXATION_ATTEMPTS_PEAK] = "invalid_relaxation_attempts"
if not errors: if not errors:
self._options.update(user_input) # Merge section data into options
return await self.async_step_price_trend() self._merge_section_data(user_input)
# async_create_entry automatically handles change detection and listener triggering
self._save_options_if_changed()
# Return to menu for more changes
return await self.async_step_init()
return self.async_show_form( return self.async_show_form(
step_id="peak_price", step_id="peak_price",
data_schema=get_peak_price_schema(self.config_entry.options), data_schema=get_peak_price_schema(self.config_entry.options),
description_placeholders=self._get_step_description_placeholders("peak_price"),
errors=errors, errors=errors,
) )
@ -346,6 +457,9 @@ class TibberPricesOptionsFlowHandler(OptionsFlow):
errors: dict[str, str] = {} errors: dict[str, str] = {}
if user_input is not None: if user_input is not None:
# Schema is now flattened - fields come directly in user_input
# Store them flat in options (no nested structure)
# Validate rising trend threshold # Validate rising trend threshold
if CONF_PRICE_TREND_THRESHOLD_RISING in user_input and not validate_price_trend_rising( if CONF_PRICE_TREND_THRESHOLD_RISING in user_input and not validate_price_trend_rising(
user_input[CONF_PRICE_TREND_THRESHOLD_RISING] user_input[CONF_PRICE_TREND_THRESHOLD_RISING]
@ -359,27 +473,29 @@ class TibberPricesOptionsFlowHandler(OptionsFlow):
errors[CONF_PRICE_TREND_THRESHOLD_FALLING] = "invalid_price_trend_falling" errors[CONF_PRICE_TREND_THRESHOLD_FALLING] = "invalid_price_trend_falling"
if not errors: if not errors:
# Store flat data directly in options (no section wrapping)
self._options.update(user_input) self._options.update(user_input)
return await self.async_step_chart_data_export() # async_create_entry automatically handles change detection and listener triggering
self._save_options_if_changed()
# Return to menu for more changes
return await self.async_step_init()
return self.async_show_form( return self.async_show_form(
step_id="price_trend", step_id="price_trend",
data_schema=get_price_trend_schema(self.config_entry.options), data_schema=get_price_trend_schema(self.config_entry.options),
description_placeholders=self._get_step_description_placeholders("price_trend"),
errors=errors, errors=errors,
) )
async def async_step_chart_data_export(self, user_input: dict[str, Any] | None = None) -> ConfigFlowResult: async def async_step_chart_data_export(self, user_input: dict[str, Any] | None = None) -> ConfigFlowResult:
"""Info page for chart data export sensor.""" """Info page for chart data export sensor."""
if user_input is not None: if user_input is not None:
# No validation needed - just an info page # No changes to save - just return to menu
return self.async_create_entry(title="", data=self._options) return await self.async_step_init()
# Show info-only form (no input fields) # Show info-only form (no input fields)
return self.async_show_form( return self.async_show_form(
step_id="chart_data_export", step_id="chart_data_export",
data_schema=get_chart_data_export_schema(self.config_entry.options), data_schema=get_chart_data_export_schema(self.config_entry.options),
description_placeholders=self._get_step_description_placeholders("chart_data_export"),
) )
async def async_step_volatility(self, user_input: dict[str, Any] | None = None) -> ConfigFlowResult: async def async_step_volatility(self, user_input: dict[str, Any] | None = None) -> ConfigFlowResult:
@ -387,6 +503,8 @@ class TibberPricesOptionsFlowHandler(OptionsFlow):
errors: dict[str, str] = {} errors: dict[str, str] = {}
if user_input is not None: if user_input is not None:
# Schema is now flattened - fields come directly in user_input
# Validate moderate volatility threshold # Validate moderate volatility threshold
if CONF_VOLATILITY_THRESHOLD_MODERATE in user_input and not validate_volatility_threshold_moderate( if CONF_VOLATILITY_THRESHOLD_MODERATE in user_input and not validate_volatility_threshold_moderate(
user_input[CONF_VOLATILITY_THRESHOLD_MODERATE] user_input[CONF_VOLATILITY_THRESHOLD_MODERATE]
@ -407,30 +525,33 @@ class TibberPricesOptionsFlowHandler(OptionsFlow):
# Cross-validation: Ensure MODERATE < HIGH < VERY_HIGH # Cross-validation: Ensure MODERATE < HIGH < VERY_HIGH
if not errors: if not errors:
existing_options = self.config_entry.options # Get current values directly from options (now flat)
moderate = user_input.get( moderate = user_input.get(
CONF_VOLATILITY_THRESHOLD_MODERATE, CONF_VOLATILITY_THRESHOLD_MODERATE,
existing_options.get(CONF_VOLATILITY_THRESHOLD_MODERATE, DEFAULT_VOLATILITY_THRESHOLD_MODERATE), self._options.get(CONF_VOLATILITY_THRESHOLD_MODERATE, DEFAULT_VOLATILITY_THRESHOLD_MODERATE),
) )
high = user_input.get( high = user_input.get(
CONF_VOLATILITY_THRESHOLD_HIGH, CONF_VOLATILITY_THRESHOLD_HIGH,
existing_options.get(CONF_VOLATILITY_THRESHOLD_HIGH, DEFAULT_VOLATILITY_THRESHOLD_HIGH), self._options.get(CONF_VOLATILITY_THRESHOLD_HIGH, DEFAULT_VOLATILITY_THRESHOLD_HIGH),
) )
very_high = user_input.get( very_high = user_input.get(
CONF_VOLATILITY_THRESHOLD_VERY_HIGH, CONF_VOLATILITY_THRESHOLD_VERY_HIGH,
existing_options.get(CONF_VOLATILITY_THRESHOLD_VERY_HIGH, DEFAULT_VOLATILITY_THRESHOLD_VERY_HIGH), self._options.get(CONF_VOLATILITY_THRESHOLD_VERY_HIGH, DEFAULT_VOLATILITY_THRESHOLD_VERY_HIGH),
) )
if not validate_volatility_thresholds(moderate, high, very_high): if not validate_volatility_thresholds(moderate, high, very_high):
errors["base"] = "invalid_volatility_thresholds" errors["base"] = "invalid_volatility_thresholds"
if not errors: if not errors:
# Store flat data directly in options (no section wrapping)
self._options.update(user_input) self._options.update(user_input)
return await self.async_step_best_price() # async_create_entry automatically handles change detection and listener triggering
self._save_options_if_changed()
# Return to menu for more changes
return await self.async_step_init()
return self.async_show_form( return self.async_show_form(
step_id="volatility", step_id="volatility",
data_schema=get_volatility_schema(self.config_entry.options), data_schema=get_volatility_schema(self.config_entry.options),
description_placeholders=self._get_step_description_placeholders("volatility"),
errors=errors, errors=errors,
) )

View file

@ -96,6 +96,7 @@ from custom_components.tibber_prices.const import (
) )
from homeassistant.const import CONF_ACCESS_TOKEN from homeassistant.const import CONF_ACCESS_TOKEN
from homeassistant.data_entry_flow import section from homeassistant.data_entry_flow import section
from homeassistant.helpers import selector
from homeassistant.helpers.selector import ( from homeassistant.helpers.selector import (
BooleanSelector, BooleanSelector,
NumberSelector, NumberSelector,
@ -258,9 +259,6 @@ def get_display_settings_schema(options: Mapping[str, Any], currency_code: str |
def get_price_rating_schema(options: Mapping[str, Any]) -> vol.Schema: def get_price_rating_schema(options: Mapping[str, Any]) -> vol.Schema:
"""Return schema for price rating thresholds configuration.""" """Return schema for price rating thresholds configuration."""
return vol.Schema( return vol.Schema(
{
vol.Required("price_rating_thresholds"): section(
vol.Schema(
{ {
vol.Optional( vol.Optional(
CONF_PRICE_RATING_THRESHOLD_LOW, CONF_PRICE_RATING_THRESHOLD_LOW,
@ -297,19 +295,12 @@ def get_price_rating_schema(options: Mapping[str, Any]) -> vol.Schema:
), ),
), ),
} }
),
{"collapsed": True},
),
}
) )
def get_volatility_schema(options: Mapping[str, Any]) -> vol.Schema: def get_volatility_schema(options: Mapping[str, Any]) -> vol.Schema:
"""Return schema for volatility thresholds configuration with collapsible sections.""" """Return schema for volatility thresholds configuration."""
return vol.Schema( return vol.Schema(
{
vol.Required("volatility_thresholds"): section(
vol.Schema(
{ {
vol.Optional( vol.Optional(
CONF_VOLATILITY_THRESHOLD_MODERATE, CONF_VOLATILITY_THRESHOLD_MODERATE,
@ -363,15 +354,12 @@ def get_volatility_schema(options: Mapping[str, Any]) -> vol.Schema:
), ),
), ),
} }
),
{"collapsed": True},
),
}
) )
def get_best_price_schema(options: Mapping[str, Any]) -> vol.Schema: def get_best_price_schema(options: Mapping[str, Any]) -> vol.Schema:
"""Return schema for best price period configuration with collapsible sections.""" """Return schema for best price period configuration with collapsible sections."""
period_settings = options.get("period_settings", {})
return vol.Schema( return vol.Schema(
{ {
vol.Required("period_settings"): section( vol.Required("period_settings"): section(
@ -380,7 +368,7 @@ def get_best_price_schema(options: Mapping[str, Any]) -> vol.Schema:
vol.Optional( vol.Optional(
CONF_BEST_PRICE_MIN_PERIOD_LENGTH, CONF_BEST_PRICE_MIN_PERIOD_LENGTH,
default=int( default=int(
options.get( period_settings.get(
CONF_BEST_PRICE_MIN_PERIOD_LENGTH, CONF_BEST_PRICE_MIN_PERIOD_LENGTH,
DEFAULT_BEST_PRICE_MIN_PERIOD_LENGTH, DEFAULT_BEST_PRICE_MIN_PERIOD_LENGTH,
) )
@ -396,7 +384,7 @@ def get_best_price_schema(options: Mapping[str, Any]) -> vol.Schema:
), ),
vol.Optional( vol.Optional(
CONF_BEST_PRICE_MAX_LEVEL, CONF_BEST_PRICE_MAX_LEVEL,
default=options.get( default=period_settings.get(
CONF_BEST_PRICE_MAX_LEVEL, CONF_BEST_PRICE_MAX_LEVEL,
DEFAULT_BEST_PRICE_MAX_LEVEL, DEFAULT_BEST_PRICE_MAX_LEVEL,
), ),
@ -410,7 +398,7 @@ def get_best_price_schema(options: Mapping[str, Any]) -> vol.Schema:
vol.Optional( vol.Optional(
CONF_BEST_PRICE_MAX_LEVEL_GAP_COUNT, CONF_BEST_PRICE_MAX_LEVEL_GAP_COUNT,
default=int( default=int(
options.get( period_settings.get(
CONF_BEST_PRICE_MAX_LEVEL_GAP_COUNT, CONF_BEST_PRICE_MAX_LEVEL_GAP_COUNT,
DEFAULT_BEST_PRICE_MAX_LEVEL_GAP_COUNT, DEFAULT_BEST_PRICE_MAX_LEVEL_GAP_COUNT,
) )
@ -425,7 +413,7 @@ def get_best_price_schema(options: Mapping[str, Any]) -> vol.Schema:
), ),
} }
), ),
{"collapsed": True}, {"collapsed": False},
), ),
vol.Required("flexibility_settings"): section( vol.Required("flexibility_settings"): section(
vol.Schema( vol.Schema(
@ -433,7 +421,7 @@ def get_best_price_schema(options: Mapping[str, Any]) -> vol.Schema:
vol.Optional( vol.Optional(
CONF_BEST_PRICE_FLEX, CONF_BEST_PRICE_FLEX,
default=int( default=int(
options.get( options.get("flexibility_settings", {}).get(
CONF_BEST_PRICE_FLEX, CONF_BEST_PRICE_FLEX,
DEFAULT_BEST_PRICE_FLEX, DEFAULT_BEST_PRICE_FLEX,
) )
@ -450,7 +438,7 @@ def get_best_price_schema(options: Mapping[str, Any]) -> vol.Schema:
vol.Optional( vol.Optional(
CONF_BEST_PRICE_MIN_DISTANCE_FROM_AVG, CONF_BEST_PRICE_MIN_DISTANCE_FROM_AVG,
default=int( default=int(
options.get( options.get("flexibility_settings", {}).get(
CONF_BEST_PRICE_MIN_DISTANCE_FROM_AVG, CONF_BEST_PRICE_MIN_DISTANCE_FROM_AVG,
DEFAULT_BEST_PRICE_MIN_DISTANCE_FROM_AVG, DEFAULT_BEST_PRICE_MIN_DISTANCE_FROM_AVG,
) )
@ -473,7 +461,7 @@ def get_best_price_schema(options: Mapping[str, Any]) -> vol.Schema:
{ {
vol.Optional( vol.Optional(
CONF_ENABLE_MIN_PERIODS_BEST, CONF_ENABLE_MIN_PERIODS_BEST,
default=options.get( default=options.get("relaxation_and_target_periods", {}).get(
CONF_ENABLE_MIN_PERIODS_BEST, CONF_ENABLE_MIN_PERIODS_BEST,
DEFAULT_ENABLE_MIN_PERIODS_BEST, DEFAULT_ENABLE_MIN_PERIODS_BEST,
), ),
@ -481,7 +469,7 @@ def get_best_price_schema(options: Mapping[str, Any]) -> vol.Schema:
vol.Optional( vol.Optional(
CONF_MIN_PERIODS_BEST, CONF_MIN_PERIODS_BEST,
default=int( default=int(
options.get( options.get("relaxation_and_target_periods", {}).get(
CONF_MIN_PERIODS_BEST, CONF_MIN_PERIODS_BEST,
DEFAULT_MIN_PERIODS_BEST, DEFAULT_MIN_PERIODS_BEST,
) )
@ -497,7 +485,7 @@ def get_best_price_schema(options: Mapping[str, Any]) -> vol.Schema:
vol.Optional( vol.Optional(
CONF_RELAXATION_ATTEMPTS_BEST, CONF_RELAXATION_ATTEMPTS_BEST,
default=int( default=int(
options.get( options.get("relaxation_and_target_periods", {}).get(
CONF_RELAXATION_ATTEMPTS_BEST, CONF_RELAXATION_ATTEMPTS_BEST,
DEFAULT_RELAXATION_ATTEMPTS_BEST, DEFAULT_RELAXATION_ATTEMPTS_BEST,
) )
@ -520,6 +508,7 @@ def get_best_price_schema(options: Mapping[str, Any]) -> vol.Schema:
def get_peak_price_schema(options: Mapping[str, Any]) -> vol.Schema: def get_peak_price_schema(options: Mapping[str, Any]) -> vol.Schema:
"""Return schema for peak price period configuration with collapsible sections.""" """Return schema for peak price period configuration with collapsible sections."""
period_settings = options.get("period_settings", {})
return vol.Schema( return vol.Schema(
{ {
vol.Required("period_settings"): section( vol.Required("period_settings"): section(
@ -528,7 +517,7 @@ def get_peak_price_schema(options: Mapping[str, Any]) -> vol.Schema:
vol.Optional( vol.Optional(
CONF_PEAK_PRICE_MIN_PERIOD_LENGTH, CONF_PEAK_PRICE_MIN_PERIOD_LENGTH,
default=int( default=int(
options.get( period_settings.get(
CONF_PEAK_PRICE_MIN_PERIOD_LENGTH, CONF_PEAK_PRICE_MIN_PERIOD_LENGTH,
DEFAULT_PEAK_PRICE_MIN_PERIOD_LENGTH, DEFAULT_PEAK_PRICE_MIN_PERIOD_LENGTH,
) )
@ -544,7 +533,7 @@ def get_peak_price_schema(options: Mapping[str, Any]) -> vol.Schema:
), ),
vol.Optional( vol.Optional(
CONF_PEAK_PRICE_MIN_LEVEL, CONF_PEAK_PRICE_MIN_LEVEL,
default=options.get( default=period_settings.get(
CONF_PEAK_PRICE_MIN_LEVEL, CONF_PEAK_PRICE_MIN_LEVEL,
DEFAULT_PEAK_PRICE_MIN_LEVEL, DEFAULT_PEAK_PRICE_MIN_LEVEL,
), ),
@ -558,7 +547,7 @@ def get_peak_price_schema(options: Mapping[str, Any]) -> vol.Schema:
vol.Optional( vol.Optional(
CONF_PEAK_PRICE_MAX_LEVEL_GAP_COUNT, CONF_PEAK_PRICE_MAX_LEVEL_GAP_COUNT,
default=int( default=int(
options.get( period_settings.get(
CONF_PEAK_PRICE_MAX_LEVEL_GAP_COUNT, CONF_PEAK_PRICE_MAX_LEVEL_GAP_COUNT,
DEFAULT_PEAK_PRICE_MAX_LEVEL_GAP_COUNT, DEFAULT_PEAK_PRICE_MAX_LEVEL_GAP_COUNT,
) )
@ -573,7 +562,7 @@ def get_peak_price_schema(options: Mapping[str, Any]) -> vol.Schema:
), ),
} }
), ),
{"collapsed": True}, {"collapsed": False},
), ),
vol.Required("flexibility_settings"): section( vol.Required("flexibility_settings"): section(
vol.Schema( vol.Schema(
@ -581,7 +570,7 @@ def get_peak_price_schema(options: Mapping[str, Any]) -> vol.Schema:
vol.Optional( vol.Optional(
CONF_PEAK_PRICE_FLEX, CONF_PEAK_PRICE_FLEX,
default=int( default=int(
options.get( options.get("flexibility_settings", {}).get(
CONF_PEAK_PRICE_FLEX, CONF_PEAK_PRICE_FLEX,
DEFAULT_PEAK_PRICE_FLEX, DEFAULT_PEAK_PRICE_FLEX,
) )
@ -598,7 +587,7 @@ def get_peak_price_schema(options: Mapping[str, Any]) -> vol.Schema:
vol.Optional( vol.Optional(
CONF_PEAK_PRICE_MIN_DISTANCE_FROM_AVG, CONF_PEAK_PRICE_MIN_DISTANCE_FROM_AVG,
default=int( default=int(
options.get( options.get("flexibility_settings", {}).get(
CONF_PEAK_PRICE_MIN_DISTANCE_FROM_AVG, CONF_PEAK_PRICE_MIN_DISTANCE_FROM_AVG,
DEFAULT_PEAK_PRICE_MIN_DISTANCE_FROM_AVG, DEFAULT_PEAK_PRICE_MIN_DISTANCE_FROM_AVG,
) )
@ -621,7 +610,7 @@ def get_peak_price_schema(options: Mapping[str, Any]) -> vol.Schema:
{ {
vol.Optional( vol.Optional(
CONF_ENABLE_MIN_PERIODS_PEAK, CONF_ENABLE_MIN_PERIODS_PEAK,
default=options.get( default=options.get("relaxation_and_target_periods", {}).get(
CONF_ENABLE_MIN_PERIODS_PEAK, CONF_ENABLE_MIN_PERIODS_PEAK,
DEFAULT_ENABLE_MIN_PERIODS_PEAK, DEFAULT_ENABLE_MIN_PERIODS_PEAK,
), ),
@ -629,7 +618,7 @@ def get_peak_price_schema(options: Mapping[str, Any]) -> vol.Schema:
vol.Optional( vol.Optional(
CONF_MIN_PERIODS_PEAK, CONF_MIN_PERIODS_PEAK,
default=int( default=int(
options.get( options.get("relaxation_and_target_periods", {}).get(
CONF_MIN_PERIODS_PEAK, CONF_MIN_PERIODS_PEAK,
DEFAULT_MIN_PERIODS_PEAK, DEFAULT_MIN_PERIODS_PEAK,
) )
@ -645,7 +634,7 @@ def get_peak_price_schema(options: Mapping[str, Any]) -> vol.Schema:
vol.Optional( vol.Optional(
CONF_RELAXATION_ATTEMPTS_PEAK, CONF_RELAXATION_ATTEMPTS_PEAK,
default=int( default=int(
options.get( options.get("relaxation_and_target_periods", {}).get(
CONF_RELAXATION_ATTEMPTS_PEAK, CONF_RELAXATION_ATTEMPTS_PEAK,
DEFAULT_RELAXATION_ATTEMPTS_PEAK, DEFAULT_RELAXATION_ATTEMPTS_PEAK,
) )
@ -669,9 +658,6 @@ def get_peak_price_schema(options: Mapping[str, Any]) -> vol.Schema:
def get_price_trend_schema(options: Mapping[str, Any]) -> vol.Schema: def get_price_trend_schema(options: Mapping[str, Any]) -> vol.Schema:
"""Return schema for price trend thresholds configuration.""" """Return schema for price trend thresholds configuration."""
return vol.Schema( return vol.Schema(
{
vol.Required("price_trend_thresholds"): section(
vol.Schema(
{ {
vol.Optional( vol.Optional(
CONF_PRICE_TREND_THRESHOLD_RISING, CONF_PRICE_TREND_THRESHOLD_RISING,
@ -708,10 +694,6 @@ def get_price_trend_schema(options: Mapping[str, Any]) -> vol.Schema:
), ),
), ),
} }
),
{"collapsed": True},
),
}
) )
@ -719,3 +701,12 @@ def get_chart_data_export_schema(_options: Mapping[str, Any]) -> vol.Schema:
"""Return schema for chart data export info page (no input fields).""" """Return schema for chart data export info page (no input fields)."""
# Empty schema - this is just an info page now # Empty schema - this is just an info page now
return vol.Schema({}) return vol.Schema({})
def get_reset_to_defaults_schema() -> vol.Schema:
"""Return schema for reset to defaults confirmation step."""
return vol.Schema(
{
vol.Required("confirm_reset", default=False): selector.BooleanSelector(),
}
)

View file

@ -125,6 +125,9 @@ class TibberPricesSubentryFlowHandler(ConfigSubentryFlow):
offset_desc = self._format_offset_description(offset_days, offset_hours, offset_minutes) offset_desc = self._format_offset_description(offset_days, offset_hours, offset_minutes)
subentry_title = f"{parent_entry.title} ({offset_desc})" subentry_title = f"{parent_entry.title} ({offset_desc})"
# Note: Subentries inherit options from parent entry automatically
# Options parameter is not supported by ConfigSubentryFlow.async_create_entry()
return self.async_create_entry( return self.async_create_entry(
title=subentry_title, title=subentry_title,
data={ data={

View file

@ -20,7 +20,12 @@ from custom_components.tibber_prices.config_flow_handlers.validators import (
TibberPricesInvalidAuthError, TibberPricesInvalidAuthError,
validate_api_token, validate_api_token,
) )
from custom_components.tibber_prices.const import DOMAIN, LOGGER, get_translation from custom_components.tibber_prices.const import (
DOMAIN,
LOGGER,
get_default_options,
get_translation,
)
from homeassistant.config_entries import ( from homeassistant.config_entries import (
ConfigEntry, ConfigEntry,
ConfigFlow, ConfigFlow,
@ -379,6 +384,16 @@ class TibberPricesConfigFlowHandler(ConfigFlow, domain=DOMAIN):
"user_login": self._user_login or "N/A", "user_login": self._user_login or "N/A",
} }
# Extract currency from home data for intelligent defaults
currency_code = None
if (
selected_home
and (subscription := selected_home.get("currentSubscription"))
and (price_info := subscription.get("priceInfo"))
and (current_price := price_info.get("current"))
):
currency_code = current_price.get("currency")
# Generate entry title from home address (not appNickname) # Generate entry title from home address (not appNickname)
entry_title = self._get_entry_title(selected_home) entry_title = self._get_entry_title(selected_home)
@ -386,6 +401,7 @@ class TibberPricesConfigFlowHandler(ConfigFlow, domain=DOMAIN):
title=entry_title, title=entry_title,
data=data, data=data,
description=f"{self._user_login} ({self._user_id})", description=f"{self._user_login} ({self._user_id})",
options=get_default_options(currency_code),
) )
home_options = [ home_options = [

View file

@ -298,6 +298,72 @@ def get_default_currency_display(currency_code: str | None) -> str:
return DEFAULT_CURRENCY_DISPLAY.get(currency_code.upper(), DISPLAY_MODE_SUBUNIT) return DEFAULT_CURRENCY_DISPLAY.get(currency_code.upper(), DISPLAY_MODE_SUBUNIT)
def get_default_options(currency_code: str | None) -> dict[str, Any]:
"""
Get complete default options for a new config entry.
This ensures new config entries have explicitly set defaults based on their currency,
distinguishing them from legacy config entries that need migration.
Options structure has been flattened for single-section steps:
- Flat values: extended_descriptions, average_sensor_display, currency_display_mode,
price_rating_thresholds, volatility_thresholds, price_trend_thresholds, time offsets
- Nested sections (multi-section steps only): period_settings, flexibility_settings,
relaxation_and_target_periods
Args:
currency_code: ISO 4217 currency code (e.g., 'EUR', 'NOK')
Returns:
Dictionary with all default option values in nested section structure
"""
return {
# Flat configuration values
CONF_EXTENDED_DESCRIPTIONS: DEFAULT_EXTENDED_DESCRIPTIONS,
CONF_AVERAGE_SENSOR_DISPLAY: DEFAULT_AVERAGE_SENSOR_DISPLAY,
CONF_CURRENCY_DISPLAY_MODE: get_default_currency_display(currency_code),
CONF_VIRTUAL_TIME_OFFSET_DAYS: DEFAULT_VIRTUAL_TIME_OFFSET_DAYS,
CONF_VIRTUAL_TIME_OFFSET_HOURS: DEFAULT_VIRTUAL_TIME_OFFSET_HOURS,
CONF_VIRTUAL_TIME_OFFSET_MINUTES: DEFAULT_VIRTUAL_TIME_OFFSET_MINUTES,
# Price rating thresholds (flat - single-section step)
CONF_PRICE_RATING_THRESHOLD_LOW: DEFAULT_PRICE_RATING_THRESHOLD_LOW,
CONF_PRICE_RATING_THRESHOLD_HIGH: DEFAULT_PRICE_RATING_THRESHOLD_HIGH,
# Volatility thresholds (flat - single-section step)
CONF_VOLATILITY_THRESHOLD_MODERATE: DEFAULT_VOLATILITY_THRESHOLD_MODERATE,
CONF_VOLATILITY_THRESHOLD_HIGH: DEFAULT_VOLATILITY_THRESHOLD_HIGH,
CONF_VOLATILITY_THRESHOLD_VERY_HIGH: DEFAULT_VOLATILITY_THRESHOLD_VERY_HIGH,
# Price trend thresholds (flat - single-section step)
CONF_PRICE_TREND_THRESHOLD_RISING: DEFAULT_PRICE_TREND_THRESHOLD_RISING,
CONF_PRICE_TREND_THRESHOLD_FALLING: DEFAULT_PRICE_TREND_THRESHOLD_FALLING,
# Nested section: Period settings (shared by best/peak price)
"period_settings": {
CONF_BEST_PRICE_MIN_PERIOD_LENGTH: DEFAULT_BEST_PRICE_MIN_PERIOD_LENGTH,
CONF_PEAK_PRICE_MIN_PERIOD_LENGTH: DEFAULT_PEAK_PRICE_MIN_PERIOD_LENGTH,
CONF_BEST_PRICE_MAX_LEVEL_GAP_COUNT: DEFAULT_BEST_PRICE_MAX_LEVEL_GAP_COUNT,
CONF_PEAK_PRICE_MAX_LEVEL_GAP_COUNT: DEFAULT_PEAK_PRICE_MAX_LEVEL_GAP_COUNT,
CONF_BEST_PRICE_MAX_LEVEL: DEFAULT_BEST_PRICE_MAX_LEVEL,
CONF_PEAK_PRICE_MIN_LEVEL: DEFAULT_PEAK_PRICE_MIN_LEVEL,
},
# Nested section: Flexibility settings (shared by best/peak price)
"flexibility_settings": {
CONF_BEST_PRICE_FLEX: DEFAULT_BEST_PRICE_FLEX,
CONF_PEAK_PRICE_FLEX: DEFAULT_PEAK_PRICE_FLEX,
CONF_BEST_PRICE_MIN_DISTANCE_FROM_AVG: DEFAULT_BEST_PRICE_MIN_DISTANCE_FROM_AVG,
CONF_PEAK_PRICE_MIN_DISTANCE_FROM_AVG: DEFAULT_PEAK_PRICE_MIN_DISTANCE_FROM_AVG,
},
# Nested section: Relaxation and target periods (shared by best/peak price)
"relaxation_and_target_periods": {
CONF_ENABLE_MIN_PERIODS_BEST: DEFAULT_ENABLE_MIN_PERIODS_BEST,
CONF_MIN_PERIODS_BEST: DEFAULT_MIN_PERIODS_BEST,
CONF_RELAXATION_ATTEMPTS_BEST: DEFAULT_RELAXATION_ATTEMPTS_BEST,
CONF_ENABLE_MIN_PERIODS_PEAK: DEFAULT_ENABLE_MIN_PERIODS_PEAK,
CONF_MIN_PERIODS_PEAK: DEFAULT_MIN_PERIODS_PEAK,
CONF_RELAXATION_ATTEMPTS_PEAK: DEFAULT_RELAXATION_ATTEMPTS_PEAK,
},
}
def get_display_unit_factor(config_entry: ConfigEntry) -> int: def get_display_unit_factor(config_entry: ConfigEntry) -> int:
""" """
Get multiplication factor for converting base to display currency. Get multiplication factor for converting base to display currency.

View file

@ -262,12 +262,21 @@ class TibberPricesDataUpdateCoordinator(DataUpdateCoordinator[dict[str, Any]]):
getattr(_LOGGER, level)(prefixed_message, *args, **kwargs) getattr(_LOGGER, level)(prefixed_message, *args, **kwargs)
async def _handle_options_update(self, _hass: HomeAssistant, _config_entry: ConfigEntry) -> None: async def _handle_options_update(self, _hass: HomeAssistant, _config_entry: ConfigEntry) -> None:
"""Handle options update by invalidating config caches.""" """Handle options update by invalidating config caches and re-transforming data."""
self._log("debug", "Options updated, invalidating config caches") self._log("debug", "Options updated, invalidating config caches")
self._data_transformer.invalidate_config_cache() self._data_transformer.invalidate_config_cache()
self._period_calculator.invalidate_config_cache() self._period_calculator.invalidate_config_cache()
# Trigger a refresh to apply new configuration
await self.async_request_refresh() # Re-transform existing cached data with new configuration
# This updates rating_levels, volatility, and period calculations
# without needing to fetch new data from the API
if self._cached_price_data:
self._log("debug", "Re-transforming cached data with new configuration")
self.data = self._transform_data(self._cached_price_data)
# Notify all listeners about the updated data
self.async_update_listeners()
else:
self._log("warning", "No cached data available to re-transform")
@callback @callback
def async_add_time_sensitive_listener(self, update_callback: TimeServiceCallback) -> CALLBACK_TYPE: def async_add_time_sensitive_listener(self, update_callback: TimeServiceCallback) -> CALLBACK_TYPE:

View file

@ -73,36 +73,52 @@ class TibberPricesDataTransformer:
return self._config_cache return self._config_cache
# Build config dictionary (expensive operation) # Build config dictionary (expensive operation)
options = self.config_entry.options
# Best/peak price remain nested (multi-section steps)
best_period_section = options.get("period_settings", {})
best_flex_section = options.get("flexibility_settings", {})
best_relax_section = options.get("relaxation_and_target_periods", {})
peak_period_section = options.get("period_settings", {})
peak_flex_section = options.get("flexibility_settings", {})
peak_relax_section = options.get("relaxation_and_target_periods", {})
config = { config = {
"thresholds": self.get_threshold_percentages(), "thresholds": self.get_threshold_percentages(),
# Volatility thresholds now flat (single-section step)
"volatility_thresholds": { "volatility_thresholds": {
"moderate": self.config_entry.options.get(_const.CONF_VOLATILITY_THRESHOLD_MODERATE, 15.0), "moderate": options.get(_const.CONF_VOLATILITY_THRESHOLD_MODERATE, 15.0),
"high": self.config_entry.options.get(_const.CONF_VOLATILITY_THRESHOLD_HIGH, 25.0), "high": options.get(_const.CONF_VOLATILITY_THRESHOLD_HIGH, 25.0),
"very_high": self.config_entry.options.get(_const.CONF_VOLATILITY_THRESHOLD_VERY_HIGH, 40.0), "very_high": options.get(_const.CONF_VOLATILITY_THRESHOLD_VERY_HIGH, 40.0),
},
# Price trend thresholds now flat (single-section step)
"price_trend_thresholds": {
"rising": options.get(
_const.CONF_PRICE_TREND_THRESHOLD_RISING, _const.DEFAULT_PRICE_TREND_THRESHOLD_RISING
),
"falling": options.get(
_const.CONF_PRICE_TREND_THRESHOLD_FALLING, _const.DEFAULT_PRICE_TREND_THRESHOLD_FALLING
),
}, },
"best_price_config": { "best_price_config": {
"flex": self.config_entry.options.get(_const.CONF_BEST_PRICE_FLEX, 15.0), "flex": best_flex_section.get(_const.CONF_BEST_PRICE_FLEX, 15.0),
"max_level": self.config_entry.options.get(_const.CONF_BEST_PRICE_MAX_LEVEL, "NORMAL"), "max_level": best_period_section.get(_const.CONF_BEST_PRICE_MAX_LEVEL, "NORMAL"),
"min_period_length": self.config_entry.options.get(_const.CONF_BEST_PRICE_MIN_PERIOD_LENGTH, 4), "min_period_length": best_period_section.get(_const.CONF_BEST_PRICE_MIN_PERIOD_LENGTH, 4),
"min_distance_from_avg": self.config_entry.options.get( "min_distance_from_avg": best_flex_section.get(_const.CONF_BEST_PRICE_MIN_DISTANCE_FROM_AVG, -5.0),
_const.CONF_BEST_PRICE_MIN_DISTANCE_FROM_AVG, -5.0 "max_level_gap_count": best_period_section.get(_const.CONF_BEST_PRICE_MAX_LEVEL_GAP_COUNT, 0),
), "enable_min_periods": best_relax_section.get(_const.CONF_ENABLE_MIN_PERIODS_BEST, False),
"max_level_gap_count": self.config_entry.options.get(_const.CONF_BEST_PRICE_MAX_LEVEL_GAP_COUNT, 0), "min_periods": best_relax_section.get(_const.CONF_MIN_PERIODS_BEST, 2),
"enable_min_periods": self.config_entry.options.get(_const.CONF_ENABLE_MIN_PERIODS_BEST, False), "relaxation_attempts": best_relax_section.get(_const.CONF_RELAXATION_ATTEMPTS_BEST, 4),
"min_periods": self.config_entry.options.get(_const.CONF_MIN_PERIODS_BEST, 2),
"relaxation_attempts": self.config_entry.options.get(_const.CONF_RELAXATION_ATTEMPTS_BEST, 4),
}, },
"peak_price_config": { "peak_price_config": {
"flex": self.config_entry.options.get(_const.CONF_PEAK_PRICE_FLEX, 15.0), "flex": peak_flex_section.get(_const.CONF_PEAK_PRICE_FLEX, 15.0),
"min_level": self.config_entry.options.get(_const.CONF_PEAK_PRICE_MIN_LEVEL, "HIGH"), "min_level": peak_period_section.get(_const.CONF_PEAK_PRICE_MIN_LEVEL, "HIGH"),
"min_period_length": self.config_entry.options.get(_const.CONF_PEAK_PRICE_MIN_PERIOD_LENGTH, 4), "min_period_length": peak_period_section.get(_const.CONF_PEAK_PRICE_MIN_PERIOD_LENGTH, 4),
"min_distance_from_avg": self.config_entry.options.get( "min_distance_from_avg": peak_flex_section.get(_const.CONF_PEAK_PRICE_MIN_DISTANCE_FROM_AVG, 5.0),
_const.CONF_PEAK_PRICE_MIN_DISTANCE_FROM_AVG, 5.0 "max_level_gap_count": peak_period_section.get(_const.CONF_PEAK_PRICE_MAX_LEVEL_GAP_COUNT, 0),
), "enable_min_periods": peak_relax_section.get(_const.CONF_ENABLE_MIN_PERIODS_PEAK, False),
"max_level_gap_count": self.config_entry.options.get(_const.CONF_PEAK_PRICE_MAX_LEVEL_GAP_COUNT, 0), "min_periods": peak_relax_section.get(_const.CONF_MIN_PERIODS_PEAK, 2),
"enable_min_periods": self.config_entry.options.get(_const.CONF_ENABLE_MIN_PERIODS_PEAK, False), "relaxation_attempts": peak_relax_section.get(_const.CONF_RELAXATION_ATTEMPTS_PEAK, 4),
"min_periods": self.config_entry.options.get(_const.CONF_MIN_PERIODS_PEAK, 2),
"relaxation_attempts": self.config_entry.options.get(_const.CONF_RELAXATION_ATTEMPTS_PEAK, 4),
}, },
} }

View file

@ -92,8 +92,9 @@ class TibberPricesPeriodCalculator:
# Get level filter overrides from options # Get level filter overrides from options
options = self.config_entry.options options = self.config_entry.options
best_level_filter = options.get(_const.CONF_BEST_PRICE_MAX_LEVEL, _const.DEFAULT_BEST_PRICE_MAX_LEVEL) period_settings = options.get("period_settings", {})
peak_level_filter = options.get(_const.CONF_PEAK_PRICE_MIN_LEVEL, _const.DEFAULT_PEAK_PRICE_MIN_LEVEL) best_level_filter = period_settings.get(_const.CONF_BEST_PRICE_MAX_LEVEL, _const.DEFAULT_BEST_PRICE_MAX_LEVEL)
peak_level_filter = period_settings.get(_const.CONF_PEAK_PRICE_MIN_LEVEL, _const.DEFAULT_PEAK_PRICE_MIN_LEVEL)
# Compute hash from all relevant data # Compute hash from all relevant data
hash_data = ( hash_data = (
@ -124,33 +125,36 @@ class TibberPricesPeriodCalculator:
self._config_cache = {} self._config_cache = {}
options = self.config_entry.options options = self.config_entry.options
data = self.config_entry.data
# Get nested sections from options
# CRITICAL: Best/Peak price settings are stored in nested sections:
# - period_settings: min_period_length, max_level, gap_count
# - flexibility_settings: flex, min_distance_from_avg
# These settings are ONLY in options (not in data), structured since initial config flow
period_settings = options.get("period_settings", {})
flexibility_settings = options.get("flexibility_settings", {})
if reverse_sort: if reverse_sort:
# Peak price configuration # Peak price configuration
flex = options.get( flex = flexibility_settings.get(_const.CONF_PEAK_PRICE_FLEX, _const.DEFAULT_PEAK_PRICE_FLEX)
_const.CONF_PEAK_PRICE_FLEX, data.get(_const.CONF_PEAK_PRICE_FLEX, _const.DEFAULT_PEAK_PRICE_FLEX) min_distance_from_avg = flexibility_settings.get(
)
min_distance_from_avg = options.get(
_const.CONF_PEAK_PRICE_MIN_DISTANCE_FROM_AVG, _const.CONF_PEAK_PRICE_MIN_DISTANCE_FROM_AVG,
data.get(_const.CONF_PEAK_PRICE_MIN_DISTANCE_FROM_AVG, _const.DEFAULT_PEAK_PRICE_MIN_DISTANCE_FROM_AVG), _const.DEFAULT_PEAK_PRICE_MIN_DISTANCE_FROM_AVG,
) )
min_period_length = options.get( min_period_length = period_settings.get(
_const.CONF_PEAK_PRICE_MIN_PERIOD_LENGTH, _const.CONF_PEAK_PRICE_MIN_PERIOD_LENGTH,
data.get(_const.CONF_PEAK_PRICE_MIN_PERIOD_LENGTH, _const.DEFAULT_PEAK_PRICE_MIN_PERIOD_LENGTH), _const.DEFAULT_PEAK_PRICE_MIN_PERIOD_LENGTH,
) )
else: else:
# Best price configuration # Best price configuration
flex = options.get( flex = flexibility_settings.get(_const.CONF_BEST_PRICE_FLEX, _const.DEFAULT_BEST_PRICE_FLEX)
_const.CONF_BEST_PRICE_FLEX, data.get(_const.CONF_BEST_PRICE_FLEX, _const.DEFAULT_BEST_PRICE_FLEX) min_distance_from_avg = flexibility_settings.get(
)
min_distance_from_avg = options.get(
_const.CONF_BEST_PRICE_MIN_DISTANCE_FROM_AVG, _const.CONF_BEST_PRICE_MIN_DISTANCE_FROM_AVG,
data.get(_const.CONF_BEST_PRICE_MIN_DISTANCE_FROM_AVG, _const.DEFAULT_BEST_PRICE_MIN_DISTANCE_FROM_AVG), _const.DEFAULT_BEST_PRICE_MIN_DISTANCE_FROM_AVG,
) )
min_period_length = options.get( min_period_length = period_settings.get(
_const.CONF_BEST_PRICE_MIN_PERIOD_LENGTH, _const.CONF_BEST_PRICE_MIN_PERIOD_LENGTH,
data.get(_const.CONF_BEST_PRICE_MIN_PERIOD_LENGTH, _const.DEFAULT_BEST_PRICE_MIN_PERIOD_LENGTH), _const.DEFAULT_BEST_PRICE_MIN_PERIOD_LENGTH,
) )
# Convert flex from percentage to decimal (e.g., 5 -> 0.05) # Convert flex from percentage to decimal (e.g., 5 -> 0.05)
@ -356,13 +360,14 @@ class TibberPricesPeriodCalculator:
# Normal check failed - try splitting at gap clusters as fallback # Normal check failed - try splitting at gap clusters as fallback
# Get minimum period length from config (convert minutes to intervals) # Get minimum period length from config (convert minutes to intervals)
period_settings = self.config_entry.options.get("period_settings", {})
if reverse_sort: if reverse_sort:
min_period_minutes = self.config_entry.options.get( min_period_minutes = period_settings.get(
_const.CONF_PEAK_PRICE_MIN_PERIOD_LENGTH, _const.CONF_PEAK_PRICE_MIN_PERIOD_LENGTH,
_const.DEFAULT_PEAK_PRICE_MIN_PERIOD_LENGTH, _const.DEFAULT_PEAK_PRICE_MIN_PERIOD_LENGTH,
) )
else: else:
min_period_minutes = self.config_entry.options.get( min_period_minutes = period_settings.get(
_const.CONF_BEST_PRICE_MIN_PERIOD_LENGTH, _const.CONF_BEST_PRICE_MIN_PERIOD_LENGTH,
_const.DEFAULT_BEST_PRICE_MIN_PERIOD_LENGTH, _const.DEFAULT_BEST_PRICE_MIN_PERIOD_LENGTH,
) )
@ -487,13 +492,15 @@ class TibberPricesPeriodCalculator:
# Get appropriate config based on sensor type # Get appropriate config based on sensor type
elif reverse_sort: elif reverse_sort:
# Peak price: minimum level filter (lower bound) # Peak price: minimum level filter (lower bound)
level_config = self.config_entry.options.get( period_settings = self.config_entry.options.get("period_settings", {})
level_config = period_settings.get(
_const.CONF_PEAK_PRICE_MIN_LEVEL, _const.CONF_PEAK_PRICE_MIN_LEVEL,
_const.DEFAULT_PEAK_PRICE_MIN_LEVEL, _const.DEFAULT_PEAK_PRICE_MIN_LEVEL,
) )
else: else:
# Best price: maximum level filter (upper bound) # Best price: maximum level filter (upper bound)
level_config = self.config_entry.options.get( period_settings = self.config_entry.options.get("period_settings", {})
level_config = period_settings.get(
_const.CONF_BEST_PRICE_MAX_LEVEL, _const.CONF_BEST_PRICE_MAX_LEVEL,
_const.DEFAULT_BEST_PRICE_MAX_LEVEL, _const.DEFAULT_BEST_PRICE_MAX_LEVEL,
) )
@ -511,13 +518,14 @@ class TibberPricesPeriodCalculator:
return True # If no data, don't filter return True # If no data, don't filter
# Get gap tolerance configuration # Get gap tolerance configuration
period_settings = self.config_entry.options.get("period_settings", {})
if reverse_sort: if reverse_sort:
max_gap_count = self.config_entry.options.get( max_gap_count = period_settings.get(
_const.CONF_PEAK_PRICE_MAX_LEVEL_GAP_COUNT, _const.CONF_PEAK_PRICE_MAX_LEVEL_GAP_COUNT,
_const.DEFAULT_PEAK_PRICE_MAX_LEVEL_GAP_COUNT, _const.DEFAULT_PEAK_PRICE_MAX_LEVEL_GAP_COUNT,
) )
else: else:
max_gap_count = self.config_entry.options.get( max_gap_count = period_settings.get(
_const.CONF_BEST_PRICE_MAX_LEVEL_GAP_COUNT, _const.CONF_BEST_PRICE_MAX_LEVEL_GAP_COUNT,
_const.DEFAULT_BEST_PRICE_MAX_LEVEL_GAP_COUNT, _const.DEFAULT_BEST_PRICE_MAX_LEVEL_GAP_COUNT,
) )
@ -574,7 +582,8 @@ class TibberPricesPeriodCalculator:
coordinator_data = {"priceInfo": price_info} coordinator_data = {"priceInfo": price_info}
all_prices = get_intervals_for_day_offsets(coordinator_data, [-2, -1, 0, 1]) all_prices = get_intervals_for_day_offsets(coordinator_data, [-2, -1, 0, 1])
# Get rating thresholds from config # Get rating thresholds from config (flat in options, not in sections)
# CRITICAL: Price rating thresholds are stored FLAT in options (no sections)
threshold_low = self.config_entry.options.get( threshold_low = self.config_entry.options.get(
_const.CONF_PRICE_RATING_THRESHOLD_LOW, _const.CONF_PRICE_RATING_THRESHOLD_LOW,
_const.DEFAULT_PRICE_RATING_THRESHOLD_LOW, _const.DEFAULT_PRICE_RATING_THRESHOLD_LOW,
@ -584,7 +593,8 @@ class TibberPricesPeriodCalculator:
_const.DEFAULT_PRICE_RATING_THRESHOLD_HIGH, _const.DEFAULT_PRICE_RATING_THRESHOLD_HIGH,
) )
# Get volatility thresholds from config # Get volatility thresholds from config (flat in options, not in sections)
# CRITICAL: Volatility thresholds are stored FLAT in options (no sections)
threshold_volatility_moderate = self.config_entry.options.get( threshold_volatility_moderate = self.config_entry.options.get(
_const.CONF_VOLATILITY_THRESHOLD_MODERATE, _const.CONF_VOLATILITY_THRESHOLD_MODERATE,
_const.DEFAULT_VOLATILITY_THRESHOLD_MODERATE, _const.DEFAULT_VOLATILITY_THRESHOLD_MODERATE,
@ -599,7 +609,9 @@ class TibberPricesPeriodCalculator:
) )
# Get relaxation configuration for best price # Get relaxation configuration for best price
enable_relaxation_best = self.config_entry.options.get( # CRITICAL: Relaxation settings are stored in nested section 'relaxation_and_target_periods'
relaxation_and_target_periods = self.config_entry.options.get("relaxation_and_target_periods", {})
enable_relaxation_best = relaxation_and_target_periods.get(
_const.CONF_ENABLE_MIN_PERIODS_BEST, _const.CONF_ENABLE_MIN_PERIODS_BEST,
_const.DEFAULT_ENABLE_MIN_PERIODS_BEST, _const.DEFAULT_ENABLE_MIN_PERIODS_BEST,
) )
@ -611,11 +623,11 @@ class TibberPricesPeriodCalculator:
show_best_price = bool(all_prices) show_best_price = bool(all_prices)
else: else:
show_best_price = self.should_show_periods(price_info, reverse_sort=False) if all_prices else False show_best_price = self.should_show_periods(price_info, reverse_sort=False) if all_prices else False
min_periods_best = self.config_entry.options.get( min_periods_best = relaxation_and_target_periods.get(
_const.CONF_MIN_PERIODS_BEST, _const.CONF_MIN_PERIODS_BEST,
_const.DEFAULT_MIN_PERIODS_BEST, _const.DEFAULT_MIN_PERIODS_BEST,
) )
relaxation_attempts_best = self.config_entry.options.get( relaxation_attempts_best = relaxation_and_target_periods.get(
_const.CONF_RELAXATION_ATTEMPTS_BEST, _const.CONF_RELAXATION_ATTEMPTS_BEST,
_const.DEFAULT_RELAXATION_ATTEMPTS_BEST, _const.DEFAULT_RELAXATION_ATTEMPTS_BEST,
) )
@ -623,12 +635,14 @@ class TibberPricesPeriodCalculator:
# Calculate best price periods (or return empty if filtered) # Calculate best price periods (or return empty if filtered)
if show_best_price: if show_best_price:
best_config = self.get_period_config(reverse_sort=False) best_config = self.get_period_config(reverse_sort=False)
# Get level filter configuration # Get level filter configuration from period_settings section
max_level_best = self.config_entry.options.get( # CRITICAL: max_level and gap_count are stored in nested section 'period_settings'
period_settings = self.config_entry.options.get("period_settings", {})
max_level_best = period_settings.get(
_const.CONF_BEST_PRICE_MAX_LEVEL, _const.CONF_BEST_PRICE_MAX_LEVEL,
_const.DEFAULT_BEST_PRICE_MAX_LEVEL, _const.DEFAULT_BEST_PRICE_MAX_LEVEL,
) )
gap_count_best = self.config_entry.options.get( gap_count_best = period_settings.get(
_const.CONF_BEST_PRICE_MAX_LEVEL_GAP_COUNT, _const.CONF_BEST_PRICE_MAX_LEVEL_GAP_COUNT,
_const.DEFAULT_BEST_PRICE_MAX_LEVEL_GAP_COUNT, _const.DEFAULT_BEST_PRICE_MAX_LEVEL_GAP_COUNT,
) )
@ -672,7 +686,8 @@ class TibberPricesPeriodCalculator:
} }
# Get relaxation configuration for peak price # Get relaxation configuration for peak price
enable_relaxation_peak = self.config_entry.options.get( # CRITICAL: Relaxation settings are stored in nested section 'relaxation_and_target_periods'
enable_relaxation_peak = relaxation_and_target_periods.get(
_const.CONF_ENABLE_MIN_PERIODS_PEAK, _const.CONF_ENABLE_MIN_PERIODS_PEAK,
_const.DEFAULT_ENABLE_MIN_PERIODS_PEAK, _const.DEFAULT_ENABLE_MIN_PERIODS_PEAK,
) )
@ -684,11 +699,11 @@ class TibberPricesPeriodCalculator:
show_peak_price = bool(all_prices) show_peak_price = bool(all_prices)
else: else:
show_peak_price = self.should_show_periods(price_info, reverse_sort=True) if all_prices else False show_peak_price = self.should_show_periods(price_info, reverse_sort=True) if all_prices else False
min_periods_peak = self.config_entry.options.get( min_periods_peak = relaxation_and_target_periods.get(
_const.CONF_MIN_PERIODS_PEAK, _const.CONF_MIN_PERIODS_PEAK,
_const.DEFAULT_MIN_PERIODS_PEAK, _const.DEFAULT_MIN_PERIODS_PEAK,
) )
relaxation_attempts_peak = self.config_entry.options.get( relaxation_attempts_peak = relaxation_and_target_periods.get(
_const.CONF_RELAXATION_ATTEMPTS_PEAK, _const.CONF_RELAXATION_ATTEMPTS_PEAK,
_const.DEFAULT_RELAXATION_ATTEMPTS_PEAK, _const.DEFAULT_RELAXATION_ATTEMPTS_PEAK,
) )
@ -696,12 +711,13 @@ class TibberPricesPeriodCalculator:
# Calculate peak price periods (or return empty if filtered) # Calculate peak price periods (or return empty if filtered)
if show_peak_price: if show_peak_price:
peak_config = self.get_period_config(reverse_sort=True) peak_config = self.get_period_config(reverse_sort=True)
# Get level filter configuration # Get level filter configuration from period_settings section
min_level_peak = self.config_entry.options.get( # CRITICAL: min_level and gap_count are stored in nested section 'period_settings'
min_level_peak = period_settings.get(
_const.CONF_PEAK_PRICE_MIN_LEVEL, _const.CONF_PEAK_PRICE_MIN_LEVEL,
_const.DEFAULT_PEAK_PRICE_MIN_LEVEL, _const.DEFAULT_PEAK_PRICE_MIN_LEVEL,
) )
gap_count_peak = self.config_entry.options.get( gap_count_peak = period_settings.get(
_const.CONF_PEAK_PRICE_MAX_LEVEL_GAP_COUNT, _const.CONF_PEAK_PRICE_MAX_LEVEL_GAP_COUNT,
_const.DEFAULT_PEAK_PRICE_MAX_LEVEL_GAP_COUNT, _const.DEFAULT_PEAK_PRICE_MAX_LEVEL_GAP_COUNT,
) )

View file

@ -289,6 +289,8 @@ class TibberPricesSensor(TibberPricesEntity, RestoreSensor):
# Clear cached trend values when coordinator data changes # Clear cached trend values when coordinator data changes
if self.entity_description.key.startswith("price_trend_"): if self.entity_description.key.startswith("price_trend_"):
self._trend_calculator.clear_trend_cache() self._trend_calculator.clear_trend_cache()
# Also clear calculation cache (e.g., when threshold config changes)
self._trend_calculator.clear_calculation_cache()
# Refresh chart data when coordinator updates (new price data or user data) # Refresh chart data when coordinator updates (new price data or user data)
if self.entity_description.key == "chart_data_export": if self.entity_description.key == "chart_data_export":

View file

@ -146,12 +146,12 @@ def _calculate_metadata( # noqa: PLR0912, PLR0913, PLR0915
return {} return {}
min_val = min(data) min_val = min(data)
max_val = max(data) max_val = max(data)
avg_val = sum(data) / len(data) mean_val = sum(data) / len(data)
median_val = sorted(data)[len(data) // 2] median_val = sorted(data)[len(data) // 2]
# Calculate avg_position and median_position (0-1 scale) # Calculate mean_position and median_position (0-1 scale)
price_range = max_val - min_val price_range = max_val - min_val
avg_position = (avg_val - min_val) / price_range if price_range > 0 else 0.5 mean_position = (mean_val - min_val) / price_range if price_range > 0 else 0.5
median_position = (median_val - min_val) / price_range if price_range > 0 else 0.5 median_position = (median_val - min_val) / price_range if price_range > 0 else 0.5
# Position precision: 2 decimals for subunit currency, 4 for base currency # Position precision: 2 decimals for subunit currency, 4 for base currency
@ -162,8 +162,8 @@ def _calculate_metadata( # noqa: PLR0912, PLR0913, PLR0915
return { return {
"min": round(min_val, price_decimals), "min": round(min_val, price_decimals),
"max": round(max_val, price_decimals), "max": round(max_val, price_decimals),
"avg": round(avg_val, price_decimals), "mean": round(mean_val, price_decimals),
"avg_position": round(avg_position, position_decimals), "mean_position": round(mean_position, position_decimals),
"median": round(median_val, price_decimals), "median": round(median_val, price_decimals),
"median_position": round(median_position, position_decimals), "median_position": round(median_position, position_decimals),
} }

View file

@ -132,8 +132,22 @@
"options": { "options": {
"step": { "step": {
"init": { "init": {
"menu_options": {
"general_settings": "⚙️ Allgemeine Einstellungen",
"display_settings": "💱 Währungsanzeige",
"current_interval_price_rating": "📊 Preisbewertung",
"volatility": "💨 Preis-Volatilität",
"best_price": "💚 Bestpreis",
"peak_price": "🔴 Spitzenpreis",
"price_trend": "📈 Preistrend",
"chart_data_export": "📊 Diagrammdaten-Export",
"reset_to_defaults": "🔄 Auf Werkseinstellungen zurücksetzen",
"finish": "⬅️ Zurück"
}
},
"general_settings": {
"title": "⚙️ Allgemeine Einstellungen", "title": "⚙️ Allgemeine Einstellungen",
"description": "_{step_progress}_\n\n**Konfiguriere allgemeine Einstellungen für Tibber-Preisinformationen und -bewertungen.**\n\n---\n\n**Benutzer:** {user_login}", "description": "**Konfiguriere allgemeine Einstellungen für Tibber-Preisinformationen und -bewertungen.**\n\n---\n\n**Benutzer:** {user_login}",
"data": { "data": {
"extended_descriptions": "Erweiterte Beschreibungen", "extended_descriptions": "Erweiterte Beschreibungen",
"average_sensor_display": "Durchschnittsensor-Anzeige" "average_sensor_display": "Durchschnittsensor-Anzeige"
@ -142,43 +156,35 @@
"extended_descriptions": "Steuert, ob Entitätsattribute ausführliche Erklärungen und Nutzungstipps enthalten.\n\n• Deaktiviert (Standard): Nur kurze Beschreibung\n• Aktiviert: Ausführliche Erklärung + praktische Nutzungsbeispiele\n\nBeispiel:\nDeaktiviert = 1 Attribut\nAktiviert = 2 zusätzliche Attribute", "extended_descriptions": "Steuert, ob Entitätsattribute ausführliche Erklärungen und Nutzungstipps enthalten.\n\n• Deaktiviert (Standard): Nur kurze Beschreibung\n• Aktiviert: Ausführliche Erklärung + praktische Nutzungsbeispiele\n\nBeispiel:\nDeaktiviert = 1 Attribut\nAktiviert = 2 zusätzliche Attribute",
"average_sensor_display": "Wähle aus, welcher statistische Wert im Sensorstatus für Durchschnitts-Preissensoren angezeigt wird. Der andere Wert wird als Attribut angezeigt. Der Median ist resistenter gegen Extremwerte, während das arithmetische Mittel dem traditionellen Durchschnitt entspricht. Standard: Median" "average_sensor_display": "Wähle aus, welcher statistische Wert im Sensorstatus für Durchschnitts-Preissensoren angezeigt wird. Der andere Wert wird als Attribut angezeigt. Der Median ist resistenter gegen Extremwerte, während das arithmetische Mittel dem traditionellen Durchschnitt entspricht. Standard: Median"
}, },
"submit": "Weiter →" "submit": "↩ Speichern & Zurück"
}, },
"display_settings": { "display_settings": {
"title": "💱 Währungsanzeige-Einstellungen", "title": "💱 Währungsanzeige-Einstellungen",
"description": "_{step_progress}_\n\n**Konfiguriere, wie Strompreise angezeigt werden - in Basiswährung (€, kr) oder Unterwährungseinheit (ct, øre).**\n\n---", "description": "**Konfiguriere, wie Strompreise angezeigt werden - in Basiswährung (€, kr) oder Unterwährungseinheit (ct, øre).**\n\n---",
"data": { "data": {
"currency_display_mode": "Anzeigemodus" "currency_display_mode": "Anzeigemodus"
}, },
"data_description": { "data_description": {
"currency_display_mode": "Wähle, wie Preise angezeigt werden:\n\n• **Basiswährung** (€/kWh, kr/kWh): Dezimalwerte (z.B. 0,25 €/kWh) - Unterschiede sichtbar ab 3.-4. Nachkommastelle\n• **Unterwährungseinheit** (ct/kWh, øre/kWh): Größere Werte (z.B. 25,00 ct/kWh) - Unterschiede bereits ab 1. Nachkommastelle sichtbar\n\nStandard abhängig von deiner Währung:\n• EUR → Unterwährungseinheit (Cent) - deutsche/niederländische Präferenz\n• NOK/SEK/DKK → Basiswährung (Kronen) - skandinavische Präferenz\n• USD/GBP → Basiswährung\n\n**💡 Tipp:** Bei Auswahl von Unterwährungseinheit kannst du den zusätzlichen Sensor \"Aktueller Strompreis (Energie-Dashboard)\" aktivieren (standardmäßig deaktiviert)." "currency_display_mode": "Wähle, wie Preise angezeigt werden:\n\n• **Basiswährung** (€/kWh, kr/kWh): Dezimalwerte (z.B. 0,25 €/kWh) - Unterschiede sichtbar ab 3.-4. Nachkommastelle\n• **Unterwährungseinheit** (ct/kWh, øre/kWh): Größere Werte (z.B. 25,00 ct/kWh) - Unterschiede bereits ab 1. Nachkommastelle sichtbar\n\nStandard abhängig von deiner Währung:\n• EUR → Unterwährungseinheit (Cent) - deutsche/niederländische Präferenz\n• NOK/SEK/DKK → Basiswährung (Kronen) - skandinavische Präferenz\n• USD/GBP → Basiswährung\n\n**💡 Tipp:** Bei Auswahl von Unterwährungseinheit kannst du den zusätzlichen Sensor \"Aktueller Strompreis (Energie-Dashboard)\" aktivieren (standardmäßig deaktiviert)."
}, },
"submit": "Weiter →" "submit": "↩ Speichern & Zurück"
}, },
"current_interval_price_rating": { "current_interval_price_rating": {
"title": "📊 Preisbewertungs-Schwellenwerte", "title": "📊 Preisbewertungs-Schwellenwerte",
"description": "_{step_progress}_\n\n**Konfiguriere Schwellenwerte für Preisbewertungsstufen (niedrig/normal/hoch) basierend auf dem Vergleich mit dem nachlaufenden 24-Stunden-Durchschnitt.**\n\n---", "description": "**Konfiguriere Schwellenwerte für Preisbewertungsstufen (niedrig/normal/hoch) basierend auf dem Vergleich mit dem nachlaufenden 24-Stunden-Durchschnitt.**",
"sections": {
"price_rating_thresholds": {
"name": "Preisbewertungs-Schwellenwerte",
"description": "Definiere die Einstufungen für die Preisbewertung.",
"data": { "data": {
"price_rating_threshold_low": "Niedrig-Schwelle", "price_rating_threshold_low": "Niedrig-Schwelle",
"price_rating_threshold_high": "Hoch-Schwelle", "price_rating_threshold_high": "Hoch-Schwelle"
"average_sensor_display": "Durchschnitts-Sensor Anzeige"
}, },
"data_description": { "data_description": {
"price_rating_threshold_low": "Prozentwert, um wie viel der aktuelle Preis unter dem nachlaufenden 24-Stunden-Durchschnitt liegen muss, damit er als 'niedrig' bewertet wird. Beispiel: 5 bedeutet mindestens 5% unter Durchschnitt. Sensoren mit dieser Bewertung zeigen günstige Zeitfenster an. Standard: 5%", "price_rating_threshold_low": "Prozentwert, um wie viel der aktuelle Preis unter dem nachlaufenden 24-Stunden-Durchschnitt liegen muss, damit er als 'niedrig' bewertet wird. Beispiel: 5 bedeutet mindestens 5% unter Durchschnitt. Sensoren mit dieser Bewertung zeigen günstige Zeitfenster an. Standard: 5%",
"price_rating_threshold_high": "Prozentwert, um wie viel der aktuelle Preis über dem nachlaufenden 24-Stunden-Durchschnitt liegen muss, damit er als 'hoch' bewertet wird. Beispiel: 10 bedeutet mindestens 10% über Durchschnitt. Sensoren mit dieser Bewertung warnen vor teuren Zeitfenstern. Standard: 10%", "price_rating_threshold_high": "Prozentwert, um wie viel der aktuelle Preis über dem nachlaufenden 24-Stunden-Durchschnitt liegen muss, damit er als 'hoch' bewertet wird. Beispiel: 10 bedeutet mindestens 10% über Durchschnitt. Sensoren mit dieser Bewertung warnen vor teuren Zeitfenstern. Standard: 10%"
"average_sensor_display": "Wähle, welches statistische Maß im Sensor-Status für Durchschnittspreissensoren angezeigt werden soll. Der andere Wert wird als Attribut angezeigt. Der Median ist widerstandsfähiger gegen Extremwerte, während das arithmetische Mittel den traditionellen Durchschnitt darstellt. Standard: Median"
}
}
}, },
"submit": "Weiter →" "submit": "↩ Speichern & Zurück"
}, },
"best_price": { "best_price": {
"title": "💚 Bestpreis-Zeitraum Einstellungen", "title": "💚 Bestpreis-Zeitraum Einstellungen",
"description": "_{step_progress}_\n\n**Konfiguration für den Bestpreis-Zeitraum mit den niedrigsten Strompreisen.**\n\n---", "description": "**Konfiguration für den Bestpreis-Zeitraum mit den niedrigsten Strompreisen.**\n\n---",
"sections": { "sections": {
"period_settings": { "period_settings": {
"name": "Zeitraumdauer & Preisniveaus", "name": "Zeitraumdauer & Preisniveaus",
@ -221,11 +227,11 @@
} }
} }
}, },
"submit": "Weiter →" "submit": "↩ Speichern & Zurück"
}, },
"peak_price": { "peak_price": {
"title": "🔴 Spitzenpreis-Zeitraum Einstellungen", "title": "🔴 Spitzenpreis-Zeitraum Einstellungen",
"description": "_{step_progress}_\n\n**Konfiguration für den Spitzenpreis-Zeitraum mit den höchsten Strompreisen.**\n\n---", "description": "**Konfiguration für den Spitzenpreis-Zeitraum mit den höchsten Strompreisen.**\n\n---",
"sections": { "sections": {
"period_settings": { "period_settings": {
"name": "Zeitraum-Einstellungen", "name": "Zeitraum-Einstellungen",
@ -268,15 +274,11 @@
} }
} }
}, },
"submit": "Weiter →" "submit": "↩ Speichern & Zurück"
}, },
"price_trend": { "price_trend": {
"title": "📈 Preistrend-Schwellenwerte", "title": "📈 Preistrend-Schwellenwerte",
"description": "_{step_progress}_\n\n**Konfiguriere 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.**\n\n---", "description": "**Konfiguriere 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.**",
"sections": {
"price_trend_thresholds": {
"name": "Preistrend-Schwellenwerte",
"description": "Definiere die Einstufungen für den Preistrend.",
"data": { "data": {
"price_trend_threshold_rising": "Steigend-Schwelle", "price_trend_threshold_rising": "Steigend-Schwelle",
"price_trend_threshold_falling": "Fallend-Schwelle" "price_trend_threshold_falling": "Fallend-Schwelle"
@ -284,18 +286,12 @@
"data_description": { "data_description": {
"price_trend_threshold_rising": "Prozentwert, um wie viel der Durchschnitt der nächsten N Stunden über dem aktuellen Preis liegen muss, damit der Trend als 'steigend' gilt. Beispiel: 5 bedeutet Durchschnitt ist mindestens 5% höher → Preise werden steigen. Typische Werte: 5-15%. Standard: 5%", "price_trend_threshold_rising": "Prozentwert, um wie viel der Durchschnitt der nächsten N Stunden über dem aktuellen Preis liegen muss, damit der Trend als 'steigend' gilt. Beispiel: 5 bedeutet Durchschnitt ist mindestens 5% höher → Preise werden steigen. Typische Werte: 5-15%. Standard: 5%",
"price_trend_threshold_falling": "Prozentwert (negativ), um wie viel der Durchschnitt der nächsten N Stunden unter dem aktuellen Preis liegen muss, damit der Trend als 'fallend' gilt. Beispiel: -5 bedeutet Durchschnitt ist mindestens 5% niedriger → Preise werden fallen. Typische Werte: -5 bis -15%. Standard: -5%" "price_trend_threshold_falling": "Prozentwert (negativ), um wie viel der Durchschnitt der nächsten N Stunden unter dem aktuellen Preis liegen muss, damit der Trend als 'fallend' gilt. Beispiel: -5 bedeutet Durchschnitt ist mindestens 5% niedriger → Preise werden fallen. Typische Werte: -5 bis -15%. Standard: -5%"
}
}
}, },
"submit": "Weiter →" "submit": "↩ Speichern & Zurück"
}, },
"volatility": { "volatility": {
"title": "💨 Volatilität Schwellenwerte", "title": "💨 Volatilität Schwellenwerte",
"description": "_{step_progress}_\n\n**Konfiguriere Schwellenwerte für die Volatilitätsklassifizierung.** Volatilität misst relative Preisschwankungen anhand des Variationskoeffizienten (VK = Standardabweichung / Durchschnitt × 100%). Diese Schwellenwerte sind Prozentwerte, die für alle Preisniveaus funktionieren.\n\nVerwendet von:\n• Volatilitätssensoren (Klassifizierung)\n• Trend-Sensoren (adaptive Schwellenanpassung: &lt;moderat = empfindlicher, ≥hoch = weniger empfindlich)\n\n---", "description": "**Konfiguriere Schwellenwerte für die Volatilitätsklassifizierung.** Volatilität misst relative Preisschwankungen anhand des Variationskoeffizienten (VK = Standardabweichung / Durchschnitt × 100%). Diese Schwellenwerte sind Prozentwerte, die für alle Preisniveaus funktionieren.\n\nVerwendet von:\n• Volatilitätssensoren (Klassifizierung)\n• Trend-Sensoren (adaptive Schwellenanpassung: &lt;moderat = empfindlicher, ≥hoch = weniger empfindlich)",
"sections": {
"volatility_thresholds": {
"name": "Volatilitätsschwellen",
"description": "Definiere Volatilitäts-Klassifizierungsstufen.",
"data": { "data": {
"volatility_threshold_moderate": "Moderat-Schwelle", "volatility_threshold_moderate": "Moderat-Schwelle",
"volatility_threshold_high": "Hoch-Schwelle", "volatility_threshold_high": "Hoch-Schwelle",
@ -305,15 +301,21 @@
"volatility_threshold_moderate": "Variationskoeffizient (VK) ab dem Preise als 'moderat volatil' gelten. VK = (Standardabweichung / Durchschnitt) × 100%. Beispiel: 15 bedeutet Preisschwankungen von ±15% um den Durchschnitt. Sensoren zeigen diese Klassifizierung an, Trend-Sensoren werden empfindlicher. Standard: 15%", "volatility_threshold_moderate": "Variationskoeffizient (VK) ab dem Preise als 'moderat volatil' gelten. VK = (Standardabweichung / Durchschnitt) × 100%. Beispiel: 15 bedeutet Preisschwankungen von ±15% um den Durchschnitt. Sensoren zeigen diese Klassifizierung an, Trend-Sensoren werden empfindlicher. Standard: 15%",
"volatility_threshold_high": "Variationskoeffizient (VK) ab dem Preise als 'hoch volatil' gelten. Beispiel: 30 bedeutet Preisschwankungen von ±30% um den Durchschnitt. Größere Preissprünge erwartet, Trend-Sensoren werden weniger empfindlich. Standard: 30%", "volatility_threshold_high": "Variationskoeffizient (VK) ab dem Preise als 'hoch volatil' gelten. Beispiel: 30 bedeutet Preisschwankungen von ±30% um den Durchschnitt. Größere Preissprünge erwartet, Trend-Sensoren werden weniger empfindlich. Standard: 30%",
"volatility_threshold_very_high": "Variationskoeffizient (VK) ab dem Preise als 'sehr hoch volatil' gelten. Beispiel: 50 bedeutet extreme Preisschwankungen von ±50% um den Durchschnitt. An solchen Tagen sind starke Preisspitzen wahrscheinlich. Standard: 50%" "volatility_threshold_very_high": "Variationskoeffizient (VK) ab dem Preise als 'sehr hoch volatil' gelten. Beispiel: 50 bedeutet extreme Preisschwankungen von ±50% um den Durchschnitt. An solchen Tagen sind starke Preisspitzen wahrscheinlich. Standard: 50%"
}
}
}, },
"submit": "Weiter →" "submit": "↩ Speichern & Zurück"
}, },
"chart_data_export": { "chart_data_export": {
"title": "📊 Chart Data Export Sensor", "title": "📊 Chart Data Export Sensor",
"description": "_{step_progress}_\n\nDer Chart Data Export Sensor stellt Preisdaten als Sensor-Attribute zur Verfügung.\n\n⚠ **Hinweis:** Dieser Sensor ist ein Legacy-Feature für Kompatibilität mit älteren Tools.\n\n**Für neue Setups empfohlen:** Nutze den `tibber_prices.get_chartdata` **Service direkt** - er ist flexibler, effizienter und der moderne Home Assistant-Ansatz.\n\n**Wann dieser Sensor sinnvoll ist:**\n\n✅ Dein Dashboard-Tool kann **nur** Attribute lesen (keine Service-Aufrufe)\n✅ Du brauchst statische Daten, die automatisch aktualisiert werden\n❌ **Nicht für Automationen:** Nutze dort direkt `tibber_prices.get_chartdata` - flexibler und effizienter!\n\n---\n\n**Sensor aktivieren:**\n\n1. Öffne **Einstellungen → Geräte & Dienste → Tibber Prices**\n2. Wähle dein Home → Finde **'Chart Data Export'** (Diagnose-Bereich)\n3. **Aktiviere den Sensor** (standardmäßig deaktiviert)\n\n**Konfiguration (optional):**\n\nStandardeinstellung funktioniert sofort (heute+morgen, 15-Minuten-Intervalle, reine Preise).\n\nFür Anpassungen füge in **`configuration.yaml`** ein:\n\n```yaml\ntibber_prices:\n chart_export:\n day:\n - today\n - tomorrow\n include_level: true\n include_rating_level: true\n```\n\n**Alle Parameter:** Siehe `tibber_prices.get_chartdata` Service-Dokumentation", "description": "Der Chart Data Export Sensor stellt Preisdaten als Sensor-Attribute zur Verfügung.\n\n⚠ **Hinweis:** Dieser Sensor ist ein Legacy-Feature für Kompatibilität mit älteren Tools.\n\n**Für neue Setups empfohlen:** Nutze den `tibber_prices.get_chartdata` **Service direkt** - er ist flexibler, effizienter und der moderne Home Assistant-Ansatz.\n\n**Wann dieser Sensor sinnvoll ist:**\n\n✅ Dein Dashboard-Tool kann **nur** Attribute lesen (keine Service-Aufrufe)\n✅ Du brauchst statische Daten, die automatisch aktualisiert werden\n❌ **Nicht für Automationen:** Nutze dort direkt `tibber_prices.get_chartdata` - flexibler und effizienter!\n\n---\n\n**Sensor aktivieren:**\n\n1. Öffne **Einstellungen → Geräte & Dienste → Tibber Prices**\n2. Wähle dein Home → Finde **'Chart Data Export'** (Diagnose-Bereich)\n3. **Aktiviere den Sensor** (standardmäßig deaktiviert)\n\n**Konfiguration (optional):**\n\nStandardeinstellung funktioniert sofort (heute+morgen, 15-Minuten-Intervalle, reine Preise).\n\nFür Anpassungen füge in **`configuration.yaml`** ein:\n\n```yaml\ntibber_prices:\n chart_export:\n day:\n - today\n - tomorrow\n include_level: true\n include_rating_level: true\n```\n\n**Alle Parameter:** Siehe `tibber_prices.get_chartdata` Service-Dokumentation",
"submit": "Abschließen ✓" "submit": "↩ Ok & Zurück"
},
"reset_to_defaults": {
"title": "🔄 Auf Werkseinstellungen zurücksetzen",
"description": "⚠️ **Warnung:** Dies setzt **ALLE** Einstellungen auf Werkseinstellungen zurück.\n\n**Was wird zurückgesetzt:**\n• Alle Preisbewertungs-Schwellwerte\n• Alle Volatilitäts-Schwellwerte\n• Alle Preistrend-Schwellwerte\n• Alle Einstellungen für Best-Price-Perioden\n• Alle Einstellungen für Peak-Price-Perioden\n• Anzeigeeinstellungen\n• Allgemeine Einstellungen\n\n**Was wird NICHT zurückgesetzt:**\n• Dein Tibber API-Token\n• Ausgewähltes Zuhause\n• Währung\n\n**💡 Tipp:** Nützlich, wenn du nach dem Experimentieren mit Einstellungen neu beginnen möchtest.",
"data": {
"confirm_reset": "Ja, alles auf Werkseinstellungen zurücksetzen"
},
"submit": "Jetzt zurücksetzen"
} }
}, },
"error": { "error": {
@ -341,7 +343,10 @@
"invalid_price_trend_falling": "Fallender Trendschwellenwert muss zwischen -50% und -1% liegen" "invalid_price_trend_falling": "Fallender Trendschwellenwert muss zwischen -50% und -1% liegen"
}, },
"abort": { "abort": {
"entry_not_found": "Tibber Konfigurationseintrag nicht gefunden." "entry_not_found": "Tibber Konfigurationseintrag nicht gefunden.",
"reset_cancelled": "Zurücksetzen abgebrochen. Es wurden keine Änderungen an deiner Konfiguration vorgenommen.",
"reset_successful": "✅ Alle Einstellungen wurden auf Werkseinstellungen zurückgesetzt. Deine Konfiguration ist jetzt wie bei einer frischen Installation.",
"finished": "Konfiguration abgeschlossen."
} }
}, },
"entity": { "entity": {

View file

@ -132,8 +132,22 @@
"options": { "options": {
"step": { "step": {
"init": { "init": {
"menu_options": {
"general_settings": "⚙️ General Settings",
"display_settings": "💱 Currency Display",
"current_interval_price_rating": "📊 Price Rating",
"volatility": "💨 Price Volatility",
"best_price": "💚 Best Price Period",
"peak_price": "🔴 Peak Price Period",
"price_trend": "📈 Price Trend",
"chart_data_export": "📊 Chart Data Export Sensor",
"reset_to_defaults": "🔄 Reset to Defaults",
"finish": "⬅️ Back"
}
},
"general_settings": {
"title": "⚙️ General Settings", "title": "⚙️ General Settings",
"description": "_{step_progress}_\n\n**Configure general settings for Tibber Price Information & Ratings.**\n\n---\n\n**User:** {user_login}", "description": "**Configure general settings for Tibber Price Information & Ratings.**\n\n---\n\n**User:** {user_login}",
"data": { "data": {
"extended_descriptions": "Extended Descriptions", "extended_descriptions": "Extended Descriptions",
"average_sensor_display": "Average Sensor Display" "average_sensor_display": "Average Sensor Display"
@ -142,26 +156,22 @@
"extended_descriptions": "Controls whether entity attributes include detailed explanations and usage tips.\n\n• Disabled (default): Brief description only\n• Enabled: Detailed explanation + practical usage examples\n\nExample:\nDisabled = 1 attribute\nEnabled = 2 additional attributes", "extended_descriptions": "Controls whether entity attributes include detailed explanations and usage tips.\n\n• Disabled (default): Brief description only\n• Enabled: Detailed explanation + practical usage examples\n\nExample:\nDisabled = 1 attribute\nEnabled = 2 additional attributes",
"average_sensor_display": "Choose which statistical measure to display in the sensor state for average price sensors. The other value will be shown as an attribute. Median is more resistant to extreme values, while arithmetic mean represents the traditional average. Default: Median" "average_sensor_display": "Choose which statistical measure to display in the sensor state for average price sensors. The other value will be shown as an attribute. Median is more resistant to extreme values, while arithmetic mean represents the traditional average. Default: Median"
}, },
"submit": "Continue →" "submit": "↩ Save & Back"
}, },
"display_settings": { "display_settings": {
"title": "💱 Currency Display Settings", "title": "💱 Currency Display Settings",
"description": "_{step_progress}_\n\n**Configure how electricity prices are displayed - in base currency (€, kr) or subunit (ct, øre).**\n\n---", "description": "**Configure how electricity prices are displayed - in base currency (€, kr) or subunit (ct, øre).**\n\n---",
"data": { "data": {
"currency_display_mode": "Display Mode" "currency_display_mode": "Display Mode"
}, },
"data_description": { "data_description": {
"currency_display_mode": "Choose how prices are displayed:\n\n• **Base Currency** (€/kWh, kr/kWh): Decimal values (e.g., 0.25 €/kWh) - differences visible from 3rd-4th decimal place\n• **Subunit Currency** (ct/kWh, øre/kWh): Larger values (e.g., 25.00 ct/kWh) - differences visible from 1st decimal place\n\nDefault depends on your currency:\n• EUR → Subunit (cents) - German/Dutch preference\n• NOK/SEK/DKK → Base (kroner) - Scandinavian preference\n• USD/GBP → Base currency\n\n**💡 Tip:** When selecting Subunit Currency, you can enable the additional \"Current Electricity Price (Energy Dashboard)\" sensor (disabled by default)." "currency_display_mode": "Choose how prices are displayed:\n\n• **Base Currency** (€/kWh, kr/kWh): Decimal values (e.g., 0.25 €/kWh) - differences visible from 3rd-4th decimal place\n• **Subunit Currency** (ct/kWh, øre/kWh): Larger values (e.g., 25.00 ct/kWh) - differences visible from 1st decimal place\n\nDefault depends on your currency:\n• EUR → Subunit (cents) - German/Dutch preference\n• NOK/SEK/DKK → Base (kroner) - Scandinavian preference\n• USD/GBP → Base currency\n\n**💡 Tip:** When selecting Subunit Currency, you can enable the additional \"Current Electricity Price (Energy Dashboard)\" sensor (disabled by default)."
}, },
"submit": "Continue →" "submit": "↩ Save & Back"
}, },
"current_interval_price_rating": { "current_interval_price_rating": {
"title": "📊 Price Rating Thresholds", "title": "📊 Price Rating Thresholds",
"description": "_{step_progress}_\n\n**Configure thresholds for price rating levels (low/normal/high) based on comparison with trailing 24-hour average.**\n\n---", "description": "**Configure thresholds for price rating levels (low/normal/high) based on comparison with trailing 24-hour average.**",
"sections": {
"price_rating_thresholds": {
"name": "Price Rating Thresholds",
"description": "Define price rating classification levels.",
"data": { "data": {
"price_rating_threshold_low": "Low Threshold", "price_rating_threshold_low": "Low Threshold",
"price_rating_threshold_high": "High Threshold" "price_rating_threshold_high": "High Threshold"
@ -169,14 +179,12 @@
"data_description": { "data_description": {
"price_rating_threshold_low": "Percentage below the trailing 24-hour average that the current price must be to qualify as 'low' rating. Example: 5 means at least 5% below average. Sensors with this rating indicate favorable time windows. Default: 5%", "price_rating_threshold_low": "Percentage below the trailing 24-hour average that the current price must be to qualify as 'low' rating. Example: 5 means at least 5% below average. Sensors with this rating indicate favorable time windows. Default: 5%",
"price_rating_threshold_high": "Percentage above the trailing 24-hour average that the current price must be to qualify as 'high' rating. Example: 10 means at least 10% above average. Sensors with this rating warn about expensive time windows. Default: 10%" "price_rating_threshold_high": "Percentage above the trailing 24-hour average that the current price must be to qualify as 'high' rating. Example: 10 means at least 10% above average. Sensors with this rating warn about expensive time windows. Default: 10%"
}
}
}, },
"submit": "Continue →" "submit": "↩ Save & Back"
}, },
"best_price": { "best_price": {
"title": "💚 Best Price Period Settings", "title": "💚 Best Price Period Settings",
"description": "_{step_progress}_\n\n**Configure settings for the Best Price Period binary sensor. This sensor is active during periods with the lowest electricity prices.**\n\n---", "description": "**Configure settings for the Best Price Period binary sensor. This sensor is active during periods with the lowest electricity prices.**\n\n---",
"sections": { "sections": {
"period_settings": { "period_settings": {
"name": "Period Duration & Levels", "name": "Period Duration & Levels",
@ -219,11 +227,11 @@
} }
} }
}, },
"submit": "Continue →" "submit": "↩ Save & Back"
}, },
"peak_price": { "peak_price": {
"title": "🔴 Peak Price Period Settings", "title": "🔴 Peak Price Period Settings",
"description": "_{step_progress}_\n\n**Configure settings for the Peak Price Period binary sensor. This sensor is active during periods with the highest electricity prices.**\n\n---", "description": "**Configure settings for the Peak Price Period binary sensor. This sensor is active during periods with the highest electricity prices.**\n\n---",
"sections": { "sections": {
"period_settings": { "period_settings": {
"name": "Period Settings", "name": "Period Settings",
@ -266,15 +274,11 @@
} }
} }
}, },
"submit": "Continue →" "submit": "↩ Save & Back"
}, },
"price_trend": { "price_trend": {
"title": "📈 Price Trend Thresholds", "title": "📈 Price Trend Thresholds",
"description": "_{step_progress}_\n\n**Configure thresholds for price trend sensors. These sensors compare current price with the average of the next N hours to determine if prices are rising, falling, or stable.**\n\n---", "description": "**Configure thresholds for price trend sensors. These sensors compare current price with the average of the next N hours to determine if prices are rising, falling, or stable.**",
"sections": {
"price_trend_thresholds": {
"name": "Price Trend Thresholds",
"description": "Define price trend classification levels.",
"data": { "data": {
"price_trend_threshold_rising": "Rising Threshold", "price_trend_threshold_rising": "Rising Threshold",
"price_trend_threshold_falling": "Falling Threshold" "price_trend_threshold_falling": "Falling Threshold"
@ -282,23 +286,12 @@
"data_description": { "data_description": {
"price_trend_threshold_rising": "Percentage that the average of the next N hours must be above the current price to qualify as 'rising' trend. Example: 5 means average is at least 5% higher → prices will rise. Typical values: 5-15%. Default: 5%", "price_trend_threshold_rising": "Percentage that the average of the next N hours must be above the current price to qualify as 'rising' trend. Example: 5 means average is at least 5% higher → prices will rise. Typical values: 5-15%. Default: 5%",
"price_trend_threshold_falling": "Percentage (negative) that the average of the next N hours must be below the current price to qualify as 'falling' trend. Example: -5 means average is at least 5% lower → prices will fall. Typical values: -5 to -15%. Default: -5%" "price_trend_threshold_falling": "Percentage (negative) that the average of the next N hours must be below the current price to qualify as 'falling' trend. Example: -5 means average is at least 5% lower → prices will fall. Typical values: -5 to -15%. Default: -5%"
}
}
}, },
"submit": "Continue →" "submit": "↩ Save & Back"
},
"chart_data_export": {
"title": "📊 Chart Data Export Sensor",
"description": "_{step_progress}_\n\nThe Chart Data Export Sensor provides price data as sensor attributes.\n\n⚠ **Note:** This sensor is a legacy feature for compatibility with older tools.\n\n**Recommended for new setups:** Use the `tibber_prices.get_chartdata` **service directly** - it's more flexible, efficient, and the modern Home Assistant approach.\n\n**When this sensor makes sense:**\n\n✅ Your dashboard tool can **only** read attributes (no service calls)\n✅ You need static data that updates automatically\n❌ **Not for automations:** Use `tibber_prices.get_chartdata` directly there - more flexible and efficient!\n\n---\n\n**Enable the sensor:**\n\n1. Open **Settings → Devices & Services → Tibber Prices**\n2. Select your home → Find **'Chart Data Export'** (Diagnostic section)\n3. **Enable the sensor** (disabled by default)\n\n**Configuration (optional):**\n\nDefault settings work out-of-the-box (today+tomorrow, 15-minute intervals, prices only).\n\nFor customization, add to **`configuration.yaml`**:\n\n```yaml\ntibber_prices:\n chart_export:\n day:\n - today\n - tomorrow\n include_level: true\n include_rating_level: true\n```\n\n**All parameters:** See `tibber_prices.get_chartdata` service documentation",
"submit": "Complete ✓"
}, },
"volatility": { "volatility": {
"title": "💨 Price Volatility Thresholds", "title": "💨 Price Volatility Thresholds",
"description": "_{step_progress}_\n\n**Configure thresholds for volatility classification.** Volatility measures relative price variation using the coefficient of variation (CV = standard deviation / mean × 100%). These thresholds are percentage values that work across all price levels.\n\nUsed by:\n• Volatility sensors (classification)\n• Trend sensors (adaptive threshold adjustment: &lt;moderate = more sensitive, ≥high = less sensitive)\n\n---", "description": "**Configure thresholds for volatility classification.** Volatility measures relative price variation using the coefficient of variation (CV = standard deviation / mean × 100%). These thresholds are percentage values that work across all price levels.\n\nUsed by:\n• Volatility sensors (classification)\n• Trend sensors (adaptive threshold adjustment: &lt;moderate = more sensitive, ≥high = less sensitive)",
"sections": {
"volatility_thresholds": {
"name": "Volatility Thresholds",
"description": "Define price volatility classification levels.",
"data": { "data": {
"volatility_threshold_moderate": "Moderate Threshold", "volatility_threshold_moderate": "Moderate Threshold",
"volatility_threshold_high": "High Threshold", "volatility_threshold_high": "High Threshold",
@ -308,10 +301,21 @@
"volatility_threshold_moderate": "Coefficient of Variation (CV) at which prices are considered 'moderately volatile'. CV = (standard deviation / mean) × 100%. Example: 15 means price fluctuations of ±15% around average. Sensors show this classification, trend sensors become more sensitive. Default: 15%", "volatility_threshold_moderate": "Coefficient of Variation (CV) at which prices are considered 'moderately volatile'. CV = (standard deviation / mean) × 100%. Example: 15 means price fluctuations of ±15% around average. Sensors show this classification, trend sensors become more sensitive. Default: 15%",
"volatility_threshold_high": "Coefficient of Variation (CV) at which prices are considered 'highly volatile'. Example: 30 means price fluctuations of ±30% around average. Larger price jumps expected, trend sensors become less sensitive. Default: 30%", "volatility_threshold_high": "Coefficient of Variation (CV) at which prices are considered 'highly volatile'. Example: 30 means price fluctuations of ±30% around average. Larger price jumps expected, trend sensors become less sensitive. Default: 30%",
"volatility_threshold_very_high": "Coefficient of Variation (CV) at which prices are considered 'very highly volatile'. Example: 50 means extreme price fluctuations of ±50% around average. On such days, strong price spikes are likely. Default: 50%" "volatility_threshold_very_high": "Coefficient of Variation (CV) at which prices are considered 'very highly volatile'. Example: 50 means extreme price fluctuations of ±50% around average. On such days, strong price spikes are likely. Default: 50%"
}
}
}, },
"submit": "Continue →" "submit": "↩ Save & Back"
},
"chart_data_export": {
"title": "📊 Chart Data Export Sensor",
"description": "The Chart Data Export Sensor provides price data as sensor attributes.\n\n⚠ **Note:** This sensor is a legacy feature for compatibility with older tools.\n\n**Recommended for new setups:** Use the `tibber_prices.get_chartdata` **service directly** - it's more flexible, efficient, and the modern Home Assistant approach.\n\n**When this sensor makes sense:**\n\n✅ Your dashboard tool can **only** read attributes (no service calls)\n✅ You need static data that updates automatically\n❌ **Not for automations:** Use `tibber_prices.get_chartdata` directly there - more flexible and efficient!\n\n---\n\n**Enable the sensor:**\n\n1. Open **Settings → Devices & Services → Tibber Prices**\n2. Select your home → Find **'Chart Data Export'** (Diagnostic section)\n3. **Enable the sensor** (disabled by default)\n\n**Configuration (optional):**\n\nDefault settings work out-of-the-box (today+tomorrow, 15-minute intervals, prices only).\n\nFor customization, add to **`configuration.yaml`**:\n\n```yaml\ntibber_prices:\n chart_export:\n day:\n - today\n - tomorrow\n include_level: true\n include_rating_level: true\n```\n\n**All parameters:** See `tibber_prices.get_chartdata` service documentation",
"submit": "↩ Ok & Back"
},
"reset_to_defaults": {
"title": "🔄 Reset to Defaults",
"description": "⚠️ **Warning:** This will reset **ALL** settings to factory defaults.\n\n**What will be reset:**\n• All price rating thresholds\n• All volatility thresholds\n• All price trend thresholds\n• All best price period settings\n• All peak price period settings\n• Display settings\n• General settings\n\n**What will NOT be reset:**\n• Your Tibber API token\n• Selected home\n• Currency\n\n**💡 Tip:** This is useful if you want to start fresh after experimenting with settings.",
"data": {
"confirm_reset": "Yes, reset everything to defaults"
},
"submit": "Reset Now"
} }
}, },
"error": { "error": {
@ -339,7 +343,10 @@
"invalid_price_trend_falling": "Falling trend threshold must be between -50% and -1%" "invalid_price_trend_falling": "Falling trend threshold must be between -50% and -1%"
}, },
"abort": { "abort": {
"entry_not_found": "Tibber configuration entry not found." "entry_not_found": "Tibber configuration entry not found.",
"reset_cancelled": "Reset cancelled. No changes were made to your configuration.",
"reset_successful": "✅ All settings have been reset to factory defaults. Your configuration is now like a fresh installation.",
"finished": "Configuration completed."
} }
}, },
"entity": { "entity": {

View file

@ -132,8 +132,22 @@
"options": { "options": {
"step": { "step": {
"init": { "init": {
"menu_options": {
"general_settings": "⚙️ Generelle innstillinger",
"display_settings": "💱 Valutavisning",
"current_interval_price_rating": "📊 Prisvurdering",
"volatility": "💨 Prisvolatilitet",
"best_price": "💚 Beste prisperiode",
"peak_price": "🔴 Toppprisperiode",
"price_trend": "📈 Pristrend",
"chart_data_export": "📊 Diagramdata-eksportsensor",
"reset_to_defaults": "🔄 Tilbakestill til standard",
"finish": "⬅️ Tilbake"
}
},
"general_settings": {
"title": "⚙️ Generelle innstillinger", "title": "⚙️ Generelle innstillinger",
"description": "_{step_progress}_\n\n**Konfigurer generelle innstillinger for Tibber prisinformasjon og vurderinger.**\n\n---\n\n**Bruker:** {user_login}", "description": "**Konfigurer generelle innstillinger for Tibber prisinformasjon og vurderinger.**\n\n---\n\n**Bruker:** {user_login}",
"data": { "data": {
"extended_descriptions": "Utvidede beskrivelser", "extended_descriptions": "Utvidede beskrivelser",
"average_sensor_display": "Gjennomsnittssensor-visning" "average_sensor_display": "Gjennomsnittssensor-visning"
@ -142,7 +156,7 @@
"extended_descriptions": "Styrer om entitetsattributter inkluderer detaljerte forklaringer og brukstips.\n\n• Deaktivert (standard): Bare kort beskrivelse\n• Aktivert: Detaljert forklaring + praktiske brukseksempler\n\nEksempel:\nDeaktivert = 1 attributt\nAktivert = 2 ekstra attributter", "extended_descriptions": "Styrer om entitetsattributter inkluderer detaljerte forklaringer og brukstips.\n\n• Deaktivert (standard): Bare kort beskrivelse\n• Aktivert: Detaljert forklaring + praktiske brukseksempler\n\nEksempel:\nDeaktivert = 1 attributt\nAktivert = 2 ekstra attributter",
"average_sensor_display": "Velg hvilket statistisk mål som skal vises i sensortilstanden for gjennomsnittspris-sensorer. Den andre verdien vises som attributt. Median er mer motstandsdyktig mot ekstremverdier, mens aritmetisk gjennomsnitt representerer tradisjonelt gjennomsnitt. Standard: Median" "average_sensor_display": "Velg hvilket statistisk mål som skal vises i sensortilstanden for gjennomsnittspris-sensorer. Den andre verdien vises som attributt. Median er mer motstandsdyktig mot ekstremverdier, mens aritmetisk gjennomsnitt representerer tradisjonelt gjennomsnitt. Standard: Median"
}, },
"submit": "Videre til trinn 2" "submit": "↩ Lagre & tilbake"
}, },
"display_settings": { "display_settings": {
"title": "💱 Valutavisningsinnstillinger", "title": "💱 Valutavisningsinnstillinger",
@ -153,15 +167,11 @@
"data_description": { "data_description": {
"currency_display_mode": "Velg hvordan priser vises:\n\n• **Basisvaluta** (€/kWh, kr/kWh): Desimalverdier (f.eks. 0,25 €/kWh) - forskjeller synlige fra 3.-4. desimalplass\n• **Underenhet** (ct/kWh, øre/kWh): Større verdier (f.eks. 25,00 ct/kWh) - forskjeller allerede synlige fra 1. desimalplass\n\nStandard avhenger av valutaen din:\n• EUR → Underenhet (cent) - tysk/nederlandsk preferanse\n• NOK/SEK/DKK → Basisvaluta (kroner) - skandinavisk preferanse\n• USD/GBP → Basisvaluta\n\n**💡 Tips:** Ved valg av underenhet kan du aktivere den ekstra sensoren \"Nåværende strømpris (Energi-dashboard)\" (deaktivert som standard)." "currency_display_mode": "Velg hvordan priser vises:\n\n• **Basisvaluta** (€/kWh, kr/kWh): Desimalverdier (f.eks. 0,25 €/kWh) - forskjeller synlige fra 3.-4. desimalplass\n• **Underenhet** (ct/kWh, øre/kWh): Større verdier (f.eks. 25,00 ct/kWh) - forskjeller allerede synlige fra 1. desimalplass\n\nStandard avhenger av valutaen din:\n• EUR → Underenhet (cent) - tysk/nederlandsk preferanse\n• NOK/SEK/DKK → Basisvaluta (kroner) - skandinavisk preferanse\n• USD/GBP → Basisvaluta\n\n**💡 Tips:** Ved valg av underenhet kan du aktivere den ekstra sensoren \"Nåværende strømpris (Energi-dashboard)\" (deaktivert som standard)."
}, },
"submit": "Videre til trinn 3" "submit": "↩ Lagre & tilbake"
}, },
"current_interval_price_rating": { "current_interval_price_rating": {
"title": "📊 Prisvurderings-terskler", "title": "📊 Prisvurderings-terskler",
"description": "_{step_progress}_\n\n**Konfigurer terskler for prisvurderingsnivåer (lav/normal/høy) basert på sammenligning med etterfølgende 24-timers gjennomsnitt.**\n\n---", "description": "**Konfigurer terskler for prisvurderingsnivåer (lav/normal/høy) basert på sammenligning med etterfølgende 24-timers gjennomsnitt.**",
"sections": {
"price_rating_thresholds": {
"name": "Prisvurderings-terskler",
"description": "Definer prisvurderingsnivåer.",
"data": { "data": {
"price_rating_threshold_low": "Lav-terskel", "price_rating_threshold_low": "Lav-terskel",
"price_rating_threshold_high": "Høy-terskel" "price_rating_threshold_high": "Høy-terskel"
@ -169,14 +179,12 @@
"data_description": { "data_description": {
"price_rating_threshold_low": "Prosentverdi for hvor mye gjeldende pris må være under det etterfølgende 24-timers gjennomsnittet for å kvalifisere som 'lav' vurdering. Eksempel: 5 betyr minst 5% under gjennomsnitt. Sensorer med denne vurderingen indikerer gunstige tidsvinduer. Standard: 5%", "price_rating_threshold_low": "Prosentverdi for hvor mye gjeldende pris må være under det etterfølgende 24-timers gjennomsnittet for å kvalifisere som 'lav' vurdering. Eksempel: 5 betyr minst 5% under gjennomsnitt. Sensorer med denne vurderingen indikerer gunstige tidsvinduer. Standard: 5%",
"price_rating_threshold_high": "Prosentverdi for hvor mye gjeldende pris må være over det etterfølgende 24-timers gjennomsnittet for å kvalifisere som 'høy' vurdering. Eksempel: 10 betyr minst 10% over gjennomsnitt. Sensorer med denne vurderingen advarer om dyre tidsvinduer. Standard: 10%" "price_rating_threshold_high": "Prosentverdi for hvor mye gjeldende pris må være over det etterfølgende 24-timers gjennomsnittet for å kvalifisere som 'høy' vurdering. Eksempel: 10 betyr minst 10% over gjennomsnitt. Sensorer med denne vurderingen advarer om dyre tidsvinduer. Standard: 10%"
}
}
}, },
"submit": "Fortsett →" "submit": "↩ Lagre & tilbake"
}, },
"best_price": { "best_price": {
"title": "💚 Beste Prisperiode Innstillinger", "title": "💚 Beste Prisperiode Innstillinger",
"description": "_{step_progress}_\n\nKonfigurer innstillinger for **Beste Prisperiode** binærsensor. Denne sensoren er aktiv i perioder med de laveste strømprisene.\n\n---", "description": "**Konfigurer innstillinger for Beste Prisperiode binærsensor. Denne sensoren er aktiv i perioder med de laveste strømprisene.**\n\n---",
"sections": { "sections": {
"period_settings": { "period_settings": {
"name": "Periodeinnstillinger", "name": "Periodeinnstillinger",
@ -219,11 +227,11 @@
} }
} }
}, },
"submit": "Fortsett →" "submit": "↩ Lagre & tilbake"
}, },
"peak_price": { "peak_price": {
"title": "🔴 Toppprisperiode Innstillinger", "title": "🔴 Toppprisperiode Innstillinger",
"description": "_{step_progress}_\n\nKonfigurer innstillinger for **Toppprisperiode** binærsensor. Denne sensoren er aktiv i perioder med de høyeste strømprisene.\n\n---", "description": "**Konfigurer innstillinger for Toppprisperiode binærsensor. Denne sensoren er aktiv i perioder med de høyeste strømprisene.**\n\n---",
"sections": { "sections": {
"period_settings": { "period_settings": {
"name": "Periodeinnstillinger", "name": "Periodeinnstillinger",
@ -266,34 +274,24 @@
} }
} }
}, },
"submit": "Fortsett →" "submit": "↩ Lagre & tilbake"
}, },
"price_trend": { "price_trend": {
"title": "📈 Pristrendterskler", "title": "📈 Pristrendterskler",
"description": "_{step_progress}_\n\n**Konfigurer 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.**\n\n---", "description": "**Konfigurer 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.**",
"sections": {
"price_trend_thresholds": {
"name": "Pristrendterskler",
"description": "Definer pristrendnivåer.",
"data": { "data": {
"price_trend_threshold_rising": "Stigende terskel", "price_trend_threshold_rising": "Stigende terskel",
"price_trend_threshold_falling": "Fallende terskel" "price_trend_threshold_falling": "Fallende terskel"
}, },
"data_description": { "data_description": {
"price_trend_threshold_rising": "Prosentverdi for gjennomsnittlig prisøkning per time som kvalifiserer trenden som 'stigende'. Eksempel: 5 betyr minst 5% økning per time. Sensorer med denne trenden indikerer at prisene vil stige raskt. Standard: 5%", "price_trend_threshold_rising": "Prosentverdi som gjennomsnittet av de neste N timene må være over den nåværende prisen for å kvalifisere som 'stigende' trend. Eksempel: 5 betyr gjennomsnittet er minst 5% høyere → prisene vil stige. Typiske verdier: 5-15%. Standard: 5%",
"price_trend_threshold_falling": "Prosentverdi for gjennomsnittlig prisnedgang per time som kvalifiserer trenden som 'synkende'. Eksempel: -5 betyr minst 5% nedgang per time. Sensorer med denne trenden indikerer at prisene vil synke raskt. Standard: -5%" "price_trend_threshold_falling": "Prosentverdi (negativ) som gjennomsnittet av de neste N timene må være under den nåværende prisen for å kvalifisere som 'synkende' trend. Eksempel: -5 betyr gjennomsnittet er minst 5% lavere → prisene vil falle. Typiske verdier: -5 til -15%. Standard: -5%"
}
}
}, },
"submit": "Fortsett →" "submit": "↩ Lagre & tilbake"
}, },
"volatility": { "volatility": {
"title": "💨 Volatilitets-terskler", "title": "💨 Volatilitets-terskler",
"description": "_{step_progress}_\n\n**Konfigurer terskler for volatilitetsklassifisering. Volatilitet måler relativ prisvariation ved hjelp av variasjonskoeffisienten (VK = standardavvik / gjennomsnitt × 100%). Disse tersklene er prosentverdier som fungerer på tvers av alle prisnivåer.**\n\nBrukes av:\n• Volatilitetssensorer (klassifisering)\n• Trendsensorer (adaptiv terskel justering: &lt;moderat = mer følsom, ≥høy = mindre følsom)\n\n---", "description": "**Konfigurer terskler for volatilitetsklassifisering.** Volatilitet måler relativ prisvariation ved hjelp av variasjonskoeffisienten (VK = standardavvik / gjennomsnitt × 100%). Disse tersklene er prosentverdier som fungerer på tvers av alle prisnivåer.\n\nBrukes av:\n• Volatilitetssensorer (klassifisering)\n• Trendsensorer (adaptiv terskel justering: &lt;moderat = mer følsom, ≥høy = mindre følsom)",
"sections": {
"volatility_thresholds": {
"name": "Volatilitetsterskler",
"description": "Definer volatilitetsklassifiseringsnivåer.",
"data": { "data": {
"volatility_threshold_moderate": "Moderat terskel", "volatility_threshold_moderate": "Moderat terskel",
"volatility_threshold_high": "Høy terskel", "volatility_threshold_high": "Høy terskel",
@ -303,15 +301,21 @@
"volatility_threshold_moderate": "Grenseverdi for standardavvik (% av gjennomsnitt) for å klassifisere prisvariasjonen som 'moderat'. Eksempel: 10 betyr standardavvik ≥ 10% av gjennomsnitt. Dette indikerer økt prisustabilitet. Standard: 10%", "volatility_threshold_moderate": "Grenseverdi for standardavvik (% av gjennomsnitt) for å klassifisere prisvariasjonen som 'moderat'. Eksempel: 10 betyr standardavvik ≥ 10% av gjennomsnitt. Dette indikerer økt prisustabilitet. Standard: 10%",
"volatility_threshold_high": "Grenseverdi for standardavvik (% av gjennomsnitt) for å klassifisere prisvariasjonen som 'høy'. Eksempel: 20 betyr standardavvik ≥ 20% av gjennomsnitt. Dette indikerer betydelige prissvingninger. Standard: 20%", "volatility_threshold_high": "Grenseverdi for standardavvik (% av gjennomsnitt) for å klassifisere prisvariasjonen som 'høy'. Eksempel: 20 betyr standardavvik ≥ 20% av gjennomsnitt. Dette indikerer betydelige prissvingninger. Standard: 20%",
"volatility_threshold_very_high": "Grenseverdi for standardavvik (% av gjennomsnitt) for å klassifisere prisvariasjonen som 'veldig høy'. Eksempel: 30 betyr standardavvik ≥ 30% av gjennomsnitt. Dette indikerer ekstrem prisustabilitet. Standard: 30%" "volatility_threshold_very_high": "Grenseverdi for standardavvik (% av gjennomsnitt) for å klassifisere prisvariasjonen som 'veldig høy'. Eksempel: 30 betyr standardavvik ≥ 30% av gjennomsnitt. Dette indikerer ekstrem prisustabilitet. Standard: 30%"
}
}
}, },
"submit": "Fortsett →" "submit": "↩ Lagre & tilbake"
}, },
"chart_data_export": { "chart_data_export": {
"title": "📊 Diagram-dataeksport Sensor", "title": "📊 Diagram-dataeksport Sensor",
"description": "_{step_progress}_\n\nDiagram-dataeksport-sensoren gir prisdata som sensorattributter.\n\n⚠ **Merk:** Denne sensoren er en legacy-funksjon for kompatibilitet med eldre verktøy.\n\n**Anbefalt for nye oppsett:** Bruk `tibber_prices.get_chartdata` **tjenesten direkte** - den er mer fleksibel, effektiv og den moderne Home Assistant-tilnærmingen.\n\n**Når denne sensoren gir mening:**\n\n✅ Dashboardverktøyet ditt kan **kun** lese attributter (ingen tjenestekall)\n✅ Du trenger statiske data som oppdateres automatisk\n❌ **Ikke for automatiseringer:** Bruk `tibber_prices.get_chartdata` direkte der - mer fleksibel og effektiv!\n\n---\n\n**Aktiver sensoren:**\n\n1. Åpne **Innstillinger → Enheter og tjenester → Tibber Prices**\n2. Velg ditt hjem → Finn **'Diagramdataeksport'** (Diagnostikk-seksjonen)\n3. **Aktiver sensoren** (deaktivert som standard)\n\n**Konfigurasjon (valgfritt):**\n\nStandardinnstillinger fungerer umiddelbart (i dag+i morgen, 15-minutters intervaller, bare priser).\n\nFor tilpasning, legg til i **`configuration.yaml`**:\n\n```yaml\ntibber_prices:\n chart_export:\n day:\n - today\n - tomorrow\n include_level: true\n include_rating_level: true\n```\n\n**Alle parametere:** Se `tibber_prices.get_chartdata` tjenestens dokumentasjon", "description": "Diagram-dataeksport-sensoren gir prisdata som sensorattributter.\n\n⚠ **Merk:** Denne sensoren er en legacy-funksjon for kompatibilitet med eldre verktøy.\n\n**Anbefalt for nye oppsett:** Bruk `tibber_prices.get_chartdata` **tjenesten direkte** - den er mer fleksibel, effektiv og den moderne Home Assistant-tilnærmingen.\n\n**Når denne sensoren gir mening:**\n\n✅ Dashboardverktøyet ditt kan **kun** lese attributter (ingen tjenestekall)\n✅ Du trenger statiske data som oppdateres automatisk\n❌ **Ikke for automatiseringer:** Bruk `tibber_prices.get_chartdata` direkte der - mer fleksibel og effektiv!\n\n---\n\n**Aktiver sensoren:**\n\n1. Åpne **Innstillinger → Enheter og tjenester → Tibber Prices**\n2. Velg ditt hjem → Finn **'Diagramdataeksport'** (Diagnostikk-seksjonen)\n3. **Aktiver sensoren** (deaktivert som standard)\n\n**Konfigurasjon (valgfritt):**\n\nStandardinnstillinger fungerer umiddelbart (i dag+i morgen, 15-minutters intervaller, bare priser).\n\nFor tilpasning, legg til i **`configuration.yaml`**:\n\n```yaml\ntibber_prices:\n chart_export:\n day:\n - today\n - tomorrow\n include_level: true\n include_rating_level: true\n```\n\n**Alle parametere:** Se `tibber_prices.get_chartdata` tjenestens dokumentasjon",
"submit": "Fullfør ✓" "submit": "↩ Ok & tilbake"
},
"reset_to_defaults": {
"title": "🔄 Tilbakestill til standard",
"description": "⚠️ **Advarsel:** Dette vil tilbakestille **ALLE** innstillinger til fabrikkstandard.\n\n**Hva vil bli tilbakestilt:**\n• Alle prisvurderingsterskler\n• Alle volatilitetsterskler\n• Alle pristrendterskler\n• Alle innstillinger for beste prisperiode\n• Alle innstillinger for toppprisperiode\n• Visningsinnstillinger\n• Generelle innstillinger\n\n**Hva vil IKKE bli tilbakestilt:**\n• Ditt Tibber API-token\n• Valgt hjem\n• Valuta\n\n**💡 Tips:** Dette er nyttig hvis du vil starte på nytt etter å ha eksperimentert med innstillinger.",
"data": {
"confirm_reset": "Ja, tilbakestill alt til standard"
},
"submit": "Tilbakestill nå"
} }
}, },
"error": { "error": {
@ -339,7 +343,10 @@
"invalid_price_trend_falling": "Fallende trendgrense må være mellom -50% og -1%" "invalid_price_trend_falling": "Fallende trendgrense må være mellom -50% og -1%"
}, },
"abort": { "abort": {
"entry_not_found": "Tibber-konfigurasjonsoppføring ikke funnet." "entry_not_found": "Tibber-konfigurasjonsoppføring ikke funnet.",
"reset_cancelled": "Tilbakestilling avbrutt. Ingen endringer ble gjort i konfigurasjonen din.",
"reset_successful": "✅ Alle innstillinger har blitt tilbakestilt til fabrikkstandard. Konfigurasjonen din er nå som en ny installasjon.",
"finished": "Konfigurasjon fullført."
} }
}, },
"entity": { "entity": {

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff