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
migrated = dict(entry.options)
# Migration: Set currency_display_mode to minor for existing configs
# New configs get currency-appropriate defaults from schema.
# This preserves legacy behavior where all prices were in subunit currency.
# Migration: Set currency_display_mode to subunit for legacy configs
# New configs (created after v1.1.0) get currency-appropriate defaults via get_default_options().
# 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:
migrated[CONF_CURRENCY_DISPLAY_MODE] = DISPLAY_MODE_SUBUNIT
migration_performed = True
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,
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
if entry.state == ConfigEntryState.SETUP_IN_PROGRESS:
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:
await coordinator.async_refresh()

View file

@ -3,7 +3,8 @@
from __future__ import annotations
import logging
from typing import TYPE_CHECKING, Any, ClassVar
from copy import deepcopy
from typing import TYPE_CHECKING, Any
if TYPE_CHECKING:
from collections.abc import Mapping
@ -16,6 +17,7 @@ from custom_components.tibber_prices.config_flow_handlers.schemas import (
get_peak_price_schema,
get_price_rating_schema,
get_price_trend_schema,
get_reset_to_defaults_schema,
get_volatility_schema,
)
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_VERY_HIGH,
DOMAIN,
get_default_options,
)
from homeassistant.config_entries import ConfigFlowResult, OptionsFlow
@ -69,23 +72,34 @@ _LOGGER = logging.getLogger(__name__)
class TibberPricesOptionsFlowHandler(OptionsFlow):
"""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:
"""Initialize options flow."""
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]:
"""
Migrate deprecated config options to current format.
@ -100,7 +114,10 @@ class TibberPricesOptionsFlowHandler(OptionsFlow):
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 1: Rename relaxation_step_* to relaxation_attempts_*
@ -144,41 +161,98 @@ class TibberPricesOptionsFlowHandler(OptionsFlow):
return migrated
def _get_step_description_placeholders(self, step_id: str) -> dict[str, str]:
"""Get description placeholders with step progress."""
if step_id not in self._STEP_INFO:
return {}
def _save_options_if_changed(self) -> bool:
"""
Save options only if they actually changed.
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}"
translations = self.hass.data.get(standard_translations_key, {})
"""
# Compare old and new options
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
step_progress_template = translations.get("common", {}).get("step_progress", "Step {step_num} of {total_steps}")
step_progress = step_progress_template.format(step_num=step_num, total_steps=self._TOTAL_STEPS)
return {
"step_progress": step_progress,
}
async def async_step_init(self, user_input: dict[str, Any] | None = None) -> ConfigFlowResult:
"""Manage the options - General Settings."""
# Initialize options from config_entry on first call
if not self._options:
# Migrate deprecated config options before processing
async def async_step_init(self, _user_input: dict[str, Any] | None = None) -> ConfigFlowResult:
"""Manage the options - show menu."""
# Always reload options from config_entry to get latest saved state
# This ensures changes from previous steps are visible
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:
# 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)
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(
step_id="init",
step_id="general_settings",
data_schema=get_options_init_schema(self.config_entry.options),
description_placeholders={
**self._get_step_description_placeholders("init"),
"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")
if user_input is not None:
# Update options with new values
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(
step_id="display_settings",
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(
@ -210,6 +287,9 @@ class TibberPricesOptionsFlowHandler(OptionsFlow):
errors: dict[str, str] = {}
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
if CONF_PRICE_RATING_THRESHOLD_LOW in user_input and not validate_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"
# Cross-validate both thresholds together (LOW must be < HIGH)
if not errors and not validate_price_rating_thresholds(
user_input.get(
if not errors:
# 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)
),
user_input.get(
)
high_val = user_input.get(
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
errors["base"] = "invalid_price_rating_thresholds"
if not errors:
# Store flat data directly in options (no section wrapping)
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(
step_id="current_interval_price_rating",
data_schema=get_price_rating_schema(self.config_entry.options),
description_placeholders=self._get_step_description_placeholders("current_interval_price_rating"),
errors=errors,
)
@ -250,46 +334,61 @@ class TibberPricesOptionsFlowHandler(OptionsFlow):
errors: dict[str, str] = {}
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
if CONF_BEST_PRICE_MIN_PERIOD_LENGTH in user_input and not validate_period_length(
user_input[CONF_BEST_PRICE_MIN_PERIOD_LENGTH]
if CONF_BEST_PRICE_MIN_PERIOD_LENGTH in period_settings and not validate_period_length(
period_settings[CONF_BEST_PRICE_MIN_PERIOD_LENGTH]
):
errors[CONF_BEST_PRICE_MIN_PERIOD_LENGTH] = "invalid_period_length"
# 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"
# 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(
user_input[CONF_BEST_PRICE_MIN_DISTANCE_FROM_AVG]
if (
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"
# 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"
# Validate gap count
if CONF_BEST_PRICE_MAX_LEVEL_GAP_COUNT in user_input and not validate_gap_count(
user_input[CONF_BEST_PRICE_MAX_LEVEL_GAP_COUNT]
if CONF_BEST_PRICE_MAX_LEVEL_GAP_COUNT in period_settings and not validate_gap_count(
period_settings[CONF_BEST_PRICE_MAX_LEVEL_GAP_COUNT]
):
errors[CONF_BEST_PRICE_MAX_LEVEL_GAP_COUNT] = "invalid_gap_count"
# Validate relaxation attempts
if CONF_RELAXATION_ATTEMPTS_BEST in user_input and not validate_relaxation_attempts(
user_input[CONF_RELAXATION_ATTEMPTS_BEST]
if CONF_RELAXATION_ATTEMPTS_BEST in relaxation_settings and not validate_relaxation_attempts(
relaxation_settings[CONF_RELAXATION_ATTEMPTS_BEST]
):
errors[CONF_RELAXATION_ATTEMPTS_BEST] = "invalid_relaxation_attempts"
if not errors:
self._options.update(user_input)
return await self.async_step_peak_price()
# Merge section data into options
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(
step_id="best_price",
data_schema=get_best_price_schema(self.config_entry.options),
description_placeholders=self._get_step_description_placeholders("best_price"),
errors=errors,
)
@ -298,46 +397,58 @@ class TibberPricesOptionsFlowHandler(OptionsFlow):
errors: dict[str, str] = {}
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
if CONF_PEAK_PRICE_MIN_PERIOD_LENGTH in user_input and not validate_period_length(
user_input[CONF_PEAK_PRICE_MIN_PERIOD_LENGTH]
if CONF_PEAK_PRICE_MIN_PERIOD_LENGTH in period_settings and not validate_period_length(
period_settings[CONF_PEAK_PRICE_MIN_PERIOD_LENGTH]
):
errors[CONF_PEAK_PRICE_MIN_PERIOD_LENGTH] = "invalid_period_length"
# 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"
# 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(
user_input[CONF_PEAK_PRICE_MIN_DISTANCE_FROM_AVG]
if CONF_PEAK_PRICE_MIN_DISTANCE_FROM_AVG in flexibility_settings and not validate_distance_percentage(
flexibility_settings[CONF_PEAK_PRICE_MIN_DISTANCE_FROM_AVG]
):
errors[CONF_PEAK_PRICE_MIN_DISTANCE_FROM_AVG] = "invalid_peak_price_distance"
# 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"
# Validate gap count
if CONF_PEAK_PRICE_MAX_LEVEL_GAP_COUNT in user_input and not validate_gap_count(
user_input[CONF_PEAK_PRICE_MAX_LEVEL_GAP_COUNT]
if CONF_PEAK_PRICE_MAX_LEVEL_GAP_COUNT in period_settings and not validate_gap_count(
period_settings[CONF_PEAK_PRICE_MAX_LEVEL_GAP_COUNT]
):
errors[CONF_PEAK_PRICE_MAX_LEVEL_GAP_COUNT] = "invalid_gap_count"
# Validate relaxation attempts
if CONF_RELAXATION_ATTEMPTS_PEAK in user_input and not validate_relaxation_attempts(
user_input[CONF_RELAXATION_ATTEMPTS_PEAK]
if CONF_RELAXATION_ATTEMPTS_PEAK in relaxation_settings and not validate_relaxation_attempts(
relaxation_settings[CONF_RELAXATION_ATTEMPTS_PEAK]
):
errors[CONF_RELAXATION_ATTEMPTS_PEAK] = "invalid_relaxation_attempts"
if not errors:
self._options.update(user_input)
return await self.async_step_price_trend()
# Merge section data into options
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(
step_id="peak_price",
data_schema=get_peak_price_schema(self.config_entry.options),
description_placeholders=self._get_step_description_placeholders("peak_price"),
errors=errors,
)
@ -346,6 +457,9 @@ class TibberPricesOptionsFlowHandler(OptionsFlow):
errors: dict[str, str] = {}
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
if CONF_PRICE_TREND_THRESHOLD_RISING in user_input and not validate_price_trend_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"
if not errors:
# Store flat data directly in options (no section wrapping)
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(
step_id="price_trend",
data_schema=get_price_trend_schema(self.config_entry.options),
description_placeholders=self._get_step_description_placeholders("price_trend"),
errors=errors,
)
async def async_step_chart_data_export(self, user_input: dict[str, Any] | None = None) -> ConfigFlowResult:
"""Info page for chart data export sensor."""
if user_input is not None:
# No validation needed - just an info page
return self.async_create_entry(title="", data=self._options)
# No changes to save - just return to menu
return await self.async_step_init()
# Show info-only form (no input fields)
return self.async_show_form(
step_id="chart_data_export",
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:
@ -387,6 +503,8 @@ class TibberPricesOptionsFlowHandler(OptionsFlow):
errors: dict[str, str] = {}
if user_input is not None:
# Schema is now flattened - fields come directly in user_input
# Validate moderate volatility threshold
if CONF_VOLATILITY_THRESHOLD_MODERATE in user_input and not validate_volatility_threshold_moderate(
user_input[CONF_VOLATILITY_THRESHOLD_MODERATE]
@ -407,30 +525,33 @@ class TibberPricesOptionsFlowHandler(OptionsFlow):
# Cross-validation: Ensure MODERATE < HIGH < VERY_HIGH
if not errors:
existing_options = self.config_entry.options
# Get current values directly from options (now flat)
moderate = user_input.get(
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(
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(
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):
errors["base"] = "invalid_volatility_thresholds"
if not errors:
# Store flat data directly in options (no section wrapping)
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(
step_id="volatility",
data_schema=get_volatility_schema(self.config_entry.options),
description_placeholders=self._get_step_description_placeholders("volatility"),
errors=errors,
)

View file

@ -96,6 +96,7 @@ from custom_components.tibber_prices.const import (
)
from homeassistant.const import CONF_ACCESS_TOKEN
from homeassistant.data_entry_flow import section
from homeassistant.helpers import selector
from homeassistant.helpers.selector import (
BooleanSelector,
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:
"""Return schema for price rating thresholds configuration."""
return vol.Schema(
{
vol.Required("price_rating_thresholds"): section(
vol.Schema(
{
vol.Optional(
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:
"""Return schema for volatility thresholds configuration with collapsible sections."""
"""Return schema for volatility thresholds configuration."""
return vol.Schema(
{
vol.Required("volatility_thresholds"): section(
vol.Schema(
{
vol.Optional(
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:
"""Return schema for best price period configuration with collapsible sections."""
period_settings = options.get("period_settings", {})
return vol.Schema(
{
vol.Required("period_settings"): section(
@ -380,7 +368,7 @@ def get_best_price_schema(options: Mapping[str, Any]) -> vol.Schema:
vol.Optional(
CONF_BEST_PRICE_MIN_PERIOD_LENGTH,
default=int(
options.get(
period_settings.get(
CONF_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(
CONF_BEST_PRICE_MAX_LEVEL,
default=options.get(
default=period_settings.get(
CONF_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(
CONF_BEST_PRICE_MAX_LEVEL_GAP_COUNT,
default=int(
options.get(
period_settings.get(
CONF_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.Schema(
@ -433,7 +421,7 @@ def get_best_price_schema(options: Mapping[str, Any]) -> vol.Schema:
vol.Optional(
CONF_BEST_PRICE_FLEX,
default=int(
options.get(
options.get("flexibility_settings", {}).get(
CONF_BEST_PRICE_FLEX,
DEFAULT_BEST_PRICE_FLEX,
)
@ -450,7 +438,7 @@ def get_best_price_schema(options: Mapping[str, Any]) -> vol.Schema:
vol.Optional(
CONF_BEST_PRICE_MIN_DISTANCE_FROM_AVG,
default=int(
options.get(
options.get("flexibility_settings", {}).get(
CONF_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(
CONF_ENABLE_MIN_PERIODS_BEST,
default=options.get(
default=options.get("relaxation_and_target_periods", {}).get(
CONF_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(
CONF_MIN_PERIODS_BEST,
default=int(
options.get(
options.get("relaxation_and_target_periods", {}).get(
CONF_MIN_PERIODS_BEST,
DEFAULT_MIN_PERIODS_BEST,
)
@ -497,7 +485,7 @@ def get_best_price_schema(options: Mapping[str, Any]) -> vol.Schema:
vol.Optional(
CONF_RELAXATION_ATTEMPTS_BEST,
default=int(
options.get(
options.get("relaxation_and_target_periods", {}).get(
CONF_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:
"""Return schema for peak price period configuration with collapsible sections."""
period_settings = options.get("period_settings", {})
return vol.Schema(
{
vol.Required("period_settings"): section(
@ -528,7 +517,7 @@ def get_peak_price_schema(options: Mapping[str, Any]) -> vol.Schema:
vol.Optional(
CONF_PEAK_PRICE_MIN_PERIOD_LENGTH,
default=int(
options.get(
period_settings.get(
CONF_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(
CONF_PEAK_PRICE_MIN_LEVEL,
default=options.get(
default=period_settings.get(
CONF_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(
CONF_PEAK_PRICE_MAX_LEVEL_GAP_COUNT,
default=int(
options.get(
period_settings.get(
CONF_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.Schema(
@ -581,7 +570,7 @@ def get_peak_price_schema(options: Mapping[str, Any]) -> vol.Schema:
vol.Optional(
CONF_PEAK_PRICE_FLEX,
default=int(
options.get(
options.get("flexibility_settings", {}).get(
CONF_PEAK_PRICE_FLEX,
DEFAULT_PEAK_PRICE_FLEX,
)
@ -598,7 +587,7 @@ def get_peak_price_schema(options: Mapping[str, Any]) -> vol.Schema:
vol.Optional(
CONF_PEAK_PRICE_MIN_DISTANCE_FROM_AVG,
default=int(
options.get(
options.get("flexibility_settings", {}).get(
CONF_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(
CONF_ENABLE_MIN_PERIODS_PEAK,
default=options.get(
default=options.get("relaxation_and_target_periods", {}).get(
CONF_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(
CONF_MIN_PERIODS_PEAK,
default=int(
options.get(
options.get("relaxation_and_target_periods", {}).get(
CONF_MIN_PERIODS_PEAK,
DEFAULT_MIN_PERIODS_PEAK,
)
@ -645,7 +634,7 @@ def get_peak_price_schema(options: Mapping[str, Any]) -> vol.Schema:
vol.Optional(
CONF_RELAXATION_ATTEMPTS_PEAK,
default=int(
options.get(
options.get("relaxation_and_target_periods", {}).get(
CONF_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:
"""Return schema for price trend thresholds configuration."""
return vol.Schema(
{
vol.Required("price_trend_thresholds"): section(
vol.Schema(
{
vol.Optional(
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)."""
# Empty schema - this is just an info page now
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)
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(
title=subentry_title,
data={

View file

@ -20,7 +20,12 @@ from custom_components.tibber_prices.config_flow_handlers.validators import (
TibberPricesInvalidAuthError,
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 (
ConfigEntry,
ConfigFlow,
@ -379,6 +384,16 @@ class TibberPricesConfigFlowHandler(ConfigFlow, domain=DOMAIN):
"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)
entry_title = self._get_entry_title(selected_home)
@ -386,6 +401,7 @@ class TibberPricesConfigFlowHandler(ConfigFlow, domain=DOMAIN):
title=entry_title,
data=data,
description=f"{self._user_login} ({self._user_id})",
options=get_default_options(currency_code),
)
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)
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:
"""
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)
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._data_transformer.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
def async_add_time_sensitive_listener(self, update_callback: TimeServiceCallback) -> CALLBACK_TYPE:

View file

@ -73,36 +73,52 @@ class TibberPricesDataTransformer:
return self._config_cache
# 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 = {
"thresholds": self.get_threshold_percentages(),
# Volatility thresholds now flat (single-section step)
"volatility_thresholds": {
"moderate": self.config_entry.options.get(_const.CONF_VOLATILITY_THRESHOLD_MODERATE, 15.0),
"high": self.config_entry.options.get(_const.CONF_VOLATILITY_THRESHOLD_HIGH, 25.0),
"very_high": self.config_entry.options.get(_const.CONF_VOLATILITY_THRESHOLD_VERY_HIGH, 40.0),
"moderate": options.get(_const.CONF_VOLATILITY_THRESHOLD_MODERATE, 15.0),
"high": options.get(_const.CONF_VOLATILITY_THRESHOLD_HIGH, 25.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": {
"flex": self.config_entry.options.get(_const.CONF_BEST_PRICE_FLEX, 15.0),
"max_level": self.config_entry.options.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_distance_from_avg": self.config_entry.options.get(
_const.CONF_BEST_PRICE_MIN_DISTANCE_FROM_AVG, -5.0
),
"max_level_gap_count": self.config_entry.options.get(_const.CONF_BEST_PRICE_MAX_LEVEL_GAP_COUNT, 0),
"enable_min_periods": self.config_entry.options.get(_const.CONF_ENABLE_MIN_PERIODS_BEST, False),
"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),
"flex": best_flex_section.get(_const.CONF_BEST_PRICE_FLEX, 15.0),
"max_level": best_period_section.get(_const.CONF_BEST_PRICE_MAX_LEVEL, "NORMAL"),
"min_period_length": best_period_section.get(_const.CONF_BEST_PRICE_MIN_PERIOD_LENGTH, 4),
"min_distance_from_avg": best_flex_section.get(_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),
"min_periods": best_relax_section.get(_const.CONF_MIN_PERIODS_BEST, 2),
"relaxation_attempts": best_relax_section.get(_const.CONF_RELAXATION_ATTEMPTS_BEST, 4),
},
"peak_price_config": {
"flex": self.config_entry.options.get(_const.CONF_PEAK_PRICE_FLEX, 15.0),
"min_level": self.config_entry.options.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_distance_from_avg": self.config_entry.options.get(
_const.CONF_PEAK_PRICE_MIN_DISTANCE_FROM_AVG, 5.0
),
"max_level_gap_count": self.config_entry.options.get(_const.CONF_PEAK_PRICE_MAX_LEVEL_GAP_COUNT, 0),
"enable_min_periods": self.config_entry.options.get(_const.CONF_ENABLE_MIN_PERIODS_PEAK, False),
"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),
"flex": peak_flex_section.get(_const.CONF_PEAK_PRICE_FLEX, 15.0),
"min_level": peak_period_section.get(_const.CONF_PEAK_PRICE_MIN_LEVEL, "HIGH"),
"min_period_length": peak_period_section.get(_const.CONF_PEAK_PRICE_MIN_PERIOD_LENGTH, 4),
"min_distance_from_avg": peak_flex_section.get(_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),
"min_periods": peak_relax_section.get(_const.CONF_MIN_PERIODS_PEAK, 2),
"relaxation_attempts": peak_relax_section.get(_const.CONF_RELAXATION_ATTEMPTS_PEAK, 4),
},
}

View file

@ -92,8 +92,9 @@ class TibberPricesPeriodCalculator:
# Get level filter overrides from options
options = self.config_entry.options
best_level_filter = options.get(_const.CONF_BEST_PRICE_MAX_LEVEL, _const.DEFAULT_BEST_PRICE_MAX_LEVEL)
peak_level_filter = options.get(_const.CONF_PEAK_PRICE_MIN_LEVEL, _const.DEFAULT_PEAK_PRICE_MIN_LEVEL)
period_settings = options.get("period_settings", {})
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
hash_data = (
@ -124,33 +125,36 @@ class TibberPricesPeriodCalculator:
self._config_cache = {}
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:
# Peak price configuration
flex = options.get(
_const.CONF_PEAK_PRICE_FLEX, data.get(_const.CONF_PEAK_PRICE_FLEX, _const.DEFAULT_PEAK_PRICE_FLEX)
)
min_distance_from_avg = options.get(
flex = flexibility_settings.get(_const.CONF_PEAK_PRICE_FLEX, _const.DEFAULT_PEAK_PRICE_FLEX)
min_distance_from_avg = flexibility_settings.get(
_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,
data.get(_const.CONF_PEAK_PRICE_MIN_PERIOD_LENGTH, _const.DEFAULT_PEAK_PRICE_MIN_PERIOD_LENGTH),
_const.DEFAULT_PEAK_PRICE_MIN_PERIOD_LENGTH,
)
else:
# Best price configuration
flex = options.get(
_const.CONF_BEST_PRICE_FLEX, data.get(_const.CONF_BEST_PRICE_FLEX, _const.DEFAULT_BEST_PRICE_FLEX)
)
min_distance_from_avg = options.get(
flex = flexibility_settings.get(_const.CONF_BEST_PRICE_FLEX, _const.DEFAULT_BEST_PRICE_FLEX)
min_distance_from_avg = flexibility_settings.get(
_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,
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)
@ -356,13 +360,14 @@ class TibberPricesPeriodCalculator:
# Normal check failed - try splitting at gap clusters as fallback
# Get minimum period length from config (convert minutes to intervals)
period_settings = self.config_entry.options.get("period_settings", {})
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.DEFAULT_PEAK_PRICE_MIN_PERIOD_LENGTH,
)
else:
min_period_minutes = self.config_entry.options.get(
min_period_minutes = period_settings.get(
_const.CONF_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
elif reverse_sort:
# 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.DEFAULT_PEAK_PRICE_MIN_LEVEL,
)
else:
# 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.DEFAULT_BEST_PRICE_MAX_LEVEL,
)
@ -511,13 +518,14 @@ class TibberPricesPeriodCalculator:
return True # If no data, don't filter
# Get gap tolerance configuration
period_settings = self.config_entry.options.get("period_settings", {})
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.DEFAULT_PEAK_PRICE_MAX_LEVEL_GAP_COUNT,
)
else:
max_gap_count = self.config_entry.options.get(
max_gap_count = period_settings.get(
_const.CONF_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}
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(
_const.CONF_PRICE_RATING_THRESHOLD_LOW,
_const.DEFAULT_PRICE_RATING_THRESHOLD_LOW,
@ -584,7 +593,8 @@ class TibberPricesPeriodCalculator:
_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(
_const.CONF_VOLATILITY_THRESHOLD_MODERATE,
_const.DEFAULT_VOLATILITY_THRESHOLD_MODERATE,
@ -599,7 +609,9 @@ class TibberPricesPeriodCalculator:
)
# 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.DEFAULT_ENABLE_MIN_PERIODS_BEST,
)
@ -611,11 +623,11 @@ class TibberPricesPeriodCalculator:
show_best_price = bool(all_prices)
else:
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.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.DEFAULT_RELAXATION_ATTEMPTS_BEST,
)
@ -623,12 +635,14 @@ class TibberPricesPeriodCalculator:
# Calculate best price periods (or return empty if filtered)
if show_best_price:
best_config = self.get_period_config(reverse_sort=False)
# Get level filter configuration
max_level_best = self.config_entry.options.get(
# Get level filter configuration from period_settings section
# 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.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.DEFAULT_BEST_PRICE_MAX_LEVEL_GAP_COUNT,
)
@ -672,7 +686,8 @@ class TibberPricesPeriodCalculator:
}
# 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.DEFAULT_ENABLE_MIN_PERIODS_PEAK,
)
@ -684,11 +699,11 @@ class TibberPricesPeriodCalculator:
show_peak_price = bool(all_prices)
else:
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.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.DEFAULT_RELAXATION_ATTEMPTS_PEAK,
)
@ -696,12 +711,13 @@ class TibberPricesPeriodCalculator:
# Calculate peak price periods (or return empty if filtered)
if show_peak_price:
peak_config = self.get_period_config(reverse_sort=True)
# Get level filter configuration
min_level_peak = self.config_entry.options.get(
# Get level filter configuration from period_settings section
# 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.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.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
if self.entity_description.key.startswith("price_trend_"):
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)
if self.entity_description.key == "chart_data_export":

View file

@ -146,12 +146,12 @@ def _calculate_metadata( # noqa: PLR0912, PLR0913, PLR0915
return {}
min_val = min(data)
max_val = max(data)
avg_val = sum(data) / len(data)
mean_val = sum(data) / len(data)
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
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
# Position precision: 2 decimals for subunit currency, 4 for base currency
@ -162,8 +162,8 @@ def _calculate_metadata( # noqa: PLR0912, PLR0913, PLR0915
return {
"min": round(min_val, price_decimals),
"max": round(max_val, price_decimals),
"avg": round(avg_val, price_decimals),
"avg_position": round(avg_position, position_decimals),
"mean": round(mean_val, price_decimals),
"mean_position": round(mean_position, position_decimals),
"median": round(median_val, price_decimals),
"median_position": round(median_position, position_decimals),
}

View file

@ -132,8 +132,22 @@
"options": {
"step": {
"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",
"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": {
"extended_descriptions": "Erweiterte Beschreibungen",
"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",
"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": {
"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": {
"currency_display_mode": "Anzeigemodus"
},
"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)."
},
"submit": "Weiter →"
"submit": "↩ Speichern & Zurück"
},
"current_interval_price_rating": {
"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---",
"sections": {
"price_rating_thresholds": {
"name": "Preisbewertungs-Schwellenwerte",
"description": "Definiere die Einstufungen für die Preisbewertung.",
"description": "**Konfiguriere Schwellenwerte für Preisbewertungsstufen (niedrig/normal/hoch) basierend auf dem Vergleich mit dem nachlaufenden 24-Stunden-Durchschnitt.**",
"data": {
"price_rating_threshold_low": "Niedrig-Schwelle",
"price_rating_threshold_high": "Hoch-Schwelle",
"average_sensor_display": "Durchschnitts-Sensor Anzeige"
"price_rating_threshold_high": "Hoch-Schwelle"
},
"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_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"
}
}
"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%"
},
"submit": "Weiter →"
"submit": "↩ Speichern & Zurück"
},
"best_price": {
"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": {
"period_settings": {
"name": "Zeitraumdauer & Preisniveaus",
@ -221,11 +227,11 @@
}
}
},
"submit": "Weiter →"
"submit": "↩ Speichern & Zurück"
},
"peak_price": {
"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": {
"period_settings": {
"name": "Zeitraum-Einstellungen",
@ -268,15 +274,11 @@
}
}
},
"submit": "Weiter →"
"submit": "↩ Speichern & Zurück"
},
"price_trend": {
"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---",
"sections": {
"price_trend_thresholds": {
"name": "Preistrend-Schwellenwerte",
"description": "Definiere die Einstufungen für den Preistrend.",
"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.**",
"data": {
"price_trend_threshold_rising": "Steigend-Schwelle",
"price_trend_threshold_falling": "Fallend-Schwelle"
@ -284,18 +286,12 @@
"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_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": {
"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---",
"sections": {
"volatility_thresholds": {
"name": "Volatilitätsschwellen",
"description": "Definiere Volatilitäts-Klassifizierungsstufen.",
"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)",
"data": {
"volatility_threshold_moderate": "Moderat-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_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%"
}
}
},
"submit": "Weiter →"
"submit": "↩ Speichern & Zurück"
},
"chart_data_export": {
"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",
"submit": "Abschließen ✓"
"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": "↩ 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": {
@ -341,7 +343,10 @@
"invalid_price_trend_falling": "Fallender Trendschwellenwert muss zwischen -50% und -1% liegen"
},
"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": {

View file

@ -132,8 +132,22 @@
"options": {
"step": {
"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",
"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": {
"extended_descriptions": "Extended Descriptions",
"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",
"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": {
"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": {
"currency_display_mode": "Display Mode"
},
"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)."
},
"submit": "Continue →"
"submit": "↩ Save & Back"
},
"current_interval_price_rating": {
"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---",
"sections": {
"price_rating_thresholds": {
"name": "Price Rating Thresholds",
"description": "Define price rating classification levels.",
"description": "**Configure thresholds for price rating levels (low/normal/high) based on comparison with trailing 24-hour average.**",
"data": {
"price_rating_threshold_low": "Low Threshold",
"price_rating_threshold_high": "High Threshold"
@ -169,14 +179,12 @@
"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_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": {
"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": {
"period_settings": {
"name": "Period Duration & Levels",
@ -219,11 +227,11 @@
}
}
},
"submit": "Continue →"
"submit": "↩ Save & Back"
},
"peak_price": {
"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": {
"period_settings": {
"name": "Period Settings",
@ -266,15 +274,11 @@
}
}
},
"submit": "Continue →"
"submit": "↩ Save & Back"
},
"price_trend": {
"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---",
"sections": {
"price_trend_thresholds": {
"name": "Price Trend Thresholds",
"description": "Define price trend classification levels.",
"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.**",
"data": {
"price_trend_threshold_rising": "Rising Threshold",
"price_trend_threshold_falling": "Falling Threshold"
@ -282,23 +286,12 @@
"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_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 →"
},
"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 ✓"
"submit": "↩ Save & Back"
},
"volatility": {
"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---",
"sections": {
"volatility_thresholds": {
"name": "Volatility Thresholds",
"description": "Define price volatility classification levels.",
"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)",
"data": {
"volatility_threshold_moderate": "Moderate 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_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%"
}
}
},
"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": {
@ -339,7 +343,10 @@
"invalid_price_trend_falling": "Falling trend threshold must be between -50% and -1%"
},
"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": {

View file

@ -132,8 +132,22 @@
"options": {
"step": {
"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",
"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": {
"extended_descriptions": "Utvidede beskrivelser",
"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",
"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": {
"title": "💱 Valutavisningsinnstillinger",
@ -153,15 +167,11 @@
"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)."
},
"submit": "Videre til trinn 3"
"submit": "↩ Lagre & tilbake"
},
"current_interval_price_rating": {
"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---",
"sections": {
"price_rating_thresholds": {
"name": "Prisvurderings-terskler",
"description": "Definer prisvurderingsnivåer.",
"description": "**Konfigurer terskler for prisvurderingsnivåer (lav/normal/høy) basert på sammenligning med etterfølgende 24-timers gjennomsnitt.**",
"data": {
"price_rating_threshold_low": "Lav-terskel",
"price_rating_threshold_high": "Høy-terskel"
@ -169,14 +179,12 @@
"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_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": {
"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": {
"period_settings": {
"name": "Periodeinnstillinger",
@ -219,11 +227,11 @@
}
}
},
"submit": "Fortsett →"
"submit": "↩ Lagre & tilbake"
},
"peak_price": {
"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": {
"period_settings": {
"name": "Periodeinnstillinger",
@ -266,34 +274,24 @@
}
}
},
"submit": "Fortsett →"
"submit": "↩ Lagre & tilbake"
},
"price_trend": {
"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---",
"sections": {
"price_trend_thresholds": {
"name": "Pristrendterskler",
"description": "Definer pristrendnivåer.",
"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.**",
"data": {
"price_trend_threshold_rising": "Stigende terskel",
"price_trend_threshold_falling": "Fallende terskel"
},
"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_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_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 (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": {
"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---",
"sections": {
"volatility_thresholds": {
"name": "Volatilitetsterskler",
"description": "Definer volatilitetsklassifiseringsnivåer.",
"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)",
"data": {
"volatility_threshold_moderate": "Moderat 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_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%"
}
}
},
"submit": "Fortsett →"
"submit": "↩ Lagre & tilbake"
},
"chart_data_export": {
"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",
"submit": "Fullfør ✓"
"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": "↩ 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": {
@ -339,7 +343,10 @@
"invalid_price_trend_falling": "Fallende trendgrense må være mellom -50% og -1%"
},
"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": {

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff