mirror of
https://github.com/jpawlowski/hass.tibber_prices.git
synced 2026-04-09 09:03:40 +00:00
feat(config_flow): add trend confirmation and noise floor settings
Added 3 new config fields to price trend options step: - Trend Change Confirmation (2-6 intervals slider) - Min Price Change for trend (display-unit-aware slider) - Min Price Change for strong trend (display-unit-aware slider) Price change sliders scale between base currency (EUR/NOK) storage and display unit (ct/øre) presentation using get_display_unit_factor(). Added migration in __init__.py to convert old display-unit values to base currency format. Impact: Users can tune trend sensitivity: higher confirmation = fewer false changes, higher min price change = no trends from tiny fluctuations.
This commit is contained in:
parent
91efeed90f
commit
798de5946d
3 changed files with 135 additions and 2 deletions
|
|
@ -21,11 +21,15 @@ from homeassistant.loader import async_get_loaded_integration
|
|||
from .api import TibberPricesApiClient
|
||||
from .const import (
|
||||
CONF_CURRENCY_DISPLAY_MODE,
|
||||
CONF_PRICE_TREND_MIN_PRICE_CHANGE,
|
||||
CONF_PRICE_TREND_MIN_PRICE_CHANGE_STRONGLY,
|
||||
DATA_CHART_CONFIG,
|
||||
DATA_CHART_METADATA_CONFIG,
|
||||
DISPLAY_MODE_SUBUNIT,
|
||||
DOMAIN,
|
||||
LOGGER,
|
||||
MAX_PRICE_TREND_MIN_PRICE_CHANGE,
|
||||
MAX_PRICE_TREND_MIN_PRICE_CHANGE_STRONGLY,
|
||||
async_load_standard_translations,
|
||||
async_load_translations,
|
||||
)
|
||||
|
|
@ -141,6 +145,29 @@ async def _migrate_config_options(hass: HomeAssistant, entry: ConfigEntry) -> No
|
|||
DISPLAY_MODE_SUBUNIT,
|
||||
)
|
||||
|
||||
# Migration: Convert min_price_change from display currency (ct/øre) to base currency (EUR/NOK)
|
||||
# Before this change, values were stored in display units. Now always stored in base currency.
|
||||
# Detection: If either value exceeds its new max, both are in old format and need conversion.
|
||||
# Old range: 0-5.0 ct / 0-10.0 ct, New range: 0-0.05 EUR / 0-0.10 EUR
|
||||
normal_val = migrated.get(CONF_PRICE_TREND_MIN_PRICE_CHANGE)
|
||||
strongly_val = migrated.get(CONF_PRICE_TREND_MIN_PRICE_CHANGE_STRONGLY)
|
||||
old_format_detected = (normal_val is not None and normal_val > MAX_PRICE_TREND_MIN_PRICE_CHANGE) or (
|
||||
strongly_val is not None and strongly_val > MAX_PRICE_TREND_MIN_PRICE_CHANGE_STRONGLY
|
||||
)
|
||||
if old_format_detected:
|
||||
for key in (CONF_PRICE_TREND_MIN_PRICE_CHANGE, CONF_PRICE_TREND_MIN_PRICE_CHANGE_STRONGLY):
|
||||
if key in migrated and migrated[key] > 0:
|
||||
old_val = migrated[key]
|
||||
migrated[key] = round(old_val / 100, 6)
|
||||
migration_performed = True
|
||||
LOGGER.info(
|
||||
"[%s] Migrated config: %s = %s -> %s (converted to base currency)",
|
||||
entry.title,
|
||||
key,
|
||||
old_val,
|
||||
migrated[key],
|
||||
)
|
||||
|
||||
# Save migrated options if any changes were made
|
||||
if migration_performed:
|
||||
hass.config_entries.async_update_entry(entry, options=migrated)
|
||||
|
|
|
|||
|
|
@ -60,6 +60,8 @@ from custom_components.tibber_prices.const import (
|
|||
CONF_PEAK_PRICE_MIN_PERIOD_LENGTH,
|
||||
CONF_PRICE_RATING_THRESHOLD_HIGH,
|
||||
CONF_PRICE_RATING_THRESHOLD_LOW,
|
||||
CONF_PRICE_TREND_MIN_PRICE_CHANGE,
|
||||
CONF_PRICE_TREND_MIN_PRICE_CHANGE_STRONGLY,
|
||||
CONF_PRICE_TREND_THRESHOLD_FALLING,
|
||||
CONF_PRICE_TREND_THRESHOLD_RISING,
|
||||
CONF_PRICE_TREND_THRESHOLD_STRONGLY_FALLING,
|
||||
|
|
@ -74,7 +76,10 @@ from custom_components.tibber_prices.const import (
|
|||
DEFAULT_VOLATILITY_THRESHOLD_VERY_HIGH,
|
||||
DOMAIN,
|
||||
async_get_translation,
|
||||
format_price_unit_base,
|
||||
format_price_unit_subunit,
|
||||
get_default_options,
|
||||
get_display_unit_factor,
|
||||
)
|
||||
from homeassistant.config_entries import ConfigFlowResult, OptionsFlow
|
||||
from homeassistant.helpers import entity_registry as er
|
||||
|
|
@ -730,6 +735,9 @@ class TibberPricesOptionsFlowHandler(OptionsFlow):
|
|||
"""Configure price trend thresholds."""
|
||||
errors: dict[str, str] = {}
|
||||
|
||||
# Get display factor for currency conversion
|
||||
display_factor = get_display_unit_factor(self.config_entry)
|
||||
|
||||
if user_input is not None:
|
||||
# Schema is now flattened - fields come directly in user_input
|
||||
# Store them flat in options (no nested structure)
|
||||
|
|
@ -775,6 +783,15 @@ class TibberPricesOptionsFlowHandler(OptionsFlow):
|
|||
)
|
||||
|
||||
if not errors:
|
||||
# Convert min_price_change values from display unit to base currency for storage
|
||||
# (dividing by 1 is a no-op for base currency mode)
|
||||
user_input[CONF_PRICE_TREND_MIN_PRICE_CHANGE] = round(
|
||||
user_input[CONF_PRICE_TREND_MIN_PRICE_CHANGE] / display_factor, 6
|
||||
)
|
||||
user_input[CONF_PRICE_TREND_MIN_PRICE_CHANGE_STRONGLY] = round(
|
||||
user_input[CONF_PRICE_TREND_MIN_PRICE_CHANGE_STRONGLY] / display_factor, 6
|
||||
)
|
||||
|
||||
# Store flat data directly in options (no section wrapping)
|
||||
self._options.update(user_input)
|
||||
# async_create_entry automatically handles change detection and listener triggering
|
||||
|
|
@ -782,9 +799,20 @@ class TibberPricesOptionsFlowHandler(OptionsFlow):
|
|||
# Return to menu for more changes
|
||||
return await self.async_step_init()
|
||||
|
||||
# Get currency code for unit label on sliders
|
||||
currency_code = self.config_entry.data.get("currency", None)
|
||||
if display_factor > 1:
|
||||
price_unit = format_price_unit_subunit(currency_code)
|
||||
else:
|
||||
price_unit = format_price_unit_base(currency_code)
|
||||
|
||||
return self.async_show_form(
|
||||
step_id="price_trend",
|
||||
data_schema=get_price_trend_schema(self.config_entry.options),
|
||||
data_schema=get_price_trend_schema(
|
||||
self.config_entry.options,
|
||||
display_factor=display_factor,
|
||||
price_unit=price_unit,
|
||||
),
|
||||
errors=errors,
|
||||
description_placeholders=self._get_entity_warning_placeholders("price_trend"),
|
||||
)
|
||||
|
|
|
|||
|
|
@ -33,6 +33,9 @@ from custom_components.tibber_prices.const import (
|
|||
CONF_PRICE_RATING_HYSTERESIS,
|
||||
CONF_PRICE_RATING_THRESHOLD_HIGH,
|
||||
CONF_PRICE_RATING_THRESHOLD_LOW,
|
||||
CONF_PRICE_TREND_CHANGE_CONFIRMATION,
|
||||
CONF_PRICE_TREND_MIN_PRICE_CHANGE,
|
||||
CONF_PRICE_TREND_MIN_PRICE_CHANGE_STRONGLY,
|
||||
CONF_PRICE_TREND_THRESHOLD_FALLING,
|
||||
CONF_PRICE_TREND_THRESHOLD_RISING,
|
||||
CONF_PRICE_TREND_THRESHOLD_STRONGLY_FALLING,
|
||||
|
|
@ -66,6 +69,9 @@ from custom_components.tibber_prices.const import (
|
|||
DEFAULT_PRICE_RATING_HYSTERESIS,
|
||||
DEFAULT_PRICE_RATING_THRESHOLD_HIGH,
|
||||
DEFAULT_PRICE_RATING_THRESHOLD_LOW,
|
||||
DEFAULT_PRICE_TREND_CHANGE_CONFIRMATION,
|
||||
DEFAULT_PRICE_TREND_MIN_PRICE_CHANGE,
|
||||
DEFAULT_PRICE_TREND_MIN_PRICE_CHANGE_STRONGLY,
|
||||
DEFAULT_PRICE_TREND_THRESHOLD_FALLING,
|
||||
DEFAULT_PRICE_TREND_THRESHOLD_RISING,
|
||||
DEFAULT_PRICE_TREND_THRESHOLD_STRONGLY_FALLING,
|
||||
|
|
@ -88,7 +94,10 @@ from custom_components.tibber_prices.const import (
|
|||
MAX_PRICE_RATING_HYSTERESIS,
|
||||
MAX_PRICE_RATING_THRESHOLD_HIGH,
|
||||
MAX_PRICE_RATING_THRESHOLD_LOW,
|
||||
MAX_PRICE_TREND_CHANGE_CONFIRMATION,
|
||||
MAX_PRICE_TREND_FALLING,
|
||||
MAX_PRICE_TREND_MIN_PRICE_CHANGE,
|
||||
MAX_PRICE_TREND_MIN_PRICE_CHANGE_STRONGLY,
|
||||
MAX_PRICE_TREND_RISING,
|
||||
MAX_PRICE_TREND_STRONGLY_FALLING,
|
||||
MAX_PRICE_TREND_STRONGLY_RISING,
|
||||
|
|
@ -103,7 +112,10 @@ from custom_components.tibber_prices.const import (
|
|||
MIN_PRICE_RATING_HYSTERESIS,
|
||||
MIN_PRICE_RATING_THRESHOLD_HIGH,
|
||||
MIN_PRICE_RATING_THRESHOLD_LOW,
|
||||
MIN_PRICE_TREND_CHANGE_CONFIRMATION,
|
||||
MIN_PRICE_TREND_FALLING,
|
||||
MIN_PRICE_TREND_MIN_PRICE_CHANGE,
|
||||
MIN_PRICE_TREND_MIN_PRICE_CHANGE_STRONGLY,
|
||||
MIN_PRICE_TREND_RISING,
|
||||
MIN_PRICE_TREND_STRONGLY_FALLING,
|
||||
MIN_PRICE_TREND_STRONGLY_RISING,
|
||||
|
|
@ -907,8 +919,16 @@ def get_peak_price_schema(
|
|||
)
|
||||
|
||||
|
||||
def get_price_trend_schema(options: Mapping[str, Any]) -> vol.Schema:
|
||||
def get_price_trend_schema(
|
||||
options: Mapping[str, Any],
|
||||
*,
|
||||
display_factor: int = 1,
|
||||
price_unit: str = "",
|
||||
) -> vol.Schema:
|
||||
"""Return schema for price trend thresholds configuration."""
|
||||
# Scale min_price_change values for display (stored in base currency, shown in display unit)
|
||||
step = 0.1 if display_factor > 1 else 0.001
|
||||
|
||||
return vol.Schema(
|
||||
{
|
||||
vol.Optional(
|
||||
|
|
@ -979,6 +999,64 @@ def get_price_trend_schema(options: Mapping[str, Any]) -> vol.Schema:
|
|||
mode=NumberSelectorMode.SLIDER,
|
||||
),
|
||||
),
|
||||
vol.Optional(
|
||||
CONF_PRICE_TREND_CHANGE_CONFIRMATION,
|
||||
default=int(
|
||||
options.get(
|
||||
CONF_PRICE_TREND_CHANGE_CONFIRMATION,
|
||||
DEFAULT_PRICE_TREND_CHANGE_CONFIRMATION,
|
||||
)
|
||||
),
|
||||
): NumberSelector(
|
||||
NumberSelectorConfig(
|
||||
min=MIN_PRICE_TREND_CHANGE_CONFIRMATION,
|
||||
max=MAX_PRICE_TREND_CHANGE_CONFIRMATION,
|
||||
step=1,
|
||||
mode=NumberSelectorMode.SLIDER,
|
||||
),
|
||||
),
|
||||
vol.Optional(
|
||||
CONF_PRICE_TREND_MIN_PRICE_CHANGE,
|
||||
default=round(
|
||||
float(
|
||||
options.get(
|
||||
CONF_PRICE_TREND_MIN_PRICE_CHANGE,
|
||||
DEFAULT_PRICE_TREND_MIN_PRICE_CHANGE,
|
||||
)
|
||||
)
|
||||
* display_factor,
|
||||
3,
|
||||
),
|
||||
): NumberSelector(
|
||||
NumberSelectorConfig(
|
||||
min=MIN_PRICE_TREND_MIN_PRICE_CHANGE * display_factor,
|
||||
max=MAX_PRICE_TREND_MIN_PRICE_CHANGE * display_factor,
|
||||
step=step,
|
||||
unit_of_measurement=price_unit,
|
||||
mode=NumberSelectorMode.SLIDER,
|
||||
),
|
||||
),
|
||||
vol.Optional(
|
||||
CONF_PRICE_TREND_MIN_PRICE_CHANGE_STRONGLY,
|
||||
default=round(
|
||||
float(
|
||||
options.get(
|
||||
CONF_PRICE_TREND_MIN_PRICE_CHANGE_STRONGLY,
|
||||
DEFAULT_PRICE_TREND_MIN_PRICE_CHANGE_STRONGLY,
|
||||
)
|
||||
)
|
||||
* display_factor,
|
||||
3,
|
||||
),
|
||||
): NumberSelector(
|
||||
NumberSelectorConfig(
|
||||
min=MIN_PRICE_TREND_MIN_PRICE_CHANGE_STRONGLY * display_factor,
|
||||
max=MAX_PRICE_TREND_MIN_PRICE_CHANGE_STRONGLY * display_factor,
|
||||
step=step,
|
||||
unit_of_measurement=price_unit,
|
||||
mode=NumberSelectorMode.SLIDER,
|
||||
),
|
||||
),
|
||||
}
|
||||
)
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue