mirror of
https://github.com/jpawlowski/hass.tibber_prices.git
synced 2026-03-29 21:03:40 +00:00
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:
parent
cc75bc53ee
commit
631cebeb55
22 changed files with 2522 additions and 311 deletions
|
|
@ -47,6 +47,8 @@ if TYPE_CHECKING:
|
|||
PLATFORMS: list[Platform] = [
|
||||
Platform.SENSOR,
|
||||
Platform.BINARY_SENSOR,
|
||||
Platform.NUMBER,
|
||||
Platform.SWITCH,
|
||||
]
|
||||
|
||||
# Configuration schema for configuration.yaml
|
||||
|
|
|
|||
|
|
@ -15,6 +15,7 @@ from custom_components.tibber_prices.config_flow_handlers.entity_check import (
|
|||
format_sensor_names_for_warning,
|
||||
)
|
||||
from custom_components.tibber_prices.config_flow_handlers.schemas import (
|
||||
ConfigOverrides,
|
||||
get_best_price_schema,
|
||||
get_chart_data_export_schema,
|
||||
get_display_settings_schema,
|
||||
|
|
@ -72,9 +73,11 @@ from custom_components.tibber_prices.const import (
|
|||
DEFAULT_VOLATILITY_THRESHOLD_MODERATE,
|
||||
DEFAULT_VOLATILITY_THRESHOLD_VERY_HIGH,
|
||||
DOMAIN,
|
||||
async_get_translation,
|
||||
get_default_options,
|
||||
)
|
||||
from homeassistant.config_entries import ConfigFlowResult, OptionsFlow
|
||||
from homeassistant.helpers import entity_registry as er
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
|
@ -216,6 +219,167 @@ class TibberPricesOptionsFlowHandler(OptionsFlow):
|
|||
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:
|
||||
"""Manage the options - show menu."""
|
||||
# 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 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(
|
||||
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,
|
||||
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:
|
||||
|
|
@ -507,11 +682,22 @@ class TibberPricesOptionsFlowHandler(OptionsFlow):
|
|||
# Return to menu for more changes
|
||||
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(
|
||||
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,
|
||||
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:
|
||||
|
|
|
|||
|
|
@ -119,6 +119,8 @@ from homeassistant.data_entry_flow import section
|
|||
from homeassistant.helpers import selector
|
||||
from homeassistant.helpers.selector import (
|
||||
BooleanSelector,
|
||||
ConstantSelector,
|
||||
ConstantSelectorConfig,
|
||||
NumberSelector,
|
||||
NumberSelectorConfig,
|
||||
NumberSelectorMode,
|
||||
|
|
@ -131,6 +133,156 @@ from homeassistant.helpers.selector import (
|
|||
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:
|
||||
"""Return schema for user step (API token input)."""
|
||||
|
|
@ -434,22 +586,50 @@ def get_volatility_schema(options: Mapping[str, Any]) -> vol.Schema:
|
|||
)
|
||||
|
||||
|
||||
def get_best_price_schema(options: Mapping[str, Any]) -> vol.Schema:
|
||||
"""Return schema for best price period configuration with collapsible sections."""
|
||||
def get_best_price_schema(
|
||||
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", {})
|
||||
return vol.Schema(
|
||||
{
|
||||
vol.Required("period_settings"): section(
|
||||
vol.Schema(
|
||||
{
|
||||
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=int(
|
||||
period_settings.get(
|
||||
CONF_BEST_PRICE_MIN_PERIOD_LENGTH,
|
||||
DEFAULT_BEST_PRICE_MIN_PERIOD_LENGTH,
|
||||
)
|
||||
),
|
||||
default=min_period_length,
|
||||
): NumberSelector(
|
||||
NumberSelectorConfig(
|
||||
min=MIN_PERIOD_LENGTH,
|
||||
|
|
@ -457,7 +637,7 @@ def get_best_price_schema(options: Mapping[str, Any]) -> vol.Schema:
|
|||
step=15,
|
||||
unit_of_measurement="min",
|
||||
mode=NumberSelectorMode.SLIDER,
|
||||
),
|
||||
)
|
||||
),
|
||||
vol.Optional(
|
||||
CONF_BEST_PRICE_MAX_LEVEL,
|
||||
|
|
@ -474,35 +654,25 @@ def get_best_price_schema(options: Mapping[str, Any]) -> vol.Schema:
|
|||
),
|
||||
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,
|
||||
)
|
||||
),
|
||||
default=max_level_gap_count,
|
||||
): NumberSelector(
|
||||
NumberSelectorConfig(
|
||||
min=MIN_GAP_COUNT,
|
||||
max=MAX_GAP_COUNT,
|
||||
step=1,
|
||||
mode=NumberSelectorMode.SLIDER,
|
||||
),
|
||||
),
|
||||
}
|
||||
),
|
||||
{"collapsed": False},
|
||||
),
|
||||
vol.Required("flexibility_settings"): section(
|
||||
vol.Schema(
|
||||
{
|
||||
vol.Optional(
|
||||
CONF_BEST_PRICE_FLEX,
|
||||
default=int(
|
||||
options.get("flexibility_settings", {}).get(
|
||||
CONF_BEST_PRICE_FLEX,
|
||||
DEFAULT_BEST_PRICE_FLEX,
|
||||
)
|
||||
),
|
||||
}
|
||||
|
||||
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,
|
||||
|
|
@ -510,16 +680,11 @@ def get_best_price_schema(options: Mapping[str, Any]) -> vol.Schema:
|
|||
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,
|
||||
)
|
||||
),
|
||||
default=min_distance,
|
||||
): NumberSelector(
|
||||
NumberSelectorConfig(
|
||||
min=-50,
|
||||
|
|
@ -527,78 +692,105 @@ def get_best_price_schema(options: Mapping[str, Any]) -> vol.Schema:
|
|||
step=1,
|
||||
unit_of_measurement="%",
|
||||
mode=NumberSelectorMode.SLIDER,
|
||||
),
|
||||
),
|
||||
}
|
||||
),
|
||||
{"collapsed": True},
|
||||
),
|
||||
vol.Required("relaxation_and_target_periods"): section(
|
||||
vol.Schema(
|
||||
{
|
||||
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,
|
||||
)
|
||||
),
|
||||
}
|
||||
|
||||
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=int(
|
||||
options.get("relaxation_and_target_periods", {}).get(
|
||||
CONF_RELAXATION_ATTEMPTS_BEST,
|
||||
DEFAULT_RELAXATION_ATTEMPTS_BEST,
|
||||
)
|
||||
),
|
||||
default=relaxation_attempts,
|
||||
): NumberSelector(
|
||||
NumberSelectorConfig(
|
||||
min=MIN_RELAXATION_ATTEMPTS,
|
||||
max=MAX_RELAXATION_ATTEMPTS,
|
||||
step=1,
|
||||
mode=NumberSelectorMode.SLIDER,
|
||||
),
|
||||
)
|
||||
),
|
||||
}
|
||||
|
||||
return vol.Schema(
|
||||
{
|
||||
vol.Required("period_settings"): section(
|
||||
vol.Schema(period_fields),
|
||||
{"collapsed": False},
|
||||
),
|
||||
vol.Required("flexibility_settings"): section(
|
||||
vol.Schema(flexibility_fields),
|
||||
{"collapsed": True},
|
||||
),
|
||||
vol.Required("relaxation_and_target_periods"): section(
|
||||
vol.Schema(relaxation_fields),
|
||||
{"collapsed": True},
|
||||
),
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
def get_peak_price_schema(options: Mapping[str, Any]) -> vol.Schema:
|
||||
"""Return schema for peak price period configuration with collapsible sections."""
|
||||
def get_peak_price_schema(
|
||||
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", {})
|
||||
return vol.Schema(
|
||||
{
|
||||
vol.Required("period_settings"): section(
|
||||
vol.Schema(
|
||||
{
|
||||
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=int(
|
||||
period_settings.get(
|
||||
CONF_PEAK_PRICE_MIN_PERIOD_LENGTH,
|
||||
DEFAULT_PEAK_PRICE_MIN_PERIOD_LENGTH,
|
||||
)
|
||||
),
|
||||
default=min_period_length,
|
||||
): NumberSelector(
|
||||
NumberSelectorConfig(
|
||||
min=MIN_PERIOD_LENGTH,
|
||||
|
|
@ -606,7 +798,7 @@ def get_peak_price_schema(options: Mapping[str, Any]) -> vol.Schema:
|
|||
step=15,
|
||||
unit_of_measurement="min",
|
||||
mode=NumberSelectorMode.SLIDER,
|
||||
),
|
||||
)
|
||||
),
|
||||
vol.Optional(
|
||||
CONF_PEAK_PRICE_MIN_LEVEL,
|
||||
|
|
@ -623,35 +815,25 @@ def get_peak_price_schema(options: Mapping[str, Any]) -> vol.Schema:
|
|||
),
|
||||
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,
|
||||
)
|
||||
),
|
||||
default=max_level_gap_count,
|
||||
): NumberSelector(
|
||||
NumberSelectorConfig(
|
||||
min=MIN_GAP_COUNT,
|
||||
max=MAX_GAP_COUNT,
|
||||
step=1,
|
||||
mode=NumberSelectorMode.SLIDER,
|
||||
),
|
||||
),
|
||||
}
|
||||
),
|
||||
{"collapsed": False},
|
||||
),
|
||||
vol.Required("flexibility_settings"): section(
|
||||
vol.Schema(
|
||||
{
|
||||
vol.Optional(
|
||||
CONF_PEAK_PRICE_FLEX,
|
||||
default=int(
|
||||
options.get("flexibility_settings", {}).get(
|
||||
CONF_PEAK_PRICE_FLEX,
|
||||
DEFAULT_PEAK_PRICE_FLEX,
|
||||
)
|
||||
),
|
||||
}
|
||||
|
||||
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,
|
||||
|
|
@ -659,16 +841,11 @@ def get_peak_price_schema(options: Mapping[str, Any]) -> vol.Schema:
|
|||
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,
|
||||
)
|
||||
),
|
||||
default=min_distance,
|
||||
): NumberSelector(
|
||||
NumberSelectorConfig(
|
||||
min=0,
|
||||
|
|
@ -676,56 +853,55 @@ def get_peak_price_schema(options: Mapping[str, Any]) -> vol.Schema:
|
|||
step=1,
|
||||
unit_of_measurement="%",
|
||||
mode=NumberSelectorMode.SLIDER,
|
||||
),
|
||||
),
|
||||
}
|
||||
),
|
||||
{"collapsed": True},
|
||||
),
|
||||
vol.Required("relaxation_and_target_periods"): section(
|
||||
vol.Schema(
|
||||
{
|
||||
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,
|
||||
)
|
||||
),
|
||||
}
|
||||
|
||||
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=int(
|
||||
options.get("relaxation_and_target_periods", {}).get(
|
||||
CONF_RELAXATION_ATTEMPTS_PEAK,
|
||||
DEFAULT_RELAXATION_ATTEMPTS_PEAK,
|
||||
)
|
||||
),
|
||||
default=relaxation_attempts,
|
||||
): NumberSelector(
|
||||
NumberSelectorConfig(
|
||||
min=MIN_RELAXATION_ATTEMPTS,
|
||||
max=MAX_RELAXATION_ATTEMPTS,
|
||||
step=1,
|
||||
mode=NumberSelectorMode.SLIDER,
|
||||
),
|
||||
)
|
||||
),
|
||||
}
|
||||
|
||||
return vol.Schema(
|
||||
{
|
||||
vol.Required("period_settings"): section(
|
||||
vol.Schema(period_fields),
|
||||
{"collapsed": False},
|
||||
),
|
||||
vol.Required("flexibility_settings"): section(
|
||||
vol.Schema(flexibility_fields),
|
||||
{"collapsed": True},
|
||||
),
|
||||
vol.Required("relaxation_and_target_periods"): section(
|
||||
vol.Schema(relaxation_fields),
|
||||
{"collapsed": True},
|
||||
),
|
||||
}
|
||||
|
|
|
|||
|
|
@ -218,6 +218,7 @@ class TibberPricesDataUpdateCoordinator(DataUpdateCoordinator[dict[str, Any]]):
|
|||
self._period_calculator = TibberPricesPeriodCalculator(
|
||||
config_entry=config_entry,
|
||||
log_prefix=self._log_prefix,
|
||||
get_config_override_fn=self.get_config_override,
|
||||
)
|
||||
self._data_transformer = TibberPricesDataTransformer(
|
||||
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._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
|
||||
self._listener_manager.schedule_quarter_hour_refresh(self._handle_quarter_hour_refresh)
|
||||
self._listener_manager.schedule_minute_refresh(self._handle_minute_refresh)
|
||||
|
|
@ -281,6 +287,114 @@ class TibberPricesDataUpdateCoordinator(DataUpdateCoordinator[dict[str, Any]]):
|
|||
else:
|
||||
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
|
||||
def async_add_time_sensitive_listener(self, update_callback: TimeServiceCallback) -> CALLBACK_TYPE:
|
||||
"""
|
||||
|
|
|
|||
|
|
@ -13,6 +13,8 @@ from typing import TYPE_CHECKING, Any
|
|||
from custom_components.tibber_prices import const as _const
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from collections.abc import Callable
|
||||
|
||||
from custom_components.tibber_prices.coordinator.time_service import TibberPricesTimeService
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
|
||||
|
|
@ -32,6 +34,7 @@ class TibberPricesPeriodCalculator:
|
|||
self,
|
||||
config_entry: ConfigEntry,
|
||||
log_prefix: str,
|
||||
get_config_override_fn: Callable[[str, str], Any | None] | None = None,
|
||||
) -> None:
|
||||
"""Initialize the period calculator."""
|
||||
self.config_entry = config_entry
|
||||
|
|
@ -39,11 +42,40 @@ class TibberPricesPeriodCalculator:
|
|||
self.time: TibberPricesTimeService # Set by coordinator before first use
|
||||
self._config_cache: dict[str, dict[str, Any]] | None = None
|
||||
self._config_cache_valid = False
|
||||
self._get_config_override = get_config_override_fn
|
||||
|
||||
# Period calculation cache
|
||||
self._cached_periods: dict[str, Any] | 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:
|
||||
"""Log with calculator-specific prefix."""
|
||||
prefixed_message = f"{self._log_prefix} {message}"
|
||||
|
|
@ -112,7 +144,7 @@ class TibberPricesPeriodCalculator:
|
|||
Get period calculation configuration from config options.
|
||||
|
||||
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"
|
||||
|
||||
|
|
@ -124,36 +156,44 @@ class TibberPricesPeriodCalculator:
|
|||
if self._config_cache is None:
|
||||
self._config_cache = {}
|
||||
|
||||
options = self.config_entry.options
|
||||
|
||||
# Get nested sections from options
|
||||
# Get config values, checking overrides first
|
||||
# CRITICAL: Best/Peak price settings are stored in nested sections:
|
||||
# - period_settings: min_period_length, max_level, gap_count
|
||||
# - flexibility_settings: flex, min_distance_from_avg
|
||||
# These settings are ONLY in options (not in data), structured since initial config flow
|
||||
period_settings = options.get("period_settings", {})
|
||||
flexibility_settings = options.get("flexibility_settings", {})
|
||||
# Override entities can override any of these values at runtime
|
||||
|
||||
if reverse_sort:
|
||||
# Peak price configuration
|
||||
flex = flexibility_settings.get(_const.CONF_PEAK_PRICE_FLEX, _const.DEFAULT_PEAK_PRICE_FLEX)
|
||||
min_distance_from_avg = flexibility_settings.get(
|
||||
flex = self._get_option(
|
||||
_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,
|
||||
"flexibility_settings",
|
||||
_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,
|
||||
"period_settings",
|
||||
_const.DEFAULT_PEAK_PRICE_MIN_PERIOD_LENGTH,
|
||||
)
|
||||
else:
|
||||
# Best price configuration
|
||||
flex = flexibility_settings.get(_const.CONF_BEST_PRICE_FLEX, _const.DEFAULT_BEST_PRICE_FLEX)
|
||||
min_distance_from_avg = flexibility_settings.get(
|
||||
flex = self._get_option(
|
||||
_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,
|
||||
"flexibility_settings",
|
||||
_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,
|
||||
"period_settings",
|
||||
_const.DEFAULT_BEST_PRICE_MIN_PERIOD_LENGTH,
|
||||
)
|
||||
|
||||
|
|
@ -610,9 +650,10 @@ class TibberPricesPeriodCalculator:
|
|||
|
||||
# Get relaxation configuration for best price
|
||||
# CRITICAL: Relaxation settings are stored in nested section 'relaxation_and_target_periods'
|
||||
relaxation_and_target_periods = self.config_entry.options.get("relaxation_and_target_periods", {})
|
||||
enable_relaxation_best = relaxation_and_target_periods.get(
|
||||
# Override entities can override any of these values at runtime
|
||||
enable_relaxation_best = self._get_option(
|
||||
_const.CONF_ENABLE_MIN_PERIODS_BEST,
|
||||
"relaxation_and_target_periods",
|
||||
_const.DEFAULT_ENABLE_MIN_PERIODS_BEST,
|
||||
)
|
||||
|
||||
|
|
@ -623,12 +664,14 @@ class TibberPricesPeriodCalculator:
|
|||
show_best_price = bool(all_prices)
|
||||
else:
|
||||
show_best_price = self.should_show_periods(price_info, reverse_sort=False) if all_prices else False
|
||||
min_periods_best = relaxation_and_target_periods.get(
|
||||
min_periods_best = self._get_option(
|
||||
_const.CONF_MIN_PERIODS_BEST,
|
||||
"relaxation_and_target_periods",
|
||||
_const.DEFAULT_MIN_PERIODS_BEST,
|
||||
)
|
||||
relaxation_attempts_best = relaxation_and_target_periods.get(
|
||||
relaxation_attempts_best = self._get_option(
|
||||
_const.CONF_RELAXATION_ATTEMPTS_BEST,
|
||||
"relaxation_and_target_periods",
|
||||
_const.DEFAULT_RELAXATION_ATTEMPTS_BEST,
|
||||
)
|
||||
|
||||
|
|
@ -637,13 +680,14 @@ class TibberPricesPeriodCalculator:
|
|||
best_config = self.get_period_config(reverse_sort=False)
|
||||
# Get level filter configuration from period_settings section
|
||||
# CRITICAL: max_level and gap_count are stored in nested section 'period_settings'
|
||||
period_settings = self.config_entry.options.get("period_settings", {})
|
||||
max_level_best = period_settings.get(
|
||||
max_level_best = self._get_option(
|
||||
_const.CONF_BEST_PRICE_MAX_LEVEL,
|
||||
"period_settings",
|
||||
_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,
|
||||
"period_settings",
|
||||
_const.DEFAULT_BEST_PRICE_MAX_LEVEL_GAP_COUNT,
|
||||
)
|
||||
best_period_config = TibberPricesPeriodConfig(
|
||||
|
|
@ -687,8 +731,10 @@ class TibberPricesPeriodCalculator:
|
|||
|
||||
# Get relaxation configuration for peak price
|
||||
# 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,
|
||||
"relaxation_and_target_periods",
|
||||
_const.DEFAULT_ENABLE_MIN_PERIODS_PEAK,
|
||||
)
|
||||
|
||||
|
|
@ -699,12 +745,14 @@ class TibberPricesPeriodCalculator:
|
|||
show_peak_price = bool(all_prices)
|
||||
else:
|
||||
show_peak_price = self.should_show_periods(price_info, reverse_sort=True) if all_prices else False
|
||||
min_periods_peak = relaxation_and_target_periods.get(
|
||||
min_periods_peak = self._get_option(
|
||||
_const.CONF_MIN_PERIODS_PEAK,
|
||||
"relaxation_and_target_periods",
|
||||
_const.DEFAULT_MIN_PERIODS_PEAK,
|
||||
)
|
||||
relaxation_attempts_peak = relaxation_and_target_periods.get(
|
||||
relaxation_attempts_peak = self._get_option(
|
||||
_const.CONF_RELAXATION_ATTEMPTS_PEAK,
|
||||
"relaxation_and_target_periods",
|
||||
_const.DEFAULT_RELAXATION_ATTEMPTS_PEAK,
|
||||
)
|
||||
|
||||
|
|
@ -713,12 +761,14 @@ class TibberPricesPeriodCalculator:
|
|||
peak_config = self.get_period_config(reverse_sort=True)
|
||||
# Get level filter configuration from period_settings section
|
||||
# CRITICAL: min_level and gap_count are stored in nested section 'period_settings'
|
||||
min_level_peak = period_settings.get(
|
||||
min_level_peak = self._get_option(
|
||||
_const.CONF_PEAK_PRICE_MIN_LEVEL,
|
||||
"period_settings",
|
||||
_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,
|
||||
"period_settings",
|
||||
_const.DEFAULT_PEAK_PRICE_MAX_LEVEL_GAP_COUNT,
|
||||
)
|
||||
peak_period_config = TibberPricesPeriodConfig(
|
||||
|
|
|
|||
|
|
@ -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."
|
||||
}
|
||||
},
|
||||
"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": {
|
||||
"APARTMENT": "Wohnung",
|
||||
"ROWHOUSE": "Reihenhaus",
|
||||
|
|
|
|||
|
|
@ -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."
|
||||
}
|
||||
},
|
||||
"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": {
|
||||
"APARTMENT": "Apartment",
|
||||
"ROWHOUSE": "Rowhouse",
|
||||
|
|
|
|||
|
|
@ -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."
|
||||
}
|
||||
},
|
||||
"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": {
|
||||
"APARTMENT": "Leilighet",
|
||||
"ROWHOUSE": "Rekkehus",
|
||||
|
|
|
|||
|
|
@ -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."
|
||||
}
|
||||
},
|
||||
"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": {
|
||||
"APARTMENT": "Appartement",
|
||||
"ROWHOUSE": "Rijhuis",
|
||||
|
|
|
|||
|
|
@ -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."
|
||||
}
|
||||
},
|
||||
"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": {
|
||||
"APARTMENT": "Lägenhet",
|
||||
"ROWHOUSE": "Radhus",
|
||||
|
|
|
|||
39
custom_components/tibber_prices/number/__init__.py
Normal file
39
custom_components/tibber_prices/number/__init__.py
Normal 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
|
||||
)
|
||||
242
custom_components/tibber_prices/number/core.py
Normal file
242
custom_components/tibber_prices/number/core.py
Normal 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())
|
||||
250
custom_components/tibber_prices/number/definitions.py
Normal file
250
custom_components/tibber_prices/number/definitions.py
Normal 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
|
||||
38
custom_components/tibber_prices/switch/__init__.py
Normal file
38
custom_components/tibber_prices/switch/__init__.py
Normal 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
|
||||
)
|
||||
245
custom_components/tibber_prices/switch/core.py
Normal file
245
custom_components/tibber_prices/switch/core.py
Normal 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())
|
||||
84
custom_components/tibber_prices/switch/definitions.py
Normal file
84
custom_components/tibber_prices/switch/definitions.py
Normal 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
|
||||
|
|
@ -77,7 +77,25 @@
|
|||
}
|
||||
},
|
||||
"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": {
|
||||
"home": {
|
||||
|
|
@ -189,7 +207,7 @@
|
|||
},
|
||||
"best_price": {
|
||||
"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": {
|
||||
"period_settings": {
|
||||
"name": "Zeitraumdauer & Preisniveaus",
|
||||
|
|
@ -236,7 +254,7 @@
|
|||
},
|
||||
"peak_price": {
|
||||
"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": {
|
||||
"period_settings": {
|
||||
"name": "Zeitraum-Einstellungen",
|
||||
|
|
@ -886,6 +904,52 @@
|
|||
"realtime_consumption_enabled": {
|
||||
"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": {
|
||||
|
|
|
|||
|
|
@ -77,7 +77,25 @@
|
|||
}
|
||||
},
|
||||
"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": {
|
||||
"home": {
|
||||
|
|
@ -200,7 +218,7 @@
|
|||
},
|
||||
"best_price": {
|
||||
"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": {
|
||||
"period_settings": {
|
||||
"name": "Period Duration & Levels",
|
||||
|
|
@ -247,7 +265,7 @@
|
|||
},
|
||||
"peak_price": {
|
||||
"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": {
|
||||
"period_settings": {
|
||||
"name": "Period Settings",
|
||||
|
|
@ -886,6 +904,52 @@
|
|||
"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": {
|
||||
|
|
|
|||
|
|
@ -77,7 +77,25 @@
|
|||
}
|
||||
},
|
||||
"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": {
|
||||
"home": {
|
||||
|
|
@ -189,7 +207,7 @@
|
|||
},
|
||||
"best_price": {
|
||||
"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": {
|
||||
"period_settings": {
|
||||
"name": "Periodeinnstillinger",
|
||||
|
|
@ -236,7 +254,7 @@
|
|||
},
|
||||
"peak_price": {
|
||||
"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": {
|
||||
"period_settings": {
|
||||
"name": "Periodeinnstillinger",
|
||||
|
|
@ -886,6 +904,52 @@
|
|||
"realtime_consumption_enabled": {
|
||||
"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": {
|
||||
|
|
|
|||
|
|
@ -77,7 +77,25 @@
|
|||
}
|
||||
},
|
||||
"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": {
|
||||
"home": {
|
||||
|
|
@ -189,7 +207,7 @@
|
|||
},
|
||||
"best_price": {
|
||||
"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": {
|
||||
"period_settings": {
|
||||
"name": "Periode Duur & Niveaus",
|
||||
|
|
@ -236,7 +254,7 @@
|
|||
},
|
||||
"peak_price": {
|
||||
"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": {
|
||||
"period_settings": {
|
||||
"name": "Periode Instellingen",
|
||||
|
|
@ -886,6 +904,52 @@
|
|||
"realtime_consumption_enabled": {
|
||||
"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": {
|
||||
|
|
|
|||
|
|
@ -77,7 +77,25 @@
|
|||
}
|
||||
},
|
||||
"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": {
|
||||
"home": {
|
||||
|
|
@ -189,7 +207,7 @@
|
|||
},
|
||||
"best_price": {
|
||||
"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": {
|
||||
"period_settings": {
|
||||
"name": "Periodlängd & Nivåer",
|
||||
|
|
@ -236,7 +254,7 @@
|
|||
},
|
||||
"peak_price": {
|
||||
"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": {
|
||||
"period_settings": {
|
||||
"name": "Periodinställningar",
|
||||
|
|
@ -886,6 +904,52 @@
|
|||
"realtime_consumption_enabled": {
|
||||
"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": {
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
||||
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
|
||||
|
|
|
|||
Loading…
Reference in a new issue