mirror of
https://github.com/jpawlowski/hass.tibber_prices.git
synced 2026-03-29 21:03:40 +00:00
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.
999 lines
34 KiB
Python
999 lines
34 KiB
Python
"""Schema definitions for tibber_prices config flow."""
|
|
|
|
from __future__ import annotations
|
|
|
|
from typing import TYPE_CHECKING, Any
|
|
|
|
if TYPE_CHECKING:
|
|
from collections.abc import Mapping
|
|
|
|
import voluptuous as vol
|
|
|
|
from custom_components.tibber_prices.const import (
|
|
BEST_PRICE_MAX_LEVEL_OPTIONS,
|
|
CONF_AVERAGE_SENSOR_DISPLAY,
|
|
CONF_BEST_PRICE_FLEX,
|
|
CONF_BEST_PRICE_MAX_LEVEL,
|
|
CONF_BEST_PRICE_MAX_LEVEL_GAP_COUNT,
|
|
CONF_BEST_PRICE_MIN_DISTANCE_FROM_AVG,
|
|
CONF_BEST_PRICE_MIN_PERIOD_LENGTH,
|
|
CONF_CURRENCY_DISPLAY_MODE,
|
|
CONF_ENABLE_MIN_PERIODS_BEST,
|
|
CONF_ENABLE_MIN_PERIODS_PEAK,
|
|
CONF_EXTENDED_DESCRIPTIONS,
|
|
CONF_MIN_PERIODS_BEST,
|
|
CONF_MIN_PERIODS_PEAK,
|
|
CONF_PEAK_PRICE_FLEX,
|
|
CONF_PEAK_PRICE_MAX_LEVEL_GAP_COUNT,
|
|
CONF_PEAK_PRICE_MIN_DISTANCE_FROM_AVG,
|
|
CONF_PEAK_PRICE_MIN_LEVEL,
|
|
CONF_PEAK_PRICE_MIN_PERIOD_LENGTH,
|
|
CONF_PRICE_LEVEL_GAP_TOLERANCE,
|
|
CONF_PRICE_RATING_GAP_TOLERANCE,
|
|
CONF_PRICE_RATING_HYSTERESIS,
|
|
CONF_PRICE_RATING_THRESHOLD_HIGH,
|
|
CONF_PRICE_RATING_THRESHOLD_LOW,
|
|
CONF_PRICE_TREND_THRESHOLD_FALLING,
|
|
CONF_PRICE_TREND_THRESHOLD_RISING,
|
|
CONF_PRICE_TREND_THRESHOLD_STRONGLY_FALLING,
|
|
CONF_PRICE_TREND_THRESHOLD_STRONGLY_RISING,
|
|
CONF_RELAXATION_ATTEMPTS_BEST,
|
|
CONF_RELAXATION_ATTEMPTS_PEAK,
|
|
CONF_VIRTUAL_TIME_OFFSET_DAYS,
|
|
CONF_VIRTUAL_TIME_OFFSET_HOURS,
|
|
CONF_VIRTUAL_TIME_OFFSET_MINUTES,
|
|
CONF_VOLATILITY_THRESHOLD_HIGH,
|
|
CONF_VOLATILITY_THRESHOLD_MODERATE,
|
|
CONF_VOLATILITY_THRESHOLD_VERY_HIGH,
|
|
DEFAULT_AVERAGE_SENSOR_DISPLAY,
|
|
DEFAULT_BEST_PRICE_FLEX,
|
|
DEFAULT_BEST_PRICE_MAX_LEVEL,
|
|
DEFAULT_BEST_PRICE_MAX_LEVEL_GAP_COUNT,
|
|
DEFAULT_BEST_PRICE_MIN_DISTANCE_FROM_AVG,
|
|
DEFAULT_BEST_PRICE_MIN_PERIOD_LENGTH,
|
|
DEFAULT_ENABLE_MIN_PERIODS_BEST,
|
|
DEFAULT_ENABLE_MIN_PERIODS_PEAK,
|
|
DEFAULT_EXTENDED_DESCRIPTIONS,
|
|
DEFAULT_MIN_PERIODS_BEST,
|
|
DEFAULT_MIN_PERIODS_PEAK,
|
|
DEFAULT_PEAK_PRICE_FLEX,
|
|
DEFAULT_PEAK_PRICE_MAX_LEVEL_GAP_COUNT,
|
|
DEFAULT_PEAK_PRICE_MIN_DISTANCE_FROM_AVG,
|
|
DEFAULT_PEAK_PRICE_MIN_LEVEL,
|
|
DEFAULT_PEAK_PRICE_MIN_PERIOD_LENGTH,
|
|
DEFAULT_PRICE_LEVEL_GAP_TOLERANCE,
|
|
DEFAULT_PRICE_RATING_GAP_TOLERANCE,
|
|
DEFAULT_PRICE_RATING_HYSTERESIS,
|
|
DEFAULT_PRICE_RATING_THRESHOLD_HIGH,
|
|
DEFAULT_PRICE_RATING_THRESHOLD_LOW,
|
|
DEFAULT_PRICE_TREND_THRESHOLD_FALLING,
|
|
DEFAULT_PRICE_TREND_THRESHOLD_RISING,
|
|
DEFAULT_PRICE_TREND_THRESHOLD_STRONGLY_FALLING,
|
|
DEFAULT_PRICE_TREND_THRESHOLD_STRONGLY_RISING,
|
|
DEFAULT_RELAXATION_ATTEMPTS_BEST,
|
|
DEFAULT_RELAXATION_ATTEMPTS_PEAK,
|
|
DEFAULT_VIRTUAL_TIME_OFFSET_DAYS,
|
|
DEFAULT_VIRTUAL_TIME_OFFSET_HOURS,
|
|
DEFAULT_VIRTUAL_TIME_OFFSET_MINUTES,
|
|
DEFAULT_VOLATILITY_THRESHOLD_HIGH,
|
|
DEFAULT_VOLATILITY_THRESHOLD_MODERATE,
|
|
DEFAULT_VOLATILITY_THRESHOLD_VERY_HIGH,
|
|
DISPLAY_MODE_BASE,
|
|
DISPLAY_MODE_SUBUNIT,
|
|
MAX_GAP_COUNT,
|
|
MAX_MIN_PERIOD_LENGTH,
|
|
MAX_MIN_PERIODS,
|
|
MAX_PRICE_LEVEL_GAP_TOLERANCE,
|
|
MAX_PRICE_RATING_GAP_TOLERANCE,
|
|
MAX_PRICE_RATING_HYSTERESIS,
|
|
MAX_PRICE_RATING_THRESHOLD_HIGH,
|
|
MAX_PRICE_RATING_THRESHOLD_LOW,
|
|
MAX_PRICE_TREND_FALLING,
|
|
MAX_PRICE_TREND_RISING,
|
|
MAX_PRICE_TREND_STRONGLY_FALLING,
|
|
MAX_PRICE_TREND_STRONGLY_RISING,
|
|
MAX_RELAXATION_ATTEMPTS,
|
|
MAX_VOLATILITY_THRESHOLD_HIGH,
|
|
MAX_VOLATILITY_THRESHOLD_MODERATE,
|
|
MAX_VOLATILITY_THRESHOLD_VERY_HIGH,
|
|
MIN_GAP_COUNT,
|
|
MIN_PERIOD_LENGTH,
|
|
MIN_PRICE_LEVEL_GAP_TOLERANCE,
|
|
MIN_PRICE_RATING_GAP_TOLERANCE,
|
|
MIN_PRICE_RATING_HYSTERESIS,
|
|
MIN_PRICE_RATING_THRESHOLD_HIGH,
|
|
MIN_PRICE_RATING_THRESHOLD_LOW,
|
|
MIN_PRICE_TREND_FALLING,
|
|
MIN_PRICE_TREND_RISING,
|
|
MIN_PRICE_TREND_STRONGLY_FALLING,
|
|
MIN_PRICE_TREND_STRONGLY_RISING,
|
|
MIN_RELAXATION_ATTEMPTS,
|
|
MIN_VOLATILITY_THRESHOLD_HIGH,
|
|
MIN_VOLATILITY_THRESHOLD_MODERATE,
|
|
MIN_VOLATILITY_THRESHOLD_VERY_HIGH,
|
|
PEAK_PRICE_MIN_LEVEL_OPTIONS,
|
|
get_default_currency_display,
|
|
)
|
|
from homeassistant.const import CONF_ACCESS_TOKEN
|
|
from homeassistant.data_entry_flow import section
|
|
from homeassistant.helpers import selector
|
|
from homeassistant.helpers.selector import (
|
|
BooleanSelector,
|
|
ConstantSelector,
|
|
ConstantSelectorConfig,
|
|
NumberSelector,
|
|
NumberSelectorConfig,
|
|
NumberSelectorMode,
|
|
SelectOptionDict,
|
|
SelectSelector,
|
|
SelectSelectorConfig,
|
|
SelectSelectorMode,
|
|
TextSelector,
|
|
TextSelectorConfig,
|
|
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)."""
|
|
return vol.Schema(
|
|
{
|
|
vol.Required(
|
|
CONF_ACCESS_TOKEN,
|
|
default=access_token if access_token is not None else vol.UNDEFINED,
|
|
): TextSelector(
|
|
TextSelectorConfig(
|
|
type=TextSelectorType.TEXT,
|
|
),
|
|
),
|
|
}
|
|
)
|
|
|
|
|
|
def get_reauth_confirm_schema() -> vol.Schema:
|
|
"""Return schema for reauth confirmation step."""
|
|
return vol.Schema(
|
|
{
|
|
vol.Required(CONF_ACCESS_TOKEN): TextSelector(
|
|
TextSelectorConfig(type=TextSelectorType.TEXT),
|
|
),
|
|
}
|
|
)
|
|
|
|
|
|
def get_select_home_schema(home_options: list[SelectOptionDict]) -> vol.Schema:
|
|
"""Return schema for home selection step."""
|
|
return vol.Schema(
|
|
{
|
|
vol.Required("home_id"): SelectSelector(
|
|
SelectSelectorConfig(
|
|
options=home_options,
|
|
mode=SelectSelectorMode.DROPDOWN,
|
|
)
|
|
)
|
|
}
|
|
)
|
|
|
|
|
|
def get_subentry_init_schema(
|
|
*,
|
|
extended_descriptions: bool = DEFAULT_EXTENDED_DESCRIPTIONS,
|
|
offset_days: int = DEFAULT_VIRTUAL_TIME_OFFSET_DAYS,
|
|
offset_hours: int = DEFAULT_VIRTUAL_TIME_OFFSET_HOURS,
|
|
offset_minutes: int = DEFAULT_VIRTUAL_TIME_OFFSET_MINUTES,
|
|
) -> vol.Schema:
|
|
"""Return schema for subentry init step (includes time-travel settings)."""
|
|
return vol.Schema(
|
|
{
|
|
vol.Optional(
|
|
CONF_EXTENDED_DESCRIPTIONS,
|
|
default=extended_descriptions,
|
|
): BooleanSelector(),
|
|
vol.Optional(
|
|
CONF_VIRTUAL_TIME_OFFSET_DAYS,
|
|
default=offset_days,
|
|
): NumberSelector(
|
|
NumberSelectorConfig(
|
|
mode=NumberSelectorMode.BOX,
|
|
min=-365, # Max 1 year back
|
|
max=0, # Only past days allowed
|
|
step=1,
|
|
)
|
|
),
|
|
vol.Optional(
|
|
CONF_VIRTUAL_TIME_OFFSET_HOURS,
|
|
default=offset_hours,
|
|
): NumberSelector(
|
|
NumberSelectorConfig(
|
|
mode=NumberSelectorMode.BOX,
|
|
min=-23,
|
|
max=23,
|
|
step=1,
|
|
)
|
|
),
|
|
vol.Optional(
|
|
CONF_VIRTUAL_TIME_OFFSET_MINUTES,
|
|
default=offset_minutes,
|
|
): NumberSelector(
|
|
NumberSelectorConfig(
|
|
mode=NumberSelectorMode.BOX,
|
|
min=-59,
|
|
max=59,
|
|
step=1,
|
|
)
|
|
),
|
|
}
|
|
)
|
|
|
|
|
|
def get_options_init_schema(options: Mapping[str, Any]) -> vol.Schema:
|
|
"""Return schema for options init step (general settings)."""
|
|
return vol.Schema(
|
|
{
|
|
vol.Optional(
|
|
CONF_EXTENDED_DESCRIPTIONS,
|
|
default=options.get(CONF_EXTENDED_DESCRIPTIONS, DEFAULT_EXTENDED_DESCRIPTIONS),
|
|
): BooleanSelector(),
|
|
vol.Optional(
|
|
CONF_AVERAGE_SENSOR_DISPLAY,
|
|
default=str(
|
|
options.get(
|
|
CONF_AVERAGE_SENSOR_DISPLAY,
|
|
DEFAULT_AVERAGE_SENSOR_DISPLAY,
|
|
)
|
|
),
|
|
): SelectSelector(
|
|
SelectSelectorConfig(
|
|
options=["median", "mean"],
|
|
mode=SelectSelectorMode.DROPDOWN,
|
|
translation_key="average_sensor_display",
|
|
),
|
|
),
|
|
}
|
|
)
|
|
|
|
|
|
def get_display_settings_schema(options: Mapping[str, Any], currency_code: str | None) -> vol.Schema:
|
|
"""Return schema for display settings configuration."""
|
|
default_display_mode = get_default_currency_display(currency_code)
|
|
|
|
return vol.Schema(
|
|
{
|
|
vol.Optional(
|
|
CONF_CURRENCY_DISPLAY_MODE,
|
|
default=str(
|
|
options.get(
|
|
CONF_CURRENCY_DISPLAY_MODE,
|
|
default_display_mode,
|
|
)
|
|
),
|
|
): SelectSelector(
|
|
SelectSelectorConfig(
|
|
options=[DISPLAY_MODE_BASE, DISPLAY_MODE_SUBUNIT],
|
|
mode=SelectSelectorMode.DROPDOWN,
|
|
translation_key="currency_display_mode",
|
|
),
|
|
),
|
|
}
|
|
)
|
|
|
|
|
|
def get_price_rating_schema(options: Mapping[str, Any]) -> vol.Schema:
|
|
"""Return schema for price rating configuration (thresholds and stabilization)."""
|
|
return vol.Schema(
|
|
{
|
|
vol.Optional(
|
|
CONF_PRICE_RATING_THRESHOLD_LOW,
|
|
default=int(
|
|
options.get(
|
|
CONF_PRICE_RATING_THRESHOLD_LOW,
|
|
DEFAULT_PRICE_RATING_THRESHOLD_LOW,
|
|
)
|
|
),
|
|
): NumberSelector(
|
|
NumberSelectorConfig(
|
|
min=MIN_PRICE_RATING_THRESHOLD_LOW,
|
|
max=MAX_PRICE_RATING_THRESHOLD_LOW,
|
|
unit_of_measurement="%",
|
|
step=1,
|
|
mode=NumberSelectorMode.SLIDER,
|
|
),
|
|
),
|
|
vol.Optional(
|
|
CONF_PRICE_RATING_THRESHOLD_HIGH,
|
|
default=int(
|
|
options.get(
|
|
CONF_PRICE_RATING_THRESHOLD_HIGH,
|
|
DEFAULT_PRICE_RATING_THRESHOLD_HIGH,
|
|
)
|
|
),
|
|
): NumberSelector(
|
|
NumberSelectorConfig(
|
|
min=MIN_PRICE_RATING_THRESHOLD_HIGH,
|
|
max=MAX_PRICE_RATING_THRESHOLD_HIGH,
|
|
unit_of_measurement="%",
|
|
step=1,
|
|
mode=NumberSelectorMode.SLIDER,
|
|
),
|
|
),
|
|
vol.Optional(
|
|
CONF_PRICE_RATING_HYSTERESIS,
|
|
default=float(
|
|
options.get(
|
|
CONF_PRICE_RATING_HYSTERESIS,
|
|
DEFAULT_PRICE_RATING_HYSTERESIS,
|
|
)
|
|
),
|
|
): NumberSelector(
|
|
NumberSelectorConfig(
|
|
min=MIN_PRICE_RATING_HYSTERESIS,
|
|
max=MAX_PRICE_RATING_HYSTERESIS,
|
|
unit_of_measurement="%",
|
|
step=0.5,
|
|
mode=NumberSelectorMode.SLIDER,
|
|
),
|
|
),
|
|
vol.Optional(
|
|
CONF_PRICE_RATING_GAP_TOLERANCE,
|
|
default=int(
|
|
options.get(
|
|
CONF_PRICE_RATING_GAP_TOLERANCE,
|
|
DEFAULT_PRICE_RATING_GAP_TOLERANCE,
|
|
)
|
|
),
|
|
): NumberSelector(
|
|
NumberSelectorConfig(
|
|
min=MIN_PRICE_RATING_GAP_TOLERANCE,
|
|
max=MAX_PRICE_RATING_GAP_TOLERANCE,
|
|
step=1,
|
|
mode=NumberSelectorMode.SLIDER,
|
|
),
|
|
),
|
|
}
|
|
)
|
|
|
|
|
|
def get_price_level_schema(options: Mapping[str, Any]) -> vol.Schema:
|
|
"""Return schema for Tibber price level stabilization (gap tolerance for API level field)."""
|
|
return vol.Schema(
|
|
{
|
|
vol.Optional(
|
|
CONF_PRICE_LEVEL_GAP_TOLERANCE,
|
|
default=int(
|
|
options.get(
|
|
CONF_PRICE_LEVEL_GAP_TOLERANCE,
|
|
DEFAULT_PRICE_LEVEL_GAP_TOLERANCE,
|
|
)
|
|
),
|
|
): NumberSelector(
|
|
NumberSelectorConfig(
|
|
min=MIN_PRICE_LEVEL_GAP_TOLERANCE,
|
|
max=MAX_PRICE_LEVEL_GAP_TOLERANCE,
|
|
step=1,
|
|
mode=NumberSelectorMode.SLIDER,
|
|
),
|
|
),
|
|
}
|
|
)
|
|
|
|
|
|
def get_volatility_schema(options: Mapping[str, Any]) -> vol.Schema:
|
|
"""Return schema for volatility thresholds configuration."""
|
|
return vol.Schema(
|
|
{
|
|
vol.Optional(
|
|
CONF_VOLATILITY_THRESHOLD_MODERATE,
|
|
default=float(
|
|
options.get(
|
|
CONF_VOLATILITY_THRESHOLD_MODERATE,
|
|
DEFAULT_VOLATILITY_THRESHOLD_MODERATE,
|
|
)
|
|
),
|
|
): NumberSelector(
|
|
NumberSelectorConfig(
|
|
min=MIN_VOLATILITY_THRESHOLD_MODERATE,
|
|
max=MAX_VOLATILITY_THRESHOLD_MODERATE,
|
|
step=1.0,
|
|
unit_of_measurement="%",
|
|
mode=NumberSelectorMode.SLIDER,
|
|
),
|
|
),
|
|
vol.Optional(
|
|
CONF_VOLATILITY_THRESHOLD_HIGH,
|
|
default=float(
|
|
options.get(
|
|
CONF_VOLATILITY_THRESHOLD_HIGH,
|
|
DEFAULT_VOLATILITY_THRESHOLD_HIGH,
|
|
)
|
|
),
|
|
): NumberSelector(
|
|
NumberSelectorConfig(
|
|
min=MIN_VOLATILITY_THRESHOLD_HIGH,
|
|
max=MAX_VOLATILITY_THRESHOLD_HIGH,
|
|
step=1.0,
|
|
unit_of_measurement="%",
|
|
mode=NumberSelectorMode.SLIDER,
|
|
),
|
|
),
|
|
vol.Optional(
|
|
CONF_VOLATILITY_THRESHOLD_VERY_HIGH,
|
|
default=float(
|
|
options.get(
|
|
CONF_VOLATILITY_THRESHOLD_VERY_HIGH,
|
|
DEFAULT_VOLATILITY_THRESHOLD_VERY_HIGH,
|
|
)
|
|
),
|
|
): NumberSelector(
|
|
NumberSelectorConfig(
|
|
min=MIN_VOLATILITY_THRESHOLD_VERY_HIGH,
|
|
max=MAX_VOLATILITY_THRESHOLD_VERY_HIGH,
|
|
step=1.0,
|
|
unit_of_measurement="%",
|
|
mode=NumberSelectorMode.SLIDER,
|
|
),
|
|
),
|
|
}
|
|
)
|
|
|
|
|
|
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", {})
|
|
flexibility_settings = options.get("flexibility_settings", {})
|
|
relaxation_settings = options.get("relaxation_and_target_periods", {})
|
|
|
|
# Get current values for override display
|
|
min_period_length = int(
|
|
period_settings.get(CONF_BEST_PRICE_MIN_PERIOD_LENGTH, DEFAULT_BEST_PRICE_MIN_PERIOD_LENGTH)
|
|
)
|
|
max_level_gap_count = int(
|
|
period_settings.get(CONF_BEST_PRICE_MAX_LEVEL_GAP_COUNT, DEFAULT_BEST_PRICE_MAX_LEVEL_GAP_COUNT)
|
|
)
|
|
best_price_flex = int(flexibility_settings.get(CONF_BEST_PRICE_FLEX, DEFAULT_BEST_PRICE_FLEX))
|
|
min_distance = int(
|
|
flexibility_settings.get(CONF_BEST_PRICE_MIN_DISTANCE_FROM_AVG, DEFAULT_BEST_PRICE_MIN_DISTANCE_FROM_AVG)
|
|
)
|
|
enable_min_periods = relaxation_settings.get(CONF_ENABLE_MIN_PERIODS_BEST, DEFAULT_ENABLE_MIN_PERIODS_BEST)
|
|
min_periods = int(relaxation_settings.get(CONF_MIN_PERIODS_BEST, DEFAULT_MIN_PERIODS_BEST))
|
|
relaxation_attempts = int(relaxation_settings.get(CONF_RELAXATION_ATTEMPTS_BEST, DEFAULT_RELAXATION_ATTEMPTS_BEST))
|
|
|
|
# Build section schemas with optional override warnings
|
|
period_warning = get_section_override_warning("best_price", "period_settings", overrides, translations) or {}
|
|
period_fields: dict[vol.Optional | vol.Required, Any] = {
|
|
**period_warning, # type: ignore[misc]
|
|
vol.Optional(
|
|
CONF_BEST_PRICE_MIN_PERIOD_LENGTH,
|
|
default=min_period_length,
|
|
): NumberSelector(
|
|
NumberSelectorConfig(
|
|
min=MIN_PERIOD_LENGTH,
|
|
max=MAX_MIN_PERIOD_LENGTH,
|
|
step=15,
|
|
unit_of_measurement="min",
|
|
mode=NumberSelectorMode.SLIDER,
|
|
)
|
|
),
|
|
vol.Optional(
|
|
CONF_BEST_PRICE_MAX_LEVEL,
|
|
default=period_settings.get(
|
|
CONF_BEST_PRICE_MAX_LEVEL,
|
|
DEFAULT_BEST_PRICE_MAX_LEVEL,
|
|
),
|
|
): SelectSelector(
|
|
SelectSelectorConfig(
|
|
options=BEST_PRICE_MAX_LEVEL_OPTIONS,
|
|
mode=SelectSelectorMode.DROPDOWN,
|
|
translation_key="current_interval_price_level",
|
|
),
|
|
),
|
|
vol.Optional(
|
|
CONF_BEST_PRICE_MAX_LEVEL_GAP_COUNT,
|
|
default=max_level_gap_count,
|
|
): NumberSelector(
|
|
NumberSelectorConfig(
|
|
min=MIN_GAP_COUNT,
|
|
max=MAX_GAP_COUNT,
|
|
step=1,
|
|
mode=NumberSelectorMode.SLIDER,
|
|
)
|
|
),
|
|
}
|
|
|
|
flexibility_warning = (
|
|
get_section_override_warning("best_price", "flexibility_settings", overrides, translations) or {}
|
|
)
|
|
flexibility_fields: dict[vol.Optional | vol.Required, Any] = {
|
|
**flexibility_warning, # type: ignore[misc]
|
|
vol.Optional(
|
|
CONF_BEST_PRICE_FLEX,
|
|
default=best_price_flex,
|
|
): NumberSelector(
|
|
NumberSelectorConfig(
|
|
min=0,
|
|
max=50,
|
|
step=1,
|
|
unit_of_measurement="%",
|
|
mode=NumberSelectorMode.SLIDER,
|
|
)
|
|
),
|
|
vol.Optional(
|
|
CONF_BEST_PRICE_MIN_DISTANCE_FROM_AVG,
|
|
default=min_distance,
|
|
): NumberSelector(
|
|
NumberSelectorConfig(
|
|
min=-50,
|
|
max=0,
|
|
step=1,
|
|
unit_of_measurement="%",
|
|
mode=NumberSelectorMode.SLIDER,
|
|
)
|
|
),
|
|
}
|
|
|
|
relaxation_warning = (
|
|
get_section_override_warning("best_price", "relaxation_and_target_periods", overrides, translations) or {}
|
|
)
|
|
relaxation_fields: dict[vol.Optional | vol.Required, Any] = {
|
|
**relaxation_warning, # type: ignore[misc]
|
|
vol.Optional(
|
|
CONF_ENABLE_MIN_PERIODS_BEST,
|
|
default=enable_min_periods,
|
|
): BooleanSelector(selector.BooleanSelectorConfig()),
|
|
vol.Optional(
|
|
CONF_MIN_PERIODS_BEST,
|
|
default=min_periods,
|
|
): NumberSelector(
|
|
NumberSelectorConfig(
|
|
min=1,
|
|
max=MAX_MIN_PERIODS,
|
|
step=1,
|
|
mode=NumberSelectorMode.SLIDER,
|
|
)
|
|
),
|
|
vol.Optional(
|
|
CONF_RELAXATION_ATTEMPTS_BEST,
|
|
default=relaxation_attempts,
|
|
): NumberSelector(
|
|
NumberSelectorConfig(
|
|
min=MIN_RELAXATION_ATTEMPTS,
|
|
max=MAX_RELAXATION_ATTEMPTS,
|
|
step=1,
|
|
mode=NumberSelectorMode.SLIDER,
|
|
)
|
|
),
|
|
}
|
|
|
|
return vol.Schema(
|
|
{
|
|
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],
|
|
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", {})
|
|
flexibility_settings = options.get("flexibility_settings", {})
|
|
relaxation_settings = options.get("relaxation_and_target_periods", {})
|
|
|
|
# Get current values for override display
|
|
min_period_length = int(
|
|
period_settings.get(CONF_PEAK_PRICE_MIN_PERIOD_LENGTH, DEFAULT_PEAK_PRICE_MIN_PERIOD_LENGTH)
|
|
)
|
|
max_level_gap_count = int(
|
|
period_settings.get(CONF_PEAK_PRICE_MAX_LEVEL_GAP_COUNT, DEFAULT_PEAK_PRICE_MAX_LEVEL_GAP_COUNT)
|
|
)
|
|
peak_price_flex = int(flexibility_settings.get(CONF_PEAK_PRICE_FLEX, DEFAULT_PEAK_PRICE_FLEX))
|
|
min_distance = int(
|
|
flexibility_settings.get(CONF_PEAK_PRICE_MIN_DISTANCE_FROM_AVG, DEFAULT_PEAK_PRICE_MIN_DISTANCE_FROM_AVG)
|
|
)
|
|
enable_min_periods = relaxation_settings.get(CONF_ENABLE_MIN_PERIODS_PEAK, DEFAULT_ENABLE_MIN_PERIODS_PEAK)
|
|
min_periods = int(relaxation_settings.get(CONF_MIN_PERIODS_PEAK, DEFAULT_MIN_PERIODS_PEAK))
|
|
relaxation_attempts = int(relaxation_settings.get(CONF_RELAXATION_ATTEMPTS_PEAK, DEFAULT_RELAXATION_ATTEMPTS_PEAK))
|
|
|
|
# Build section schemas with optional override warnings
|
|
period_warning = get_section_override_warning("peak_price", "period_settings", overrides, translations) or {}
|
|
period_fields: dict[vol.Optional | vol.Required, Any] = {
|
|
**period_warning, # type: ignore[misc]
|
|
vol.Optional(
|
|
CONF_PEAK_PRICE_MIN_PERIOD_LENGTH,
|
|
default=min_period_length,
|
|
): NumberSelector(
|
|
NumberSelectorConfig(
|
|
min=MIN_PERIOD_LENGTH,
|
|
max=MAX_MIN_PERIOD_LENGTH,
|
|
step=15,
|
|
unit_of_measurement="min",
|
|
mode=NumberSelectorMode.SLIDER,
|
|
)
|
|
),
|
|
vol.Optional(
|
|
CONF_PEAK_PRICE_MIN_LEVEL,
|
|
default=period_settings.get(
|
|
CONF_PEAK_PRICE_MIN_LEVEL,
|
|
DEFAULT_PEAK_PRICE_MIN_LEVEL,
|
|
),
|
|
): SelectSelector(
|
|
SelectSelectorConfig(
|
|
options=PEAK_PRICE_MIN_LEVEL_OPTIONS,
|
|
mode=SelectSelectorMode.DROPDOWN,
|
|
translation_key="current_interval_price_level",
|
|
),
|
|
),
|
|
vol.Optional(
|
|
CONF_PEAK_PRICE_MAX_LEVEL_GAP_COUNT,
|
|
default=max_level_gap_count,
|
|
): NumberSelector(
|
|
NumberSelectorConfig(
|
|
min=MIN_GAP_COUNT,
|
|
max=MAX_GAP_COUNT,
|
|
step=1,
|
|
mode=NumberSelectorMode.SLIDER,
|
|
)
|
|
),
|
|
}
|
|
|
|
flexibility_warning = (
|
|
get_section_override_warning("peak_price", "flexibility_settings", overrides, translations) or {}
|
|
)
|
|
flexibility_fields: dict[vol.Optional | vol.Required, Any] = {
|
|
**flexibility_warning, # type: ignore[misc]
|
|
vol.Optional(
|
|
CONF_PEAK_PRICE_FLEX,
|
|
default=peak_price_flex,
|
|
): NumberSelector(
|
|
NumberSelectorConfig(
|
|
min=-50,
|
|
max=0,
|
|
step=1,
|
|
unit_of_measurement="%",
|
|
mode=NumberSelectorMode.SLIDER,
|
|
)
|
|
),
|
|
vol.Optional(
|
|
CONF_PEAK_PRICE_MIN_DISTANCE_FROM_AVG,
|
|
default=min_distance,
|
|
): NumberSelector(
|
|
NumberSelectorConfig(
|
|
min=0,
|
|
max=50,
|
|
step=1,
|
|
unit_of_measurement="%",
|
|
mode=NumberSelectorMode.SLIDER,
|
|
)
|
|
),
|
|
}
|
|
|
|
relaxation_warning = (
|
|
get_section_override_warning("peak_price", "relaxation_and_target_periods", overrides, translations) or {}
|
|
)
|
|
relaxation_fields: dict[vol.Optional | vol.Required, Any] = {
|
|
**relaxation_warning, # type: ignore[misc]
|
|
vol.Optional(
|
|
CONF_ENABLE_MIN_PERIODS_PEAK,
|
|
default=enable_min_periods,
|
|
): BooleanSelector(selector.BooleanSelectorConfig()),
|
|
vol.Optional(
|
|
CONF_MIN_PERIODS_PEAK,
|
|
default=min_periods,
|
|
): NumberSelector(
|
|
NumberSelectorConfig(
|
|
min=1,
|
|
max=MAX_MIN_PERIODS,
|
|
step=1,
|
|
mode=NumberSelectorMode.SLIDER,
|
|
)
|
|
),
|
|
vol.Optional(
|
|
CONF_RELAXATION_ATTEMPTS_PEAK,
|
|
default=relaxation_attempts,
|
|
): NumberSelector(
|
|
NumberSelectorConfig(
|
|
min=MIN_RELAXATION_ATTEMPTS,
|
|
max=MAX_RELAXATION_ATTEMPTS,
|
|
step=1,
|
|
mode=NumberSelectorMode.SLIDER,
|
|
)
|
|
),
|
|
}
|
|
|
|
return vol.Schema(
|
|
{
|
|
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_price_trend_schema(options: Mapping[str, Any]) -> vol.Schema:
|
|
"""Return schema for price trend thresholds configuration."""
|
|
return vol.Schema(
|
|
{
|
|
vol.Optional(
|
|
CONF_PRICE_TREND_THRESHOLD_RISING,
|
|
default=int(
|
|
options.get(
|
|
CONF_PRICE_TREND_THRESHOLD_RISING,
|
|
DEFAULT_PRICE_TREND_THRESHOLD_RISING,
|
|
)
|
|
),
|
|
): NumberSelector(
|
|
NumberSelectorConfig(
|
|
min=MIN_PRICE_TREND_RISING,
|
|
max=MAX_PRICE_TREND_RISING,
|
|
step=1,
|
|
unit_of_measurement="%",
|
|
mode=NumberSelectorMode.SLIDER,
|
|
),
|
|
),
|
|
vol.Optional(
|
|
CONF_PRICE_TREND_THRESHOLD_STRONGLY_RISING,
|
|
default=int(
|
|
options.get(
|
|
CONF_PRICE_TREND_THRESHOLD_STRONGLY_RISING,
|
|
DEFAULT_PRICE_TREND_THRESHOLD_STRONGLY_RISING,
|
|
)
|
|
),
|
|
): NumberSelector(
|
|
NumberSelectorConfig(
|
|
min=MIN_PRICE_TREND_STRONGLY_RISING,
|
|
max=MAX_PRICE_TREND_STRONGLY_RISING,
|
|
step=1,
|
|
unit_of_measurement="%",
|
|
mode=NumberSelectorMode.SLIDER,
|
|
),
|
|
),
|
|
vol.Optional(
|
|
CONF_PRICE_TREND_THRESHOLD_FALLING,
|
|
default=int(
|
|
options.get(
|
|
CONF_PRICE_TREND_THRESHOLD_FALLING,
|
|
DEFAULT_PRICE_TREND_THRESHOLD_FALLING,
|
|
)
|
|
),
|
|
): NumberSelector(
|
|
NumberSelectorConfig(
|
|
min=MIN_PRICE_TREND_FALLING,
|
|
max=MAX_PRICE_TREND_FALLING,
|
|
step=1,
|
|
unit_of_measurement="%",
|
|
mode=NumberSelectorMode.SLIDER,
|
|
),
|
|
),
|
|
vol.Optional(
|
|
CONF_PRICE_TREND_THRESHOLD_STRONGLY_FALLING,
|
|
default=int(
|
|
options.get(
|
|
CONF_PRICE_TREND_THRESHOLD_STRONGLY_FALLING,
|
|
DEFAULT_PRICE_TREND_THRESHOLD_STRONGLY_FALLING,
|
|
)
|
|
),
|
|
): NumberSelector(
|
|
NumberSelectorConfig(
|
|
min=MIN_PRICE_TREND_STRONGLY_FALLING,
|
|
max=MAX_PRICE_TREND_STRONGLY_FALLING,
|
|
step=1,
|
|
unit_of_measurement="%",
|
|
mode=NumberSelectorMode.SLIDER,
|
|
),
|
|
),
|
|
}
|
|
)
|
|
|
|
|
|
def get_chart_data_export_schema(_options: Mapping[str, Any]) -> vol.Schema:
|
|
"""Return schema for chart data export info page (no input fields)."""
|
|
# Empty schema - this is just an info page now
|
|
return vol.Schema({})
|
|
|
|
|
|
def get_reset_to_defaults_schema() -> vol.Schema:
|
|
"""Return schema for reset to defaults confirmation step."""
|
|
return vol.Schema(
|
|
{
|
|
vol.Required("confirm_reset", default=False): selector.BooleanSelector(),
|
|
}
|
|
)
|