diff --git a/custom_components/tibber_prices/config_flow.py b/custom_components/tibber_prices/config_flow.py index 160d918..d5a7be9 100644 --- a/custom_components/tibber_prices/config_flow.py +++ b/custom_components/tibber_prices/config_flow.py @@ -42,6 +42,7 @@ from .const import ( BEST_PRICE_MAX_LEVEL_OPTIONS, 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_BEST_PRICE_MIN_VOLATILITY, @@ -51,6 +52,7 @@ from .const import ( 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, @@ -66,6 +68,7 @@ from .const import ( CONF_VOLATILITY_THRESHOLD_VERY_HIGH, 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_BEST_PRICE_MIN_VOLATILITY, @@ -75,6 +78,7 @@ from .const import ( 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, @@ -679,6 +683,22 @@ class TibberPricesOptionsFlowHandler(OptionsFlow): translation_key="price_level", ), ), + vol.Optional( + CONF_BEST_PRICE_MAX_LEVEL_GAP_COUNT, + default=int( + self.config_entry.options.get( + CONF_BEST_PRICE_MAX_LEVEL_GAP_COUNT, + DEFAULT_BEST_PRICE_MAX_LEVEL_GAP_COUNT, + ) + ), + ): NumberSelector( + NumberSelectorConfig( + min=0, + max=8, + step=1, + mode=NumberSelectorMode.SLIDER, + ), + ), vol.Optional( CONF_ENABLE_MIN_PERIODS_BEST, default=self.config_entry.options.get( @@ -811,6 +831,22 @@ class TibberPricesOptionsFlowHandler(OptionsFlow): translation_key="price_level", ), ), + vol.Optional( + CONF_PEAK_PRICE_MAX_LEVEL_GAP_COUNT, + default=int( + self.config_entry.options.get( + CONF_PEAK_PRICE_MAX_LEVEL_GAP_COUNT, + DEFAULT_PEAK_PRICE_MAX_LEVEL_GAP_COUNT, + ) + ), + ): NumberSelector( + NumberSelectorConfig( + min=0, + max=8, + step=1, + mode=NumberSelectorMode.SLIDER, + ), + ), vol.Optional( CONF_ENABLE_MIN_PERIODS_PEAK, default=self.config_entry.options.get( diff --git a/custom_components/tibber_prices/const.py b/custom_components/tibber_prices/const.py index 4fbf527..ac0d275 100644 --- a/custom_components/tibber_prices/const.py +++ b/custom_components/tibber_prices/const.py @@ -35,6 +35,8 @@ CONF_BEST_PRICE_MIN_VOLATILITY = "best_price_min_volatility" CONF_PEAK_PRICE_MIN_VOLATILITY = "peak_price_min_volatility" CONF_BEST_PRICE_MAX_LEVEL = "best_price_max_level" CONF_PEAK_PRICE_MIN_LEVEL = "peak_price_min_level" +CONF_BEST_PRICE_MAX_LEVEL_GAP_COUNT = "best_price_max_level_gap_count" +CONF_PEAK_PRICE_MAX_LEVEL_GAP_COUNT = "peak_price_max_level_gap_count" CONF_ENABLE_MIN_PERIODS_BEST = "enable_min_periods_best" CONF_MIN_PERIODS_BEST = "min_periods_best" CONF_RELAXATION_STEP_BEST = "relaxation_step_best" @@ -64,6 +66,9 @@ DEFAULT_BEST_PRICE_MIN_VOLATILITY = "low" # Show best price at any volatility ( DEFAULT_PEAK_PRICE_MIN_VOLATILITY = "low" # Always show peak price (warning relevant even at low spreads) DEFAULT_BEST_PRICE_MAX_LEVEL = "any" # Default: show best price periods regardless of price level DEFAULT_PEAK_PRICE_MIN_LEVEL = "any" # Default: show peak price periods regardless of price level +DEFAULT_BEST_PRICE_MAX_LEVEL_GAP_COUNT = 0 # Default: no tolerance for level gaps (strict filtering) +DEFAULT_PEAK_PRICE_MAX_LEVEL_GAP_COUNT = 0 # Default: no tolerance for level gaps (strict filtering) +MIN_INTERVALS_FOR_GAP_TOLERANCE = 6 # Minimum period length (in 15-min intervals = 1.5h) required for gap tolerance DEFAULT_ENABLE_MIN_PERIODS_BEST = False # Default: minimum periods feature disabled for best price DEFAULT_MIN_PERIODS_BEST = 2 # Default: require at least 2 best price periods (when enabled) DEFAULT_RELAXATION_STEP_BEST = 25 # Default: 25% of original threshold per relaxation step for best price diff --git a/custom_components/tibber_prices/coordinator.py b/custom_components/tibber_prices/coordinator.py index 267044f..ddc6c8a 100644 --- a/custom_components/tibber_prices/coordinator.py +++ b/custom_components/tibber_prices/coordinator.py @@ -27,6 +27,7 @@ from .api import ( from .const import ( 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_BEST_PRICE_MIN_VOLATILITY, @@ -35,6 +36,7 @@ from .const import ( 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, @@ -48,6 +50,7 @@ from .const import ( CONF_VOLATILITY_THRESHOLD_VERY_HIGH, 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_BEST_PRICE_MIN_VOLATILITY, @@ -56,6 +59,7 @@ from .const import ( 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, @@ -68,6 +72,7 @@ from .const import ( DEFAULT_VOLATILITY_THRESHOLD_MODERATE, DEFAULT_VOLATILITY_THRESHOLD_VERY_HIGH, DOMAIN, + MIN_INTERVALS_FOR_GAP_TOLERANCE, PRICE_LEVEL_MAPPING, ) from .period_utils import ( @@ -778,6 +783,242 @@ class TibberPricesDataUpdateCoordinator(DataUpdateCoordinator[dict[str, Any]]): override=level_override, ) + def _split_at_gap_clusters( + self, + today_intervals: list[dict[str, Any]], + level_order: int, + min_period_length: int, + *, + reverse_sort: bool, + ) -> list[list[dict[str, Any]]]: + """ + Split intervals into sub-sequences at gap clusters. + + A gap cluster is 2+ consecutive intervals that don't meet the level requirement. + This allows recovering usable periods from sequences that would otherwise be rejected. + + Args: + today_intervals: List of price intervals for today + level_order: Required level order from PRICE_LEVEL_MAPPING + min_period_length: Minimum number of intervals required for a valid sub-sequence + reverse_sort: True for peak price, False for best price + + Returns: + List of sub-sequences, each at least min_period_length long. + + """ + sub_sequences = [] + current_sequence = [] + consecutive_non_qualifying = 0 + + for interval in today_intervals: + interval_level = PRICE_LEVEL_MAPPING.get(interval.get("level", "NORMAL"), 0) + meets_requirement = interval_level >= level_order if reverse_sort else interval_level <= level_order + + if meets_requirement: + # Qualifying interval - add to current sequence + current_sequence.append(interval) + consecutive_non_qualifying = 0 + elif consecutive_non_qualifying == 0: + # First non-qualifying interval (single gap) - add to current sequence + current_sequence.append(interval) + consecutive_non_qualifying = 1 + else: + # Second+ consecutive non-qualifying interval = gap cluster starts + # Save current sequence if long enough (excluding the first gap we just added) + if len(current_sequence) - 1 >= min_period_length: + sub_sequences.append(current_sequence[:-1]) # Exclude the first gap + current_sequence = [] + consecutive_non_qualifying = 0 + + # Don't forget last sequence + if len(current_sequence) >= min_period_length: + sub_sequences.append(current_sequence) + + return sub_sequences + + def _check_short_period_strict( + self, + today_intervals: list[dict[str, Any]], + level_order: int, + *, + reverse_sort: bool, + ) -> bool: + """ + Strict filtering for short periods (< 1.5h) without gap tolerance. + + All intervals must meet the requirement perfectly, or at least one does + and all others are exact matches. + + Args: + today_intervals: List of price intervals for today + level_order: Required level order from PRICE_LEVEL_MAPPING + reverse_sort: True for peak price, False for best price + + Returns: + True if all intervals meet requirement (with at least one qualifying), False otherwise. + + """ + has_qualifying = False + for interval in today_intervals: + interval_level = PRICE_LEVEL_MAPPING.get(interval.get("level", "NORMAL"), 0) + meets_requirement = interval_level >= level_order if reverse_sort else interval_level <= level_order + if meets_requirement: + has_qualifying = True + elif interval_level != level_order: + # Any deviation in short periods disqualifies the entire sequence + return False + return has_qualifying + + def _check_level_filter_with_gaps( + self, + today_intervals: list[dict[str, Any]], + level_order: int, + max_gap_count: int, + *, + reverse_sort: bool, + ) -> bool: + """ + Check if intervals meet level requirements with gap tolerance and minimum distance. + + A "gap" is an interval that deviates by exactly 1 level step. + For best price: CHEAP allows NORMAL as gap (but not EXPENSIVE). + For peak price: EXPENSIVE allows NORMAL as gap (but not CHEAP). + + Gap tolerance is only applied to periods with at least MIN_INTERVALS_FOR_GAP_TOLERANCE + intervals (1.5h). Shorter periods use strict filtering (zero tolerance). + + Between gaps, there must be a minimum number of "good" intervals to prevent + periods that are mostly interrupted by gaps. + + Args: + today_intervals: List of price intervals for today + level_order: Required level order from PRICE_LEVEL_MAPPING + max_gap_count: Maximum total gaps allowed + reverse_sort: True for peak price, False for best price + + Returns: + True if any qualifying sequence exists, False otherwise. + + """ + if not today_intervals: + return False + + interval_count = len(today_intervals) + + # Periods shorter than MIN_INTERVALS_FOR_GAP_TOLERANCE (1.5h) use strict filtering + if interval_count < MIN_INTERVALS_FOR_GAP_TOLERANCE: + return self._check_short_period_strict(today_intervals, level_order, reverse_sort=reverse_sort) + + # Try normal gap tolerance check first + if self._check_sequence_with_gap_tolerance( + today_intervals, level_order, max_gap_count, reverse_sort=reverse_sort + ): + return True + + # Normal check failed - try splitting at gap clusters as fallback + # Get minimum period length from config (convert minutes to intervals) + if reverse_sort: + min_period_minutes = self.config_entry.options.get( + CONF_PEAK_PRICE_MIN_PERIOD_LENGTH, + DEFAULT_PEAK_PRICE_MIN_PERIOD_LENGTH, + ) + else: + min_period_minutes = self.config_entry.options.get( + CONF_BEST_PRICE_MIN_PERIOD_LENGTH, + DEFAULT_BEST_PRICE_MIN_PERIOD_LENGTH, + ) + + min_period_intervals = min_period_minutes // 15 + + sub_sequences = self._split_at_gap_clusters( + today_intervals, + level_order, + min_period_intervals, + reverse_sort=reverse_sort, + ) + + # Check if ANY sub-sequence passes gap tolerance + for sub_seq in sub_sequences: + if self._check_sequence_with_gap_tolerance(sub_seq, level_order, max_gap_count, reverse_sort=reverse_sort): + return True + + return False + + def _check_sequence_with_gap_tolerance( + self, + intervals: list[dict[str, Any]], + level_order: int, + max_gap_count: int, + *, + reverse_sort: bool, + ) -> bool: + """ + Check if a single interval sequence passes gap tolerance requirements. + + This is the core gap tolerance logic extracted for reuse with sub-sequences. + + Args: + intervals: List of price intervals to check + level_order: Required level order from PRICE_LEVEL_MAPPING + max_gap_count: Maximum total gaps allowed + reverse_sort: True for peak price, False for best price + + Returns: + True if sequence meets all gap tolerance requirements, False otherwise. + + """ + if not intervals: + return False + + interval_count = len(intervals) + + # Calculate minimum distance between gaps dynamically. + # Shorter periods require relatively larger distances. + # Longer periods allow gaps closer together. + # Distance is never less than 2 intervals between gaps. + min_distance_between_gaps = max(2, (interval_count // max_gap_count) // 2) + + # Limit total gaps to max 25% of period length to prevent too many outliers. + # This ensures periods remain predominantly "good" even when long. + effective_max_gaps = min(max_gap_count, interval_count // 4) + + gap_count = 0 + consecutive_good_count = 0 + has_qualifying_interval = False + + for interval in intervals: + interval_level = PRICE_LEVEL_MAPPING.get(interval.get("level", "NORMAL"), 0) + + # Check if interval meets the strict requirement + meets_requirement = interval_level >= level_order if reverse_sort else interval_level <= level_order + + if meets_requirement: + has_qualifying_interval = True + consecutive_good_count += 1 + continue + + # Check if this is a tolerable gap (exactly 1 step deviation) + is_tolerable_gap = interval_level == level_order - 1 if reverse_sort else interval_level == level_order + 1 + + if is_tolerable_gap: + # If we already had gaps, check minimum distance + if gap_count > 0 and consecutive_good_count < min_distance_between_gaps: + # Not enough "good" intervals between gaps + return False + + gap_count += 1 + if gap_count > effective_max_gaps: + return False + + # Reset counter for next gap + consecutive_good_count = 0 + else: + # Too far from required level (more than 1 step deviation) + return False + + return has_qualifying_interval + def _check_level_filter( self, price_info: dict[str, Any], @@ -786,7 +1027,10 @@ class TibberPricesDataUpdateCoordinator(DataUpdateCoordinator[dict[str, Any]]): override: str | None = None, ) -> bool: """ - Check if today has any intervals that meet the level requirement. + Check if today has any intervals that meet the level requirement with gap tolerance. + + Gap tolerance allows a configurable number of intervals within a qualifying sequence + to deviate by one level step (e.g., CHEAP allows NORMAL, but not EXPENSIVE). Args: price_info: Price information dict with today data @@ -795,7 +1039,8 @@ class TibberPricesDataUpdateCoordinator(DataUpdateCoordinator[dict[str, Any]]): override: Optional override value (e.g., "any" to disable filter) Returns: - True if ANY interval meets the level requirement, False otherwise. + True if ANY sequence of intervals meets the level requirement + (considering gap tolerance), False otherwise. """ # Use override if provided @@ -825,19 +1070,41 @@ class TibberPricesDataUpdateCoordinator(DataUpdateCoordinator[dict[str, Any]]): if not today_intervals: return True # If no data, don't filter - # Check if ANY interval today meets the level requirement + # Get gap tolerance configuration + if reverse_sort: + max_gap_count = self.config_entry.options.get( + CONF_PEAK_PRICE_MAX_LEVEL_GAP_COUNT, + DEFAULT_PEAK_PRICE_MAX_LEVEL_GAP_COUNT, + ) + else: + max_gap_count = self.config_entry.options.get( + CONF_BEST_PRICE_MAX_LEVEL_GAP_COUNT, + DEFAULT_BEST_PRICE_MAX_LEVEL_GAP_COUNT, + ) + # Note: level_config is lowercase from selector, but PRICE_LEVEL_MAPPING uses uppercase level_order = PRICE_LEVEL_MAPPING.get(level_config.upper(), 0) - if reverse_sort: - # Peak price: level >= min_level (show if ANY interval is expensive enough) + # If gap tolerance is 0, use simple ANY check (backwards compatible) + if max_gap_count == 0: + if reverse_sort: + # Peak price: level >= min_level (show if ANY interval is expensive enough) + return any( + PRICE_LEVEL_MAPPING.get(interval.get("level", "NORMAL"), 0) >= level_order + for interval in today_intervals + ) + # Best price: level <= max_level (show if ANY interval is cheap enough) return any( - PRICE_LEVEL_MAPPING.get(interval.get("level", "NORMAL"), 0) >= level_order + PRICE_LEVEL_MAPPING.get(interval.get("level", "NORMAL"), 0) <= level_order for interval in today_intervals ) - # Best price: level <= max_level (show if ANY interval is cheap enough) - return any( - PRICE_LEVEL_MAPPING.get(interval.get("level", "NORMAL"), 0) <= level_order for interval in today_intervals + + # Use gap-tolerant check + return self._check_level_filter_with_gaps( + today_intervals, + level_order, + max_gap_count, + reverse_sort=reverse_sort, ) def _calculate_periods_for_price_info(self, price_info: dict[str, Any]) -> dict[str, Any]: diff --git a/custom_components/tibber_prices/translations/de.json b/custom_components/tibber_prices/translations/de.json index 6fa861d..2b21361 100644 --- a/custom_components/tibber_prices/translations/de.json +++ b/custom_components/tibber_prices/translations/de.json @@ -106,6 +106,7 @@ "best_price_min_distance_from_avg": "Mindestabstand: Erforderlich unter dem Tagesdurchschnitt", "best_price_min_volatility": "Mindest-Volatilitätsfilter", "best_price_max_level": "Preisniveau-Filter (Optional)", + "best_price_max_level_gap_count": "Lückentoleranz für Niveaufilter", "enable_min_periods_best": "Mindestanzahl Perioden anstreben", "min_periods_best": "Mindestanzahl Perioden", "relaxation_step_best": "Lockerungsschritt" @@ -113,6 +114,7 @@ "data_description": { "best_price_min_volatility": "Zeigt Bestpreis-Perioden nur an, wenn die interne Preisvolatilität der Periode (Preisspanne innerhalb der Periode) mindestens diesem Level entspricht. Standard: 'Niedrig' (zeigt Perioden mit beliebigem Volatilitätslevel) - ermöglicht das Finden günstiger Perioden auch wenn die Preise stabil sind. Wähle 'Moderat'/'Hoch' um nur Perioden mit signifikanten Preisschwankungen anzuzeigen, was auf dynamischere Preismöglichkeiten hinweisen kann.", "best_price_max_level": "Zeigt Bestpreis-Perioden nur an, wenn sie Intervalle mit Preisniveaus ≤ dem gewählten Wert enthalten. Beispiel: Wahl von 'Günstig' bedeutet, dass die Periode mindestens ein 'SEHR_GÜNSTIG' oder 'GÜNSTIG' Intervall haben muss. Dies stellt sicher, dass 'Bestpreis'-Perioden nicht nur relativ günstig für den Tag sind, sondern tatsächlich günstig in absoluten Zahlen. Wähle 'Beliebig' um Bestpreise unabhängig vom absoluten Preisniveau anzuzeigen.", + "best_price_max_level_gap_count": "Maximale Anzahl aufeinanderfolgender Intervalle, die exakt um eine Niveaustufe vom geforderten Level abweichen dürfen. Beispiel: Bei Filter 'Günstig' und Lückentoleranz 1 wird die Sequenz 'GÜNSTIG, GÜNSTIG, NORMAL, GÜNSTIG' akzeptiert (NORMAL ist eine Stufe über GÜNSTIG). Dies verhindert, dass Perioden durch gelegentliche Niveau-Abweichungen aufgespalten werden. Standard: 0 (strenge Filterung, keine Toleranz).", "enable_min_periods_best": "Wenn aktiviert, werden Filter schrittweise gelockert, falls nicht genug Perioden gefunden wurden. Dies versucht die gewünschte Mindestanzahl zu erreichen, was dazu führen kann, dass auch weniger optimale Zeiträume als Bestpreis-Perioden markiert werden.", "min_periods_best": "Mindestanzahl an Bestpreis-Perioden, die pro Tag angestrebt werden. Filter werden schrittweise gelockert, um diese Anzahl zu erreichen. Nur aktiv, wenn 'Mindestanzahl Perioden anstreben' aktiviert ist. Standard: 1", "relaxation_step_best": "Prozentsatz des ursprünglichen Flexibilitätsschwellwerts, der pro Lockerungsschritt addiert wird. Beispiel: Bei 15% Flexibilität und 25% Schrittgröße werden 15%, 18,75%, 22,5% usw. versucht. Höhere Werte bedeuten schnellere Lockerung, aber geringere Präzision." @@ -128,6 +130,7 @@ "peak_price_min_distance_from_avg": "Mindestabstand: Erforderlich über dem Tagesdurchschnitt", "peak_price_min_volatility": "Mindest-Volatilitätsfilter", "peak_price_min_level": "Preisniveau-Filter (Optional)", + "peak_price_max_level_gap_count": "Lückentoleranz für Niveaufilter", "enable_min_periods_peak": "Mindestanzahl Perioden anstreben", "min_periods_peak": "Mindestanzahl Perioden", "relaxation_step_peak": "Lockerungsschritt" @@ -135,6 +138,7 @@ "data_description": { "peak_price_min_volatility": "Zeigt Spitzenpreis-Perioden nur an, wenn die interne Preisvolatilität der Periode (Preisspanne innerhalb der Periode) mindestens diesem Level entspricht. Standard: 'Niedrig' (zeigt Perioden mit beliebigem Volatilitätslevel) - ermöglicht das Identifizieren teurer Perioden auch wenn die Preise stabil sind. Wähle 'Moderat'/'Hoch' um nur Perioden mit signifikanten Preisschwankungen anzuzeigen, was auf dringenderen Bedarf hinweisen kann, diese Zeiten zu vermeiden.", "peak_price_min_level": "Zeigt Spitzenpreis-Perioden nur an, wenn sie Intervalle mit Preisniveaus ≥ dem gewählten Wert enthalten. Beispiel: Wahl von 'Teuer' bedeutet, dass die Periode mindestens ein 'TEUER' oder 'SEHR_TEUER' Intervall haben muss. Dies stellt sicher, dass 'Spitzenpreis'-Perioden nicht nur relativ teuer für den Tag sind, sondern tatsächlich teuer in absoluten Zahlen. Wähle 'Beliebig' um Spitzenpreise unabhängig vom absoluten Preisniveau anzuzeigen.", + "peak_price_max_level_gap_count": "Maximale Anzahl aufeinanderfolgender Intervalle, die exakt um eine Niveaustufe vom geforderten Level abweichen dürfen. Beispiel: Bei Filter 'Teuer' und Lückentoleranz 2 wird die Sequenz 'TEUER, NORMAL, NORMAL, TEUER' akzeptiert (NORMAL ist eine Stufe unter TEUER). Dies verhindert, dass Perioden durch gelegentliche Niveau-Abweichungen aufgespalten werden. Standard: 0 (strenge Filterung, keine Toleranz).", "enable_min_periods_peak": "Wenn aktiviert, werden Filter schrittweise gelockert, falls nicht genug Perioden gefunden wurden. Dies versucht die gewünschte Mindestanzahl zu erreichen, um sicherzustellen, dass du auch an Tagen mit ungewöhnlichen Preismustern vor teuren Perioden gewarnt wirst.", "min_periods_peak": "Mindestanzahl an Spitzenpreis-Perioden, die pro Tag angestrebt werden. Filter werden schrittweise gelockert, um diese Anzahl zu erreichen. Nur aktiv, wenn 'Mindestanzahl Perioden anstreben' aktiviert ist. Standard: 1", "relaxation_step_peak": "Prozentsatz des ursprünglichen Flexibilitätsschwellwerts, der pro Lockerungsschritt addiert wird. Beispiel: Bei -15% Flexibilität und 25% Schrittgröße werden -15%, -18,75%, -22,5% usw. versucht. Höhere Werte bedeuten schnellere Lockerung, aber geringere Präzision." diff --git a/custom_components/tibber_prices/translations/en.json b/custom_components/tibber_prices/translations/en.json index c584f74..ad0a6a1 100644 --- a/custom_components/tibber_prices/translations/en.json +++ b/custom_components/tibber_prices/translations/en.json @@ -106,6 +106,7 @@ "best_price_min_distance_from_avg": "Minimum Distance: Required below daily average", "best_price_min_volatility": "Minimum Volatility Filter", "best_price_max_level": "Price Level Filter (Optional)", + "best_price_max_level_gap_count": "Level Filter Gap Tolerance", "enable_min_periods_best": "Try to Achieve Minimum Period Count", "min_periods_best": "Minimum Periods Required", "relaxation_step_best": "Filter Relaxation Step Size" @@ -113,6 +114,7 @@ "data_description": { "best_price_min_volatility": "Only show best price periods when the period's internal price volatility (spread within the period) meets or exceeds this level. Default: 'Low' (show periods with any volatility level) - allows finding cheap periods even if prices are stable. Select 'Moderate'/'High' to only show periods with significant price variations, which may indicate more dynamic pricing opportunities.", "best_price_max_level": "Only show best price periods if they contain intervals with price levels ≤ selected value. For example, selecting 'Cheap' means the period must have at least one 'VERY_CHEAP' or 'CHEAP' interval. This ensures 'best price' periods are not just relatively cheap for the day, but actually cheap in absolute terms. Select 'Any' to show best prices regardless of their absolute price level.", + "best_price_max_level_gap_count": "Maximum number of consecutive intervals allowed that deviate by exactly one level step from the required level. For example: with 'Cheap' filter and gap count 1, a sequence 'CHEAP, CHEAP, NORMAL, CHEAP' is accepted (NORMAL is one step above CHEAP). This prevents periods from being split by occasional level deviations. Default: 0 (strict filtering, no tolerance).", "enable_min_periods_best": "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.", "min_periods_best": "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 'Try to Achieve Minimum Period Count' is enabled. Default: 1", "relaxation_step_best": "Percentage of the original flexibility threshold to add per relaxation step. For example: with 15% flexibility and 25% step size, filters will try 15%, 18.75%, 22.5%, etc. Higher values mean faster relaxation but less precision." @@ -128,6 +130,7 @@ "peak_price_min_distance_from_avg": "Minimum Distance: Required above daily average", "peak_price_min_volatility": "Minimum Volatility Filter", "peak_price_min_level": "Price Level Filter (Optional)", + "peak_price_max_level_gap_count": "Level Filter Gap Tolerance", "enable_min_periods_peak": "Try to Achieve Minimum Period Count", "min_periods_peak": "Minimum Periods Required", "relaxation_step_peak": "Filter Relaxation Step Size" @@ -135,6 +138,7 @@ "data_description": { "peak_price_min_volatility": "Only show peak price periods when the period's internal price volatility (spread within the period) meets or exceeds this level. Default: 'Low' (show periods with any volatility level) - allows identifying expensive periods even if prices are stable. Select 'Moderate'/'High' to only show periods with significant price variations, which may indicate more urgent need to avoid these times.", "peak_price_min_level": "Only show peak price periods if they contain intervals with price levels ≥ selected value. For example, selecting 'Expensive' means the period must have at least one 'EXPENSIVE' or 'VERY_EXPENSIVE' interval. This ensures 'peak price' periods are not just relatively expensive for the day, but actually expensive in absolute terms. Select 'Any' to show peak prices regardless of their absolute price level.", + "peak_price_max_level_gap_count": "Maximum number of consecutive intervals allowed that deviate by exactly one level step from the required level. For example: with 'Expensive' filter and gap count 2, a sequence 'EXPENSIVE, NORMAL, NORMAL, EXPENSIVE' is accepted (NORMAL is one step below EXPENSIVE). This prevents periods from being split by occasional level deviations. Default: 0 (strict filtering, no tolerance).", "enable_min_periods_peak": "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.", "min_periods_peak": "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 'Try to Achieve Minimum Period Count' is enabled. Default: 1", "relaxation_step_peak": "Percentage of the original flexibility threshold to add per relaxation step. For example: with -15% flexibility and 25% step size, filters will try -15%, -18.75%, -22.5%, etc. Higher values mean faster relaxation but less precision." diff --git a/custom_components/tibber_prices/translations/nb.json b/custom_components/tibber_prices/translations/nb.json index 350f41d..dd57633 100644 --- a/custom_components/tibber_prices/translations/nb.json +++ b/custom_components/tibber_prices/translations/nb.json @@ -106,6 +106,7 @@ "best_price_min_distance_from_avg": "Minimumsavstand: Påkrevd % under daglig gjennomsnitt", "best_price_min_volatility": "Minimum volatilitetsfilter", "best_price_max_level": "Prisnivåfilter (valgfritt)", + "best_price_max_level_gap_count": "Gaptoleranse for nivåfilter", "enable_min_periods_best": "Prøv å oppnå minimum antall perioder", "min_periods_best": "Minimum antall perioder", "relaxation_step_best": "Avslappingstrinn" @@ -115,7 +116,8 @@ "best_price_max_level": "Vis kun beste prisperioder hvis de inneholder intervaller med prisnivåer ≤ valgt verdi. For eksempel: å velge 'Billig' betyr at perioden må ha minst ett 'VELDIG_BILLIG' eller 'BILLIG' intervall. Dette sikrer at 'beste pris'-perioder ikke bare er relativt billige for dagen, men faktisk billige i absolutte tall. Velg 'Alle' for å vise beste priser uavhengig av deres absolutte prisnivå.", "enable_min_periods_best": "Når aktivert vil filtrene gradvis bli lempeligere hvis det ikke blir funnet nok perioder. Dette forsøker å nå ønsket minimum antall perioder, noe som kan føre til at mindre optimale tidsrom blir markert som beste-pris-perioder.", "min_periods_best": "Minimum antall beste-pris-perioder å sikte mot per dag. Filtre vil bli lempet trinn for trinn for å prøve å oppnå dette antallet. Kun aktiv når 'Prøv å oppnå minimum antall perioder' er aktivert. Standard: 1", - "relaxation_step_best": "Prosentandel av den opprinnelige fleksibilitetsterskealen som legges til per avslappingstrinn. For eksempel: med 15% fleksibilitet og 25% trinnstørrelse vil filtrene prøve 15%, 18,75%, 22,5%, osv. Høyere verdier betyr raskere avslapping men mindre presisjon." + "relaxation_step_best": "Prosentandel av den opprinnelige fleksibilitetsterskealen som legges til per avslappingstrinn. For eksempel: med 15% fleksibilitet og 25% trinnstørrelse vil filtrene prøve 15%, 18,75%, 22,5%, osv. Høyere verdier betyr raskere avslapping men mindre presisjon.", + "best_price_max_level_gap_count": "Maksimalt antall påfølgende intervaller som kan avvike med nøyaktig ett nivåtrinn fra det nødvendige nivået. For eksempel: med 'Billig' filter og gapantall 1, aksepteres sekvensen 'BILLIG, BILLIG, NORMAL, BILLIG' (NORMAL er ett trinn over BILLIG). Dette forhindrer at perioder blir delt opp av tilfeldige nivåavvik. Standard: 0 (streng filtrering, ingen toleranse)." }, "submit": "Neste til steg 5" }, @@ -128,6 +130,7 @@ "peak_price_min_distance_from_avg": "Minimumsavstand: Påkrevd % over daglig gjennomsnitt", "peak_price_min_volatility": "Minimum volatilitetsfilter", "peak_price_min_level": "Prisnivåfilter (valgfritt)", + "peak_price_max_level_gap_count": "Gaptoleranse for nivåfilter", "enable_min_periods_peak": "Prøv å oppnå minimum antall perioder", "min_periods_peak": "Minimum antall perioder", "relaxation_step_peak": "Avslappingstrinn" @@ -137,7 +140,8 @@ "peak_price_min_level": "Vis kun topprisperioder hvis de inneholder intervaller med prisnivåer ≥ valgt verdi. For eksempel: å velge 'Dyr' betyr at perioden må ha minst ett 'DYR' eller 'VELDIG_DYR' intervall. Dette sikrer at 'topppris'-perioder ikke bare er relativt dyre for dagen, men faktisk dyre i absolutte tall. Velg 'Alle' for å vise topppriser uavhengig av deres absolutte prisnivå.", "enable_min_periods_peak": "Når aktivert vil filtrene gradvis bli lempeligere hvis det ikke blir funnet nok perioder. Dette forsøker å nå ønsket minimum antall perioder for å sikre at du blir advart om dyre perioder selv på dager med uvanlige prismønstre.", "min_periods_peak": "Minimum antall topp-pris-perioder å sikte mot per dag. Filtre vil bli lempet trinn for trinn for å prøve å oppnå dette antallet. Kun aktiv når 'Prøv å oppnå minimum antall perioder' er aktivert. Standard: 1", - "relaxation_step_peak": "Prosentandel av den opprinnelige fleksibilitetsterskealen som legges til per avslappingstrinn. For eksempel: med -15% fleksibilitet og 25% trinnstørrelse vil filtrene prøve -15%, -18,75%, -22,5%, osv. Høyere verdier betyr raskere avslapping men mindre presisjon." + "relaxation_step_peak": "Prosentandel av den opprinnelige fleksibilitetsterskealen som legges til per avslappingstrinn. For eksempel: med -15% fleksibilitet og 25% trinnstørrelse vil filtrene prøve -15%, -18,75%, -22,5%, osv. Høyere verdier betyr raskere avslapping men mindre presisjon.", + "peak_price_max_level_gap_count": "Maksimalt antall påfølgende intervaller som kan avvike med nøyaktig ett nivåtrinn fra det nødvendige nivået. For eksempel: med 'Dyr' filter og gapantall 2, aksepteres sekvensen 'DYR, NORMAL, NORMAL, DYR' (NORMAL er ett trinn under DYR). Dette forhindrer at perioder blir delt opp av tilfeldige nivåavvik. Standard: 0 (streng filtrering, ingen toleranse)." }, "submit": "Neste til steg 6" }, @@ -511,4 +515,4 @@ } }, "title": "Tibber Prisinformasjon & Vurderinger" -} +} \ No newline at end of file diff --git a/custom_components/tibber_prices/translations/nl.json b/custom_components/tibber_prices/translations/nl.json index 5a75094..71117d8 100644 --- a/custom_components/tibber_prices/translations/nl.json +++ b/custom_components/tibber_prices/translations/nl.json @@ -106,6 +106,7 @@ "best_price_min_distance_from_avg": "Minimale afstand: Vereist % onder dagelijks gemiddelde", "best_price_min_volatility": "Minimum volatiliteitsfilter", "best_price_max_level": "Prijsniveaufilter (Optioneel)", + "best_price_max_level_gap_count": "Gaptolerantie voor niveaufilter", "enable_min_periods_best": "Probeer minimum aantal periodes te bereiken", "min_periods_best": "Minimum aantal periodes", "relaxation_step_best": "Ontspanningsstap" @@ -115,7 +116,8 @@ "best_price_max_level": "Toon alleen beste prijsperiodes als ze intervallen bevatten met prijsniveaus ≤ geselecteerde waarde. Bijvoorbeeld: selecteren van 'Goedkoop' betekent dat de periode minstens één 'ZEER_GOEDKOOP' of 'GOEDKOOP' interval moet hebben. Dit zorgt ervoor dat 'beste prijs'-periodes niet alleen relatief goedkoop zijn voor de dag, maar daadwerkelijk goedkoop in absolute termen. Selecteer 'Alle' om beste prijzen te tonen ongeacht hun absolute prijsniveau.", "enable_min_periods_best": "Wanneer ingeschakeld worden filters geleidelijk versoepeld als er niet genoeg periodes worden gevonden. Dit probeert het gewenste minimum aantal periodes te bereiken om ervoor te zorgen dat je kansen hebt om van lage prijzen te profiteren, zelfs op dagen met ongebruikelijke prijspatronen.", "min_periods_best": "Minimum aantal beste prijsperiodes om naar te streven per dag. Filters worden stap voor stap versoepeld om dit aantal te proberen bereiken. Alleen actief wanneer 'Probeer minimum aantal periodes te bereiken' is ingeschakeld. Standaard: 1", - "relaxation_step_best": "Percentage van de oorspronkelijke flexibiliteitsdrempel om toe te voegen per ontspanningsstap. Bijvoorbeeld: met 15% flexibiliteit en 25% stapgrootte zullen de filters 15%, 18,75%, 22,5%, enz. proberen. Hogere waarden betekenen snellere ontspanning maar minder precisie." + "relaxation_step_best": "Percentage van de oorspronkelijke flexibiliteitsdrempel om toe te voegen per ontspanningsstap. Bijvoorbeeld: met 15% flexibiliteit en 25% stapgrootte zullen de filters 15%, 18,75%, 22,5%, enz. proberen. Hogere waarden betekenen snellere ontspanning maar minder precisie.", + "best_price_max_level_gap_count": "Maximum aantal opeenvolgende intervallen dat precies één niveaustap mag afwijken van het vereiste niveau. Bijvoorbeeld: met 'Goedkoop' filter en gaptelling 1 wordt de reeks 'GOEDKOOP, GOEDKOOP, NORMAAL, GOEDKOOP' geaccepteerd (NORMAAL is één stap boven GOEDKOOP). Dit voorkomt dat periodes worden opgesplitst door incidentele niveauafwijkingen. Standaard: 0 (strikte filtering, geen tolerantie)." }, "submit": "Volgende naar stap 5" }, @@ -128,6 +130,7 @@ "peak_price_min_distance_from_avg": "Minimale afstand: Vereist % boven dagelijks gemiddelde", "peak_price_min_volatility": "Minimum volatiliteitsfilter", "peak_price_min_level": "Prijsniveaufilter (Optioneel)", + "peak_price_max_level_gap_count": "Gaptolerantie voor niveaufilter", "enable_min_periods_peak": "Probeer minimum aantal periodes te bereiken", "min_periods_peak": "Minimum aantal periodes", "relaxation_step_peak": "Ontspanningsstap" @@ -137,7 +140,8 @@ "peak_price_min_level": "Toon alleen piekprijsperiodes als ze intervallen bevatten met prijsniveaus ≥ geselecteerde waarde. Bijvoorbeeld: selecteren van 'Duur' betekent dat de periode minstens één 'DUUR' of 'ZEER_DUUR' interval moet hebben. Dit zorgt ervoor dat 'piekprijs'-periodes niet alleen relatief duur zijn voor de dag, maar daadwerkelijk duur in absolute termen. Selecteer 'Alle' om piekprijzen te tonen ongeacht hun absolute prijsniveau.", "enable_min_periods_peak": "Wanneer ingeschakeld worden filters geleidelijk versoepeld als er niet genoeg periodes worden gevonden. Dit probeert het gewenste minimum aantal periodes te bereiken om ervoor te zorgen dat je wordt gewaarschuwd voor dure periodes, zelfs op dagen met ongebruikelijke prijspatronen.", "min_periods_peak": "Minimum aantal piekprijsperiodes om naar te streven per dag. Filters worden stap voor stap versoepeld om dit aantal te proberen bereiken. Alleen actief wanneer 'Probeer minimum aantal periodes te bereiken' is ingeschakeld. Standaard: 1", - "relaxation_step_peak": "Percentage van de oorspronkelijke flexibiliteitsdrempel om toe te voegen per ontspanningsstap. Bijvoorbeeld: met -15% flexibiliteit en 25% stapgrootte zullen de filters -15%, -18,75%, -22,5%, enz. proberen. Hogere waarden betekenen snellere ontspanning maar minder precisie." + "relaxation_step_peak": "Percentage van de oorspronkelijke flexibiliteitsdrempel om toe te voegen per ontspanningsstap. Bijvoorbeeld: met -15% flexibiliteit en 25% stapgrootte zullen de filters -15%, -18,75%, -22,5%, enz. proberen. Hogere waarden betekenen snellere ontspanning maar minder precisie.", + "peak_price_max_level_gap_count": "Maximum aantal opeenvolgende intervallen dat precies één niveaustap mag afwijken van het vereiste niveau. Bijvoorbeeld: met 'Duur' filter en gaptelling 2 wordt de reeks 'DUUR, NORMAAL, NORMAAL, DUUR' geaccepteerd (NORMAAL is één stap onder DUUR). Dit voorkomt dat periodes worden opgesplitst door incidentele niveauafwijkingen. Standaard: 0 (strikte filtering, geen tolerantie)." }, "submit": "Volgende naar stap 6" }, @@ -511,4 +515,4 @@ } }, "title": "Tibber Prijsinformatie & Beoordelingen" -} +} \ No newline at end of file diff --git a/custom_components/tibber_prices/translations/sv.json b/custom_components/tibber_prices/translations/sv.json index b0f9ee5..298bb2b 100644 --- a/custom_components/tibber_prices/translations/sv.json +++ b/custom_components/tibber_prices/translations/sv.json @@ -106,6 +106,7 @@ "best_price_min_distance_from_avg": "Minimiavstånd: Krävd % under dagligt genomsnitt", "best_price_min_volatility": "Minimum volatilitetsfilter", "best_price_max_level": "Prisnivåfilter (Valfritt)", + "best_price_max_level_gap_count": "Gaptolerens för nivåfilter", "enable_min_periods_best": "Försök uppnå minsta antal perioder", "min_periods_best": "Minsta antal perioder", "relaxation_step_best": "Avslappningssteg" @@ -115,7 +116,8 @@ "best_price_max_level": "Visa endast bästa prisperioder om de innehåller intervall med prisnivåer ≤ valt värde. Till exempel: att välja 'Billigt' betyder att perioden måste ha minst ett 'MYCKET_BILLIGT' eller 'BILLIGT' intervall. Detta säkerställer att 'bästa pris'-perioder inte bara är relativt billiga för dagen, utan faktiskt billiga i absoluta tal. Välj 'Alla' för att visa bästa priser oavsett deras absoluta prisnivå.", "enable_min_periods_best": "När aktiverad kommer filtren att gradvis luckras upp om inte tillräckligt många perioder hittas. Detta försöker uppnå det önskade minsta antalet perioder för att säkerställa att du har möjligheter att dra nytta av låga priser även på dagar med ovanliga prismönster.", "min_periods_best": "Minsta antal bästa prisperioder att sträva efter per dag. Filtren kommer att luckras upp steg för steg för att försöka uppnå detta antal. Endast aktiv när 'Försök uppnå minsta antal perioder' är aktiverad. Standard: 1", - "relaxation_step_best": "Procentandel av den ursprungliga flexibilitetströskeln att lägga till per avslappningssteg. Till exempel: med 15% flexibilitet och 25% stegstorlek kommer filtren att prova 15%, 18,75%, 22,5%, osv. Högre värden innebär snabbare avslappning men mindre precision." + "relaxation_step_best": "Procentandel av den ursprungliga flexibilitetströskeln att lägga till per avslappningssteg. Till exempel: med 15% flexibilitet och 25% stegstorlek kommer filtren att prova 15%, 18,75%, 22,5%, osv. Högre värden innebär snabbare avslappning men mindre precision.", + "best_price_max_level_gap_count": "Maximalt antal på varandra följande intervaller som får avvika med exakt ett nivåsteg från det erforderliga nivået. Till exempel: med 'Billigt' filter och gapantal 1 accepteras sekvensen 'BILLIGT, BILLIGT, NORMALT, BILLIGT' (NORMALT är ett steg över BILLIGT). Detta förhindrar att perioder delas upp av tillfälliga nivåavvikelser. Standard: 0 (strikt filtrering, ingen tolerans)." }, "submit": "Nästa till steg 5" }, @@ -128,6 +130,7 @@ "peak_price_min_distance_from_avg": "Minimiavstånd: Krävd % över dagligt genomsnitt", "peak_price_min_volatility": "Minimum volatilitetsfilter", "peak_price_min_level": "Prisnivåfilter (Valfritt)", + "peak_price_max_level_gap_count": "Gaptolerens för nivåfilter", "enable_min_periods_peak": "Försök uppnå minsta antal perioder", "min_periods_peak": "Minsta antal perioder", "relaxation_step_peak": "Avslappningssteg" @@ -137,7 +140,8 @@ "peak_price_min_level": "Visa endast topprisperioder om de innehåller intervall med prisnivåer ≥ valt värde. Till exempel måste perioden om du väljer 'Dyr' ha minst ett 'DYR' eller 'MYCKET_DYR' intervall. Detta säkerställer att 'toppris'-perioder inte bara är relativt dyra för dagen, utan faktiskt dyra i absoluta termer (inte bara 'lite dyrare än genomsnittet på en billig dag').", "enable_min_periods_peak": "När aktiverad kommer filtren att gradvis luckras upp om inte tillräckligt många perioder hittas. Detta försöker uppnå det önskade minsta antalet perioder för att säkerställa att du blir varnad för dyra perioder även på dagar med ovanliga prismönster.", "min_periods_peak": "Minsta antal topprisperioder att sträva efter per dag. Filtren kommer att luckras upp steg för steg för att försöka uppnå detta antal. Endast aktiv när 'Försök uppnå minsta antal perioder' är aktiverad. Standard: 1", - "relaxation_step_peak": "Procentandel av den ursprungliga flexibilitetströskeln att lägga till per avslappningssteg. Till exempel: med -15% flexibilitet och 25% stegstorlek kommer filtren att prova -15%, -18,75%, -22,5%, osv. Högre värden innebär snabbare avslappning men mindre precision." + "relaxation_step_peak": "Procentandel av den ursprungliga flexibilitetströskeln att lägga till per avslappningssteg. Till exempel: med -15% flexibilitet och 25% stegstorlek kommer filtren att prova -15%, -18,75%, -22,5%, osv. Högre värden innebär snabbare avslappning men mindre precision.", + "peak_price_max_level_gap_count": "Maximalt antal på varandra följande intervaller som får avvika med exakt ett nivåsteg från det erforderliga nivået. Till exempel: med 'Dyrt' filter och gapantal 2 accepteras sekvensen 'DYRT, NORMALT, NORMALT, DYRT' (NORMALT är ett steg under DYRT). Detta förhindrar att perioder delas upp av tillfälliga nivåavvikelser. Standard: 0 (strikt filtrering, ingen tolerans)." }, "submit": "Nästa till steg 6" }, @@ -511,4 +515,4 @@ } }, "title": "Tibber Prisinformation & Betyg" -} +} \ No newline at end of file