From 798de5946de0c3704f954840b8a87ed7fd819586 Mon Sep 17 00:00:00 2001 From: Julian Pawlowski Date: Tue, 7 Apr 2026 13:44:47 +0000 Subject: [PATCH] feat(config_flow): add trend confirmation and noise floor settings MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- custom_components/tibber_prices/__init__.py | 27 +++++++ .../config_flow_handlers/options_flow.py | 30 ++++++- .../config_flow_handlers/schemas.py | 80 ++++++++++++++++++- 3 files changed, 135 insertions(+), 2 deletions(-) diff --git a/custom_components/tibber_prices/__init__.py b/custom_components/tibber_prices/__init__.py index fa045de..c55674d 100644 --- a/custom_components/tibber_prices/__init__.py +++ b/custom_components/tibber_prices/__init__.py @@ -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) diff --git a/custom_components/tibber_prices/config_flow_handlers/options_flow.py b/custom_components/tibber_prices/config_flow_handlers/options_flow.py index 9aeb38d..fb53fba 100644 --- a/custom_components/tibber_prices/config_flow_handlers/options_flow.py +++ b/custom_components/tibber_prices/config_flow_handlers/options_flow.py @@ -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"), ) diff --git a/custom_components/tibber_prices/config_flow_handlers/schemas.py b/custom_components/tibber_prices/config_flow_handlers/schemas.py index c3a03e7..0617022 100644 --- a/custom_components/tibber_prices/config_flow_handlers/schemas.py +++ b/custom_components/tibber_prices/config_flow_handlers/schemas.py @@ -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, + ), + ), } )