From 75da094c81d9dd0fcc931465fa3849b4bab8289c Mon Sep 17 00:00:00 2001 From: Julian Pawlowski Date: Fri, 17 Apr 2026 14:37:17 +0000 Subject: [PATCH] refactor(day_patterns): rename double valley/peak to double dip/duck curve Updated pattern names for clarity and consistency in the codebase. The changes include renaming constants and updating related logic to reflect the new terminology. Impact: Improved readability and understanding of day pattern classifications for developers. --- .../coordinator/period_handlers/__init__.py | 8 +- .../coordinator/period_handlers/core.py | 14 ++-- .../period_handlers/day_pattern.py | 48 ++++++------ .../period_handlers/level_filtering.py | 4 +- .../period_handlers/period_building.py | 76 +++++++++++++++---- .../coordinator/period_handlers/types.py | 11 ++- .../tibber_prices/sensor/definitions.py | 12 +-- .../tibber_prices/translations/de.json | 12 +-- .../tibber_prices/translations/en.json | 16 ++-- .../tibber_prices/translations/nb.json | 12 +-- .../tibber_prices/translations/nl.json | 12 +-- .../tibber_prices/translations/sv.json | 12 +-- 12 files changed, 143 insertions(+), 94 deletions(-) diff --git a/custom_components/tibber_prices/coordinator/period_handlers/__init__.py b/custom_components/tibber_prices/coordinator/period_handlers/__init__.py index 4b30037..2e1dd9b 100644 --- a/custom_components/tibber_prices/coordinator/period_handlers/__init__.py +++ b/custom_components/tibber_prices/coordinator/period_handlers/__init__.py @@ -34,8 +34,8 @@ from .shape_extension import extend_periods_for_shape # Re-export constants and types from .types import ( ALL_DAY_PATTERNS, - DAY_PATTERN_DOUBLE_PEAK, - DAY_PATTERN_DOUBLE_VALLEY, + DAY_PATTERN_DOUBLE_DIP, + DAY_PATTERN_DUCK_CURVE, DAY_PATTERN_FALLING, DAY_PATTERN_FLAT, DAY_PATTERN_MIXED, @@ -59,8 +59,8 @@ from .types import ( __all__ = [ "ALL_DAY_PATTERNS", - "DAY_PATTERN_DOUBLE_PEAK", - "DAY_PATTERN_DOUBLE_VALLEY", + "DAY_PATTERN_DOUBLE_DIP", + "DAY_PATTERN_DUCK_CURVE", "DAY_PATTERN_FALLING", "DAY_PATTERN_FLAT", "DAY_PATTERN_MIXED", diff --git a/custom_components/tibber_prices/coordinator/period_handlers/core.py b/custom_components/tibber_prices/coordinator/period_handlers/core.py index 25ba7da..e5dfac8 100644 --- a/custom_components/tibber_prices/coordinator/period_handlers/core.py +++ b/custom_components/tibber_prices/coordinator/period_handlers/core.py @@ -183,7 +183,7 @@ def calculate_periods( ) # Step 3.5: Segment forcing for W/M-shaped days (opt-in, default disabled) - # For days detected as W-shape (DOUBLE_VALLEY for best) or M-shape (DOUBLE_PEAK for peak), + # For days detected as W-shape (DOUBLE_DIP for best) or M-shape (DUCK_CURVE for peak), # ensures each price valley/peak segment has at least segment_min_periods periods. if config.segment_forcing and day_patterns_by_date: raw_periods = _apply_segment_forcing( @@ -315,9 +315,9 @@ def _apply_segment_forcing( """ Force at least segment_min_periods periods per segment for W/M-shaped days. - For DOUBLE_VALLEY days (best price): splits at the central price peak and + For DOUBLE_DIP days (best price): splits at the central price peak and ensures each valley side has the required number of periods. - For DOUBLE_PEAK days (peak price): splits at the central price valley and + For DUCK_CURVE days (peak price): splits at the central price valley and ensures each peak side has the required number of periods. Args: @@ -335,12 +335,12 @@ def _apply_segment_forcing( import logging # noqa: PLC0415 from .period_building import build_periods # noqa: PLC0415 - from .types import DAY_PATTERN_DOUBLE_PEAK, DAY_PATTERN_DOUBLE_VALLEY, INDENT_L1, INDENT_L2 # noqa: PLC0415 + from .types import DAY_PATTERN_DOUBLE_DIP, DAY_PATTERN_DUCK_CURVE, INDENT_L1, INDENT_L2 # noqa: PLC0415 _LOGGER = logging.getLogger(__name__) reverse_sort = config.reverse_sort - target_pattern = DAY_PATTERN_DOUBLE_PEAK if reverse_sort else DAY_PATTERN_DOUBLE_VALLEY + target_pattern = DAY_PATTERN_DUCK_CURVE if reverse_sort else DAY_PATTERN_DOUBLE_DIP segment_min_periods = config.segment_min_periods merged_periods = list(periods) @@ -362,8 +362,8 @@ def _apply_segment_forcing( continue # Find the central extremum in the middle 50% of the day - # DOUBLE_VALLEY → central peak = highest price between the two valleys - # DOUBLE_PEAK → central valley = lowest price between the two peaks + # DOUBLE_DIP → central peak = highest price between the two valleys + # DUCK_CURVE → central valley = lowest price between the two peaks n = len(day_intervals) middle = day_intervals[n // 4 : 3 * n // 4] if not middle: diff --git a/custom_components/tibber_prices/coordinator/period_handlers/day_pattern.py b/custom_components/tibber_prices/coordinator/period_handlers/day_pattern.py index 92325a3..4e3a46d 100644 --- a/custom_components/tibber_prices/coordinator/period_handlers/day_pattern.py +++ b/custom_components/tibber_prices/coordinator/period_handlers/day_pattern.py @@ -6,8 +6,8 @@ into a small set of patterns that are meaningful for switching decisions: VALLEY - Single price minimum (U/V-shape, cheap middle) PEAK - Single price maximum (Lambda-shape, expensive middle) - DOUBLE_VALLEY - Two minima separated by a peak (W-shape) - DOUBLE_PEAK - Two peaks separated by a valley (M-shape) + DOUBLE_DIP - Two minima separated by a peak (W-shape) + DUCK_CURVE - Two peaks with midday valley (M-shape, solar duck curve) FLAT - No significant variation (CV <= 10 %) RISING - Monotonically / persistently rising FALLING - Monotonically / persistently falling @@ -63,8 +63,8 @@ _EDGE_ZONE = 0.25 # Pattern string constants DAY_PATTERN_VALLEY = "valley" DAY_PATTERN_PEAK = "peak" -DAY_PATTERN_DOUBLE_VALLEY = "double_valley" -DAY_PATTERN_DOUBLE_PEAK = "double_peak" +DAY_PATTERN_DOUBLE_DIP = "double_dip" +DAY_PATTERN_DUCK_CURVE = "duck_curve" DAY_PATTERN_FLAT = "flat" DAY_PATTERN_RISING = "rising" DAY_PATTERN_FALLING = "falling" @@ -202,14 +202,14 @@ def _detect_single_day_pattern( peak_start = times[lk] if lk is not None and lk < len(times) else None peak_end = times[rk] if rk is not None and rk < len(times) else None - elif pattern == DAY_PATTERN_DOUBLE_VALLEY and extrema: + elif pattern == DAY_PATTERN_DOUBLE_DIP and extrema: # Primary extreme = deeper of the two minima min_extrema = [e for e in extrema if e["type"] == "min"] if min_extrema: primary = min(min_extrema, key=lambda e: e["price"]) extreme_time = times[primary["idx"]] if primary["idx"] < len(times) else None - elif pattern == DAY_PATTERN_DOUBLE_PEAK and extrema: + elif pattern == DAY_PATTERN_DUCK_CURVE and extrema: max_extrema = [e for e in extrema if e["type"] == "max"] if max_extrema: primary = max(max_extrema, key=lambda e: e["price"]) @@ -223,15 +223,6 @@ def _detect_single_day_pattern( lk, rk = _find_knee_points(smoothed, valley_extreme["idx"]) valley_start = times[lk] if lk is not None and lk < len(times) else None valley_end = times[rk] if rk is not None and rk < len(times) else None - # The valley between the two peaks is the cheap zone for best-price periods. - # Compute knee points around the deepest minimum so that compute_geometric_flex_bonus - # can apply extra flex to intervals inside this zone (same mechanism as VALLEY). - min_extrema_dp = [e for e in extrema if e["type"] == "min"] - if min_extrema_dp: - valley_extreme = min(min_extrema_dp, key=lambda e: e["price"]) - lk, rk = _find_knee_points(smoothed, valley_extreme["idx"]) - valley_start = times[lk] if lk is not None and lk < len(times) else None - valley_end = times[rk] if rk is not None and rk < len(times) else None # ── intra-day segments ────────────────────────────────────────────────────── segments = _detect_segments(extrema, prices_raw, times) @@ -415,11 +406,20 @@ def _classify_pattern( confidence = max(0.5, 1.0 - cv_pct / FLAT_CV_THRESHOLD) return DAY_PATTERN_FLAT, confidence - # ── no significant extrema → monotone (rising or falling) ────────────────── + # ── no significant extrema → check for monotone trend ────────────────────── if not extrema: - # Cannot determine direction without access to underlying prices from here. - # The caller (_detect_single_day_pattern) handles the RISING/FALLING case - # before calling _classify_pattern when there are no extrema but prices exist. + # Without extrema, check if prices have a clear directional trend using + # the day's start/end price difference relative to span. + if start_price > 0 and end_price > 0 and n_times >= MIN_DAY_INTERVALS: + price_change = end_price - start_price + # Require at least 5% absolute change relative to the mean price to + # distinguish a genuine trend from flat-ish noise above FLAT_CV_THRESHOLD. + mean_price = (start_price + end_price) / 2 + relative_change = abs(price_change) / mean_price if mean_price > 0 else 0 + if relative_change > 0.05: + if price_change > 0: + return DAY_PATTERN_RISING, min(0.65, 0.4 + relative_change) + return DAY_PATTERN_FALLING, min(0.65, 0.4 + relative_change) return DAY_PATTERN_MIXED, 0.4 n_extrema = len(extrema) @@ -461,18 +461,18 @@ def _classify_pattern( return DAY_PATTERN_VALLEY, 0.65 return DAY_PATTERN_RISING, 0.7 if types == ["min", "min"]: - return DAY_PATTERN_DOUBLE_VALLEY, 0.65 + return DAY_PATTERN_DOUBLE_DIP, 0.65 if types == ["max", "max"]: - return DAY_PATTERN_DOUBLE_PEAK, 0.65 + return DAY_PATTERN_DUCK_CURVE, 0.65 # ── three extrema ──────────────────────────────────────────────────────────── if n_extrema == 3: # min-max-min → W-shape if types == ["min", "max", "min"]: - return DAY_PATTERN_DOUBLE_VALLEY, 0.75 - # max-min-max → M-shape + return DAY_PATTERN_DOUBLE_DIP, 0.75 + # max-min-max → duck curve (solar midday valley between morning/evening peaks) if types == ["max", "min", "max"]: - return DAY_PATTERN_DOUBLE_PEAK, 0.75 + return DAY_PATTERN_DUCK_CURVE, 0.75 # min-max or max-min with trailing → RISING/FALLING with extra bump if types[0] == "min" and types[-1] == "max": return DAY_PATTERN_RISING, 0.55 diff --git a/custom_components/tibber_prices/coordinator/period_handlers/level_filtering.py b/custom_components/tibber_prices/coordinator/period_handlers/level_filtering.py index 7a2fe93..98a59c2 100644 --- a/custom_components/tibber_prices/coordinator/period_handlers/level_filtering.py +++ b/custom_components/tibber_prices/coordinator/period_handlers/level_filtering.py @@ -283,9 +283,9 @@ def compute_geometric_flex_bonus( zone_end = day_pattern.get("peak_end") else: # Best price: expand inside VALLEY zone. - # Also handles DOUBLE_PEAK (solar duck-curve: expensive morning/evening, cheap midday) + # Also handles DUCK_CURVE (solar duck-curve: expensive morning/evening, cheap midday) # where valley_start/valley_end mark the knee points around the midday minimum. - if pattern not in ("valley", "double_peak"): + if pattern not in ("valley", "duck_curve"): return 0.0 zone_start = day_pattern.get("valley_start") zone_end = day_pattern.get("valley_end") diff --git a/custom_components/tibber_prices/coordinator/period_handlers/period_building.py b/custom_components/tibber_prices/coordinator/period_handlers/period_building.py index 5f099ee..c4c056a 100644 --- a/custom_components/tibber_prices/coordinator/period_handlers/period_building.py +++ b/custom_components/tibber_prices/coordinator/period_handlers/period_building.py @@ -508,6 +508,11 @@ def _find_extension_intervals( criteria: Any, max_extension_time: datetime, interval_duration: timedelta, + *, + max_intervals: int = 0, + period_mean_price: float = 0.0, + max_price_deviation: float = 0.0, + reverse_sort: bool = False, ) -> list[dict]: """ Find consecutive intervals after period_end that meet criteria. @@ -516,6 +521,14 @@ def _find_extension_intervals( meet the flex and min_distance criteria. Stops at first failure or when reaching max_extension_time. + Additional guards: + - max_intervals: Hard cap on number of extension intervals (0 = unlimited) + - period_mean_price + max_price_deviation: Stop extending when the candidate + interval's price deviates too far from the original period's mean price. + For peak periods (reverse_sort=True): stops when price drops below + mean × (1 - deviation). For best periods: stops when price rises above + mean × (1 + deviation). + """ from .level_filtering import check_interval_criteria # noqa: PLC0415 @@ -523,11 +536,26 @@ def _find_extension_intervals( check_time = period_end while check_time < max_extension_time: + # Hard cap on extension length + if max_intervals > 0 and len(extension_intervals) >= max_intervals: + break + price_data = price_lookup.get(check_time.isoformat()) if not price_data: break # No more data price = float(price_data["total"]) + + # Price deviation gate: stop if price drifts too far from original period mean + if period_mean_price > 0 and max_price_deviation > 0: + if reverse_sort: + # Peak: stop if price drops below mean × (1 - deviation) + if price < period_mean_price * (1 - max_price_deviation): + break + elif price > period_mean_price * (1 + max_price_deviation): + # Best: stop if price rises above mean × (1 + deviation) + break + in_flex, meets_min_distance = check_interval_criteria(price, criteria) if not (in_flex and meets_min_distance): @@ -625,6 +653,9 @@ def extend_periods_across_midnight( from .types import ( # noqa: PLC0415 CROSS_DAY_LATE_PERIOD_START_HOUR, CROSS_DAY_MAX_EXTENSION_HOUR, + CROSS_DAY_MAX_EXTENSION_INTERVALS, + CROSS_DAY_MAX_PRICE_DEVIATION, + CROSS_DAY_PROPORTIONAL_EXTENSION_FACTOR, PERIOD_MAX_CV, TibberPricesIntervalCriteria, ) @@ -677,26 +708,40 @@ def extend_periods_across_midnight( reverse_sort=reverse_sort, ) - # Find extension intervals - extension_intervals = _find_extension_intervals( - period["end"], - price_lookup, - criteria, - max_extension_time, - interval_duration, - ) - - if not extension_intervals: - extended_summaries.append(period) - continue - - # Collect all prices for CV check + # Collect original prices once (reused for cap calculation, deviation gate, and CV check) original_prices = _collect_original_period_prices( period["start"], period["end"], price_lookup, interval_duration, ) + + # Calculate max extension intervals: min(hard cap, proportional cap) + original_interval_count = max(1, len(original_prices)) + proportional_cap = int(original_interval_count * CROSS_DAY_PROPORTIONAL_EXTENSION_FACTOR) + max_intervals = min(CROSS_DAY_MAX_EXTENSION_INTERVALS, proportional_cap) + + # Original period mean price for deviation gate + period_mean_price = sum(original_prices) / len(original_prices) if original_prices else 0.0 + + # Find extension intervals (with cap + price deviation gate) + extension_intervals = _find_extension_intervals( + period["end"], + price_lookup, + criteria, + max_extension_time, + interval_duration, + max_intervals=max_intervals, + period_mean_price=period_mean_price, + max_price_deviation=CROSS_DAY_MAX_PRICE_DEVIATION, + reverse_sort=reverse_sort, + ) + + if not extension_intervals: + extended_summaries.append(period) + continue + + # CV check using already-collected original prices extension_prices = [float(p["total"]) for p in extension_intervals] combined_prices = original_prices + extension_prices @@ -714,11 +759,12 @@ def extend_periods_across_midnight( ) _LOGGER.info( - "Cross-day extension: Period %s-%s extended to %s (+%d intervals, CV=%.1f%%)", + "Cross-day extension: Period %s-%s extended to %s (+%d intervals, max=%d, CV=%.1f%%)", period["start"].strftime("%H:%M"), period["end"].strftime("%H:%M"), extended_period["end"].strftime("%H:%M"), len(extension_intervals), + max_intervals, combined_cv, ) extended_summaries.append(extended_period) diff --git a/custom_components/tibber_prices/coordinator/period_handlers/types.py b/custom_components/tibber_prices/coordinator/period_handlers/types.py index 1ebf309..f395c0b 100644 --- a/custom_components/tibber_prices/coordinator/period_handlers/types.py +++ b/custom_components/tibber_prices/coordinator/period_handlers/types.py @@ -34,6 +34,9 @@ LOW_PRICE_QUALITY_BYPASS_THRESHOLD = 0.10 # EUR/NOK major unit (= 10 ct/øre) # we can extend it past midnight if prices remain favorable CROSS_DAY_LATE_PERIOD_START_HOUR = 20 # Consider periods starting at 20:00 or later for extension CROSS_DAY_MAX_EXTENSION_HOUR = 8 # Don't extend beyond 08:00 next day (covers typical night low) +CROSS_DAY_MAX_EXTENSION_INTERVALS = 16 # Hard cap: max 4 hours of extension (16 × 15-minute intervals) +CROSS_DAY_PROPORTIONAL_EXTENSION_FACTOR = 2.0 # Extension ≤ 2× original period length +CROSS_DAY_MAX_PRICE_DEVIATION = 0.15 # Stop if price deviates >15% from original period mean # Cross-Day Supersession: When tomorrow data arrives, late-night periods that are # worse than early-morning tomorrow periods become obsolete @@ -122,8 +125,8 @@ class TibberPricesIntervalCriteria(NamedTuple): DAY_PATTERN_VALLEY = "valley" # Single price minimum (U/V-shape) DAY_PATTERN_PEAK = "peak" # Single price maximum (Λ-shape) -DAY_PATTERN_DOUBLE_VALLEY = "double_valley" # Two minima, W-shape -DAY_PATTERN_DOUBLE_PEAK = "double_peak" # Two peaks, M-shape +DAY_PATTERN_DOUBLE_DIP = "double_dip" # Two minima, W-shape +DAY_PATTERN_DUCK_CURVE = "duck_curve" # Two peaks with midday valley (solar duck curve) DAY_PATTERN_FLAT = "flat" # No significant variation DAY_PATTERN_RISING = "rising" # Persistently rising throughout the day DAY_PATTERN_FALLING = "falling" # Persistently falling throughout the day @@ -133,8 +136,8 @@ DAY_PATTERN_MIXED = "mixed" # Multiple extrema with no clear pattern ALL_DAY_PATTERNS: list[str] = [ DAY_PATTERN_VALLEY, DAY_PATTERN_PEAK, - DAY_PATTERN_DOUBLE_VALLEY, - DAY_PATTERN_DOUBLE_PEAK, + DAY_PATTERN_DOUBLE_DIP, + DAY_PATTERN_DUCK_CURVE, DAY_PATTERN_FLAT, DAY_PATTERN_RISING, DAY_PATTERN_FALLING, diff --git a/custom_components/tibber_prices/sensor/definitions.py b/custom_components/tibber_prices/sensor/definitions.py index 519092e..ca9f599 100644 --- a/custom_components/tibber_prices/sensor/definitions.py +++ b/custom_components/tibber_prices/sensor/definitions.py @@ -999,8 +999,8 @@ DAY_PATTERN_SENSORS = ( options=[ "valley", "peak", - "double_valley", - "double_peak", + "double_dip", + "duck_curve", "flat", "rising", "falling", @@ -1017,8 +1017,8 @@ DAY_PATTERN_SENSORS = ( options=[ "valley", "peak", - "double_valley", - "double_peak", + "double_dip", + "duck_curve", "flat", "rising", "falling", @@ -1035,8 +1035,8 @@ DAY_PATTERN_SENSORS = ( options=[ "valley", "peak", - "double_valley", - "double_peak", + "double_dip", + "duck_curve", "flat", "rising", "falling", diff --git a/custom_components/tibber_prices/translations/de.json b/custom_components/tibber_prices/translations/de.json index d22b46b..023c301 100644 --- a/custom_components/tibber_prices/translations/de.json +++ b/custom_components/tibber_prices/translations/de.json @@ -978,8 +978,8 @@ "state": { "valley": "Tal", "peak": "Gipfel", - "double_valley": "Doppeltal", - "double_peak": "Doppelgipfel", + "double_dip": "Doppelmulde", + "duck_curve": "Entenkurve", "flat": "Flach", "rising": "Steigend", "falling": "Fallend", @@ -991,8 +991,8 @@ "state": { "valley": "Tal", "peak": "Gipfel", - "double_valley": "Doppeltal", - "double_peak": "Doppelgipfel", + "double_dip": "Doppelmulde", + "duck_curve": "Entenkurve", "flat": "Flach", "rising": "Steigend", "falling": "Fallend", @@ -1004,8 +1004,8 @@ "state": { "valley": "Tal", "peak": "Gipfel", - "double_valley": "Doppeltal", - "double_peak": "Doppelgipfel", + "double_dip": "Doppelmulde", + "duck_curve": "Entenkurve", "flat": "Flach", "rising": "Steigend", "falling": "Fallend", diff --git a/custom_components/tibber_prices/translations/en.json b/custom_components/tibber_prices/translations/en.json index 6e12364..b3defc8 100644 --- a/custom_components/tibber_prices/translations/en.json +++ b/custom_components/tibber_prices/translations/en.json @@ -264,7 +264,7 @@ "best_price_extend_to_very_cheap": "When enabled, detected best price periods expand outward to absorb adjacent intervals with a 'Very cheap' price level. This widens low-price windows to better capture extremely cheap intervals at the edges of detected periods.", "best_price_max_extension_intervals": "Maximum number of additional intervals to absorb per side (left and right edge). Each interval is 15 minutes. Example: 4 intervals = up to 1 hour extension per edge. Default: 4", "best_price_geometric_flex": "Extra flex percentage applied to intervals that fall inside a detected price valley (V-shape). When a valley pattern is detected for the day, intervals within the valley zone get this additional tolerance, making the period detector more likely to include them. 0 = disabled. Default: 0", - "best_price_segment_forcing": "When enabled, days with a W-shaped price curve (two valleys separated by a central peak) split at the central peak. Period detection runs independently for each valley side, ensuring each valley has the required number of periods. This prevents both periods from clustering in the same valley. Requires the day pattern sensor to detect a 'double_valley' pattern.", + "best_price_segment_forcing": "When enabled, days with a W-shaped price curve (two valleys separated by a central peak) split at the central peak. Period detection runs independently for each valley side, ensuring each valley has the required number of periods. This prevents both periods from clustering in the same valley. Requires the day pattern sensor to detect a 'double_dip' pattern.", "best_price_segment_min_periods": "Minimum number of best price periods required per valley side when W-shape segment forcing is enabled. Each side must independently produce at least this many periods. Default: 1" } } @@ -329,7 +329,7 @@ "peak_price_extend_to_very_expensive": "When enabled, detected peak price periods expand outward to absorb adjacent intervals with a 'Very expensive' price level. This widens high-price windows to better capture extremely expensive intervals at the edges of detected periods.", "peak_price_max_extension_intervals": "Maximum number of additional intervals to absorb per side (left and right edge). Each interval is 15 minutes. Example: 4 intervals = up to 1 hour extension per edge. Default: 4", "peak_price_geometric_flex": "Extra flex percentage applied to intervals that fall inside a detected price peak (Λ-shape). When a peak pattern is detected for the day, intervals within the peak zone get this additional tolerance, making the period detector more likely to include them. 0 = disabled. Default: 0", - "peak_price_segment_forcing": "When enabled, days with an M-shaped price curve (two peaks separated by a central valley) split at the central valley. Period detection runs independently for each peak side, ensuring each peak has the required number of periods. This prevents both periods from clustering in the same peak. Requires the day pattern sensor to detect a 'double_peak' pattern.", + "peak_price_segment_forcing": "When enabled, days with an M-shaped price curve (two peaks separated by a central valley) split at the central valley. Period detection runs independently for each peak side, ensuring each peak has the required number of periods. This prevents both periods from clustering in the same peak. Requires the day pattern sensor to detect a 'duck_curve' pattern.", "peak_price_segment_min_periods": "Minimum number of peak price periods required per peak side when M-shape segment forcing is enabled. Each side must independently produce at least this many periods. Default: 1" } } @@ -978,8 +978,8 @@ "state": { "valley": "Valley", "peak": "Peak", - "double_valley": "Double Valley", - "double_peak": "Double Peak", + "double_dip": "Double Dip", + "duck_curve": "Duck Curve", "flat": "Flat", "rising": "Rising", "falling": "Falling", @@ -991,8 +991,8 @@ "state": { "valley": "Valley", "peak": "Peak", - "double_valley": "Double Valley", - "double_peak": "Double Peak", + "double_dip": "Double Dip", + "duck_curve": "Duck Curve", "flat": "Flat", "rising": "Rising", "falling": "Falling", @@ -1004,8 +1004,8 @@ "state": { "valley": "Valley", "peak": "Peak", - "double_valley": "Double Valley", - "double_peak": "Double Peak", + "double_dip": "Double Dip", + "duck_curve": "Duck Curve", "flat": "Flat", "rising": "Rising", "falling": "Falling", diff --git a/custom_components/tibber_prices/translations/nb.json b/custom_components/tibber_prices/translations/nb.json index 5f7ffc9..3e0d8d4 100644 --- a/custom_components/tibber_prices/translations/nb.json +++ b/custom_components/tibber_prices/translations/nb.json @@ -978,8 +978,8 @@ "state": { "valley": "Dal", "peak": "Topp", - "double_valley": "Dobbel dal", - "double_peak": "Dobbel topp", + "double_dip": "Dobbel bunn", + "duck_curve": "Andekurve", "flat": "Flat", "rising": "Stigende", "falling": "Fallende", @@ -991,8 +991,8 @@ "state": { "valley": "Dal", "peak": "Topp", - "double_valley": "Dobbel dal", - "double_peak": "Dobbel topp", + "double_dip": "Dobbel bunn", + "duck_curve": "Andekurve", "flat": "Flat", "rising": "Stigende", "falling": "Fallende", @@ -1004,8 +1004,8 @@ "state": { "valley": "Dal", "peak": "Topp", - "double_valley": "Dobbel dal", - "double_peak": "Dobbel topp", + "double_dip": "Dobbel bunn", + "duck_curve": "Andekurve", "flat": "Flat", "rising": "Stigende", "falling": "Fallende", diff --git a/custom_components/tibber_prices/translations/nl.json b/custom_components/tibber_prices/translations/nl.json index cfbf741..b8519cb 100644 --- a/custom_components/tibber_prices/translations/nl.json +++ b/custom_components/tibber_prices/translations/nl.json @@ -978,8 +978,8 @@ "state": { "valley": "Dal", "peak": "Piek", - "double_valley": "Dubbel Dal", - "double_peak": "Dubbele Piek", + "double_dip": "Dubbele Dip", + "duck_curve": "Eendcurve", "flat": "Vlak", "rising": "Stijgend", "falling": "Dalend", @@ -991,8 +991,8 @@ "state": { "valley": "Dal", "peak": "Piek", - "double_valley": "Dubbel Dal", - "double_peak": "Dubbele Piek", + "double_dip": "Dubbele Dip", + "duck_curve": "Eendcurve", "flat": "Vlak", "rising": "Stijgend", "falling": "Dalend", @@ -1004,8 +1004,8 @@ "state": { "valley": "Dal", "peak": "Piek", - "double_valley": "Dubbel Dal", - "double_peak": "Dubbele Piek", + "double_dip": "Dubbele Dip", + "duck_curve": "Eendcurve", "flat": "Vlak", "rising": "Stijgend", "falling": "Dalend", diff --git a/custom_components/tibber_prices/translations/sv.json b/custom_components/tibber_prices/translations/sv.json index b3c8462..0e82d4d 100644 --- a/custom_components/tibber_prices/translations/sv.json +++ b/custom_components/tibber_prices/translations/sv.json @@ -978,8 +978,8 @@ "state": { "valley": "Dal", "peak": "Topp", - "double_valley": "Dubbeldal", - "double_peak": "Dubbeltopp", + "double_dip": "Dubbel dipp", + "duck_curve": "Ankkurva", "flat": "Flat", "rising": "Stigande", "falling": "Fallande", @@ -991,8 +991,8 @@ "state": { "valley": "Dal", "peak": "Topp", - "double_valley": "Dubbeldal", - "double_peak": "Dubbeltopp", + "double_dip": "Dubbel dipp", + "duck_curve": "Ankkurva", "flat": "Flat", "rising": "Stigande", "falling": "Fallande", @@ -1004,8 +1004,8 @@ "state": { "valley": "Dal", "peak": "Topp", - "double_valley": "Dubbeldal", - "double_peak": "Dubbeltopp", + "double_dip": "Dubbel dipp", + "duck_curve": "Ankkurva", "flat": "Flat", "rising": "Stigande", "falling": "Fallande",