feat(config_flow): show override warnings when config entities control settings

When runtime config override entities (number/switch) are enabled,
the Options Flow now displays warning indicators at the top of each
affected section. Users see which fields are being managed by config
entities and can still edit the base values if needed.

Changes:
- Add ConstantSelector warnings in Best Price/Peak Price sections
- Implement multi-language support for override warnings (de, en, nb, nl, sv)
- Add _get_override_translations() to load translated field labels
- Add _get_active_overrides() to detect enabled override entities
- Extend get_best_price_schema/get_peak_price_schema with translations param
- Add 14 number/switch config entities for runtime period tuning
- Document runtime configuration entities in user docs

Warning format adapts to overridden fields:
- Single: "⚠️ Flexibility controlled by config entity"
- Multiple: "⚠️ Flexibility and Minimum Distance controlled by config entity"

Impact: Users can now dynamically adjust period calculation parameters
via Home Assistant automations, scripts, or dashboards without entering
the Options Flow. Clear UI indicators show which settings are currently
overridden.
This commit is contained in:
Julian Pawlowski 2026-01-21 17:36:51 +00:00
parent cc75bc53ee
commit 631cebeb55
22 changed files with 2522 additions and 311 deletions

View file

@ -47,6 +47,8 @@ if TYPE_CHECKING:
PLATFORMS: list[Platform] = [ PLATFORMS: list[Platform] = [
Platform.SENSOR, Platform.SENSOR,
Platform.BINARY_SENSOR, Platform.BINARY_SENSOR,
Platform.NUMBER,
Platform.SWITCH,
] ]
# Configuration schema for configuration.yaml # Configuration schema for configuration.yaml

View file

@ -15,6 +15,7 @@ from custom_components.tibber_prices.config_flow_handlers.entity_check import (
format_sensor_names_for_warning, format_sensor_names_for_warning,
) )
from custom_components.tibber_prices.config_flow_handlers.schemas import ( from custom_components.tibber_prices.config_flow_handlers.schemas import (
ConfigOverrides,
get_best_price_schema, get_best_price_schema,
get_chart_data_export_schema, get_chart_data_export_schema,
get_display_settings_schema, get_display_settings_schema,
@ -72,9 +73,11 @@ 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,
async_get_translation,
get_default_options, get_default_options,
) )
from homeassistant.config_entries import ConfigFlowResult, OptionsFlow from homeassistant.config_entries import ConfigFlowResult, OptionsFlow
from homeassistant.helpers import entity_registry as er
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
@ -216,6 +219,167 @@ class TibberPricesOptionsFlowHandler(OptionsFlow):
f"in **Settings → Devices & Services → Tibber Prices → Entities**." f"in **Settings → Devices & Services → Tibber Prices → Entities**."
} }
def _get_enabled_config_entities(self) -> set[str]:
"""
Get config keys that have their config entity enabled.
Checks the entity registry for number/switch entities that override
config values. Returns the config_key for each enabled entity.
Returns:
Set of config keys (e.g., "best_price_flex", "enable_min_periods_best")
"""
enabled_keys: set[str] = set()
ent_reg = er.async_get(self.hass)
_LOGGER.debug(
"Checking for enabled config override entities for entry %s",
self.config_entry.entry_id,
)
# Map entity keys to their config keys
# Entity keys are defined in number/definitions.py and switch/definitions.py
override_entities = {
# Number entities (best price)
"number.best_price_flex_override": "best_price_flex",
"number.best_price_min_distance_override": "best_price_min_distance_from_avg",
"number.best_price_min_period_length_override": "best_price_min_period_length",
"number.best_price_min_periods_override": "min_periods_best",
"number.best_price_relaxation_attempts_override": "relaxation_attempts_best",
"number.best_price_gap_count_override": "best_price_max_level_gap_count",
# Number entities (peak price)
"number.peak_price_flex_override": "peak_price_flex",
"number.peak_price_min_distance_override": "peak_price_min_distance_from_avg",
"number.peak_price_min_period_length_override": "peak_price_min_period_length",
"number.peak_price_min_periods_override": "min_periods_peak",
"number.peak_price_relaxation_attempts_override": "relaxation_attempts_peak",
"number.peak_price_gap_count_override": "peak_price_max_level_gap_count",
# Switch entities
"switch.best_price_enable_relaxation_override": "enable_min_periods_best",
"switch.peak_price_enable_relaxation_override": "enable_min_periods_peak",
}
# Check each possible override entity
for entity_id_suffix, config_key in override_entities.items():
# Entity IDs include device name, so we need to search by unique_id pattern
# The unique_id follows pattern: {config_entry_id}_{entity_key}
domain, entity_key = entity_id_suffix.split(".", 1)
# Find entity by iterating through registry
for entity_entry in ent_reg.entities.values():
if (
entity_entry.domain == domain
and entity_entry.config_entry_id == self.config_entry.entry_id
and entity_entry.unique_id
and entity_entry.unique_id.endswith(entity_key)
and not entity_entry.disabled
):
_LOGGER.debug(
"Found enabled config override entity: %s -> config_key=%s",
entity_entry.entity_id,
config_key,
)
enabled_keys.add(config_key)
break
_LOGGER.debug("Enabled config override keys: %s", enabled_keys)
return enabled_keys
def _get_active_overrides(self) -> ConfigOverrides:
"""
Build override dict from enabled config entities.
Returns a dict structure compatible with schema functions.
"""
enabled_keys = self._get_enabled_config_entities()
if not enabled_keys:
_LOGGER.debug("No enabled config override entities found")
return {}
# Build structure expected by schema: {section: {key: True}}
# Section doesn't matter for read_only check, we just need the key present
overrides: ConfigOverrides = {"_enabled": {}}
for key in enabled_keys:
overrides["_enabled"][key] = True
_LOGGER.debug("Active overrides structure: %s", overrides)
return overrides
def _get_override_warning_placeholder(self, step_id: str, overrides: ConfigOverrides) -> dict[str, str]:
"""
Get description placeholder for config override warning.
Args:
step_id: The options flow step ID (e.g., "best_price", "peak_price")
overrides: Active overrides dictionary
Returns:
Dictionary with 'override_warning' placeholder
"""
# Define which config keys belong to each step
step_keys: dict[str, set[str]] = {
"best_price": {
"best_price_flex",
"best_price_min_distance_from_avg",
"best_price_min_period_length",
"min_periods_best",
"relaxation_attempts_best",
"enable_min_periods_best",
},
"peak_price": {
"peak_price_flex",
"peak_price_min_distance_from_avg",
"peak_price_min_period_length",
"min_periods_peak",
"relaxation_attempts_peak",
"enable_min_periods_peak",
},
}
keys_to_check = step_keys.get(step_id, set())
enabled_keys = overrides.get("_enabled", {})
override_count = sum(1 for k in enabled_keys if k in keys_to_check)
if override_count > 0:
field_word = "field is" if override_count == 1 else "fields are"
return {
"override_warning": (
f"\n\n🔒 **{override_count} {field_word} managed by configuration entities** "
"(grayed out). Disable the config entity to edit here, "
"or change the value directly via the entity."
)
}
return {"override_warning": ""}
async def _get_override_translations(self) -> dict[str, Any]:
"""
Load override translations from common section.
Returns:
Dictionary with override_warning_template, override_warning_and,
and override_field_labels keys
"""
language = self.hass.config.language
translations: dict[str, Any] = {}
# Load template, and connector, and field labels from common section
template = await async_get_translation(self.hass, ["common", "override_warning_template"], language)
if template:
translations["override_warning_template"] = template
and_connector = await async_get_translation(self.hass, ["common", "override_warning_and"], language)
if and_connector:
translations["override_warning_and"] = and_connector
field_labels = await async_get_translation(self.hass, ["common", "override_field_labels"], language)
if field_labels:
translations["override_field_labels"] = field_labels
return translations
async def async_step_init(self, _user_input: dict[str, Any] | None = None) -> ConfigFlowResult: async def async_step_init(self, _user_input: dict[str, Any] | None = None) -> ConfigFlowResult:
"""Manage the options - show menu.""" """Manage the options - show menu."""
# Always reload options from config_entry to get latest saved state # Always reload options from config_entry to get latest saved state
@ -446,11 +610,22 @@ class TibberPricesOptionsFlowHandler(OptionsFlow):
# Return to menu for more changes # Return to menu for more changes
return await self.async_step_init() return await self.async_step_init()
overrides = self._get_active_overrides()
placeholders = self._get_entity_warning_placeholders("best_price")
placeholders.update(self._get_override_warning_placeholder("best_price", overrides))
# Load translations for override warnings
override_translations = await self._get_override_translations()
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,
overrides=overrides,
translations=override_translations,
),
errors=errors, errors=errors,
description_placeholders=self._get_entity_warning_placeholders("best_price"), description_placeholders=placeholders,
) )
async def async_step_peak_price(self, user_input: dict[str, Any] | None = None) -> ConfigFlowResult: async def async_step_peak_price(self, user_input: dict[str, Any] | None = None) -> ConfigFlowResult:
@ -507,11 +682,22 @@ class TibberPricesOptionsFlowHandler(OptionsFlow):
# Return to menu for more changes # Return to menu for more changes
return await self.async_step_init() return await self.async_step_init()
overrides = self._get_active_overrides()
placeholders = self._get_entity_warning_placeholders("peak_price")
placeholders.update(self._get_override_warning_placeholder("peak_price", overrides))
# Load translations for override warnings
override_translations = await self._get_override_translations()
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,
overrides=overrides,
translations=override_translations,
),
errors=errors, errors=errors,
description_placeholders=self._get_entity_warning_placeholders("peak_price"), description_placeholders=placeholders,
) )
async def async_step_price_trend(self, user_input: dict[str, Any] | None = None) -> ConfigFlowResult: async def async_step_price_trend(self, user_input: dict[str, Any] | None = None) -> ConfigFlowResult:

View file

@ -119,6 +119,8 @@ from homeassistant.data_entry_flow import section
from homeassistant.helpers import selector from homeassistant.helpers import selector
from homeassistant.helpers.selector import ( from homeassistant.helpers.selector import (
BooleanSelector, BooleanSelector,
ConstantSelector,
ConstantSelectorConfig,
NumberSelector, NumberSelector,
NumberSelectorConfig, NumberSelectorConfig,
NumberSelectorMode, NumberSelectorMode,
@ -131,6 +133,156 @@ from homeassistant.helpers.selector import (
TextSelectorType, TextSelectorType,
) )
# Type alias for config override structure: {section: {config_key: value}}
ConfigOverrides = dict[str, dict[str, Any]]
def is_field_overridden(
config_key: str,
config_section: str, # noqa: ARG001 - kept for API compatibility
overrides: ConfigOverrides | None,
) -> bool:
"""
Check if a config field has an active runtime override.
Args:
config_key: The configuration key to check (e.g., "best_price_flex")
config_section: Unused, kept for API compatibility
overrides: Dictionary of active overrides (with "_enabled" key)
Returns:
True if this field is being overridden by a config entity, False otherwise
"""
if overrides is None:
return False
# Check if key is in the _enabled section (from entity registry check)
return config_key in overrides.get("_enabled", {})
# Override translations structure from common section
# This will be loaded at runtime and passed to schema functions
OverrideTranslations = dict[str, Any] # Type alias
# Fallback labels when translations not available
# Used only as fallback - translations should be loaded from common.override_field_labels
DEFAULT_FIELD_LABELS: dict[str, str] = {
# Best Price
CONF_BEST_PRICE_MIN_PERIOD_LENGTH: "Minimum Period Length",
CONF_BEST_PRICE_MAX_LEVEL_GAP_COUNT: "Gap Tolerance",
CONF_BEST_PRICE_FLEX: "Flexibility",
CONF_BEST_PRICE_MIN_DISTANCE_FROM_AVG: "Minimum Distance",
CONF_ENABLE_MIN_PERIODS_BEST: "Achieve Minimum Count",
CONF_MIN_PERIODS_BEST: "Minimum Periods",
CONF_RELAXATION_ATTEMPTS_BEST: "Relaxation Attempts",
# Peak Price
CONF_PEAK_PRICE_MIN_PERIOD_LENGTH: "Minimum Period Length",
CONF_PEAK_PRICE_MAX_LEVEL_GAP_COUNT: "Gap Tolerance",
CONF_PEAK_PRICE_FLEX: "Flexibility",
CONF_PEAK_PRICE_MIN_DISTANCE_FROM_AVG: "Minimum Distance",
CONF_ENABLE_MIN_PERIODS_PEAK: "Achieve Minimum Count",
CONF_MIN_PERIODS_PEAK: "Minimum Periods",
CONF_RELAXATION_ATTEMPTS_PEAK: "Relaxation Attempts",
}
# Section to config keys mapping for override detection
SECTION_CONFIG_KEYS: dict[str, dict[str, list[str]]] = {
"best_price": {
"period_settings": [
CONF_BEST_PRICE_MIN_PERIOD_LENGTH,
CONF_BEST_PRICE_MAX_LEVEL_GAP_COUNT,
],
"flexibility_settings": [
CONF_BEST_PRICE_FLEX,
CONF_BEST_PRICE_MIN_DISTANCE_FROM_AVG,
],
"relaxation_and_target_periods": [
CONF_ENABLE_MIN_PERIODS_BEST,
CONF_MIN_PERIODS_BEST,
CONF_RELAXATION_ATTEMPTS_BEST,
],
},
"peak_price": {
"period_settings": [
CONF_PEAK_PRICE_MIN_PERIOD_LENGTH,
CONF_PEAK_PRICE_MAX_LEVEL_GAP_COUNT,
],
"flexibility_settings": [
CONF_PEAK_PRICE_FLEX,
CONF_PEAK_PRICE_MIN_DISTANCE_FROM_AVG,
],
"relaxation_and_target_periods": [
CONF_ENABLE_MIN_PERIODS_PEAK,
CONF_MIN_PERIODS_PEAK,
CONF_RELAXATION_ATTEMPTS_PEAK,
],
},
}
def get_section_override_warning(
step_id: str,
section_id: str,
overrides: ConfigOverrides | None,
translations: OverrideTranslations | None = None,
) -> dict[vol.Optional, ConstantSelector] | None:
"""
Return a warning constant selector if any fields in the section are overridden.
Args:
step_id: The step ID (best_price or peak_price)
section_id: The section ID within the step
overrides: Active runtime overrides from coordinator
translations: Override translations from common section (optional)
Returns:
Dict with override warning selector if any fields overridden, None otherwise
"""
if not overrides:
return None
section_keys = SECTION_CONFIG_KEYS.get(step_id, {}).get(section_id, [])
overridden_fields = []
# Get field labels from translations or use fallback
field_labels = DEFAULT_FIELD_LABELS
if translations and "override_field_labels" in translations:
field_labels = translations["override_field_labels"]
for config_key in section_keys:
if is_field_overridden(config_key, section_id, overrides):
label = field_labels.get(config_key, config_key)
overridden_fields.append(label)
if not overridden_fields:
return None
# Get translated "and" connector or use fallback
and_connector = " and "
if translations and "override_warning_and" in translations:
and_connector = f" {translations['override_warning_and']} "
# Build warning message with list of overridden fields
if len(overridden_fields) == 1:
fields_text = overridden_fields[0]
else:
fields_text = ", ".join(overridden_fields[:-1]) + and_connector + overridden_fields[-1]
# Get translated warning template or use fallback
warning_template = "⚠️ {fields} controlled by config entity"
if translations and "override_warning_template" in translations:
warning_template = translations["override_warning_template"]
return {
vol.Optional("_override_warning"): ConstantSelector(
ConstantSelectorConfig(
value=True,
label=warning_template.format(fields=fields_text),
)
),
}
def get_user_schema(access_token: str | None = None) -> vol.Schema: def get_user_schema(access_token: str | None = None) -> vol.Schema:
"""Return schema for user step (API token input).""" """Return schema for user step (API token input)."""
@ -434,298 +586,322 @@ def get_volatility_schema(options: Mapping[str, Any]) -> vol.Schema:
) )
def get_best_price_schema(options: Mapping[str, Any]) -> vol.Schema: def get_best_price_schema(
"""Return schema for best price period configuration with collapsible sections.""" options: Mapping[str, Any],
overrides: ConfigOverrides | None = None,
translations: OverrideTranslations | None = None,
) -> vol.Schema:
"""
Return schema for best price period configuration with collapsible sections.
Args:
options: Current options from config entry
overrides: Active runtime overrides from coordinator. Fields with active
overrides will be replaced with a constant placeholder.
translations: Override translations from common section (optional)
Returns:
Voluptuous schema for the best price configuration form
"""
period_settings = options.get("period_settings", {}) period_settings = options.get("period_settings", {})
flexibility_settings = options.get("flexibility_settings", {})
relaxation_settings = options.get("relaxation_and_target_periods", {})
# Get current values for override display
min_period_length = int(
period_settings.get(CONF_BEST_PRICE_MIN_PERIOD_LENGTH, DEFAULT_BEST_PRICE_MIN_PERIOD_LENGTH)
)
max_level_gap_count = int(
period_settings.get(CONF_BEST_PRICE_MAX_LEVEL_GAP_COUNT, DEFAULT_BEST_PRICE_MAX_LEVEL_GAP_COUNT)
)
best_price_flex = int(flexibility_settings.get(CONF_BEST_PRICE_FLEX, DEFAULT_BEST_PRICE_FLEX))
min_distance = int(
flexibility_settings.get(CONF_BEST_PRICE_MIN_DISTANCE_FROM_AVG, DEFAULT_BEST_PRICE_MIN_DISTANCE_FROM_AVG)
)
enable_min_periods = relaxation_settings.get(CONF_ENABLE_MIN_PERIODS_BEST, DEFAULT_ENABLE_MIN_PERIODS_BEST)
min_periods = int(relaxation_settings.get(CONF_MIN_PERIODS_BEST, DEFAULT_MIN_PERIODS_BEST))
relaxation_attempts = int(relaxation_settings.get(CONF_RELAXATION_ATTEMPTS_BEST, DEFAULT_RELAXATION_ATTEMPTS_BEST))
# Build section schemas with optional override warnings
period_warning = get_section_override_warning("best_price", "period_settings", overrides, translations) or {}
period_fields: dict[vol.Optional | vol.Required, Any] = {
**period_warning, # type: ignore[misc]
vol.Optional(
CONF_BEST_PRICE_MIN_PERIOD_LENGTH,
default=min_period_length,
): NumberSelector(
NumberSelectorConfig(
min=MIN_PERIOD_LENGTH,
max=MAX_MIN_PERIOD_LENGTH,
step=15,
unit_of_measurement="min",
mode=NumberSelectorMode.SLIDER,
)
),
vol.Optional(
CONF_BEST_PRICE_MAX_LEVEL,
default=period_settings.get(
CONF_BEST_PRICE_MAX_LEVEL,
DEFAULT_BEST_PRICE_MAX_LEVEL,
),
): SelectSelector(
SelectSelectorConfig(
options=BEST_PRICE_MAX_LEVEL_OPTIONS,
mode=SelectSelectorMode.DROPDOWN,
translation_key="current_interval_price_level",
),
),
vol.Optional(
CONF_BEST_PRICE_MAX_LEVEL_GAP_COUNT,
default=max_level_gap_count,
): NumberSelector(
NumberSelectorConfig(
min=MIN_GAP_COUNT,
max=MAX_GAP_COUNT,
step=1,
mode=NumberSelectorMode.SLIDER,
)
),
}
flexibility_warning = (
get_section_override_warning("best_price", "flexibility_settings", overrides, translations) or {}
)
flexibility_fields: dict[vol.Optional | vol.Required, Any] = {
**flexibility_warning, # type: ignore[misc]
vol.Optional(
CONF_BEST_PRICE_FLEX,
default=best_price_flex,
): NumberSelector(
NumberSelectorConfig(
min=0,
max=50,
step=1,
unit_of_measurement="%",
mode=NumberSelectorMode.SLIDER,
)
),
vol.Optional(
CONF_BEST_PRICE_MIN_DISTANCE_FROM_AVG,
default=min_distance,
): NumberSelector(
NumberSelectorConfig(
min=-50,
max=0,
step=1,
unit_of_measurement="%",
mode=NumberSelectorMode.SLIDER,
)
),
}
relaxation_warning = (
get_section_override_warning("best_price", "relaxation_and_target_periods", overrides, translations) or {}
)
relaxation_fields: dict[vol.Optional | vol.Required, Any] = {
**relaxation_warning, # type: ignore[misc]
vol.Optional(
CONF_ENABLE_MIN_PERIODS_BEST,
default=enable_min_periods,
): BooleanSelector(selector.BooleanSelectorConfig()),
vol.Optional(
CONF_MIN_PERIODS_BEST,
default=min_periods,
): NumberSelector(
NumberSelectorConfig(
min=1,
max=MAX_MIN_PERIODS,
step=1,
mode=NumberSelectorMode.SLIDER,
)
),
vol.Optional(
CONF_RELAXATION_ATTEMPTS_BEST,
default=relaxation_attempts,
): NumberSelector(
NumberSelectorConfig(
min=MIN_RELAXATION_ATTEMPTS,
max=MAX_RELAXATION_ATTEMPTS,
step=1,
mode=NumberSelectorMode.SLIDER,
)
),
}
return vol.Schema( return vol.Schema(
{ {
vol.Required("period_settings"): section( vol.Required("period_settings"): section(
vol.Schema( vol.Schema(period_fields),
{
vol.Optional(
CONF_BEST_PRICE_MIN_PERIOD_LENGTH,
default=int(
period_settings.get(
CONF_BEST_PRICE_MIN_PERIOD_LENGTH,
DEFAULT_BEST_PRICE_MIN_PERIOD_LENGTH,
)
),
): NumberSelector(
NumberSelectorConfig(
min=MIN_PERIOD_LENGTH,
max=MAX_MIN_PERIOD_LENGTH,
step=15,
unit_of_measurement="min",
mode=NumberSelectorMode.SLIDER,
),
),
vol.Optional(
CONF_BEST_PRICE_MAX_LEVEL,
default=period_settings.get(
CONF_BEST_PRICE_MAX_LEVEL,
DEFAULT_BEST_PRICE_MAX_LEVEL,
),
): SelectSelector(
SelectSelectorConfig(
options=BEST_PRICE_MAX_LEVEL_OPTIONS,
mode=SelectSelectorMode.DROPDOWN,
translation_key="current_interval_price_level",
),
),
vol.Optional(
CONF_BEST_PRICE_MAX_LEVEL_GAP_COUNT,
default=int(
period_settings.get(
CONF_BEST_PRICE_MAX_LEVEL_GAP_COUNT,
DEFAULT_BEST_PRICE_MAX_LEVEL_GAP_COUNT,
)
),
): NumberSelector(
NumberSelectorConfig(
min=MIN_GAP_COUNT,
max=MAX_GAP_COUNT,
step=1,
mode=NumberSelectorMode.SLIDER,
),
),
}
),
{"collapsed": False}, {"collapsed": False},
), ),
vol.Required("flexibility_settings"): section( vol.Required("flexibility_settings"): section(
vol.Schema( vol.Schema(flexibility_fields),
{
vol.Optional(
CONF_BEST_PRICE_FLEX,
default=int(
options.get("flexibility_settings", {}).get(
CONF_BEST_PRICE_FLEX,
DEFAULT_BEST_PRICE_FLEX,
)
),
): NumberSelector(
NumberSelectorConfig(
min=0,
max=50,
step=1,
unit_of_measurement="%",
mode=NumberSelectorMode.SLIDER,
),
),
vol.Optional(
CONF_BEST_PRICE_MIN_DISTANCE_FROM_AVG,
default=int(
options.get("flexibility_settings", {}).get(
CONF_BEST_PRICE_MIN_DISTANCE_FROM_AVG,
DEFAULT_BEST_PRICE_MIN_DISTANCE_FROM_AVG,
)
),
): NumberSelector(
NumberSelectorConfig(
min=-50,
max=0,
step=1,
unit_of_measurement="%",
mode=NumberSelectorMode.SLIDER,
),
),
}
),
{"collapsed": True}, {"collapsed": True},
), ),
vol.Required("relaxation_and_target_periods"): section( vol.Required("relaxation_and_target_periods"): section(
vol.Schema( vol.Schema(relaxation_fields),
{
vol.Optional(
CONF_ENABLE_MIN_PERIODS_BEST,
default=options.get("relaxation_and_target_periods", {}).get(
CONF_ENABLE_MIN_PERIODS_BEST,
DEFAULT_ENABLE_MIN_PERIODS_BEST,
),
): BooleanSelector(),
vol.Optional(
CONF_MIN_PERIODS_BEST,
default=int(
options.get("relaxation_and_target_periods", {}).get(
CONF_MIN_PERIODS_BEST,
DEFAULT_MIN_PERIODS_BEST,
)
),
): NumberSelector(
NumberSelectorConfig(
min=1,
max=MAX_MIN_PERIODS,
step=1,
mode=NumberSelectorMode.SLIDER,
),
),
vol.Optional(
CONF_RELAXATION_ATTEMPTS_BEST,
default=int(
options.get("relaxation_and_target_periods", {}).get(
CONF_RELAXATION_ATTEMPTS_BEST,
DEFAULT_RELAXATION_ATTEMPTS_BEST,
)
),
): NumberSelector(
NumberSelectorConfig(
min=MIN_RELAXATION_ATTEMPTS,
max=MAX_RELAXATION_ATTEMPTS,
step=1,
mode=NumberSelectorMode.SLIDER,
),
),
}
),
{"collapsed": True}, {"collapsed": True},
), ),
} }
) )
def get_peak_price_schema(options: Mapping[str, Any]) -> vol.Schema: def get_peak_price_schema(
"""Return schema for peak price period configuration with collapsible sections.""" options: Mapping[str, Any],
overrides: ConfigOverrides | None = None,
translations: OverrideTranslations | None = None,
) -> vol.Schema:
"""
Return schema for peak price period configuration with collapsible sections.
Args:
options: Current options from config entry
overrides: Active runtime overrides from coordinator. Fields with active
overrides will be replaced with a constant placeholder.
translations: Override translations from common section (optional)
Returns:
Voluptuous schema for the peak price configuration form
"""
period_settings = options.get("period_settings", {}) period_settings = options.get("period_settings", {})
flexibility_settings = options.get("flexibility_settings", {})
relaxation_settings = options.get("relaxation_and_target_periods", {})
# Get current values for override display
min_period_length = int(
period_settings.get(CONF_PEAK_PRICE_MIN_PERIOD_LENGTH, DEFAULT_PEAK_PRICE_MIN_PERIOD_LENGTH)
)
max_level_gap_count = int(
period_settings.get(CONF_PEAK_PRICE_MAX_LEVEL_GAP_COUNT, DEFAULT_PEAK_PRICE_MAX_LEVEL_GAP_COUNT)
)
peak_price_flex = int(flexibility_settings.get(CONF_PEAK_PRICE_FLEX, DEFAULT_PEAK_PRICE_FLEX))
min_distance = int(
flexibility_settings.get(CONF_PEAK_PRICE_MIN_DISTANCE_FROM_AVG, DEFAULT_PEAK_PRICE_MIN_DISTANCE_FROM_AVG)
)
enable_min_periods = relaxation_settings.get(CONF_ENABLE_MIN_PERIODS_PEAK, DEFAULT_ENABLE_MIN_PERIODS_PEAK)
min_periods = int(relaxation_settings.get(CONF_MIN_PERIODS_PEAK, DEFAULT_MIN_PERIODS_PEAK))
relaxation_attempts = int(relaxation_settings.get(CONF_RELAXATION_ATTEMPTS_PEAK, DEFAULT_RELAXATION_ATTEMPTS_PEAK))
# Build section schemas with optional override warnings
period_warning = get_section_override_warning("peak_price", "period_settings", overrides, translations) or {}
period_fields: dict[vol.Optional | vol.Required, Any] = {
**period_warning, # type: ignore[misc]
vol.Optional(
CONF_PEAK_PRICE_MIN_PERIOD_LENGTH,
default=min_period_length,
): NumberSelector(
NumberSelectorConfig(
min=MIN_PERIOD_LENGTH,
max=MAX_MIN_PERIOD_LENGTH,
step=15,
unit_of_measurement="min",
mode=NumberSelectorMode.SLIDER,
)
),
vol.Optional(
CONF_PEAK_PRICE_MIN_LEVEL,
default=period_settings.get(
CONF_PEAK_PRICE_MIN_LEVEL,
DEFAULT_PEAK_PRICE_MIN_LEVEL,
),
): SelectSelector(
SelectSelectorConfig(
options=PEAK_PRICE_MIN_LEVEL_OPTIONS,
mode=SelectSelectorMode.DROPDOWN,
translation_key="current_interval_price_level",
),
),
vol.Optional(
CONF_PEAK_PRICE_MAX_LEVEL_GAP_COUNT,
default=max_level_gap_count,
): NumberSelector(
NumberSelectorConfig(
min=MIN_GAP_COUNT,
max=MAX_GAP_COUNT,
step=1,
mode=NumberSelectorMode.SLIDER,
)
),
}
flexibility_warning = (
get_section_override_warning("peak_price", "flexibility_settings", overrides, translations) or {}
)
flexibility_fields: dict[vol.Optional | vol.Required, Any] = {
**flexibility_warning, # type: ignore[misc]
vol.Optional(
CONF_PEAK_PRICE_FLEX,
default=peak_price_flex,
): NumberSelector(
NumberSelectorConfig(
min=-50,
max=0,
step=1,
unit_of_measurement="%",
mode=NumberSelectorMode.SLIDER,
)
),
vol.Optional(
CONF_PEAK_PRICE_MIN_DISTANCE_FROM_AVG,
default=min_distance,
): NumberSelector(
NumberSelectorConfig(
min=0,
max=50,
step=1,
unit_of_measurement="%",
mode=NumberSelectorMode.SLIDER,
)
),
}
relaxation_warning = (
get_section_override_warning("peak_price", "relaxation_and_target_periods", overrides, translations) or {}
)
relaxation_fields: dict[vol.Optional | vol.Required, Any] = {
**relaxation_warning, # type: ignore[misc]
vol.Optional(
CONF_ENABLE_MIN_PERIODS_PEAK,
default=enable_min_periods,
): BooleanSelector(selector.BooleanSelectorConfig()),
vol.Optional(
CONF_MIN_PERIODS_PEAK,
default=min_periods,
): NumberSelector(
NumberSelectorConfig(
min=1,
max=MAX_MIN_PERIODS,
step=1,
mode=NumberSelectorMode.SLIDER,
)
),
vol.Optional(
CONF_RELAXATION_ATTEMPTS_PEAK,
default=relaxation_attempts,
): NumberSelector(
NumberSelectorConfig(
min=MIN_RELAXATION_ATTEMPTS,
max=MAX_RELAXATION_ATTEMPTS,
step=1,
mode=NumberSelectorMode.SLIDER,
)
),
}
return vol.Schema( return vol.Schema(
{ {
vol.Required("period_settings"): section( vol.Required("period_settings"): section(
vol.Schema( vol.Schema(period_fields),
{
vol.Optional(
CONF_PEAK_PRICE_MIN_PERIOD_LENGTH,
default=int(
period_settings.get(
CONF_PEAK_PRICE_MIN_PERIOD_LENGTH,
DEFAULT_PEAK_PRICE_MIN_PERIOD_LENGTH,
)
),
): NumberSelector(
NumberSelectorConfig(
min=MIN_PERIOD_LENGTH,
max=MAX_MIN_PERIOD_LENGTH,
step=15,
unit_of_measurement="min",
mode=NumberSelectorMode.SLIDER,
),
),
vol.Optional(
CONF_PEAK_PRICE_MIN_LEVEL,
default=period_settings.get(
CONF_PEAK_PRICE_MIN_LEVEL,
DEFAULT_PEAK_PRICE_MIN_LEVEL,
),
): SelectSelector(
SelectSelectorConfig(
options=PEAK_PRICE_MIN_LEVEL_OPTIONS,
mode=SelectSelectorMode.DROPDOWN,
translation_key="current_interval_price_level",
),
),
vol.Optional(
CONF_PEAK_PRICE_MAX_LEVEL_GAP_COUNT,
default=int(
period_settings.get(
CONF_PEAK_PRICE_MAX_LEVEL_GAP_COUNT,
DEFAULT_PEAK_PRICE_MAX_LEVEL_GAP_COUNT,
)
),
): NumberSelector(
NumberSelectorConfig(
min=MIN_GAP_COUNT,
max=MAX_GAP_COUNT,
step=1,
mode=NumberSelectorMode.SLIDER,
),
),
}
),
{"collapsed": False}, {"collapsed": False},
), ),
vol.Required("flexibility_settings"): section( vol.Required("flexibility_settings"): section(
vol.Schema( vol.Schema(flexibility_fields),
{
vol.Optional(
CONF_PEAK_PRICE_FLEX,
default=int(
options.get("flexibility_settings", {}).get(
CONF_PEAK_PRICE_FLEX,
DEFAULT_PEAK_PRICE_FLEX,
)
),
): NumberSelector(
NumberSelectorConfig(
min=-50,
max=0,
step=1,
unit_of_measurement="%",
mode=NumberSelectorMode.SLIDER,
),
),
vol.Optional(
CONF_PEAK_PRICE_MIN_DISTANCE_FROM_AVG,
default=int(
options.get("flexibility_settings", {}).get(
CONF_PEAK_PRICE_MIN_DISTANCE_FROM_AVG,
DEFAULT_PEAK_PRICE_MIN_DISTANCE_FROM_AVG,
)
),
): NumberSelector(
NumberSelectorConfig(
min=0,
max=50,
step=1,
unit_of_measurement="%",
mode=NumberSelectorMode.SLIDER,
),
),
}
),
{"collapsed": True}, {"collapsed": True},
), ),
vol.Required("relaxation_and_target_periods"): section( vol.Required("relaxation_and_target_periods"): section(
vol.Schema( vol.Schema(relaxation_fields),
{
vol.Optional(
CONF_ENABLE_MIN_PERIODS_PEAK,
default=options.get("relaxation_and_target_periods", {}).get(
CONF_ENABLE_MIN_PERIODS_PEAK,
DEFAULT_ENABLE_MIN_PERIODS_PEAK,
),
): BooleanSelector(),
vol.Optional(
CONF_MIN_PERIODS_PEAK,
default=int(
options.get("relaxation_and_target_periods", {}).get(
CONF_MIN_PERIODS_PEAK,
DEFAULT_MIN_PERIODS_PEAK,
)
),
): NumberSelector(
NumberSelectorConfig(
min=1,
max=MAX_MIN_PERIODS,
step=1,
mode=NumberSelectorMode.SLIDER,
),
),
vol.Optional(
CONF_RELAXATION_ATTEMPTS_PEAK,
default=int(
options.get("relaxation_and_target_periods", {}).get(
CONF_RELAXATION_ATTEMPTS_PEAK,
DEFAULT_RELAXATION_ATTEMPTS_PEAK,
)
),
): NumberSelector(
NumberSelectorConfig(
min=MIN_RELAXATION_ATTEMPTS,
max=MAX_RELAXATION_ATTEMPTS,
step=1,
mode=NumberSelectorMode.SLIDER,
),
),
}
),
{"collapsed": True}, {"collapsed": True},
), ),
} }

View file

@ -218,6 +218,7 @@ class TibberPricesDataUpdateCoordinator(DataUpdateCoordinator[dict[str, Any]]):
self._period_calculator = TibberPricesPeriodCalculator( self._period_calculator = TibberPricesPeriodCalculator(
config_entry=config_entry, config_entry=config_entry,
log_prefix=self._log_prefix, log_prefix=self._log_prefix,
get_config_override_fn=self.get_config_override,
) )
self._data_transformer = TibberPricesDataTransformer( self._data_transformer = TibberPricesDataTransformer(
config_entry=config_entry, config_entry=config_entry,
@ -255,6 +256,11 @@ class TibberPricesDataUpdateCoordinator(DataUpdateCoordinator[dict[str, Any]]):
self._is_fetching: bool = False # Flag to track active API fetch (read by lifecycle sensor) self._is_fetching: bool = False # Flag to track active API fetch (read by lifecycle sensor)
self._last_coordinator_update: datetime | None = None # When Timer #1 last ran (_async_update_data) self._last_coordinator_update: datetime | None = None # When Timer #1 last ran (_async_update_data)
# Runtime config overrides from config entities (number/switch)
# Structure: {"section_name": {"config_key": value, ...}, ...}
# When set, these override the corresponding options from config_entry.options
self._config_overrides: dict[str, dict[str, Any]] = {}
# Start timers # Start timers
self._listener_manager.schedule_quarter_hour_refresh(self._handle_quarter_hour_refresh) self._listener_manager.schedule_quarter_hour_refresh(self._handle_quarter_hour_refresh)
self._listener_manager.schedule_minute_refresh(self._handle_minute_refresh) self._listener_manager.schedule_minute_refresh(self._handle_minute_refresh)
@ -281,6 +287,114 @@ class TibberPricesDataUpdateCoordinator(DataUpdateCoordinator[dict[str, Any]]):
else: else:
self._log("debug", "No data to re-transform") self._log("debug", "No data to re-transform")
# =========================================================================
# Runtime Config Override Methods (for number/switch entities)
# =========================================================================
def set_config_override(self, config_key: str, config_section: str, value: Any) -> None:
"""
Set a runtime config override value.
These overrides take precedence over options from config_entry.options
and are used by number/switch entities for runtime configuration.
Args:
config_key: The configuration key (e.g., CONF_BEST_PRICE_FLEX)
config_section: The section in options (e.g., "flexibility_settings")
value: The override value
"""
if config_section not in self._config_overrides:
self._config_overrides[config_section] = {}
self._config_overrides[config_section][config_key] = value
self._log(
"debug",
"Config override set: %s.%s = %s",
config_section,
config_key,
value,
)
def remove_config_override(self, config_key: str, config_section: str) -> None:
"""
Remove a runtime config override value.
After removal, the value from config_entry.options will be used again.
Args:
config_key: The configuration key to remove
config_section: The section the key belongs to
"""
if config_section in self._config_overrides:
self._config_overrides[config_section].pop(config_key, None)
# Clean up empty sections
if not self._config_overrides[config_section]:
del self._config_overrides[config_section]
self._log(
"debug",
"Config override removed: %s.%s",
config_section,
config_key,
)
def get_config_override(self, config_key: str, config_section: str) -> Any | None:
"""
Get a runtime config override value if set.
Args:
config_key: The configuration key to check
config_section: The section the key belongs to
Returns:
The override value if set, None otherwise
"""
return self._config_overrides.get(config_section, {}).get(config_key)
def has_config_override(self, config_key: str, config_section: str) -> bool:
"""
Check if a runtime config override is set.
Args:
config_key: The configuration key to check
config_section: The section the key belongs to
Returns:
True if an override is set, False otherwise
"""
return config_key in self._config_overrides.get(config_section, {})
def get_active_overrides(self) -> dict[str, dict[str, Any]]:
"""
Get all active config overrides.
Returns:
Dictionary of all active overrides by section
"""
return self._config_overrides.copy()
async def async_handle_config_override_update(self) -> None:
"""
Handle config override change by invalidating caches and re-transforming data.
This is called by number/switch entities when their values change.
Uses the same logic as options update to ensure consistent behavior.
"""
self._log("debug", "Config override update triggered, re-transforming data")
self._data_transformer.invalidate_config_cache()
self._period_calculator.invalidate_config_cache()
# Re-transform existing data with new configuration
if self.data and "priceInfo" in self.data:
raw_data = {"price_info": self.data["priceInfo"]}
self.data = self._transform_data(raw_data)
self.async_update_listeners()
else:
self._log("debug", "No data 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

@ -13,6 +13,8 @@ from typing import TYPE_CHECKING, Any
from custom_components.tibber_prices import const as _const from custom_components.tibber_prices import const as _const
if TYPE_CHECKING: if TYPE_CHECKING:
from collections.abc import Callable
from custom_components.tibber_prices.coordinator.time_service import TibberPricesTimeService from custom_components.tibber_prices.coordinator.time_service import TibberPricesTimeService
from homeassistant.config_entries import ConfigEntry from homeassistant.config_entries import ConfigEntry
@ -32,6 +34,7 @@ class TibberPricesPeriodCalculator:
self, self,
config_entry: ConfigEntry, config_entry: ConfigEntry,
log_prefix: str, log_prefix: str,
get_config_override_fn: Callable[[str, str], Any | None] | None = None,
) -> None: ) -> None:
"""Initialize the period calculator.""" """Initialize the period calculator."""
self.config_entry = config_entry self.config_entry = config_entry
@ -39,11 +42,40 @@ class TibberPricesPeriodCalculator:
self.time: TibberPricesTimeService # Set by coordinator before first use self.time: TibberPricesTimeService # Set by coordinator before first use
self._config_cache: dict[str, dict[str, Any]] | None = None self._config_cache: dict[str, dict[str, Any]] | None = None
self._config_cache_valid = False self._config_cache_valid = False
self._get_config_override = get_config_override_fn
# Period calculation cache # Period calculation cache
self._cached_periods: dict[str, Any] | None = None self._cached_periods: dict[str, Any] | None = None
self._last_periods_hash: str | None = None self._last_periods_hash: str | None = None
def _get_option(
self,
config_key: str,
config_section: str,
default: Any,
) -> Any:
"""
Get a config option, checking overrides first.
Args:
config_key: The configuration key
config_section: The section in options (e.g., "flexibility_settings")
default: Default value if not set
Returns:
Override value if set, otherwise options value, otherwise default
"""
# Check overrides first
if self._get_config_override is not None:
override = self._get_config_override(config_key, config_section)
if override is not None:
return override
# Fall back to options
section = self.config_entry.options.get(config_section, {})
return section.get(config_key, default)
def _log(self, level: str, message: str, *args: object, **kwargs: object) -> None: def _log(self, level: str, message: str, *args: object, **kwargs: object) -> None:
"""Log with calculator-specific prefix.""" """Log with calculator-specific prefix."""
prefixed_message = f"{self._log_prefix} {message}" prefixed_message = f"{self._log_prefix} {message}"
@ -112,7 +144,7 @@ class TibberPricesPeriodCalculator:
Get period calculation configuration from config options. Get period calculation configuration from config options.
Uses cached config to avoid multiple options.get() calls. Uses cached config to avoid multiple options.get() calls.
Cache is invalidated when config_entry.options change. Cache is invalidated when config_entry.options change or override entities update.
""" """
cache_key = "peak" if reverse_sort else "best" cache_key = "peak" if reverse_sort else "best"
@ -124,36 +156,44 @@ class TibberPricesPeriodCalculator:
if self._config_cache is None: if self._config_cache is None:
self._config_cache = {} self._config_cache = {}
options = self.config_entry.options # Get config values, checking overrides first
# Get nested sections from options
# CRITICAL: Best/Peak price settings are stored in nested sections: # CRITICAL: Best/Peak price settings are stored in nested sections:
# - period_settings: min_period_length, max_level, gap_count # - period_settings: min_period_length, max_level, gap_count
# - flexibility_settings: flex, min_distance_from_avg # - flexibility_settings: flex, min_distance_from_avg
# These settings are ONLY in options (not in data), structured since initial config flow # Override entities can override any of these values at runtime
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 = flexibility_settings.get(_const.CONF_PEAK_PRICE_FLEX, _const.DEFAULT_PEAK_PRICE_FLEX) flex = self._get_option(
min_distance_from_avg = flexibility_settings.get( _const.CONF_PEAK_PRICE_FLEX,
"flexibility_settings",
_const.DEFAULT_PEAK_PRICE_FLEX,
)
min_distance_from_avg = self._get_option(
_const.CONF_PEAK_PRICE_MIN_DISTANCE_FROM_AVG, _const.CONF_PEAK_PRICE_MIN_DISTANCE_FROM_AVG,
"flexibility_settings",
_const.DEFAULT_PEAK_PRICE_MIN_DISTANCE_FROM_AVG, _const.DEFAULT_PEAK_PRICE_MIN_DISTANCE_FROM_AVG,
) )
min_period_length = period_settings.get( min_period_length = self._get_option(
_const.CONF_PEAK_PRICE_MIN_PERIOD_LENGTH, _const.CONF_PEAK_PRICE_MIN_PERIOD_LENGTH,
"period_settings",
_const.DEFAULT_PEAK_PRICE_MIN_PERIOD_LENGTH, _const.DEFAULT_PEAK_PRICE_MIN_PERIOD_LENGTH,
) )
else: else:
# Best price configuration # Best price configuration
flex = flexibility_settings.get(_const.CONF_BEST_PRICE_FLEX, _const.DEFAULT_BEST_PRICE_FLEX) flex = self._get_option(
min_distance_from_avg = flexibility_settings.get( _const.CONF_BEST_PRICE_FLEX,
"flexibility_settings",
_const.DEFAULT_BEST_PRICE_FLEX,
)
min_distance_from_avg = self._get_option(
_const.CONF_BEST_PRICE_MIN_DISTANCE_FROM_AVG, _const.CONF_BEST_PRICE_MIN_DISTANCE_FROM_AVG,
"flexibility_settings",
_const.DEFAULT_BEST_PRICE_MIN_DISTANCE_FROM_AVG, _const.DEFAULT_BEST_PRICE_MIN_DISTANCE_FROM_AVG,
) )
min_period_length = period_settings.get( min_period_length = self._get_option(
_const.CONF_BEST_PRICE_MIN_PERIOD_LENGTH, _const.CONF_BEST_PRICE_MIN_PERIOD_LENGTH,
"period_settings",
_const.DEFAULT_BEST_PRICE_MIN_PERIOD_LENGTH, _const.DEFAULT_BEST_PRICE_MIN_PERIOD_LENGTH,
) )
@ -610,9 +650,10 @@ class TibberPricesPeriodCalculator:
# Get relaxation configuration for best price # Get relaxation configuration for best price
# CRITICAL: Relaxation settings are stored in nested section 'relaxation_and_target_periods' # 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", {}) # Override entities can override any of these values at runtime
enable_relaxation_best = relaxation_and_target_periods.get( enable_relaxation_best = self._get_option(
_const.CONF_ENABLE_MIN_PERIODS_BEST, _const.CONF_ENABLE_MIN_PERIODS_BEST,
"relaxation_and_target_periods",
_const.DEFAULT_ENABLE_MIN_PERIODS_BEST, _const.DEFAULT_ENABLE_MIN_PERIODS_BEST,
) )
@ -623,12 +664,14 @@ 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 = relaxation_and_target_periods.get( min_periods_best = self._get_option(
_const.CONF_MIN_PERIODS_BEST, _const.CONF_MIN_PERIODS_BEST,
"relaxation_and_target_periods",
_const.DEFAULT_MIN_PERIODS_BEST, _const.DEFAULT_MIN_PERIODS_BEST,
) )
relaxation_attempts_best = relaxation_and_target_periods.get( relaxation_attempts_best = self._get_option(
_const.CONF_RELAXATION_ATTEMPTS_BEST, _const.CONF_RELAXATION_ATTEMPTS_BEST,
"relaxation_and_target_periods",
_const.DEFAULT_RELAXATION_ATTEMPTS_BEST, _const.DEFAULT_RELAXATION_ATTEMPTS_BEST,
) )
@ -637,13 +680,14 @@ class TibberPricesPeriodCalculator:
best_config = self.get_period_config(reverse_sort=False) best_config = self.get_period_config(reverse_sort=False)
# Get level filter configuration from period_settings section # Get level filter configuration from period_settings section
# CRITICAL: max_level and gap_count are stored in nested section 'period_settings' # 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 = self._get_option(
max_level_best = period_settings.get(
_const.CONF_BEST_PRICE_MAX_LEVEL, _const.CONF_BEST_PRICE_MAX_LEVEL,
"period_settings",
_const.DEFAULT_BEST_PRICE_MAX_LEVEL, _const.DEFAULT_BEST_PRICE_MAX_LEVEL,
) )
gap_count_best = period_settings.get( gap_count_best = self._get_option(
_const.CONF_BEST_PRICE_MAX_LEVEL_GAP_COUNT, _const.CONF_BEST_PRICE_MAX_LEVEL_GAP_COUNT,
"period_settings",
_const.DEFAULT_BEST_PRICE_MAX_LEVEL_GAP_COUNT, _const.DEFAULT_BEST_PRICE_MAX_LEVEL_GAP_COUNT,
) )
best_period_config = TibberPricesPeriodConfig( best_period_config = TibberPricesPeriodConfig(
@ -687,8 +731,10 @@ class TibberPricesPeriodCalculator:
# Get relaxation configuration for peak price # Get relaxation configuration for peak price
# CRITICAL: Relaxation settings are stored in nested section 'relaxation_and_target_periods' # CRITICAL: Relaxation settings are stored in nested section 'relaxation_and_target_periods'
enable_relaxation_peak = relaxation_and_target_periods.get( # Override entities can override any of these values at runtime
enable_relaxation_peak = self._get_option(
_const.CONF_ENABLE_MIN_PERIODS_PEAK, _const.CONF_ENABLE_MIN_PERIODS_PEAK,
"relaxation_and_target_periods",
_const.DEFAULT_ENABLE_MIN_PERIODS_PEAK, _const.DEFAULT_ENABLE_MIN_PERIODS_PEAK,
) )
@ -699,12 +745,14 @@ 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 = relaxation_and_target_periods.get( min_periods_peak = self._get_option(
_const.CONF_MIN_PERIODS_PEAK, _const.CONF_MIN_PERIODS_PEAK,
"relaxation_and_target_periods",
_const.DEFAULT_MIN_PERIODS_PEAK, _const.DEFAULT_MIN_PERIODS_PEAK,
) )
relaxation_attempts_peak = relaxation_and_target_periods.get( relaxation_attempts_peak = self._get_option(
_const.CONF_RELAXATION_ATTEMPTS_PEAK, _const.CONF_RELAXATION_ATTEMPTS_PEAK,
"relaxation_and_target_periods",
_const.DEFAULT_RELAXATION_ATTEMPTS_PEAK, _const.DEFAULT_RELAXATION_ATTEMPTS_PEAK,
) )
@ -713,12 +761,14 @@ class TibberPricesPeriodCalculator:
peak_config = self.get_period_config(reverse_sort=True) peak_config = self.get_period_config(reverse_sort=True)
# Get level filter configuration from period_settings section # Get level filter configuration from period_settings section
# CRITICAL: min_level and gap_count are stored in nested section 'period_settings' # CRITICAL: min_level and gap_count are stored in nested section 'period_settings'
min_level_peak = period_settings.get( min_level_peak = self._get_option(
_const.CONF_PEAK_PRICE_MIN_LEVEL, _const.CONF_PEAK_PRICE_MIN_LEVEL,
"period_settings",
_const.DEFAULT_PEAK_PRICE_MIN_LEVEL, _const.DEFAULT_PEAK_PRICE_MIN_LEVEL,
) )
gap_count_peak = period_settings.get( gap_count_peak = self._get_option(
_const.CONF_PEAK_PRICE_MAX_LEVEL_GAP_COUNT, _const.CONF_PEAK_PRICE_MAX_LEVEL_GAP_COUNT,
"period_settings",
_const.DEFAULT_PEAK_PRICE_MAX_LEVEL_GAP_COUNT, _const.DEFAULT_PEAK_PRICE_MAX_LEVEL_GAP_COUNT,
) )
peak_period_config = TibberPricesPeriodConfig( peak_period_config = TibberPricesPeriodConfig(

View file

@ -489,6 +489,80 @@
"usage_tips": "Verwende dies, um zu überprüfen, ob Echtzeit-Verbrauchsdaten verfügbar sind. Aktiviere Benachrichtigungen, falls dies unerwartet auf 'Aus' wechselt, was auf potenzielle Hardware- oder Verbindungsprobleme hinweist." "usage_tips": "Verwende dies, um zu überprüfen, ob Echtzeit-Verbrauchsdaten verfügbar sind. Aktiviere Benachrichtigungen, falls dies unerwartet auf 'Aus' wechselt, was auf potenzielle Hardware- oder Verbindungsprobleme hinweist."
} }
}, },
"number": {
"best_price_flex_override": {
"description": "Maximaler Prozentsatz über dem Tagesminimumpreis, den Intervalle haben können und trotzdem als 'Bestpreis' gelten. Empfohlen: 15-20 mit Lockerung aktiviert (Standard), oder 25-35 ohne Lockerung. Maximum: 50 (Obergrenze für zuverlässige Periodenerkennung).",
"long_description": "Wenn diese Entität aktiviert ist, überschreibt ihr Wert die Einstellung 'Flexibilität' aus dem Optionen-Dialog für die Bestpreis-Periodenberechnung.",
"usage_tips": "Aktiviere diese Entität, um die Bestpreiserkennung dynamisch über Automatisierungen anzupassen, z.B. höhere Flexibilität bei kritischen Lasten oder engere Anforderungen für flexible Geräte."
},
"best_price_min_distance_override": {
"description": "Minimaler prozentualer Abstand unter dem Tagesdurchschnitt. Intervalle müssen so weit unter dem Durchschnitt liegen, um als 'Bestpreis' zu gelten. Hilft, echte Niedrigpreis-Perioden von durchschnittlichen Preisen zu unterscheiden.",
"long_description": "Wenn diese Entität aktiviert ist, überschreibt ihr Wert die Einstellung 'Mindestabstand' aus dem Optionen-Dialog für die Bestpreis-Periodenberechnung.",
"usage_tips": "Erhöhe den Wert, wenn du strengere Bestpreis-Kriterien möchtest. Verringere ihn, wenn zu wenige Perioden erkannt werden."
},
"best_price_min_period_length_override": {
"description": "Minimale Periodenl\u00e4nge in 15-Minuten-Intervallen. Perioden kürzer als diese werden nicht gemeldet. Beispiel: 2 = mindestens 30 Minuten.",
"long_description": "Wenn diese Entität aktiviert ist, überschreibt ihr Wert die Einstellung 'Mindestperiodenlänge' aus dem Optionen-Dialog für die Bestpreis-Periodenberechnung.",
"usage_tips": "Passe an die typische Laufzeit deiner Geräte an: 2 (30 Min) für Schnellprogramme, 4-8 (1-2 Std) für normale Zyklen, 8+ für lange ECO-Programme."
},
"best_price_min_periods_override": {
"description": "Minimale Anzahl an Bestpreis-Perioden, die täglich gefunden werden sollen. Wenn Lockerung aktiviert ist, wird das System die Kriterien automatisch anpassen, um diese Zahl zu erreichen.",
"long_description": "Wenn diese Entität aktiviert ist, überschreibt ihr Wert die Einstellung 'Mindestperioden' aus dem Optionen-Dialog für die Bestpreis-Periodenberechnung.",
"usage_tips": "Setze dies auf die Anzahl zeitkritischer Aufgaben, die du täglich hast. Beispiel: 2 für zwei Waschmaschinenladungen."
},
"best_price_relaxation_attempts_override": {
"description": "Anzahl der Versuche, die Kriterien schrittweise zu lockern, um die Mindestperiodenanzahl zu erreichen. Jeder Versuch erhöht die Flexibilität um 3 Prozent. Bei 0 werden nur Basis-Kriterien verwendet.",
"long_description": "Wenn diese Entität aktiviert ist, überschreibt ihr Wert die Einstellung 'Lockerungsversuche' aus dem Optionen-Dialog für die Bestpreis-Periodenberechnung.",
"usage_tips": "Höhere Werte machen die Periodenerkennung anpassungsfähiger an Tage mit stabilen Preisen. Setze auf 0, um strenge Kriterien ohne Lockerung zu erzwingen."
},
"best_price_gap_count_override": {
"description": "Maximale Anzahl teurerer Intervalle, die zwischen günstigen Intervallen erlaubt sind und trotzdem als eine zusammenhängende Periode gelten. Bei 0 müssen günstige Intervalle aufeinander folgen.",
"long_description": "Wenn diese Entität aktiviert ist, überschreibt ihr Wert die Einstellung 'Lückentoleranz' aus dem Optionen-Dialog für die Bestpreis-Periodenberechnung.",
"usage_tips": "Erhöhe dies für Geräte mit variabler Last (z.B. Wärmepumpen), die kurze teurere Intervalle tolerieren können. Setze auf 0 für kontinuierliche günstige Perioden."
},
"peak_price_flex_override": {
"description": "Maximaler Prozentsatz unter dem Tagesmaximumpreis, den Intervalle haben können und trotzdem als 'Spitzenpreis' gelten. Gleiche Empfehlungen wie für Bestpreis-Flexibilität.",
"long_description": "Wenn diese Entität aktiviert ist, überschreibt ihr Wert die Einstellung 'Flexibilität' aus dem Optionen-Dialog für die Spitzenpreis-Periodenberechnung.",
"usage_tips": "Nutze dies, um den Spitzenpreis-Schwellenwert zur Laufzeit für Automatisierungen anzupassen, die den Verbrauch während teurer Stunden vermeiden."
},
"peak_price_min_distance_override": {
"description": "Minimaler prozentualer Abstand über dem Tagesdurchschnitt. Intervalle müssen so weit über dem Durchschnitt liegen, um als 'Spitzenpreis' zu gelten.",
"long_description": "Wenn diese Entität aktiviert ist, überschreibt ihr Wert die Einstellung 'Mindestabstand' aus dem Optionen-Dialog für die Spitzenpreis-Periodenberechnung.",
"usage_tips": "Erhöhe den Wert, um nur extreme Preisspitzen zu erfassen. Verringere ihn, um mehr Hochpreiszeiten einzubeziehen."
},
"peak_price_min_period_length_override": {
"description": "Minimale Periodenl\u00e4nge in 15-Minuten-Intervallen für Spitzenpreise. Kürzere Preisspitzen werden nicht als Perioden gemeldet.",
"long_description": "Wenn diese Entität aktiviert ist, überschreibt ihr Wert die Einstellung 'Mindestperiodenlänge' aus dem Optionen-Dialog für die Spitzenpreis-Periodenberechnung.",
"usage_tips": "Kürzere Werte erfassen kurze Preisspitzen. Längere Werte fokussieren auf anhaltende Hochpreisphasen."
},
"peak_price_min_periods_override": {
"description": "Minimale Anzahl an Spitzenpreis-Perioden, die täglich gefunden werden sollen.",
"long_description": "Wenn diese Entität aktiviert ist, überschreibt ihr Wert die Einstellung 'Mindestperioden' aus dem Optionen-Dialog für die Spitzenpreis-Periodenberechnung.",
"usage_tips": "Setze dies basierend darauf, wie viele Hochpreisphasen du pro Tag für Automatisierungen erfassen möchtest."
},
"peak_price_relaxation_attempts_override": {
"description": "Anzahl der Versuche, die Kriterien zu lockern, um die Mindestanzahl an Spitzenpreis-Perioden zu erreichen.",
"long_description": "Wenn diese Entität aktiviert ist, überschreibt ihr Wert die Einstellung 'Lockerungsversuche' aus dem Optionen-Dialog für die Spitzenpreis-Periodenberechnung.",
"usage_tips": "Erhöhe dies, wenn an Tagen mit stabilen Preisen keine Perioden gefunden werden. Setze auf 0, um strenge Kriterien zu erzwingen."
},
"peak_price_gap_count_override": {
"description": "Maximale Anzahl günstigerer Intervalle, die zwischen teuren Intervallen erlaubt sind und trotzdem als eine Spitzenpreis-Periode gelten.",
"long_description": "Wenn diese Entität aktiviert ist, überschreibt ihr Wert die Einstellung 'Lückentoleranz' aus dem Optionen-Dialog für die Spitzenpreis-Periodenberechnung.",
"usage_tips": "Höhere Werte erfassen längere Hochpreisphasen auch mit kurzen Preiseinbrüchen. Setze auf 0, um strikt zusammenhängende Spitzenpreise zu erfassen."
}
},
"switch": {
"best_price_enable_relaxation_override": {
"description": "Wenn aktiviert, werden die Kriterien automatisch gelockert, um die Mindestperiodenanzahl zu erreichen. Wenn deaktiviert, werden nur Perioden gemeldet, die die strengen Kriterien erfüllen (möglicherweise null Perioden bei stabilen Preisen).",
"long_description": "Wenn diese Entität aktiviert ist, überschreibt ihr Wert die Einstellung 'Mindestanzahl erreichen' aus dem Optionen-Dialog für die Bestpreis-Periodenberechnung.",
"usage_tips": "Aktiviere dies für garantierte tägliche Automatisierungsmöglichkeiten. Deaktiviere es, wenn du nur wirklich günstige Zeiträume willst, auch wenn das bedeutet, dass an manchen Tagen keine Perioden gefunden werden."
},
"peak_price_enable_relaxation_override": {
"description": "Wenn aktiviert, werden die Kriterien automatisch gelockert, um die Mindestperiodenanzahl zu erreichen. Wenn deaktiviert, werden nur echte Preisspitzen gemeldet.",
"long_description": "Wenn diese Entität aktiviert ist, überschreibt ihr Wert die Einstellung 'Mindestanzahl erreichen' aus dem Optionen-Dialog für die Spitzenpreis-Periodenberechnung.",
"usage_tips": "Aktiviere dies für konsistente Spitzenpreis-Warnungen. Deaktiviere es, um nur extreme Preisspitzen zu erfassen."
}
},
"home_types": { "home_types": {
"APARTMENT": "Wohnung", "APARTMENT": "Wohnung",
"ROWHOUSE": "Reihenhaus", "ROWHOUSE": "Reihenhaus",

View file

@ -489,6 +489,80 @@
"usage_tips": "Use this to verify that realtime consumption data is available. Enable notifications if this changes to 'off' unexpectedly, indicating potential hardware or connectivity issues." "usage_tips": "Use this to verify that realtime consumption data is available. Enable notifications if this changes to 'off' unexpectedly, indicating potential hardware or connectivity issues."
} }
}, },
"number": {
"best_price_flex_override": {
"description": "Maximum above the daily minimum price that intervals can be and still qualify as 'best price'. Recommended: 15-20 with relaxation enabled (default), or 25-35 without relaxation. Maximum: 50 (hard cap for reliable period detection).",
"long_description": "When this entity is enabled, its value overrides the 'Flexibility' setting from the options flow for best price period calculations.",
"usage_tips": "Enable this entity to dynamically adjust best price detection via automations. Higher values create longer periods, lower values are stricter."
},
"best_price_min_distance_override": {
"description": "Ensures periods are significantly cheaper than the daily average, not just marginally below it. This filters out noise and prevents marking slightly-below-average periods as 'best price' on days with flat prices. Higher values = stricter filtering (only truly cheap periods qualify).",
"long_description": "When this entity is enabled, its value overrides the 'Minimum Distance' setting from the options flow for best price period calculations.",
"usage_tips": "Use in automations to adjust how much better than average the best price periods must be. Higher values require prices to be further below average."
},
"best_price_min_period_length_override": {
"description": "Minimum duration for a period to be considered as 'best price'. Longer periods are more practical for running appliances like dishwashers or heat pumps. Best price periods require 60 minutes minimum (vs. 30 minutes for peak price warnings) because they should provide meaningful time windows for consumption planning.",
"long_description": "When this entity is enabled, its value overrides the 'Minimum Period Length' setting from the options flow for best price period calculations.",
"usage_tips": "Increase when your appliances need longer uninterrupted run times (e.g., washing machines, dishwashers)."
},
"best_price_min_periods_override": {
"description": "Minimum number of best price periods to aim for per day. Filters will be relaxed step-by-step to try achieving this count. Only active when 'Achieve Minimum Count' is enabled.",
"long_description": "When this entity is enabled, its value overrides the 'Minimum Periods' setting from the options flow for best price period calculations.",
"usage_tips": "Adjust dynamically based on how many times per day you need cheap electricity windows."
},
"best_price_relaxation_attempts_override": {
"description": "How many flex levels (attempts) to try before giving up. Each attempt runs all filter combinations at the new flex level. More attempts increase the chance of finding additional periods at the cost of longer processing time.",
"long_description": "When this entity is enabled, its value overrides the 'Relaxation Attempts' setting from the options flow for best price period calculations.",
"usage_tips": "Increase when periods are hard to find. Decrease for stricter price filtering."
},
"best_price_gap_count_override": {
"description": "Maximum number of consecutive intervals allowed that deviate by exactly one level step from the required level. This prevents periods from being split by occasional level deviations. Gap tolerance requires periods ≥90 minutes (6 intervals) to detect outliers effectively.",
"long_description": "When this entity is enabled, its value overrides the 'Gap Tolerance' setting from the options flow for best price period calculations.",
"usage_tips": "Increase to allow longer periods with occasional price spikes. Keep low for stricter continuous cheap periods."
},
"peak_price_flex_override": {
"description": "Maximum below the daily maximum price that intervals can be and still qualify as 'peak price'. Recommended: -15 to -20 with relaxation enabled (default), or -25 to -35 without relaxation. Maximum: -50 (hard cap for reliable period detection). Note: Negative values indicate distance below maximum.",
"long_description": "When this entity is enabled, its value overrides the 'Flexibility' setting from the options flow for peak price period calculations.",
"usage_tips": "Enable this entity to dynamically adjust peak price detection via automations. Higher values create longer peak periods."
},
"peak_price_min_distance_override": {
"description": "Ensures periods are significantly more expensive than the daily average, not just marginally above it. This filters out noise and prevents marking slightly-above-average periods as 'peak price' on days with flat prices. Higher values = stricter filtering (only truly expensive periods qualify).",
"long_description": "When this entity is enabled, its value overrides the 'Minimum Distance' setting from the options flow for peak price period calculations.",
"usage_tips": "Use in automations to adjust how much higher than average the peak price periods must be."
},
"peak_price_min_period_length_override": {
"description": "Minimum duration for a period to be considered as 'peak price'. Peak price warnings are allowed for shorter periods (30 minutes minimum vs. 60 minutes for best price) because brief expensive spikes are worth alerting about, even if they're too short for consumption planning.",
"long_description": "When this entity is enabled, its value overrides the 'Minimum Period Length' setting from the options flow for peak price period calculations.",
"usage_tips": "Increase to filter out brief price spikes, focusing on sustained expensive periods."
},
"peak_price_min_periods_override": {
"description": "Minimum number of peak price periods to aim for per day. Filters will be relaxed step-by-step to try achieving this count. Only active when 'Achieve Minimum Count' is enabled.",
"long_description": "When this entity is enabled, its value overrides the 'Minimum Periods' setting from the options flow for peak price period calculations.",
"usage_tips": "Adjust based on how many peak periods you want to identify and avoid."
},
"peak_price_relaxation_attempts_override": {
"description": "How many flex levels (attempts) to try before giving up. Each attempt runs all filter combinations at the new flex level. More attempts increase the chance of finding additional peak periods at the cost of longer processing time.",
"long_description": "When this entity is enabled, its value overrides the 'Relaxation Attempts' setting from the options flow for peak price period calculations.",
"usage_tips": "Increase when peak periods are hard to detect. Decrease for stricter peak price filtering."
},
"peak_price_gap_count_override": {
"description": "Maximum number of consecutive intervals allowed that deviate by exactly one level step from the required level. This prevents periods from being split by occasional level deviations. Gap tolerance requires periods ≥90 minutes (6 intervals) to detect outliers effectively.",
"long_description": "When this entity is enabled, its value overrides the 'Gap Tolerance' setting from the options flow for peak price period calculations.",
"usage_tips": "Increase to identify sustained expensive periods with brief dips. Keep low for stricter continuous peak detection."
}
},
"switch": {
"best_price_enable_relaxation_override": {
"description": "When enabled, filters will be gradually relaxed if not enough periods are found. This attempts to reach the desired minimum number of periods, which may include less optimal time windows as best-price periods.",
"long_description": "When this entity is enabled, its value overrides the 'Achieve Minimum Count' setting from the options flow for best price period calculations.",
"usage_tips": "Turn OFF to disable relaxation and use strict filtering only. Turn ON to allow the algorithm to relax criteria to find more periods."
},
"peak_price_enable_relaxation_override": {
"description": "When enabled, filters will be gradually relaxed if not enough periods are found. This attempts to reach the desired minimum number of periods to ensure you're warned about expensive periods even on days with unusual price patterns.",
"long_description": "When this entity is enabled, its value overrides the 'Achieve Minimum Count' setting from the options flow for peak price period calculations.",
"usage_tips": "Turn OFF to disable relaxation and use strict filtering only. Turn ON to allow the algorithm to relax criteria to find more peak periods."
}
},
"home_types": { "home_types": {
"APARTMENT": "Apartment", "APARTMENT": "Apartment",
"ROWHOUSE": "Rowhouse", "ROWHOUSE": "Rowhouse",

View file

@ -489,6 +489,80 @@
"usage_tips": "Bruk dette for å bekrefte at sanntidsforbruksdata er tilgjengelig. Aktiver varsler hvis dette endres til 'av' uventet, noe som indikerer potensielle maskinvare- eller tilkoblingsproblemer." "usage_tips": "Bruk dette for å bekrefte at sanntidsforbruksdata er tilgjengelig. Aktiver varsler hvis dette endres til 'av' uventet, noe som indikerer potensielle maskinvare- eller tilkoblingsproblemer."
} }
}, },
"number": {
"best_price_flex_override": {
"description": "Maksimal prosent over daglig minimumspris som intervaller kan ha og fortsatt kvalifisere som 'beste pris'. Anbefalt: 15-20 med lemping aktivert (standard), eller 25-35 uten lemping. Maksimum: 50 (tak for pålitelig periodedeteksjon).",
"long_description": "Når denne entiteten er aktivert, overstyrer verdien 'Fleksibilitet'-innstillingen fra alternativer-dialogen for beste pris-periodeberegninger.",
"usage_tips": "Aktiver denne entiteten for å dynamisk justere beste pris-deteksjon via automatiseringer, f.eks. høyere fleksibilitet for kritiske laster eller strengere krav for fleksible apparater."
},
"best_price_min_distance_override": {
"description": "Minimum prosentavstand under daglig gjennomsnitt. Intervaller må være så langt under gjennomsnittet for å kvalifisere som 'beste pris'. Hjelper med å skille ekte lavprisperioder fra gjennomsnittspriser.",
"long_description": "Når denne entiteten er aktivert, overstyrer verdien 'Minimumsavstand'-innstillingen fra alternativer-dialogen for beste pris-periodeberegninger.",
"usage_tips": "Øk verdien for strengere beste pris-kriterier. Reduser hvis for få perioder blir oppdaget."
},
"best_price_min_period_length_override": {
"description": "Minimum periodelengde i 15-minutters intervaller. Perioder kortere enn dette blir ikke rapportert. Eksempel: 2 = minimum 30 minutter.",
"long_description": "Når denne entiteten er aktivert, overstyrer verdien 'Minimum periodelengde'-innstillingen fra alternativer-dialogen for beste pris-periodeberegninger.",
"usage_tips": "Juster til typisk apparatkjøretid: 2 (30 min) for hurtigprogrammer, 4-8 (1-2 timer) for normale sykluser, 8+ for lange ECO-programmer."
},
"best_price_min_periods_override": {
"description": "Minimum antall beste pris-perioder å finne daglig. Når lemping er aktivert, vil systemet automatisk justere kriterier for å oppnå dette antallet.",
"long_description": "Når denne entiteten er aktivert, overstyrer verdien 'Minimum perioder'-innstillingen fra alternativer-dialogen for beste pris-periodeberegninger.",
"usage_tips": "Sett dette til antall tidskritiske oppgaver du har daglig. Eksempel: 2 for to vaskemaskinkjøringer."
},
"best_price_relaxation_attempts_override": {
"description": "Antall forsøk på å gradvis lempe kriteriene for å oppnå minimum periodeantall. Hvert forsøk øker fleksibiliteten med 3 prosent. Ved 0 brukes kun basiskriterier.",
"long_description": "Når denne entiteten er aktivert, overstyrer verdien 'Lemping forsøk'-innstillingen fra alternativer-dialogen for beste pris-periodeberegninger.",
"usage_tips": "Høyere verdier gjør periodedeteksjon mer adaptiv for dager med stabile priser. Sett til 0 for å tvinge strenge kriterier uten lemping."
},
"best_price_gap_count_override": {
"description": "Maksimalt antall dyrere intervaller som kan tillates mellom billige intervaller mens de fortsatt regnes som en sammenhengende periode. Ved 0 må billige intervaller være påfølgende.",
"long_description": "Når denne entiteten er aktivert, overstyrer verdien 'Gaptoleranse'-innstillingen fra alternativer-dialogen for beste pris-periodeberegninger.",
"usage_tips": "Øk dette for apparater med variabel last (f.eks. varmepumper) som kan tåle korte dyrere intervaller. Sett til 0 for kontinuerlige billige perioder."
},
"peak_price_flex_override": {
"description": "Maksimal prosent under daglig maksimumspris som intervaller kan ha og fortsatt kvalifisere som 'topppris'. Samme anbefalinger som for beste pris-fleksibilitet.",
"long_description": "Når denne entiteten er aktivert, overstyrer verdien 'Fleksibilitet'-innstillingen fra alternativer-dialogen for topppris-periodeberegninger.",
"usage_tips": "Bruk dette for å justere topppris-terskelen ved kjøretid for automatiseringer som unngår forbruk under dyre timer."
},
"peak_price_min_distance_override": {
"description": "Minimum prosentavstand over daglig gjennomsnitt. Intervaller må være så langt over gjennomsnittet for å kvalifisere som 'topppris'.",
"long_description": "Når denne entiteten er aktivert, overstyrer verdien 'Minimumsavstand'-innstillingen fra alternativer-dialogen for topppris-periodeberegninger.",
"usage_tips": "Øk verdien for kun å fange ekstreme pristopper. Reduser for å inkludere flere høypristider."
},
"peak_price_min_period_length_override": {
"description": "Minimum periodelengde i 15-minutters intervaller for topppriser. Kortere pristopper rapporteres ikke som perioder.",
"long_description": "Når denne entiteten er aktivert, overstyrer verdien 'Minimum periodelengde'-innstillingen fra alternativer-dialogen for topppris-periodeberegninger.",
"usage_tips": "Kortere verdier fanger korte pristopper. Lengre verdier fokuserer på vedvarende høyprisperioder."
},
"peak_price_min_periods_override": {
"description": "Minimum antall topppris-perioder å finne daglig.",
"long_description": "Når denne entiteten er aktivert, overstyrer verdien 'Minimum perioder'-innstillingen fra alternativer-dialogen for topppris-periodeberegninger.",
"usage_tips": "Sett dette basert på hvor mange høyprisperioder du vil fange per dag for automatiseringer."
},
"peak_price_relaxation_attempts_override": {
"description": "Antall forsøk på å lempe kriteriene for å oppnå minimum antall topppris-perioder.",
"long_description": "Når denne entiteten er aktivert, overstyrer verdien 'Lemping forsøk'-innstillingen fra alternativer-dialogen for topppris-periodeberegninger.",
"usage_tips": "Øk dette hvis ingen perioder blir funnet på dager med stabile priser. Sett til 0 for å tvinge strenge kriterier."
},
"peak_price_gap_count_override": {
"description": "Maksimalt antall billigere intervaller som kan tillates mellom dyre intervaller mens de fortsatt regnes som en topppris-periode.",
"long_description": "Når denne entiteten er aktivert, overstyrer verdien 'Gaptoleranse'-innstillingen fra alternativer-dialogen for topppris-periodeberegninger.",
"usage_tips": "Høyere verdier fanger lengre høyprisperioder selv med korte prisdykk. Sett til 0 for strengt sammenhengende topppriser."
}
},
"switch": {
"best_price_enable_relaxation_override": {
"description": "Når aktivert, lempes kriteriene automatisk for å oppnå minimum periodeantall. Når deaktivert, rapporteres kun perioder som oppfyller strenge kriterier (muligens null perioder på dager med stabile priser).",
"long_description": "Når denne entiteten er aktivert, overstyrer verdien 'Oppnå minimumsantall'-innstillingen fra alternativer-dialogen for beste pris-periodeberegninger.",
"usage_tips": "Aktiver dette for garanterte daglige automatiseringsmuligheter. Deaktiver hvis du kun vil ha virkelig billige perioder, selv om det betyr ingen perioder på noen dager."
},
"peak_price_enable_relaxation_override": {
"description": "Når aktivert, lempes kriteriene automatisk for å oppnå minimum periodeantall. Når deaktivert, rapporteres kun ekte pristopper.",
"long_description": "Når denne entiteten er aktivert, overstyrer verdien 'Oppnå minimumsantall'-innstillingen fra alternativer-dialogen for topppris-periodeberegninger.",
"usage_tips": "Aktiver dette for konsistente topppris-varsler. Deaktiver for kun å fange ekstreme pristopper."
}
},
"home_types": { "home_types": {
"APARTMENT": "Leilighet", "APARTMENT": "Leilighet",
"ROWHOUSE": "Rekkehus", "ROWHOUSE": "Rekkehus",

View file

@ -489,6 +489,80 @@
"usage_tips": "Gebruik dit om te verifiëren dat realtimeverbruiksgegevens beschikbaar zijn. Schakel meldingen in als dit onverwacht verandert naar 'uit', wat wijst op mogelijke hardware- of verbindingsproblemen." "usage_tips": "Gebruik dit om te verifiëren dat realtimeverbruiksgegevens beschikbaar zijn. Schakel meldingen in als dit onverwacht verandert naar 'uit', wat wijst op mogelijke hardware- of verbindingsproblemen."
} }
}, },
"number": {
"best_price_flex_override": {
"description": "Maximaal percentage boven de dagelijkse minimumprijs dat intervallen kunnen hebben en nog steeds als 'beste prijs' kwalificeren. Aanbevolen: 15-20 met versoepeling ingeschakeld (standaard), of 25-35 zonder versoepeling. Maximum: 50 (harde limiet voor betrouwbare periodedetectie).",
"long_description": "Wanneer deze entiteit is ingeschakeld, overschrijft de waarde de 'Flexibiliteit'-instelling uit de opties-dialoog voor beste prijs-periodeberekeningen.",
"usage_tips": "Schakel deze entiteit in om beste prijs-detectie dynamisch aan te passen via automatiseringen, bijv. hogere flexibiliteit voor kritieke lasten of strengere eisen voor flexibele apparaten."
},
"best_price_min_distance_override": {
"description": "Minimale procentuele afstand onder het daggemiddelde. Intervallen moeten zo ver onder het gemiddelde liggen om als 'beste prijs' te kwalificeren. Helpt echte lage prijsperioden te onderscheiden van gemiddelde prijzen.",
"long_description": "Wanneer deze entiteit is ingeschakeld, overschrijft de waarde de 'Minimale afstand'-instelling uit de opties-dialoog voor beste prijs-periodeberekeningen.",
"usage_tips": "Verhoog de waarde voor strengere beste prijs-criteria. Verlaag als te weinig perioden worden gedetecteerd."
},
"best_price_min_period_length_override": {
"description": "Minimale periodelengte in 15-minuten intervallen. Perioden korter dan dit worden niet gerapporteerd. Voorbeeld: 2 = minimaal 30 minuten.",
"long_description": "Wanneer deze entiteit is ingeschakeld, overschrijft de waarde de 'Minimale periodelengte'-instelling uit de opties-dialoog voor beste prijs-periodeberekeningen.",
"usage_tips": "Pas aan op typische apparaatlooptijd: 2 (30 min) voor snelle programma's, 4-8 (1-2 uur) voor normale cycli, 8+ voor lange ECO-programma's."
},
"best_price_min_periods_override": {
"description": "Minimum aantal beste prijs-perioden om dagelijks te vinden. Wanneer versoepeling is ingeschakeld, past het systeem automatisch de criteria aan om dit aantal te bereiken.",
"long_description": "Wanneer deze entiteit is ingeschakeld, overschrijft de waarde de 'Minimum periodes'-instelling uit de opties-dialoog voor beste prijs-periodeberekeningen.",
"usage_tips": "Stel dit in op het aantal tijdkritieke taken dat je dagelijks hebt. Voorbeeld: 2 voor twee wasladingen."
},
"best_price_relaxation_attempts_override": {
"description": "Aantal pogingen om de criteria geleidelijk te versoepelen om het minimum aantal perioden te bereiken. Elke poging verhoogt de flexibiliteit met 3 procent. Bij 0 worden alleen basiscriteria gebruikt.",
"long_description": "Wanneer deze entiteit is ingeschakeld, overschrijft de waarde de 'Versoepeling pogingen'-instelling uit de opties-dialoog voor beste prijs-periodeberekeningen.",
"usage_tips": "Hogere waarden maken periodedetectie adaptiever voor dagen met stabiele prijzen. Stel in op 0 om strikte criteria af te dwingen zonder versoepeling."
},
"best_price_gap_count_override": {
"description": "Maximum aantal duurdere intervallen dat mag worden toegestaan tussen goedkope intervallen terwijl ze nog steeds als één aaneengesloten periode tellen. Bij 0 moeten goedkope intervallen opeenvolgend zijn.",
"long_description": "Wanneer deze entiteit is ingeschakeld, overschrijft de waarde de 'Gap tolerantie'-instelling uit de opties-dialoog voor beste prijs-periodeberekeningen.",
"usage_tips": "Verhoog dit voor apparaten met variabele belasting (bijv. warmtepompen) die korte duurdere intervallen kunnen tolereren. Stel in op 0 voor continu goedkope perioden."
},
"peak_price_flex_override": {
"description": "Maximaal percentage onder de dagelijkse maximumprijs dat intervallen kunnen hebben en nog steeds als 'piekprijs' kwalificeren. Dezelfde aanbevelingen als voor beste prijs-flexibiliteit.",
"long_description": "Wanneer deze entiteit is ingeschakeld, overschrijft de waarde de 'Flexibiliteit'-instelling uit de opties-dialoog voor piekprijs-periodeberekeningen.",
"usage_tips": "Gebruik dit om de piekprijs-drempel tijdens runtime aan te passen voor automatiseringen die verbruik tijdens dure uren vermijden."
},
"peak_price_min_distance_override": {
"description": "Minimale procentuele afstand boven het daggemiddelde. Intervallen moeten zo ver boven het gemiddelde liggen om als 'piekprijs' te kwalificeren.",
"long_description": "Wanneer deze entiteit is ingeschakeld, overschrijft de waarde de 'Minimale afstand'-instelling uit de opties-dialoog voor piekprijs-periodeberekeningen.",
"usage_tips": "Verhoog de waarde om alleen extreme prijspieken te vangen. Verlaag om meer dure tijden mee te nemen."
},
"peak_price_min_period_length_override": {
"description": "Minimale periodelengte in 15-minuten intervallen voor piekprijzen. Kortere prijspieken worden niet als perioden gerapporteerd.",
"long_description": "Wanneer deze entiteit is ingeschakeld, overschrijft de waarde de 'Minimale periodelengte'-instelling uit de opties-dialoog voor piekprijs-periodeberekeningen.",
"usage_tips": "Kortere waarden vangen korte prijspieken. Langere waarden focussen op aanhoudende dure perioden."
},
"peak_price_min_periods_override": {
"description": "Minimum aantal piekprijs-perioden om dagelijks te vinden.",
"long_description": "Wanneer deze entiteit is ingeschakeld, overschrijft de waarde de 'Minimum periodes'-instelling uit de opties-dialoog voor piekprijs-periodeberekeningen.",
"usage_tips": "Stel dit in op basis van hoeveel dure perioden je per dag wilt vangen voor automatiseringen."
},
"peak_price_relaxation_attempts_override": {
"description": "Aantal pogingen om de criteria te versoepelen om het minimum aantal piekprijs-perioden te bereiken.",
"long_description": "Wanneer deze entiteit is ingeschakeld, overschrijft de waarde de 'Versoepeling pogingen'-instelling uit de opties-dialoog voor piekprijs-periodeberekeningen.",
"usage_tips": "Verhoog dit als geen perioden worden gevonden op dagen met stabiele prijzen. Stel in op 0 om strikte criteria af te dwingen."
},
"peak_price_gap_count_override": {
"description": "Maximum aantal goedkopere intervallen dat mag worden toegestaan tussen dure intervallen terwijl ze nog steeds als één piekprijs-periode tellen.",
"long_description": "Wanneer deze entiteit is ingeschakeld, overschrijft de waarde de 'Gap tolerantie'-instelling uit de opties-dialoog voor piekprijs-periodeberekeningen.",
"usage_tips": "Hogere waarden vangen langere dure perioden zelfs met korte prijsdips. Stel in op 0 voor strikt aaneengesloten piekprijzen."
}
},
"switch": {
"best_price_enable_relaxation_override": {
"description": "Indien ingeschakeld, worden criteria automatisch versoepeld om het minimum aantal perioden te bereiken. Indien uitgeschakeld, worden alleen perioden gerapporteerd die aan strikte criteria voldoen (mogelijk nul perioden op dagen met stabiele prijzen).",
"long_description": "Wanneer deze entiteit is ingeschakeld, overschrijft de waarde de 'Minimum aantal bereiken'-instelling uit de opties-dialoog voor beste prijs-periodeberekeningen.",
"usage_tips": "Schakel dit in voor gegarandeerde dagelijkse automatiseringsmogelijkheden. Schakel uit als je alleen echt goedkope perioden wilt, ook als dat betekent dat er op sommige dagen geen perioden zijn."
},
"peak_price_enable_relaxation_override": {
"description": "Indien ingeschakeld, worden criteria automatisch versoepeld om het minimum aantal perioden te bereiken. Indien uitgeschakeld, worden alleen echte prijspieken gerapporteerd.",
"long_description": "Wanneer deze entiteit is ingeschakeld, overschrijft de waarde de 'Minimum aantal bereiken'-instelling uit de opties-dialoog voor piekprijs-periodeberekeningen.",
"usage_tips": "Schakel dit in voor consistente piekprijs-waarschuwingen. Schakel uit om alleen extreme prijspieken te vangen."
}
},
"home_types": { "home_types": {
"APARTMENT": "Appartement", "APARTMENT": "Appartement",
"ROWHOUSE": "Rijhuis", "ROWHOUSE": "Rijhuis",

View file

@ -489,6 +489,80 @@
"usage_tips": "Använd detta för att verifiera att realtidsförbrukningen är tillgänglig. Aktivera meddelanden om detta oväntat ändras till 'av', vilket indikerar potentiella hårdvaru- eller anslutningsproblem." "usage_tips": "Använd detta för att verifiera att realtidsförbrukningen är tillgänglig. Aktivera meddelanden om detta oväntat ändras till 'av', vilket indikerar potentiella hårdvaru- eller anslutningsproblem."
} }
}, },
"number": {
"best_price_flex_override": {
"description": "Maximal procent över daglig minimumpris som intervaller kan ha och fortfarande kvalificera som 'bästa pris'. Rekommenderas: 15-20 med lättnad aktiverad (standard), eller 25-35 utan lättnad. Maximum: 50 (hårt tak för tillförlitlig perioddetektering).",
"long_description": "När denna entitet är aktiverad överskriver värdet 'Flexibilitet'-inställningen från alternativ-dialogen för bästa pris-periodberäkningar.",
"usage_tips": "Aktivera denna entitet för att dynamiskt justera bästa pris-detektering via automatiseringar, t.ex. högre flexibilitet för kritiska laster eller striktare krav för flexibla apparater."
},
"best_price_min_distance_override": {
"description": "Minsta procentuella avstånd under dagligt genomsnitt. Intervaller måste vara så långt under genomsnittet för att kvalificera som 'bästa pris'. Hjälper att skilja äkta lågprisperioder från genomsnittspriser.",
"long_description": "När denna entitet är aktiverad överskriver värdet 'Minimiavstånd'-inställningen från alternativ-dialogen för bästa pris-periodberäkningar.",
"usage_tips": "Öka värdet för striktare bästa pris-kriterier. Minska om för få perioder detekteras."
},
"best_price_min_period_length_override": {
"description": "Minsta periodlängd i 15-minuters intervaller. Perioder kortare än detta rapporteras inte. Exempel: 2 = minst 30 minuter.",
"long_description": "När denna entitet är aktiverad överskriver värdet 'Minsta periodlängd'-inställningen från alternativ-dialogen för bästa pris-periodberäkningar.",
"usage_tips": "Anpassa till typisk apparatkörtid: 2 (30 min) för snabbprogram, 4-8 (1-2 timmar) för normala cykler, 8+ för långa ECO-program."
},
"best_price_min_periods_override": {
"description": "Minsta antal bästa pris-perioder att hitta dagligen. När lättnad är aktiverad kommer systemet automatiskt att justera kriterierna för att uppnå detta antal.",
"long_description": "När denna entitet är aktiverad överskriver värdet 'Minsta antal perioder'-inställningen från alternativ-dialogen för bästa pris-periodberäkningar.",
"usage_tips": "Ställ in detta på antalet tidskritiska uppgifter du har dagligen. Exempel: 2 för två tvattmaskinskörningar."
},
"best_price_relaxation_attempts_override": {
"description": "Antal försök att gradvis lätta på kriterierna för att uppnå minsta periodantal. Varje försök ökar flexibiliteten med 3 procent. Vid 0 används endast baskriterier.",
"long_description": "När denna entitet är aktiverad överskriver värdet 'Lättnadsförsök'-inställningen från alternativ-dialogen för bästa pris-periodberäkningar.",
"usage_tips": "Högre värden gör perioddetektering mer adaptiv för dagar med stabila priser. Ställ in på 0 för att tvinga strikta kriterier utan lättnad."
},
"best_price_gap_count_override": {
"description": "Maximalt antal dyrare intervaller som kan tillåtas mellan billiga intervaller medan de fortfarande räknas som en sammanhängande period. Vid 0 måste billiga intervaller vara påföljande.",
"long_description": "När denna entitet är aktiverad överskriver värdet 'Glaptolerans'-inställningen från alternativ-dialogen för bästa pris-periodberäkningar.",
"usage_tips": "Öka detta för apparater med variabel last (t.ex. värmepumpar) som kan tolerera korta dyrare intervaller. Ställ in på 0 för kontinuerligt billiga perioder."
},
"peak_price_flex_override": {
"description": "Maximal procent under daglig maximumpris som intervaller kan ha och fortfarande kvalificera som 'topppris'. Samma rekommendationer som för bästa pris-flexibilitet.",
"long_description": "När denna entitet är aktiverad överskriver värdet 'Flexibilitet'-inställningen från alternativ-dialogen för topppris-periodberäkningar.",
"usage_tips": "Använd detta för att justera topppris-tröskeln vid körtid för automatiseringar som undviker förbrukning under dyra timmar."
},
"peak_price_min_distance_override": {
"description": "Minsta procentuella avstånd över dagligt genomsnitt. Intervaller måste vara så långt över genomsnittet för att kvalificera som 'topppris'.",
"long_description": "När denna entitet är aktiverad överskriver värdet 'Minimiavstånd'-inställningen från alternativ-dialogen för topppris-periodberäkningar.",
"usage_tips": "Öka värdet för att endast fånga extrema pristoppar. Minska för att inkludera fler högpristider."
},
"peak_price_min_period_length_override": {
"description": "Minsta periodlängd i 15-minuters intervaller för topppriser. Kortare pristoppar rapporteras inte som perioder.",
"long_description": "När denna entitet är aktiverad överskriver värdet 'Minsta periodlängd'-inställningen från alternativ-dialogen för topppris-periodberäkningar.",
"usage_tips": "Kortare värden fångar korta pristoppar. Längre värden fokuserar på ihållande högprisperioder."
},
"peak_price_min_periods_override": {
"description": "Minsta antal topppris-perioder att hitta dagligen.",
"long_description": "När denna entitet är aktiverad överskriver värdet 'Minsta antal perioder'-inställningen från alternativ-dialogen för topppris-periodberäkningar.",
"usage_tips": "Ställ in detta baserat på hur många högprisperioder du vill fånga per dag för automatiseringar."
},
"peak_price_relaxation_attempts_override": {
"description": "Antal försök att lätta på kriterierna för att uppnå minsta antal topppris-perioder.",
"long_description": "När denna entitet är aktiverad överskriver värdet 'Lättnadsförsök'-inställningen från alternativ-dialogen för topppris-periodberäkningar.",
"usage_tips": "Öka detta om inga perioder hittas på dagar med stabila priser. Ställ in på 0 för att tvinga strikta kriterier."
},
"peak_price_gap_count_override": {
"description": "Maximalt antal billigare intervaller som kan tillåtas mellan dyra intervaller medan de fortfarande räknas som en topppris-period.",
"long_description": "När denna entitet är aktiverad överskriver värdet 'Glaptolerans'-inställningen från alternativ-dialogen för topppris-periodberäkningar.",
"usage_tips": "Högre värden fångar längre högprisperioder även med korta prisdipp. Ställ in på 0 för strikt sammanhängande topppriser."
}
},
"switch": {
"best_price_enable_relaxation_override": {
"description": "När aktiverad lättas kriterierna automatiskt för att uppnå minsta periodantal. När inaktiverad rapporteras endast perioder som uppfyller strikta kriterier (möjligen noll perioder på dagar med stabila priser).",
"long_description": "När denna entitet är aktiverad överskriver värdet 'Uppnå minimiantal'-inställningen från alternativ-dialogen för bästa pris-periodberäkningar.",
"usage_tips": "Aktivera detta för garanterade dagliga automatiseringsmöjligheter. Inaktivera om du endast vill ha riktigt billiga perioder, även om det innebär inga perioder vissa dagar."
},
"peak_price_enable_relaxation_override": {
"description": "När aktiverad lättas kriterierna automatiskt för att uppnå minsta periodantal. När inaktiverad rapporteras endast äkta pristoppar.",
"long_description": "När denna entitet är aktiverad överskriver värdet 'Uppnå minimiantal'-inställningen från alternativ-dialogen för topppris-periodberäkningar.",
"usage_tips": "Aktivera detta för konsekventa topppris-varningar. Inaktivera för att endast fånga extrema pristoppar."
}
},
"home_types": { "home_types": {
"APARTMENT": "Lägenhet", "APARTMENT": "Lägenhet",
"ROWHOUSE": "Radhus", "ROWHOUSE": "Radhus",

View file

@ -0,0 +1,39 @@
"""
Number platform for Tibber Prices integration.
Provides configurable number entities for runtime overrides of Best Price
and Peak Price period calculation settings. These entities allow automation
of configuration parameters without using the options flow.
When enabled, these entities take precedence over the options flow settings.
When disabled (default), the options flow settings are used.
"""
from __future__ import annotations
from typing import TYPE_CHECKING
from .core import TibberPricesConfigNumber
from .definitions import NUMBER_ENTITY_DESCRIPTIONS
if TYPE_CHECKING:
from custom_components.tibber_prices.data import TibberPricesConfigEntry
from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddEntitiesCallback
async def async_setup_entry(
_hass: HomeAssistant,
entry: TibberPricesConfigEntry,
async_add_entities: AddEntitiesCallback,
) -> None:
"""Set up Tibber Prices number entities based on a config entry."""
coordinator = entry.runtime_data.coordinator
async_add_entities(
TibberPricesConfigNumber(
coordinator=coordinator,
entity_description=entity_description,
)
for entity_description in NUMBER_ENTITY_DESCRIPTIONS
)

View file

@ -0,0 +1,242 @@
"""
Number entity implementation for Tibber Prices configuration overrides.
These entities allow runtime configuration of period calculation settings.
When a config entity is enabled, its value takes precedence over the
options flow setting for period calculations.
"""
from __future__ import annotations
import logging
from typing import TYPE_CHECKING, Any
from custom_components.tibber_prices.const import (
DOMAIN,
get_home_type_translation,
get_translation,
)
from homeassistant.components.number import NumberEntity, RestoreNumber
from homeassistant.core import callback
from homeassistant.helpers.device_registry import DeviceEntryType, DeviceInfo
if TYPE_CHECKING:
from custom_components.tibber_prices.coordinator import (
TibberPricesDataUpdateCoordinator,
)
from .definitions import TibberPricesNumberEntityDescription
_LOGGER = logging.getLogger(__name__)
class TibberPricesConfigNumber(RestoreNumber, NumberEntity):
"""
A number entity for configuring period calculation settings at runtime.
When this entity is enabled, its value overrides the corresponding
options flow setting. When disabled (default), the options flow
setting is used for period calculations.
The entity restores its value after Home Assistant restart.
"""
_attr_has_entity_name = True
entity_description: TibberPricesNumberEntityDescription
# Exclude all attributes from recorder history - config entities don't need history
_unrecorded_attributes = frozenset(
{
"description",
"long_description",
"usage_tips",
"friendly_name",
"icon",
"unit_of_measurement",
"mode",
"min",
"max",
"step",
}
)
def __init__(
self,
coordinator: TibberPricesDataUpdateCoordinator,
entity_description: TibberPricesNumberEntityDescription,
) -> None:
"""Initialize the config number entity."""
self.coordinator = coordinator
self.entity_description = entity_description
# Set unique ID
self._attr_unique_id = (
f"{coordinator.config_entry.unique_id or coordinator.config_entry.entry_id}_{entity_description.key}"
)
# Initialize with None - will be set in async_added_to_hass
self._attr_native_value: float | None = None
# Setup device info
self._setup_device_info()
def _setup_device_info(self) -> None:
"""Set up device information."""
home_name, home_id, home_type = self._get_device_info()
language = self.coordinator.hass.config.language or "en"
translated_model = get_home_type_translation(home_type, language) if home_type else "Unknown"
self._attr_device_info = DeviceInfo(
entry_type=DeviceEntryType.SERVICE,
identifiers={
(
DOMAIN,
self.coordinator.config_entry.unique_id or self.coordinator.config_entry.entry_id,
)
},
name=home_name,
manufacturer="Tibber",
model=translated_model,
serial_number=home_id if home_id else None,
configuration_url="https://developer.tibber.com/explorer",
)
def _get_device_info(self) -> tuple[str, str | None, str | None]:
"""Get device name, ID and type."""
user_profile = self.coordinator.get_user_profile()
is_subentry = bool(self.coordinator.config_entry.data.get("home_id"))
home_id = self.coordinator.config_entry.unique_id
home_type = None
if is_subentry:
home_data = self.coordinator.config_entry.data.get("home_data", {})
home_id = self.coordinator.config_entry.data.get("home_id")
address = home_data.get("address", {})
address1 = address.get("address1", "")
city = address.get("city", "")
app_nickname = home_data.get("appNickname", "")
home_type = home_data.get("type", "")
if app_nickname and app_nickname.strip():
home_name = app_nickname.strip()
elif address1:
home_name = address1
if city:
home_name = f"{home_name}, {city}"
else:
home_name = f"Tibber Home {home_id[:8]}" if home_id else "Tibber Home"
elif user_profile:
home_name = user_profile.get("name") or "Tibber Home"
else:
home_name = "Tibber Home"
return home_name, home_id, home_type
async def async_added_to_hass(self) -> None:
"""Handle entity which was added to Home Assistant."""
await super().async_added_to_hass()
# Try to restore previous state
last_number_data = await self.async_get_last_number_data()
if last_number_data is not None and last_number_data.native_value is not None:
self._attr_native_value = last_number_data.native_value
_LOGGER.debug(
"Restored %s value: %s",
self.entity_description.key,
self._attr_native_value,
)
else:
# Initialize with value from options flow (or default)
self._attr_native_value = self._get_value_from_options()
_LOGGER.debug(
"Initialized %s from options: %s",
self.entity_description.key,
self._attr_native_value,
)
# Register override with coordinator if entity is enabled
# This happens during add, so check entity registry
await self._sync_override_state()
async def async_will_remove_from_hass(self) -> None:
"""Handle entity removal from Home Assistant."""
# Remove override when entity is removed
self.coordinator.remove_config_override(
self.entity_description.config_key,
self.entity_description.config_section,
)
await super().async_will_remove_from_hass()
def _get_value_from_options(self) -> float:
"""Get the current value from options flow or default."""
options = self.coordinator.config_entry.options
section = options.get(self.entity_description.config_section, {})
value = section.get(
self.entity_description.config_key,
self.entity_description.default_value,
)
return float(value)
async def _sync_override_state(self) -> None:
"""Sync the override state with the coordinator based on entity enabled state."""
# Check if entity is enabled in registry
if self.registry_entry is not None and not self.registry_entry.disabled:
# Entity is enabled - register the override
if self._attr_native_value is not None:
self.coordinator.set_config_override(
self.entity_description.config_key,
self.entity_description.config_section,
self._attr_native_value,
)
else:
# Entity is disabled - remove override
self.coordinator.remove_config_override(
self.entity_description.config_key,
self.entity_description.config_section,
)
async def async_set_native_value(self, value: float) -> None:
"""Update the current value and trigger recalculation."""
self._attr_native_value = value
# Update the coordinator's runtime override
self.coordinator.set_config_override(
self.entity_description.config_key,
self.entity_description.config_section,
value,
)
# Trigger period recalculation (same path as options update)
await self.coordinator.async_handle_config_override_update()
_LOGGER.debug(
"Updated %s to %s, triggered period recalculation",
self.entity_description.key,
value,
)
@property
def extra_state_attributes(self) -> dict[str, Any] | None:
"""Return entity state attributes with description."""
language = self.coordinator.hass.config.language or "en"
# Try to get description from custom translations
# Custom translations use direct path: number.{key}.description
translation_path = [
"number",
self.entity_description.translation_key or self.entity_description.key,
"description",
]
description = get_translation(translation_path, language)
attrs: dict[str, Any] = {}
if description:
attrs["description"] = description
return attrs if attrs else None
@callback
def async_registry_entry_updated(self) -> None:
"""Handle entity registry update (enabled/disabled state change)."""
# This is called when the entity is enabled/disabled in the UI
self.hass.async_create_task(self._sync_override_state())

View file

@ -0,0 +1,250 @@
"""
Number entity definitions for Tibber Prices configuration overrides.
These number entities allow runtime configuration of Best Price and Peak Price
period calculation settings. They are disabled by default - users can enable
individual entities to override specific settings at runtime.
When enabled, the entity value takes precedence over the options flow setting.
When disabled (default), the options flow setting is used.
"""
from __future__ import annotations
from dataclasses import dataclass
from homeassistant.components.number import (
NumberEntityDescription,
NumberMode,
)
from homeassistant.const import PERCENTAGE, EntityCategory
@dataclass(frozen=True, kw_only=True)
class TibberPricesNumberEntityDescription(NumberEntityDescription):
"""Describes a Tibber Prices number entity for config overrides."""
# The config key this entity overrides (matches CONF_* constants)
config_key: str
# The section in options where this setting is stored (e.g., "flexibility_settings")
config_section: str
# Whether this is for best_price (False) or peak_price (True)
is_peak_price: bool = False
# Default value from const.py
default_value: float | int = 0
# ============================================================================
# BEST PRICE PERIOD CONFIGURATION OVERRIDES
# ============================================================================
BEST_PRICE_NUMBER_ENTITIES = (
TibberPricesNumberEntityDescription(
key="best_price_flex_override",
translation_key="best_price_flex_override",
name="Best Price: Flexibility",
icon="mdi:arrow-down-bold-circle",
entity_category=EntityCategory.CONFIG,
entity_registry_enabled_default=False,
native_min_value=0,
native_max_value=50,
native_step=1,
native_unit_of_measurement=PERCENTAGE,
mode=NumberMode.SLIDER,
config_key="best_price_flex",
config_section="flexibility_settings",
is_peak_price=False,
default_value=15, # DEFAULT_BEST_PRICE_FLEX
),
TibberPricesNumberEntityDescription(
key="best_price_min_distance_override",
translation_key="best_price_min_distance_override",
name="Best Price: Minimum Distance",
icon="mdi:arrow-down-bold-circle",
entity_category=EntityCategory.CONFIG,
entity_registry_enabled_default=False,
native_min_value=-50,
native_max_value=0,
native_step=1,
native_unit_of_measurement=PERCENTAGE,
mode=NumberMode.SLIDER,
config_key="best_price_min_distance_from_avg",
config_section="flexibility_settings",
is_peak_price=False,
default_value=-5, # DEFAULT_BEST_PRICE_MIN_DISTANCE_FROM_AVG
),
TibberPricesNumberEntityDescription(
key="best_price_min_period_length_override",
translation_key="best_price_min_period_length_override",
name="Best Price: Minimum Period Length",
icon="mdi:arrow-down-bold-circle",
entity_category=EntityCategory.CONFIG,
entity_registry_enabled_default=False,
native_min_value=15,
native_max_value=180,
native_step=15,
native_unit_of_measurement="min",
mode=NumberMode.SLIDER,
config_key="best_price_min_period_length",
config_section="period_settings",
is_peak_price=False,
default_value=60, # DEFAULT_BEST_PRICE_MIN_PERIOD_LENGTH
),
TibberPricesNumberEntityDescription(
key="best_price_min_periods_override",
translation_key="best_price_min_periods_override",
name="Best Price: Minimum Periods",
icon="mdi:arrow-down-bold-circle",
entity_category=EntityCategory.CONFIG,
entity_registry_enabled_default=False,
native_min_value=1,
native_max_value=10,
native_step=1,
mode=NumberMode.SLIDER,
config_key="min_periods_best",
config_section="relaxation_and_target_periods",
is_peak_price=False,
default_value=2, # DEFAULT_MIN_PERIODS_BEST
),
TibberPricesNumberEntityDescription(
key="best_price_relaxation_attempts_override",
translation_key="best_price_relaxation_attempts_override",
name="Best Price: Relaxation Attempts",
icon="mdi:arrow-down-bold-circle",
entity_category=EntityCategory.CONFIG,
entity_registry_enabled_default=False,
native_min_value=1,
native_max_value=12,
native_step=1,
mode=NumberMode.SLIDER,
config_key="relaxation_attempts_best",
config_section="relaxation_and_target_periods",
is_peak_price=False,
default_value=11, # DEFAULT_RELAXATION_ATTEMPTS_BEST
),
TibberPricesNumberEntityDescription(
key="best_price_gap_count_override",
translation_key="best_price_gap_count_override",
name="Best Price: Gap Tolerance",
icon="mdi:arrow-down-bold-circle",
entity_category=EntityCategory.CONFIG,
entity_registry_enabled_default=False,
native_min_value=0,
native_max_value=8,
native_step=1,
mode=NumberMode.SLIDER,
config_key="best_price_max_level_gap_count",
config_section="period_settings",
is_peak_price=False,
default_value=1, # DEFAULT_BEST_PRICE_MAX_LEVEL_GAP_COUNT
),
)
# ============================================================================
# PEAK PRICE PERIOD CONFIGURATION OVERRIDES
# ============================================================================
PEAK_PRICE_NUMBER_ENTITIES = (
TibberPricesNumberEntityDescription(
key="peak_price_flex_override",
translation_key="peak_price_flex_override",
name="Peak Price: Flexibility",
icon="mdi:arrow-up-bold-circle",
entity_category=EntityCategory.CONFIG,
entity_registry_enabled_default=False,
native_min_value=-50,
native_max_value=0,
native_step=1,
native_unit_of_measurement=PERCENTAGE,
mode=NumberMode.SLIDER,
config_key="peak_price_flex",
config_section="flexibility_settings",
is_peak_price=True,
default_value=-20, # DEFAULT_PEAK_PRICE_FLEX
),
TibberPricesNumberEntityDescription(
key="peak_price_min_distance_override",
translation_key="peak_price_min_distance_override",
name="Peak Price: Minimum Distance",
icon="mdi:arrow-up-bold-circle",
entity_category=EntityCategory.CONFIG,
entity_registry_enabled_default=False,
native_min_value=0,
native_max_value=50,
native_step=1,
native_unit_of_measurement=PERCENTAGE,
mode=NumberMode.SLIDER,
config_key="peak_price_min_distance_from_avg",
config_section="flexibility_settings",
is_peak_price=True,
default_value=5, # DEFAULT_PEAK_PRICE_MIN_DISTANCE_FROM_AVG
),
TibberPricesNumberEntityDescription(
key="peak_price_min_period_length_override",
translation_key="peak_price_min_period_length_override",
name="Peak Price: Minimum Period Length",
icon="mdi:arrow-up-bold-circle",
entity_category=EntityCategory.CONFIG,
entity_registry_enabled_default=False,
native_min_value=15,
native_max_value=180,
native_step=15,
native_unit_of_measurement="min",
mode=NumberMode.SLIDER,
config_key="peak_price_min_period_length",
config_section="period_settings",
is_peak_price=True,
default_value=30, # DEFAULT_PEAK_PRICE_MIN_PERIOD_LENGTH
),
TibberPricesNumberEntityDescription(
key="peak_price_min_periods_override",
translation_key="peak_price_min_periods_override",
name="Peak Price: Minimum Periods",
icon="mdi:arrow-up-bold-circle",
entity_category=EntityCategory.CONFIG,
entity_registry_enabled_default=False,
native_min_value=1,
native_max_value=10,
native_step=1,
mode=NumberMode.SLIDER,
config_key="min_periods_peak",
config_section="relaxation_and_target_periods",
is_peak_price=True,
default_value=2, # DEFAULT_MIN_PERIODS_PEAK
),
TibberPricesNumberEntityDescription(
key="peak_price_relaxation_attempts_override",
translation_key="peak_price_relaxation_attempts_override",
name="Peak Price: Relaxation Attempts",
icon="mdi:arrow-up-bold-circle",
entity_category=EntityCategory.CONFIG,
entity_registry_enabled_default=False,
native_min_value=1,
native_max_value=12,
native_step=1,
mode=NumberMode.SLIDER,
config_key="relaxation_attempts_peak",
config_section="relaxation_and_target_periods",
is_peak_price=True,
default_value=11, # DEFAULT_RELAXATION_ATTEMPTS_PEAK
),
TibberPricesNumberEntityDescription(
key="peak_price_gap_count_override",
translation_key="peak_price_gap_count_override",
name="Peak Price: Gap Tolerance",
icon="mdi:arrow-up-bold-circle",
entity_category=EntityCategory.CONFIG,
entity_registry_enabled_default=False,
native_min_value=0,
native_max_value=8,
native_step=1,
mode=NumberMode.SLIDER,
config_key="peak_price_max_level_gap_count",
config_section="period_settings",
is_peak_price=True,
default_value=1, # DEFAULT_PEAK_PRICE_MAX_LEVEL_GAP_COUNT
),
)
# All number entity descriptions combined
NUMBER_ENTITY_DESCRIPTIONS = BEST_PRICE_NUMBER_ENTITIES + PEAK_PRICE_NUMBER_ENTITIES

View file

@ -0,0 +1,38 @@
"""
Switch platform for Tibber Prices integration.
Provides configurable switch entities for runtime overrides of Best Price
and Peak Price period calculation boolean settings (enable_min_periods).
When enabled, these entities take precedence over the options flow settings.
When disabled (default), the options flow settings are used.
"""
from __future__ import annotations
from typing import TYPE_CHECKING
from .core import TibberPricesConfigSwitch
from .definitions import SWITCH_ENTITY_DESCRIPTIONS
if TYPE_CHECKING:
from custom_components.tibber_prices.data import TibberPricesConfigEntry
from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddEntitiesCallback
async def async_setup_entry(
_hass: HomeAssistant,
entry: TibberPricesConfigEntry,
async_add_entities: AddEntitiesCallback,
) -> None:
"""Set up Tibber Prices switch entities based on a config entry."""
coordinator = entry.runtime_data.coordinator
async_add_entities(
TibberPricesConfigSwitch(
coordinator=coordinator,
entity_description=entity_description,
)
for entity_description in SWITCH_ENTITY_DESCRIPTIONS
)

View file

@ -0,0 +1,245 @@
"""
Switch entity implementation for Tibber Prices configuration overrides.
These entities allow runtime configuration of boolean period calculation settings.
When a config entity is enabled, its value takes precedence over the
options flow setting for period calculations.
"""
from __future__ import annotations
import logging
from typing import TYPE_CHECKING, Any
from custom_components.tibber_prices.const import (
DOMAIN,
get_home_type_translation,
get_translation,
)
from homeassistant.components.switch import SwitchEntity
from homeassistant.core import callback
from homeassistant.helpers.device_registry import DeviceEntryType, DeviceInfo
from homeassistant.helpers.restore_state import RestoreEntity
if TYPE_CHECKING:
from custom_components.tibber_prices.coordinator import (
TibberPricesDataUpdateCoordinator,
)
from .definitions import TibberPricesSwitchEntityDescription
_LOGGER = logging.getLogger(__name__)
class TibberPricesConfigSwitch(RestoreEntity, SwitchEntity):
"""
A switch entity for configuring boolean period calculation settings at runtime.
When this entity is enabled, its value overrides the corresponding
options flow setting. When disabled (default), the options flow
setting is used for period calculations.
The entity restores its value after Home Assistant restart.
"""
_attr_has_entity_name = True
entity_description: TibberPricesSwitchEntityDescription
# Exclude all attributes from recorder history - config entities don't need history
_unrecorded_attributes = frozenset(
{
"description",
"long_description",
"usage_tips",
"friendly_name",
"icon",
}
)
def __init__(
self,
coordinator: TibberPricesDataUpdateCoordinator,
entity_description: TibberPricesSwitchEntityDescription,
) -> None:
"""Initialize the config switch entity."""
self.coordinator = coordinator
self.entity_description = entity_description
# Set unique ID
self._attr_unique_id = (
f"{coordinator.config_entry.unique_id or coordinator.config_entry.entry_id}_{entity_description.key}"
)
# Initialize with None - will be set in async_added_to_hass
self._attr_is_on: bool | None = None
# Setup device info
self._setup_device_info()
def _setup_device_info(self) -> None:
"""Set up device information."""
home_name, home_id, home_type = self._get_device_info()
language = self.coordinator.hass.config.language or "en"
translated_model = get_home_type_translation(home_type, language) if home_type else "Unknown"
self._attr_device_info = DeviceInfo(
entry_type=DeviceEntryType.SERVICE,
identifiers={
(
DOMAIN,
self.coordinator.config_entry.unique_id or self.coordinator.config_entry.entry_id,
)
},
name=home_name,
manufacturer="Tibber",
model=translated_model,
serial_number=home_id if home_id else None,
configuration_url="https://developer.tibber.com/explorer",
)
def _get_device_info(self) -> tuple[str, str | None, str | None]:
"""Get device name, ID and type."""
user_profile = self.coordinator.get_user_profile()
is_subentry = bool(self.coordinator.config_entry.data.get("home_id"))
home_id = self.coordinator.config_entry.unique_id
home_type = None
if is_subentry:
home_data = self.coordinator.config_entry.data.get("home_data", {})
home_id = self.coordinator.config_entry.data.get("home_id")
address = home_data.get("address", {})
address1 = address.get("address1", "")
city = address.get("city", "")
app_nickname = home_data.get("appNickname", "")
home_type = home_data.get("type", "")
if app_nickname and app_nickname.strip():
home_name = app_nickname.strip()
elif address1:
home_name = address1
if city:
home_name = f"{home_name}, {city}"
else:
home_name = f"Tibber Home {home_id[:8]}" if home_id else "Tibber Home"
elif user_profile:
home_name = user_profile.get("name") or "Tibber Home"
else:
home_name = "Tibber Home"
return home_name, home_id, home_type
async def async_added_to_hass(self) -> None:
"""Handle entity which was added to Home Assistant."""
await super().async_added_to_hass()
# Try to restore previous state
last_state = await self.async_get_last_state()
if last_state is not None and last_state.state in ("on", "off"):
self._attr_is_on = last_state.state == "on"
_LOGGER.debug(
"Restored %s value: %s",
self.entity_description.key,
self._attr_is_on,
)
else:
# Initialize with value from options flow (or default)
self._attr_is_on = self._get_value_from_options()
_LOGGER.debug(
"Initialized %s from options: %s",
self.entity_description.key,
self._attr_is_on,
)
# Register override with coordinator if entity is enabled
await self._sync_override_state()
async def async_will_remove_from_hass(self) -> None:
"""Handle entity removal from Home Assistant."""
# Remove override when entity is removed
self.coordinator.remove_config_override(
self.entity_description.config_key,
self.entity_description.config_section,
)
await super().async_will_remove_from_hass()
def _get_value_from_options(self) -> bool:
"""Get the current value from options flow or default."""
options = self.coordinator.config_entry.options
section = options.get(self.entity_description.config_section, {})
value = section.get(
self.entity_description.config_key,
self.entity_description.default_value,
)
return bool(value)
async def _sync_override_state(self) -> None:
"""Sync the override state with the coordinator based on entity enabled state."""
# Check if entity is enabled in registry
if self.registry_entry is not None and not self.registry_entry.disabled:
# Entity is enabled - register the override
if self._attr_is_on is not None:
self.coordinator.set_config_override(
self.entity_description.config_key,
self.entity_description.config_section,
self._attr_is_on,
)
else:
# Entity is disabled - remove override
self.coordinator.remove_config_override(
self.entity_description.config_key,
self.entity_description.config_section,
)
async def async_turn_on(self, **_kwargs: Any) -> None:
"""Turn the switch on."""
await self._set_value(is_on=True)
async def async_turn_off(self, **_kwargs: Any) -> None:
"""Turn the switch off."""
await self._set_value(is_on=False)
async def _set_value(self, *, is_on: bool) -> None:
"""Update the current value and trigger recalculation."""
self._attr_is_on = is_on
# Update the coordinator's runtime override
self.coordinator.set_config_override(
self.entity_description.config_key,
self.entity_description.config_section,
is_on,
)
# Trigger period recalculation (same path as options update)
await self.coordinator.async_handle_config_override_update()
_LOGGER.debug(
"Updated %s to %s, triggered period recalculation",
self.entity_description.key,
is_on,
)
@property
def extra_state_attributes(self) -> dict[str, Any] | None:
"""Return entity state attributes with description."""
language = self.coordinator.hass.config.language or "en"
# Try to get description from custom translations
# Custom translations use direct path: switch.{key}.description
translation_path = [
"switch",
self.entity_description.translation_key or self.entity_description.key,
"description",
]
description = get_translation(translation_path, language)
attrs: dict[str, Any] = {}
if description:
attrs["description"] = description
return attrs if attrs else None
@callback
def async_registry_entry_updated(self) -> None:
"""Handle entity registry update (enabled/disabled state change)."""
# This is called when the entity is enabled/disabled in the UI
self.hass.async_create_task(self._sync_override_state())

View file

@ -0,0 +1,84 @@
"""
Switch entity definitions for Tibber Prices configuration overrides.
These switch entities allow runtime configuration of boolean settings
for Best Price and Peak Price period calculations.
When enabled, the entity value takes precedence over the options flow setting.
When disabled (default), the options flow setting is used.
"""
from __future__ import annotations
from dataclasses import dataclass
from homeassistant.components.switch import SwitchEntityDescription
from homeassistant.const import EntityCategory
@dataclass(frozen=True, kw_only=True)
class TibberPricesSwitchEntityDescription(SwitchEntityDescription):
"""Describes a Tibber Prices switch entity for config overrides."""
# The config key this entity overrides (matches CONF_* constants)
config_key: str
# The section in options where this setting is stored
config_section: str
# Whether this is for best_price (False) or peak_price (True)
is_peak_price: bool = False
# Default value from const.py
default_value: bool = True
# ============================================================================
# BEST PRICE PERIOD CONFIGURATION OVERRIDES (Boolean)
# ============================================================================
BEST_PRICE_SWITCH_ENTITIES = (
SwitchEntityDescription(
key="best_price_enable_relaxation_override",
translation_key="best_price_enable_relaxation_override",
name="Best Price: Achieve Minimum Count",
icon="mdi:arrow-down-bold-circle",
entity_category=EntityCategory.CONFIG,
entity_registry_enabled_default=False,
),
)
# Custom descriptions with extra fields
BEST_PRICE_SWITCH_ENTITY_DESCRIPTIONS = (
TibberPricesSwitchEntityDescription(
key="best_price_enable_relaxation_override",
translation_key="best_price_enable_relaxation_override",
name="Best Price: Achieve Minimum Count",
icon="mdi:arrow-down-bold-circle",
entity_category=EntityCategory.CONFIG,
entity_registry_enabled_default=False,
config_key="enable_min_periods_best",
config_section="relaxation_and_target_periods",
is_peak_price=False,
default_value=True, # DEFAULT_ENABLE_MIN_PERIODS_BEST
),
)
# ============================================================================
# PEAK PRICE PERIOD CONFIGURATION OVERRIDES (Boolean)
# ============================================================================
PEAK_PRICE_SWITCH_ENTITY_DESCRIPTIONS = (
TibberPricesSwitchEntityDescription(
key="peak_price_enable_relaxation_override",
translation_key="peak_price_enable_relaxation_override",
name="Peak Price: Achieve Minimum Count",
icon="mdi:arrow-up-bold-circle",
entity_category=EntityCategory.CONFIG,
entity_registry_enabled_default=False,
config_key="enable_min_periods_peak",
config_section="relaxation_and_target_periods",
is_peak_price=True,
default_value=True, # DEFAULT_ENABLE_MIN_PERIODS_PEAK
),
)
# All switch entity descriptions combined
SWITCH_ENTITY_DESCRIPTIONS = BEST_PRICE_SWITCH_ENTITY_DESCRIPTIONS + PEAK_PRICE_SWITCH_ENTITY_DESCRIPTIONS

View file

@ -77,7 +77,25 @@
} }
}, },
"common": { "common": {
"step_progress": "{step_num} / {total_steps}" "step_progress": "{step_num} / {total_steps}",
"override_warning_template": "⚠️ {fields} wird durch Konfigurations-Entität gesteuert",
"override_warning_and": "und",
"override_field_labels": {
"best_price_min_period_length": "Mindestperiodenlänge",
"best_price_max_level_gap_count": "Lückentoleranz",
"best_price_flex": "Flexibilität",
"best_price_min_distance_from_avg": "Mindestabstand",
"enable_min_periods_best": "Mindestzahl erreichen",
"min_periods_best": "Mindestperioden",
"relaxation_attempts_best": "Lockerungsversuche",
"peak_price_min_period_length": "Mindestperiodenlänge",
"peak_price_max_level_gap_count": "Lückentoleranz",
"peak_price_flex": "Flexibilität",
"peak_price_min_distance_from_avg": "Mindestabstand",
"enable_min_periods_peak": "Mindestzahl erreichen",
"min_periods_peak": "Mindestperioden",
"relaxation_attempts_peak": "Lockerungsversuche"
}
}, },
"config_subentries": { "config_subentries": {
"home": { "home": {
@ -189,7 +207,7 @@
}, },
"best_price": { "best_price": {
"title": "💚 Bestpreis-Zeitraum Einstellungen", "title": "💚 Bestpreis-Zeitraum Einstellungen",
"description": "**Konfiguration für den Bestpreis-Zeitraum mit den niedrigsten Strompreisen.**{entity_warning}\n\n---", "description": "**Konfiguration für den Bestpreis-Zeitraum mit den niedrigsten Strompreisen.**{entity_warning}{override_warning}\n\n---",
"sections": { "sections": {
"period_settings": { "period_settings": {
"name": "Zeitraumdauer & Preisniveaus", "name": "Zeitraumdauer & Preisniveaus",
@ -236,7 +254,7 @@
}, },
"peak_price": { "peak_price": {
"title": "🔴 Spitzenpreis-Zeitraum Einstellungen", "title": "🔴 Spitzenpreis-Zeitraum Einstellungen",
"description": "**Konfiguration für den Spitzenpreis-Zeitraum mit den höchsten Strompreisen.**{entity_warning}\n\n---", "description": "**Konfiguration für den Spitzenpreis-Zeitraum mit den höchsten Strompreisen.**{entity_warning}{override_warning}\n\n---",
"sections": { "sections": {
"period_settings": { "period_settings": {
"name": "Zeitraum-Einstellungen", "name": "Zeitraum-Einstellungen",
@ -886,6 +904,52 @@
"realtime_consumption_enabled": { "realtime_consumption_enabled": {
"name": "Echtzeitverbrauch aktiviert" "name": "Echtzeitverbrauch aktiviert"
} }
},
"number": {
"best_price_flex_override": {
"name": "Bestpreis: Flexibilität"
},
"best_price_min_distance_override": {
"name": "Bestpreis: Mindestabstand"
},
"best_price_min_period_length_override": {
"name": "Bestpreis: Mindestperiodenlänge"
},
"best_price_min_periods_override": {
"name": "Bestpreis: Mindestperioden"
},
"best_price_relaxation_attempts_override": {
"name": "Bestpreis: Lockerungsversuche"
},
"best_price_gap_count_override": {
"name": "Bestpreis: Lückentoleranz"
},
"peak_price_flex_override": {
"name": "Spitzenpreis: Flexibilität"
},
"peak_price_min_distance_override": {
"name": "Spitzenpreis: Mindestabstand"
},
"peak_price_min_period_length_override": {
"name": "Spitzenpreis: Mindestperiodenlänge"
},
"peak_price_min_periods_override": {
"name": "Spitzenpreis: Mindestperioden"
},
"peak_price_relaxation_attempts_override": {
"name": "Spitzenpreis: Lockerungsversuche"
},
"peak_price_gap_count_override": {
"name": "Spitzenpreis: Lückentoleranz"
}
},
"switch": {
"best_price_enable_relaxation_override": {
"name": "Bestpreis: Mindestanzahl erreichen"
},
"peak_price_enable_relaxation_override": {
"name": "Spitzenpreis: Mindestanzahl erreichen"
}
} }
}, },
"issues": { "issues": {

View file

@ -77,7 +77,25 @@
} }
}, },
"common": { "common": {
"step_progress": "{step_num} / {total_steps}" "step_progress": "{step_num} / {total_steps}",
"override_warning_template": "⚠️ {fields} controlled by config entity",
"override_warning_and": "and",
"override_field_labels": {
"best_price_min_period_length": "Minimum Period Length",
"best_price_max_level_gap_count": "Gap Tolerance",
"best_price_flex": "Flexibility",
"best_price_min_distance_from_avg": "Minimum Distance",
"enable_min_periods_best": "Achieve Minimum Count",
"min_periods_best": "Minimum Periods",
"relaxation_attempts_best": "Relaxation Attempts",
"peak_price_min_period_length": "Minimum Period Length",
"peak_price_max_level_gap_count": "Gap Tolerance",
"peak_price_flex": "Flexibility",
"peak_price_min_distance_from_avg": "Minimum Distance",
"enable_min_periods_peak": "Achieve Minimum Count",
"min_periods_peak": "Minimum Periods",
"relaxation_attempts_peak": "Relaxation Attempts"
}
}, },
"config_subentries": { "config_subentries": {
"home": { "home": {
@ -200,7 +218,7 @@
}, },
"best_price": { "best_price": {
"title": "💚 Best Price Period Settings", "title": "💚 Best Price Period Settings",
"description": "**Configure settings for the Best Price Period binary sensor. This sensor is active during periods with the lowest electricity prices.**{entity_warning}\n\n---", "description": "**Configure settings for the Best Price Period binary sensor. This sensor is active during periods with the lowest electricity prices.**{entity_warning}{override_warning}\n\n---",
"sections": { "sections": {
"period_settings": { "period_settings": {
"name": "Period Duration & Levels", "name": "Period Duration & Levels",
@ -247,7 +265,7 @@
}, },
"peak_price": { "peak_price": {
"title": "🔴 Peak Price Period Settings", "title": "🔴 Peak Price Period Settings",
"description": "**Configure settings for the Peak Price Period binary sensor. This sensor is active during periods with the highest electricity prices.**{entity_warning}\n\n---", "description": "**Configure settings for the Peak Price Period binary sensor. This sensor is active during periods with the highest electricity prices.**{entity_warning}{override_warning}\n\n---",
"sections": { "sections": {
"period_settings": { "period_settings": {
"name": "Period Settings", "name": "Period Settings",
@ -886,6 +904,52 @@
"realtime_consumption_enabled": { "realtime_consumption_enabled": {
"name": "Realtime Consumption Enabled" "name": "Realtime Consumption Enabled"
} }
},
"number": {
"best_price_flex_override": {
"name": "Best Price: Flexibility"
},
"best_price_min_distance_override": {
"name": "Best Price: Minimum Distance"
},
"best_price_min_period_length_override": {
"name": "Best Price: Minimum Period Length"
},
"best_price_min_periods_override": {
"name": "Best Price: Minimum Periods"
},
"best_price_relaxation_attempts_override": {
"name": "Best Price: Relaxation Attempts"
},
"best_price_gap_count_override": {
"name": "Best Price: Gap Tolerance"
},
"peak_price_flex_override": {
"name": "Peak Price: Flexibility"
},
"peak_price_min_distance_override": {
"name": "Peak Price: Minimum Distance"
},
"peak_price_min_period_length_override": {
"name": "Peak Price: Minimum Period Length"
},
"peak_price_min_periods_override": {
"name": "Peak Price: Minimum Periods"
},
"peak_price_relaxation_attempts_override": {
"name": "Peak Price: Relaxation Attempts"
},
"peak_price_gap_count_override": {
"name": "Peak Price: Gap Tolerance"
}
},
"switch": {
"best_price_enable_relaxation_override": {
"name": "Best Price: Achieve Minimum Count"
},
"peak_price_enable_relaxation_override": {
"name": "Peak Price: Achieve Minimum Count"
}
} }
}, },
"issues": { "issues": {

View file

@ -77,7 +77,25 @@
} }
}, },
"common": { "common": {
"step_progress": "{step_num} / {total_steps}" "step_progress": "{step_num} / {total_steps}",
"override_warning_template": "⚠️ {fields} styres av konfigurasjons-entitet",
"override_warning_and": "og",
"override_field_labels": {
"best_price_min_period_length": "Minste periodelengde",
"best_price_max_level_gap_count": "Gaptoleranse",
"best_price_flex": "Fleksibilitet",
"best_price_min_distance_from_avg": "Minimumsavstand",
"enable_min_periods_best": "Oppnå minimum antall",
"min_periods_best": "Minimumperioder",
"relaxation_attempts_best": "Avslapningsforsøk",
"peak_price_min_period_length": "Minste periodelengde",
"peak_price_max_level_gap_count": "Gaptoleranse",
"peak_price_flex": "Fleksibilitet",
"peak_price_min_distance_from_avg": "Minimumsavstand",
"enable_min_periods_peak": "Oppnå minimum antall",
"min_periods_peak": "Minimumperioder",
"relaxation_attempts_peak": "Avslapningsforsøk"
}
}, },
"config_subentries": { "config_subentries": {
"home": { "home": {
@ -189,7 +207,7 @@
}, },
"best_price": { "best_price": {
"title": "💚 Beste Prisperiode Innstillinger", "title": "💚 Beste Prisperiode Innstillinger",
"description": "**Konfigurer innstillinger for Beste Prisperiode binærsensor. Denne sensoren er aktiv i perioder med de laveste strømprisene.**{entity_warning}\n\n---", "description": "**Konfigurer innstillinger for Beste Prisperiode binærsensor. Denne sensoren er aktiv i perioder med de laveste strømprisene.**{entity_warning}{override_warning}\n\n---",
"sections": { "sections": {
"period_settings": { "period_settings": {
"name": "Periodeinnstillinger", "name": "Periodeinnstillinger",
@ -236,7 +254,7 @@
}, },
"peak_price": { "peak_price": {
"title": "🔴 Toppprisperiode Innstillinger", "title": "🔴 Toppprisperiode Innstillinger",
"description": "**Konfigurer innstillinger for Toppprisperiode binærsensor. Denne sensoren er aktiv i perioder med de høyeste strømprisene.**{entity_warning}\n\n---", "description": "**Konfigurer innstillinger for Toppprisperiode binærsensor. Denne sensoren er aktiv i perioder med de høyeste strømprisene.**{entity_warning}{override_warning}\n\n---",
"sections": { "sections": {
"period_settings": { "period_settings": {
"name": "Periodeinnstillinger", "name": "Periodeinnstillinger",
@ -886,6 +904,52 @@
"realtime_consumption_enabled": { "realtime_consumption_enabled": {
"name": "Sanntidsforbruk aktivert" "name": "Sanntidsforbruk aktivert"
} }
},
"number": {
"best_price_flex_override": {
"name": "Beste pris: Fleksibilitet"
},
"best_price_min_distance_override": {
"name": "Beste pris: Minimumsavstand"
},
"best_price_min_period_length_override": {
"name": "Beste pris: Minimum periodelengde"
},
"best_price_min_periods_override": {
"name": "Beste pris: Minimum perioder"
},
"best_price_relaxation_attempts_override": {
"name": "Beste pris: Lemping forsøk"
},
"best_price_gap_count_override": {
"name": "Beste pris: Gaptoleranse"
},
"peak_price_flex_override": {
"name": "Topppris: Fleksibilitet"
},
"peak_price_min_distance_override": {
"name": "Topppris: Minimumsavstand"
},
"peak_price_min_period_length_override": {
"name": "Topppris: Minimum periodelengde"
},
"peak_price_min_periods_override": {
"name": "Topppris: Minimum perioder"
},
"peak_price_relaxation_attempts_override": {
"name": "Topppris: Lemping forsøk"
},
"peak_price_gap_count_override": {
"name": "Topppris: Gaptoleranse"
}
},
"switch": {
"best_price_enable_relaxation_override": {
"name": "Beste pris: Oppnå minimumsantall"
},
"peak_price_enable_relaxation_override": {
"name": "Topppris: Oppnå minimumsantall"
}
} }
}, },
"issues": { "issues": {

View file

@ -77,7 +77,25 @@
} }
}, },
"common": { "common": {
"step_progress": "{step_num} / {total_steps}" "step_progress": "{step_num} / {total_steps}",
"override_warning_template": "⚠️ {fields} wordt beheerd door configuratie-entiteit",
"override_warning_and": "en",
"override_field_labels": {
"best_price_min_period_length": "Minimale periodelengte",
"best_price_max_level_gap_count": "Gaptolerantie",
"best_price_flex": "Flexibiliteit",
"best_price_min_distance_from_avg": "Minimale afstand",
"enable_min_periods_best": "Minimum aantal bereiken",
"min_periods_best": "Minimale periodes",
"relaxation_attempts_best": "Ontspanningspogingen",
"peak_price_min_period_length": "Minimale periodelengte",
"peak_price_max_level_gap_count": "Gaptolerantie",
"peak_price_flex": "Flexibiliteit",
"peak_price_min_distance_from_avg": "Minimale afstand",
"enable_min_periods_peak": "Minimum aantal bereiken",
"min_periods_peak": "Minimale periodes",
"relaxation_attempts_peak": "Ontspanningspogingen"
}
}, },
"config_subentries": { "config_subentries": {
"home": { "home": {
@ -189,7 +207,7 @@
}, },
"best_price": { "best_price": {
"title": "💚 Beste Prijs Periode Instellingen", "title": "💚 Beste Prijs Periode Instellingen",
"description": "**Configureer instellingen voor de Beste Prijs Periode binaire sensor. Deze sensor is actief tijdens periodes met de laagste elektriciteitsprijzen.**{entity_warning}\n\n---", "description": "**Configureer instellingen voor de Beste Prijs Periode binaire sensor. Deze sensor is actief tijdens periodes met de laagste elektriciteitsprijzen.**{entity_warning}{override_warning}\n\n---",
"sections": { "sections": {
"period_settings": { "period_settings": {
"name": "Periode Duur & Niveaus", "name": "Periode Duur & Niveaus",
@ -236,7 +254,7 @@
}, },
"peak_price": { "peak_price": {
"title": "🔴 Piekprijs Periode Instellingen", "title": "🔴 Piekprijs Periode Instellingen",
"description": "**Configureer instellingen voor de Piekprijs Periode binaire sensor. Deze sensor is actief tijdens periodes met de hoogste elektriciteitsprijzen.**{entity_warning}\n\n---", "description": "**Configureer instellingen voor de Piekprijs Periode binaire sensor. Deze sensor is actief tijdens periodes met de hoogste elektriciteitsprijzen.**{entity_warning}{override_warning}\n\n---",
"sections": { "sections": {
"period_settings": { "period_settings": {
"name": "Periode Instellingen", "name": "Periode Instellingen",
@ -886,6 +904,52 @@
"realtime_consumption_enabled": { "realtime_consumption_enabled": {
"name": "Realtime Verbruik Ingeschakeld" "name": "Realtime Verbruik Ingeschakeld"
} }
},
"number": {
"best_price_flex_override": {
"name": "Beste prijs: Flexibiliteit"
},
"best_price_min_distance_override": {
"name": "Beste prijs: Minimale afstand"
},
"best_price_min_period_length_override": {
"name": "Beste prijs: Minimale periodelengte"
},
"best_price_min_periods_override": {
"name": "Beste prijs: Minimum periodes"
},
"best_price_relaxation_attempts_override": {
"name": "Beste prijs: Versoepeling pogingen"
},
"best_price_gap_count_override": {
"name": "Beste prijs: Gap tolerantie"
},
"peak_price_flex_override": {
"name": "Piekprijs: Flexibiliteit"
},
"peak_price_min_distance_override": {
"name": "Piekprijs: Minimale afstand"
},
"peak_price_min_period_length_override": {
"name": "Piekprijs: Minimale periodelengte"
},
"peak_price_min_periods_override": {
"name": "Piekprijs: Minimum periodes"
},
"peak_price_relaxation_attempts_override": {
"name": "Piekprijs: Versoepeling pogingen"
},
"peak_price_gap_count_override": {
"name": "Piekprijs: Gap tolerantie"
}
},
"switch": {
"best_price_enable_relaxation_override": {
"name": "Beste prijs: Minimum aantal bereiken"
},
"peak_price_enable_relaxation_override": {
"name": "Piekprijs: Minimum aantal bereiken"
}
} }
}, },
"issues": { "issues": {

View file

@ -77,7 +77,25 @@
} }
}, },
"common": { "common": {
"step_progress": "{step_num} / {total_steps}" "step_progress": "{step_num} / {total_steps}",
"override_warning_template": "⚠️ {fields} styrs av konfigurationsentitet",
"override_warning_and": "och",
"override_field_labels": {
"best_price_min_period_length": "Minsta periodlängd",
"best_price_max_level_gap_count": "Glappstolerans",
"best_price_flex": "Flexibilitet",
"best_price_min_distance_from_avg": "Minsta avstånd",
"enable_min_periods_best": "Uppnå minsta antal",
"min_periods_best": "Minimiperioder",
"relaxation_attempts_best": "Avslappningsförsök",
"peak_price_min_period_length": "Minsta periodlängd",
"peak_price_max_level_gap_count": "Glappstolerans",
"peak_price_flex": "Flexibilitet",
"peak_price_min_distance_from_avg": "Minsta avstånd",
"enable_min_periods_peak": "Uppnå minsta antal",
"min_periods_peak": "Minimiperioder",
"relaxation_attempts_peak": "Avslappningsförsök"
}
}, },
"config_subentries": { "config_subentries": {
"home": { "home": {
@ -189,7 +207,7 @@
}, },
"best_price": { "best_price": {
"title": "💚 Bästa Prisperiod-inställningar", "title": "💚 Bästa Prisperiod-inställningar",
"description": "**Konfigurera inställningar för binärsensorn Bästa Prisperiod. Denna sensor är aktiv under perioder med lägsta elpriserna.**{entity_warning}\n\n---", "description": "**Konfigurera inställningar för binärsensorn Bästa Prisperiod. Denna sensor är aktiv under perioder med lägsta elpriserna.**{entity_warning}{override_warning}\n\n---",
"sections": { "sections": {
"period_settings": { "period_settings": {
"name": "Periodlängd & Nivåer", "name": "Periodlängd & Nivåer",
@ -236,7 +254,7 @@
}, },
"peak_price": { "peak_price": {
"title": "🔴 Topprisperiod-inställningar", "title": "🔴 Topprisperiod-inställningar",
"description": "**Konfigurera inställningar för binärsensorn Topprisperiod. Denna sensor är aktiv under perioder med högsta elpriserna.**{entity_warning}\n\n---", "description": "**Konfigurera inställningar för binärsensorn Topprisperiod. Denna sensor är aktiv under perioder med högsta elpriserna.**{entity_warning}{override_warning}\n\n---",
"sections": { "sections": {
"period_settings": { "period_settings": {
"name": "Periodinställningar", "name": "Periodinställningar",
@ -886,6 +904,52 @@
"realtime_consumption_enabled": { "realtime_consumption_enabled": {
"name": "Realtidsförbrukning aktiverad" "name": "Realtidsförbrukning aktiverad"
} }
},
"number": {
"best_price_flex_override": {
"name": "Bästa pris: Flexibilitet"
},
"best_price_min_distance_override": {
"name": "Bästa pris: Minimiavstånd"
},
"best_price_min_period_length_override": {
"name": "Bästa pris: Minsta periodlängd"
},
"best_price_min_periods_override": {
"name": "Bästa pris: Minsta antal perioder"
},
"best_price_relaxation_attempts_override": {
"name": "Bästa pris: Lättnadsförsök"
},
"best_price_gap_count_override": {
"name": "Bästa pris: Glaptolerans"
},
"peak_price_flex_override": {
"name": "Topppris: Flexibilitet"
},
"peak_price_min_distance_override": {
"name": "Topppris: Minimiavstånd"
},
"peak_price_min_period_length_override": {
"name": "Topppris: Minsta periodlängd"
},
"peak_price_min_periods_override": {
"name": "Topppris: Minsta antal perioder"
},
"peak_price_relaxation_attempts_override": {
"name": "Topppris: Lättnadsförsök"
},
"peak_price_gap_count_override": {
"name": "Topppris: Glaptolerans"
}
},
"switch": {
"best_price_enable_relaxation_override": {
"name": "Bästa pris: Uppnå minimiantal"
},
"peak_price_enable_relaxation_override": {
"name": "Topppris: Uppnå minimiantal"
}
} }
}, },
"issues": { "issues": {

View file

@ -83,4 +83,99 @@ See the **[Sensors Guide](sensors.md#average-price-sensors)** for detailed examp
**Pro Tip:** Most users prefer **Median** for displays (more intuitive), but use `price_mean` attribute in cost calculation automations. **Pro Tip:** Most users prefer **Median** for displays (more intuitive), but use `price_mean` attribute in cost calculation automations.
Coming soon... ## Runtime Configuration Entities
The integration provides optional configuration entities that allow you to override period calculation settings at runtime through automations. These entities are **disabled by default** and can be enabled individually as needed.
### Available Configuration Entities
When enabled, these entities override the corresponding Options Flow settings:
#### Best Price Period Settings
| Entity | Type | Range | Description |
|--------|------|-------|-------------|
| **Best Price: Flexibility** | Number | 0-50% | Maximum above daily minimum for "best price" intervals |
| **Best Price: Minimum Distance** | Number | -50-0% | Required distance below daily average |
| **Best Price: Minimum Period Length** | Number | 15-180 min | Shortest period duration to consider |
| **Best Price: Minimum Periods** | Number | 1-10 | Target number of periods per day |
| **Best Price: Relaxation Attempts** | Number | 1-12 | Steps to try when relaxing criteria |
| **Best Price: Gap Tolerance** | Number | 0-8 | Consecutive intervals allowed above threshold |
| **Best Price: Achieve Minimum Count** | Switch | On/Off | Enable relaxation algorithm |
#### Peak Price Period Settings
| Entity | Type | Range | Description |
|--------|------|-------|-------------|
| **Peak Price: Flexibility** | Number | -50-0% | Maximum below daily maximum for "peak price" intervals |
| **Peak Price: Minimum Distance** | Number | 0-50% | Required distance above daily average |
| **Peak Price: Minimum Period Length** | Number | 15-180 min | Shortest period duration to consider |
| **Peak Price: Minimum Periods** | Number | 1-10 | Target number of periods per day |
| **Peak Price: Relaxation Attempts** | Number | 1-12 | Steps to try when relaxing criteria |
| **Peak Price: Gap Tolerance** | Number | 0-8 | Consecutive intervals allowed below threshold |
| **Peak Price: Achieve Minimum Count** | Switch | On/Off | Enable relaxation algorithm |
### How Runtime Overrides Work
1. **Disabled (default):** The Options Flow setting is used
2. **Enabled:** The entity value overrides the Options Flow setting
3. **Value changes:** Trigger immediate period recalculation
4. **HA restart:** Entity values are restored automatically
### Viewing Entity Descriptions
Each configuration entity includes a detailed description attribute explaining what the setting does - the same information shown in the Options Flow.
**Note:** For **Number entities**, Home Assistant displays a history graph by default, which hides the attributes panel. To view the `description` attribute:
1. Go to **Developer Tools → States**
2. Search for the entity (e.g., `number.<home_name>_best_price_flexibility_override`)
3. Expand the attributes section to see the full description
**Switch entities** display their attributes normally in the entity details view.
### Example: Seasonal Automation
```yaml
automation:
- alias: "Winter: Stricter Best Price Detection"
trigger:
- platform: time
at: "00:00:00"
condition:
- condition: template
value_template: "{{ now().month in [11, 12, 1, 2] }}"
action:
- service: number.set_value
target:
entity_id: number.<home_name>_best_price_flexibility_override
data:
value: 10 # Stricter than default 15%
```
### Recorder Optimization (Optional)
These configuration entities are designed to minimize database impact:
- **EntityCategory.CONFIG** - Excluded from Long-Term Statistics
- All attributes excluded from history recording
- Only state value changes are recorded
If you frequently adjust these settings via automations or want to track configuration changes over time, the default behavior is fine.
However, if you prefer to **completely exclude** these entities from the recorder (no history graph, no database entries), add this to your `configuration.yaml`:
```yaml
recorder:
exclude:
entity_globs:
# Exclude all Tibber Prices configuration entities
- number.*_best_price_*_override
- number.*_peak_price_*_override
- switch.*_best_price_*_override
- switch.*_peak_price_*_override
```
This is especially useful if:
- You rarely change these settings
- You want the smallest possible database footprint
- You don't need to see the history graph for these entities