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 05e56af..0d2a240 100644 --- a/custom_components/tibber_prices/config_flow_handlers/options_flow.py +++ b/custom_components/tibber_prices/config_flow_handlers/options_flow.py @@ -29,7 +29,10 @@ from custom_components.tibber_prices.config_flow_handlers.validators import ( validate_price_trend_falling, validate_price_trend_rising, validate_relaxation_attempts, - validate_volatility_threshold, + validate_volatility_threshold_high, + validate_volatility_threshold_moderate, + validate_volatility_threshold_very_high, + validate_volatility_thresholds, ) from custom_components.tibber_prices.const import ( CONF_BEST_PRICE_FLEX, @@ -51,6 +54,9 @@ from custom_components.tibber_prices.const import ( CONF_VOLATILITY_THRESHOLD_HIGH, CONF_VOLATILITY_THRESHOLD_MODERATE, CONF_VOLATILITY_THRESHOLD_VERY_HIGH, + DEFAULT_VOLATILITY_THRESHOLD_HIGH, + DEFAULT_VOLATILITY_THRESHOLD_MODERATE, + DEFAULT_VOLATILITY_THRESHOLD_VERY_HIGH, DOMAIN, ) from homeassistant.config_entries import ConfigFlowResult, OptionsFlow @@ -359,22 +365,41 @@ class TibberPricesOptionsFlowHandler(OptionsFlow): if user_input is not None: # Validate moderate volatility threshold - if CONF_VOLATILITY_THRESHOLD_MODERATE in user_input and not validate_volatility_threshold( + if CONF_VOLATILITY_THRESHOLD_MODERATE in user_input and not validate_volatility_threshold_moderate( user_input[CONF_VOLATILITY_THRESHOLD_MODERATE] ): - errors[CONF_VOLATILITY_THRESHOLD_MODERATE] = "invalid_volatility_threshold" + errors[CONF_VOLATILITY_THRESHOLD_MODERATE] = "invalid_volatility_threshold_moderate" # Validate high volatility threshold - if CONF_VOLATILITY_THRESHOLD_HIGH in user_input and not validate_volatility_threshold( + if CONF_VOLATILITY_THRESHOLD_HIGH in user_input and not validate_volatility_threshold_high( user_input[CONF_VOLATILITY_THRESHOLD_HIGH] ): - errors[CONF_VOLATILITY_THRESHOLD_HIGH] = "invalid_volatility_threshold" + errors[CONF_VOLATILITY_THRESHOLD_HIGH] = "invalid_volatility_threshold_high" # Validate very high volatility threshold - if CONF_VOLATILITY_THRESHOLD_VERY_HIGH in user_input and not validate_volatility_threshold( + if CONF_VOLATILITY_THRESHOLD_VERY_HIGH in user_input and not validate_volatility_threshold_very_high( user_input[CONF_VOLATILITY_THRESHOLD_VERY_HIGH] ): - errors[CONF_VOLATILITY_THRESHOLD_VERY_HIGH] = "invalid_volatility_threshold" + errors[CONF_VOLATILITY_THRESHOLD_VERY_HIGH] = "invalid_volatility_threshold_very_high" + + # Cross-validation: Ensure MODERATE < HIGH < VERY_HIGH + if not errors: + existing_options = self.config_entry.options + moderate = user_input.get( + CONF_VOLATILITY_THRESHOLD_MODERATE, + existing_options.get(CONF_VOLATILITY_THRESHOLD_MODERATE, DEFAULT_VOLATILITY_THRESHOLD_MODERATE), + ) + high = user_input.get( + CONF_VOLATILITY_THRESHOLD_HIGH, + existing_options.get(CONF_VOLATILITY_THRESHOLD_HIGH, DEFAULT_VOLATILITY_THRESHOLD_HIGH), + ) + very_high = user_input.get( + CONF_VOLATILITY_THRESHOLD_VERY_HIGH, + existing_options.get(CONF_VOLATILITY_THRESHOLD_VERY_HIGH, DEFAULT_VOLATILITY_THRESHOLD_VERY_HIGH), + ) + + if not validate_volatility_thresholds(moderate, high, very_high): + errors["base"] = "invalid_volatility_thresholds" if not errors: self._options.update(user_input) diff --git a/custom_components/tibber_prices/config_flow_handlers/schemas.py b/custom_components/tibber_prices/config_flow_handlers/schemas.py index f7aca99..9dabcb5 100644 --- a/custom_components/tibber_prices/config_flow_handlers/schemas.py +++ b/custom_components/tibber_prices/config_flow_handlers/schemas.py @@ -67,7 +67,9 @@ from custom_components.tibber_prices.const import ( MAX_PRICE_TREND_FALLING, MAX_PRICE_TREND_RISING, MAX_RELAXATION_ATTEMPTS, - MAX_VOLATILITY_THRESHOLD, + MAX_VOLATILITY_THRESHOLD_HIGH, + MAX_VOLATILITY_THRESHOLD_MODERATE, + MAX_VOLATILITY_THRESHOLD_VERY_HIGH, MIN_GAP_COUNT, MIN_PERIOD_LENGTH, MIN_PRICE_RATING_THRESHOLD_HIGH, @@ -75,7 +77,9 @@ from custom_components.tibber_prices.const import ( MIN_PRICE_TREND_FALLING, MIN_PRICE_TREND_RISING, MIN_RELAXATION_ATTEMPTS, - MIN_VOLATILITY_THRESHOLD, + MIN_VOLATILITY_THRESHOLD_HIGH, + MIN_VOLATILITY_THRESHOLD_MODERATE, + MIN_VOLATILITY_THRESHOLD_VERY_HIGH, PEAK_PRICE_MIN_LEVEL_OPTIONS, ) from homeassistant.const import CONF_ACCESS_TOKEN @@ -215,11 +219,11 @@ def get_volatility_schema(options: Mapping[str, Any]) -> vol.Schema: ), ): NumberSelector( NumberSelectorConfig( - min=MIN_VOLATILITY_THRESHOLD, - max=MAX_VOLATILITY_THRESHOLD, - step=0.1, + min=MIN_VOLATILITY_THRESHOLD_MODERATE, + max=MAX_VOLATILITY_THRESHOLD_MODERATE, + step=1.0, unit_of_measurement="%", - mode=NumberSelectorMode.BOX, + mode=NumberSelectorMode.SLIDER, ), ), vol.Optional( @@ -232,11 +236,11 @@ def get_volatility_schema(options: Mapping[str, Any]) -> vol.Schema: ), ): NumberSelector( NumberSelectorConfig( - min=MIN_VOLATILITY_THRESHOLD, - max=MAX_VOLATILITY_THRESHOLD, - step=0.1, + min=MIN_VOLATILITY_THRESHOLD_HIGH, + max=MAX_VOLATILITY_THRESHOLD_HIGH, + step=1.0, unit_of_measurement="%", - mode=NumberSelectorMode.BOX, + mode=NumberSelectorMode.SLIDER, ), ), vol.Optional( @@ -249,11 +253,11 @@ def get_volatility_schema(options: Mapping[str, Any]) -> vol.Schema: ), ): NumberSelector( NumberSelectorConfig( - min=MIN_VOLATILITY_THRESHOLD, - max=MAX_VOLATILITY_THRESHOLD, - step=0.1, + min=MIN_VOLATILITY_THRESHOLD_VERY_HIGH, + max=MAX_VOLATILITY_THRESHOLD_VERY_HIGH, + step=1.0, unit_of_measurement="%", - mode=NumberSelectorMode.BOX, + mode=NumberSelectorMode.SLIDER, ), ), } diff --git a/custom_components/tibber_prices/config_flow_handlers/validators.py b/custom_components/tibber_prices/config_flow_handlers/validators.py index 7a7cf76..0c6ef84 100644 --- a/custom_components/tibber_prices/config_flow_handlers/validators.py +++ b/custom_components/tibber_prices/config_flow_handlers/validators.py @@ -21,7 +21,9 @@ from custom_components.tibber_prices.const import ( MAX_PRICE_TREND_FALLING, MAX_PRICE_TREND_RISING, MAX_RELAXATION_ATTEMPTS, - MAX_VOLATILITY_THRESHOLD, + MAX_VOLATILITY_THRESHOLD_HIGH, + MAX_VOLATILITY_THRESHOLD_MODERATE, + MAX_VOLATILITY_THRESHOLD_VERY_HIGH, MIN_GAP_COUNT, MIN_PERIOD_LENGTH, MIN_PRICE_RATING_THRESHOLD_HIGH, @@ -29,7 +31,9 @@ from custom_components.tibber_prices.const import ( MIN_PRICE_TREND_FALLING, MIN_PRICE_TREND_RISING, MIN_RELAXATION_ATTEMPTS, - MIN_VOLATILITY_THRESHOLD, + MIN_VOLATILITY_THRESHOLD_HIGH, + MIN_VOLATILITY_THRESHOLD_MODERATE, + MIN_VOLATILITY_THRESHOLD_VERY_HIGH, ) from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers.aiohttp_client import async_create_clientsession @@ -219,18 +223,78 @@ def validate_price_rating_thresholds(threshold_low: int, threshold_high: int) -> return threshold_low < threshold_high -def validate_volatility_threshold(threshold: float) -> bool: +def validate_volatility_threshold_moderate(threshold: float) -> bool: """ - Validate volatility threshold percentage. + Validate moderate volatility threshold. Args: - threshold: Volatility threshold percentage (0.0 to 100.0) + threshold: Moderate volatility threshold percentage (5.0 to 25.0) Returns: - True if threshold is valid (MIN_VOLATILITY_THRESHOLD to MAX_VOLATILITY_THRESHOLD) + True if threshold is valid (MIN_VOLATILITY_THRESHOLD_MODERATE to MAX_VOLATILITY_THRESHOLD_MODERATE) """ - return MIN_VOLATILITY_THRESHOLD <= threshold <= MAX_VOLATILITY_THRESHOLD + return MIN_VOLATILITY_THRESHOLD_MODERATE <= threshold <= MAX_VOLATILITY_THRESHOLD_MODERATE + + +def validate_volatility_threshold_high(threshold: float) -> bool: + """ + Validate high volatility threshold. + + Args: + threshold: High volatility threshold percentage (20.0 to 40.0) + + Returns: + True if threshold is valid (MIN_VOLATILITY_THRESHOLD_HIGH to MAX_VOLATILITY_THRESHOLD_HIGH) + + """ + return MIN_VOLATILITY_THRESHOLD_HIGH <= threshold <= MAX_VOLATILITY_THRESHOLD_HIGH + + +def validate_volatility_threshold_very_high(threshold: float) -> bool: + """ + Validate very high volatility threshold. + + Args: + threshold: Very high volatility threshold percentage (35.0 to 80.0) + + Returns: + True if threshold is valid (MIN_VOLATILITY_THRESHOLD_VERY_HIGH to MAX_VOLATILITY_THRESHOLD_VERY_HIGH) + + """ + return MIN_VOLATILITY_THRESHOLD_VERY_HIGH <= threshold <= MAX_VOLATILITY_THRESHOLD_VERY_HIGH + + +def validate_volatility_thresholds( + threshold_moderate: float, + threshold_high: float, + threshold_very_high: float, +) -> bool: + """ + Cross-validate all three volatility thresholds together. + + Ensures that MODERATE < HIGH < VERY_HIGH to maintain logical classification + boundaries. Each threshold represents an escalating level of price volatility. + + Args: + threshold_moderate: Moderate volatility threshold (5.0 to 25.0) + threshold_high: High volatility threshold (20.0 to 40.0) + threshold_very_high: Very high volatility threshold (35.0 to 80.0) + + Returns: + True if all thresholds are valid individually AND maintain proper ordering + + """ + # Validate individual ranges first + if not validate_volatility_threshold_moderate(threshold_moderate): + return False + if not validate_volatility_threshold_high(threshold_high): + return False + if not validate_volatility_threshold_very_high(threshold_very_high): + return False + + # Ensure cascading order: MODERATE < HIGH < VERY_HIGH + return threshold_moderate < threshold_high < threshold_very_high def validate_price_trend_rising(threshold: int) -> bool: diff --git a/custom_components/tibber_prices/const.py b/custom_components/tibber_prices/const.py index 1c1c09b..81792bd 100644 --- a/custom_components/tibber_prices/const.py +++ b/custom_components/tibber_prices/const.py @@ -112,8 +112,16 @@ MIN_PRICE_RATING_THRESHOLD_HIGH = 5 # Minimum value for high rating threshold ( MAX_PRICE_RATING_THRESHOLD_HIGH = 50 # Maximum value for high rating threshold # Volatility threshold limits -MIN_VOLATILITY_THRESHOLD = 0.0 # Minimum volatility threshold percentage -MAX_VOLATILITY_THRESHOLD = 100.0 # Maximum volatility threshold percentage +# MODERATE threshold: practical range 5% to 25% (entry point for noticeable fluctuation) +# HIGH threshold: practical range 20% to 40% (significant price swings) +# VERY_HIGH threshold: practical range 35% to 80% (extreme volatility) +# Ensure cascading: MODERATE < HIGH < VERY_HIGH with ~5% minimum gaps +MIN_VOLATILITY_THRESHOLD_MODERATE = 5.0 # Minimum for moderate volatility threshold +MAX_VOLATILITY_THRESHOLD_MODERATE = 25.0 # Maximum for moderate volatility threshold (must be < HIGH) +MIN_VOLATILITY_THRESHOLD_HIGH = 20.0 # Minimum for high volatility threshold (must be > MODERATE) +MAX_VOLATILITY_THRESHOLD_HIGH = 40.0 # Maximum for high volatility threshold (must be < VERY_HIGH) +MIN_VOLATILITY_THRESHOLD_VERY_HIGH = 35.0 # Minimum for very high volatility threshold (must be > HIGH) +MAX_VOLATILITY_THRESHOLD_VERY_HIGH = 80.0 # Maximum for very high volatility threshold # Price trend threshold limits MIN_PRICE_TREND_RISING = 1 # Minimum rising trend threshold diff --git a/custom_components/tibber_prices/translations/de.json b/custom_components/tibber_prices/translations/de.json index b7313f5..6442b5b 100644 --- a/custom_components/tibber_prices/translations/de.json +++ b/custom_components/tibber_prices/translations/de.json @@ -207,7 +207,10 @@ "invalid_price_rating_low": "Untere Preis-Bewertungsschwelle muss zwischen -50% und -5% liegen", "invalid_price_rating_high": "Obere Preis-Bewertungsschwelle muss zwischen 5% und 50% liegen", "invalid_price_rating_thresholds": "Untere Schwelle muss kleiner als obere Schwelle sein", - "invalid_volatility_threshold": "Volatilitätsschwelle muss zwischen 0% und 100% liegen", + "invalid_volatility_threshold_moderate": "Moderate Volatilitätsschwelle muss zwischen 5% und 25% liegen", + "invalid_volatility_threshold_high": "Hohe Volatilitätsschwelle muss zwischen 20% und 40% liegen", + "invalid_volatility_threshold_very_high": "Sehr hohe Volatilitätsschwelle muss zwischen 35% und 80% liegen", + "invalid_volatility_thresholds": "Schwellenwerte müssen aufsteigend sein: moderat < hoch < sehr hoch", "invalid_price_trend_rising": "Steigender Trendschwellenwert muss zwischen 1% und 50% liegen", "invalid_price_trend_falling": "Fallender Trendschwellenwert muss zwischen -50% und -1% liegen" }, diff --git a/custom_components/tibber_prices/translations/en.json b/custom_components/tibber_prices/translations/en.json index b95d6ce..60246eb 100644 --- a/custom_components/tibber_prices/translations/en.json +++ b/custom_components/tibber_prices/translations/en.json @@ -207,7 +207,10 @@ "invalid_price_rating_low": "Low price rating threshold must be between -50% and -5%", "invalid_price_rating_high": "High price rating threshold must be between 5% and 50%", "invalid_price_rating_thresholds": "Low threshold must be less than high threshold", - "invalid_volatility_threshold": "Volatility threshold must be between 0% and 100%", + "invalid_volatility_threshold_moderate": "Moderate volatility threshold must be between 5% and 25%", + "invalid_volatility_threshold_high": "High volatility threshold must be between 20% and 40%", + "invalid_volatility_threshold_very_high": "Very high volatility threshold must be between 35% and 80%", + "invalid_volatility_thresholds": "Thresholds must be in ascending order: moderate < high < very high", "invalid_price_trend_rising": "Rising trend threshold must be between 1% and 50%", "invalid_price_trend_falling": "Falling trend threshold must be between -50% and -1%" }, diff --git a/custom_components/tibber_prices/translations/nb.json b/custom_components/tibber_prices/translations/nb.json index 65ddd04..f9c6431 100644 --- a/custom_components/tibber_prices/translations/nb.json +++ b/custom_components/tibber_prices/translations/nb.json @@ -207,7 +207,10 @@ "invalid_price_rating_low": "Lav prisvurderingsgrense må være mellom -50% og -5%", "invalid_price_rating_high": "Høy prisvurderingsgrense må være mellom 5% og 50%", "invalid_price_rating_thresholds": "Lav grense må være mindre enn høy grense", - "invalid_volatility_threshold": "Volatilitetsgrense må være mellom 0% og 100%", + "invalid_volatility_threshold_moderate": "Moderat volatilitetsgrense må være mellom 5% og 25%", + "invalid_volatility_threshold_high": "Høy volatilitetsgrense må være mellom 20% og 40%", + "invalid_volatility_threshold_very_high": "Svært høy volatilitetsgrense må være mellom 35% og 80%", + "invalid_volatility_thresholds": "Grensene må være i stigende rekkefølge: moderat < høy < svært høy", "invalid_price_trend_rising": "Stigende trendgrense må være mellom 1% og 50%", "invalid_price_trend_falling": "Fallende trendgrense må være mellom -50% og -1%" }, diff --git a/custom_components/tibber_prices/translations/nl.json b/custom_components/tibber_prices/translations/nl.json index c297dba..a8f5b71 100644 --- a/custom_components/tibber_prices/translations/nl.json +++ b/custom_components/tibber_prices/translations/nl.json @@ -207,7 +207,10 @@ "invalid_price_rating_low": "Lage prijsbeoordelingsdrempel moet tussen -50% en -5% liggen", "invalid_price_rating_high": "Hoge prijsbeoordelingsdrempel moet tussen 5% en 50% liggen", "invalid_price_rating_thresholds": "Lage drempel moet lager zijn dan hoge drempel", - "invalid_volatility_threshold": "Volatiliteitsdrempel moet tussen 0% en 100% liggen", + "invalid_volatility_threshold_moderate": "Gematigde volatiliteitsdrempel moet tussen 5% en 25% liggen", + "invalid_volatility_threshold_high": "Hoge volatiliteitsdrempel moet tussen 20% en 40% liggen", + "invalid_volatility_threshold_very_high": "Zeer hoge volatiliteitsdrempel moet tussen 35% en 80% liggen", + "invalid_volatility_thresholds": "Drempels moeten in oplopende volgorde zijn: gematigd < hoog < zeer hoog", "invalid_price_trend_rising": "Stijgende trenddrempel moet tussen 1% en 50% liggen", "invalid_price_trend_falling": "Dalende trenddrempel moet tussen -50% en -1% liggen" }, diff --git a/custom_components/tibber_prices/translations/sv.json b/custom_components/tibber_prices/translations/sv.json index 8ed687e..670ed4d 100644 --- a/custom_components/tibber_prices/translations/sv.json +++ b/custom_components/tibber_prices/translations/sv.json @@ -207,7 +207,10 @@ "invalid_price_rating_low": "Låg prisklassificeringströskel måste vara mellan -50% och -5%", "invalid_price_rating_high": "Hög prisklassificeringströskel måste vara mellan 5% och 50%", "invalid_price_rating_thresholds": "Låg tröskel måste vara mindre än hög tröskel", - "invalid_volatility_threshold": "Volatilitetströskel måste vara mellan 0% och 100%", + "invalid_volatility_threshold_moderate": "Måttlig volatilitetströskel måste vara mellan 5% och 25%", + "invalid_volatility_threshold_high": "Hög volatilitetströskel måste vara mellan 20% och 40%", + "invalid_volatility_threshold_very_high": "Mycket hög volatilitetströskel måste vara mellan 35% och 80%", + "invalid_volatility_thresholds": "Trösklar måste vara i stigande ordning: måttlig < hög < mycket hög", "invalid_price_trend_rising": "Stigande trendtröskel måste vara mellan 1% och 50%", "invalid_price_trend_falling": "Fallande trendtröskel måste vara mellan -50% och -1%" },