mirror of
https://github.com/jpawlowski/hass.tibber_prices.git
synced 2026-05-28 18:43:40 +00:00
feat(periods): geometric V-shape flex extension for period detection
Uses valley/peak knee points from day pattern analysis to grant extra flex to price intervals that fall inside detected geometric zones, making period detection more permissive within V-shape (best price) or Λ-shape (peak price) price formations. New options: - CONF_BEST_PRICE_GEOMETRIC_FLEX (0-25%, default 0 = disabled) - CONF_PEAK_PRICE_GEOMETRIC_FLEX (0-25%, default 0 = disabled) Implementation: - compute_geometric_flex_bonus() in level_filtering.py checks if interval falls inside valley/peak zone and returns extra_flex - period_building.py applies geo bonus per-interval via criteria._replace(flex=...) and sets geometric_bonus_applied flag - period_statistics.py reports geometric_extension_active and geometric_extension_intervals in period summaries - Day patterns threaded through full pipeline: data_transformation → coordinator/core → periods → relaxation → calculate_periods → price_context - UI sliders in both extension_settings sections - Translations: en, de, nb, nl, sv Impact: Users with clearly V-shaped or Λ-shaped daily price curves can enable geometric flex to improve period detection accuracy within those characteristic shapes without increasing global flex.
This commit is contained in:
parent
e44f639b41
commit
4ddd19b132
16 changed files with 230 additions and 45 deletions
|
|
@ -14,6 +14,7 @@ from custom_components.tibber_prices.const import (
|
||||||
CONF_AVERAGE_SENSOR_DISPLAY,
|
CONF_AVERAGE_SENSOR_DISPLAY,
|
||||||
CONF_BEST_PRICE_EXTEND_TO_VERY_CHEAP,
|
CONF_BEST_PRICE_EXTEND_TO_VERY_CHEAP,
|
||||||
CONF_BEST_PRICE_FLEX,
|
CONF_BEST_PRICE_FLEX,
|
||||||
|
CONF_BEST_PRICE_GEOMETRIC_FLEX,
|
||||||
CONF_BEST_PRICE_MAX_EXTENSION_INTERVALS,
|
CONF_BEST_PRICE_MAX_EXTENSION_INTERVALS,
|
||||||
CONF_BEST_PRICE_MAX_LEVEL,
|
CONF_BEST_PRICE_MAX_LEVEL,
|
||||||
CONF_BEST_PRICE_MAX_LEVEL_GAP_COUNT,
|
CONF_BEST_PRICE_MAX_LEVEL_GAP_COUNT,
|
||||||
|
|
@ -27,6 +28,7 @@ from custom_components.tibber_prices.const import (
|
||||||
CONF_MIN_PERIODS_PEAK,
|
CONF_MIN_PERIODS_PEAK,
|
||||||
CONF_PEAK_PRICE_EXTEND_TO_VERY_EXPENSIVE,
|
CONF_PEAK_PRICE_EXTEND_TO_VERY_EXPENSIVE,
|
||||||
CONF_PEAK_PRICE_FLEX,
|
CONF_PEAK_PRICE_FLEX,
|
||||||
|
CONF_PEAK_PRICE_GEOMETRIC_FLEX,
|
||||||
CONF_PEAK_PRICE_MAX_EXTENSION_INTERVALS,
|
CONF_PEAK_PRICE_MAX_EXTENSION_INTERVALS,
|
||||||
CONF_PEAK_PRICE_MAX_LEVEL_GAP_COUNT,
|
CONF_PEAK_PRICE_MAX_LEVEL_GAP_COUNT,
|
||||||
CONF_PEAK_PRICE_MIN_DISTANCE_FROM_AVG,
|
CONF_PEAK_PRICE_MIN_DISTANCE_FROM_AVG,
|
||||||
|
|
@ -55,6 +57,7 @@ from custom_components.tibber_prices.const import (
|
||||||
DEFAULT_AVERAGE_SENSOR_DISPLAY,
|
DEFAULT_AVERAGE_SENSOR_DISPLAY,
|
||||||
DEFAULT_BEST_PRICE_EXTEND_TO_VERY_CHEAP,
|
DEFAULT_BEST_PRICE_EXTEND_TO_VERY_CHEAP,
|
||||||
DEFAULT_BEST_PRICE_FLEX,
|
DEFAULT_BEST_PRICE_FLEX,
|
||||||
|
DEFAULT_BEST_PRICE_GEOMETRIC_FLEX,
|
||||||
DEFAULT_BEST_PRICE_MAX_EXTENSION_INTERVALS,
|
DEFAULT_BEST_PRICE_MAX_EXTENSION_INTERVALS,
|
||||||
DEFAULT_BEST_PRICE_MAX_LEVEL,
|
DEFAULT_BEST_PRICE_MAX_LEVEL,
|
||||||
DEFAULT_BEST_PRICE_MAX_LEVEL_GAP_COUNT,
|
DEFAULT_BEST_PRICE_MAX_LEVEL_GAP_COUNT,
|
||||||
|
|
@ -67,6 +70,7 @@ from custom_components.tibber_prices.const import (
|
||||||
DEFAULT_MIN_PERIODS_PEAK,
|
DEFAULT_MIN_PERIODS_PEAK,
|
||||||
DEFAULT_PEAK_PRICE_EXTEND_TO_VERY_EXPENSIVE,
|
DEFAULT_PEAK_PRICE_EXTEND_TO_VERY_EXPENSIVE,
|
||||||
DEFAULT_PEAK_PRICE_FLEX,
|
DEFAULT_PEAK_PRICE_FLEX,
|
||||||
|
DEFAULT_PEAK_PRICE_GEOMETRIC_FLEX,
|
||||||
DEFAULT_PEAK_PRICE_MAX_EXTENSION_INTERVALS,
|
DEFAULT_PEAK_PRICE_MAX_EXTENSION_INTERVALS,
|
||||||
DEFAULT_PEAK_PRICE_MAX_LEVEL_GAP_COUNT,
|
DEFAULT_PEAK_PRICE_MAX_LEVEL_GAP_COUNT,
|
||||||
DEFAULT_PEAK_PRICE_MIN_DISTANCE_FROM_AVG,
|
DEFAULT_PEAK_PRICE_MIN_DISTANCE_FROM_AVG,
|
||||||
|
|
@ -96,6 +100,7 @@ from custom_components.tibber_prices.const import (
|
||||||
DISPLAY_MODE_SUBUNIT,
|
DISPLAY_MODE_SUBUNIT,
|
||||||
MAX_EXTENSION_INTERVALS,
|
MAX_EXTENSION_INTERVALS,
|
||||||
MAX_GAP_COUNT,
|
MAX_GAP_COUNT,
|
||||||
|
MAX_GEOMETRIC_FLEX,
|
||||||
MAX_MIN_PERIOD_LENGTH,
|
MAX_MIN_PERIOD_LENGTH,
|
||||||
MAX_MIN_PERIODS,
|
MAX_MIN_PERIODS,
|
||||||
MAX_PRICE_LEVEL_GAP_TOLERANCE,
|
MAX_PRICE_LEVEL_GAP_TOLERANCE,
|
||||||
|
|
@ -649,6 +654,7 @@ def get_best_price_schema(
|
||||||
max_extension_intervals_best = int(
|
max_extension_intervals_best = int(
|
||||||
extension_settings.get(CONF_BEST_PRICE_MAX_EXTENSION_INTERVALS, DEFAULT_BEST_PRICE_MAX_EXTENSION_INTERVALS)
|
extension_settings.get(CONF_BEST_PRICE_MAX_EXTENSION_INTERVALS, DEFAULT_BEST_PRICE_MAX_EXTENSION_INTERVALS)
|
||||||
)
|
)
|
||||||
|
geometric_flex_best = int(extension_settings.get(CONF_BEST_PRICE_GEOMETRIC_FLEX, DEFAULT_BEST_PRICE_GEOMETRIC_FLEX))
|
||||||
|
|
||||||
# Build section schemas with optional override warnings
|
# Build section schemas with optional override warnings
|
||||||
period_warning = get_section_override_warning("best_price", "period_settings", overrides, translations) or {}
|
period_warning = get_section_override_warning("best_price", "period_settings", overrides, translations) or {}
|
||||||
|
|
@ -788,6 +794,18 @@ def get_best_price_schema(
|
||||||
mode=NumberSelectorMode.SLIDER,
|
mode=NumberSelectorMode.SLIDER,
|
||||||
)
|
)
|
||||||
),
|
),
|
||||||
|
vol.Optional(
|
||||||
|
CONF_BEST_PRICE_GEOMETRIC_FLEX,
|
||||||
|
default=geometric_flex_best,
|
||||||
|
): NumberSelector(
|
||||||
|
NumberSelectorConfig(
|
||||||
|
min=0,
|
||||||
|
max=MAX_GEOMETRIC_FLEX,
|
||||||
|
step=1,
|
||||||
|
unit_of_measurement="%",
|
||||||
|
mode=NumberSelectorMode.SLIDER,
|
||||||
|
)
|
||||||
|
),
|
||||||
}
|
}
|
||||||
),
|
),
|
||||||
{"collapsed": True},
|
{"collapsed": True},
|
||||||
|
|
@ -839,6 +857,7 @@ def get_peak_price_schema(
|
||||||
max_extension_intervals_peak = int(
|
max_extension_intervals_peak = int(
|
||||||
extension_settings.get(CONF_PEAK_PRICE_MAX_EXTENSION_INTERVALS, DEFAULT_PEAK_PRICE_MAX_EXTENSION_INTERVALS)
|
extension_settings.get(CONF_PEAK_PRICE_MAX_EXTENSION_INTERVALS, DEFAULT_PEAK_PRICE_MAX_EXTENSION_INTERVALS)
|
||||||
)
|
)
|
||||||
|
geometric_flex_peak = int(extension_settings.get(CONF_PEAK_PRICE_GEOMETRIC_FLEX, DEFAULT_PEAK_PRICE_GEOMETRIC_FLEX))
|
||||||
|
|
||||||
# Build section schemas with optional override warnings
|
# Build section schemas with optional override warnings
|
||||||
period_warning = get_section_override_warning("peak_price", "period_settings", overrides, translations) or {}
|
period_warning = get_section_override_warning("peak_price", "period_settings", overrides, translations) or {}
|
||||||
|
|
@ -978,6 +997,18 @@ def get_peak_price_schema(
|
||||||
mode=NumberSelectorMode.SLIDER,
|
mode=NumberSelectorMode.SLIDER,
|
||||||
)
|
)
|
||||||
),
|
),
|
||||||
|
vol.Optional(
|
||||||
|
CONF_PEAK_PRICE_GEOMETRIC_FLEX,
|
||||||
|
default=geometric_flex_peak,
|
||||||
|
): NumberSelector(
|
||||||
|
NumberSelectorConfig(
|
||||||
|
min=0,
|
||||||
|
max=MAX_GEOMETRIC_FLEX,
|
||||||
|
step=1,
|
||||||
|
unit_of_measurement="%",
|
||||||
|
mode=NumberSelectorMode.SLIDER,
|
||||||
|
)
|
||||||
|
),
|
||||||
}
|
}
|
||||||
),
|
),
|
||||||
{"collapsed": True},
|
{"collapsed": True},
|
||||||
|
|
|
||||||
|
|
@ -70,8 +70,10 @@ CONF_MIN_PERIODS_PEAK = "min_periods_peak"
|
||||||
CONF_RELAXATION_ATTEMPTS_PEAK = "relaxation_attempts_peak"
|
CONF_RELAXATION_ATTEMPTS_PEAK = "relaxation_attempts_peak"
|
||||||
CONF_BEST_PRICE_EXTEND_TO_VERY_CHEAP = "best_price_extend_to_very_cheap"
|
CONF_BEST_PRICE_EXTEND_TO_VERY_CHEAP = "best_price_extend_to_very_cheap"
|
||||||
CONF_BEST_PRICE_MAX_EXTENSION_INTERVALS = "best_price_max_extension_intervals"
|
CONF_BEST_PRICE_MAX_EXTENSION_INTERVALS = "best_price_max_extension_intervals"
|
||||||
|
CONF_BEST_PRICE_GEOMETRIC_FLEX = "best_price_geometric_flex"
|
||||||
CONF_PEAK_PRICE_EXTEND_TO_VERY_EXPENSIVE = "peak_price_extend_to_very_expensive"
|
CONF_PEAK_PRICE_EXTEND_TO_VERY_EXPENSIVE = "peak_price_extend_to_very_expensive"
|
||||||
CONF_PEAK_PRICE_MAX_EXTENSION_INTERVALS = "peak_price_max_extension_intervals"
|
CONF_PEAK_PRICE_MAX_EXTENSION_INTERVALS = "peak_price_max_extension_intervals"
|
||||||
|
CONF_PEAK_PRICE_GEOMETRIC_FLEX = "peak_price_geometric_flex"
|
||||||
|
|
||||||
ATTRIBUTION = "Data provided by Tibber"
|
ATTRIBUTION = "Data provided by Tibber"
|
||||||
|
|
||||||
|
|
@ -137,8 +139,10 @@ DEFAULT_MIN_PERIODS_PEAK = 2 # Default: require at least 2 peak price periods (
|
||||||
DEFAULT_RELAXATION_ATTEMPTS_PEAK = 11 # Default: 11 steps allows escalation from 20% to 50% (3% increment per step)
|
DEFAULT_RELAXATION_ATTEMPTS_PEAK = 11 # Default: 11 steps allows escalation from 20% to 50% (3% increment per step)
|
||||||
DEFAULT_BEST_PRICE_EXTEND_TO_VERY_CHEAP = False # Default: disabled (opt-in feature)
|
DEFAULT_BEST_PRICE_EXTEND_TO_VERY_CHEAP = False # Default: disabled (opt-in feature)
|
||||||
DEFAULT_BEST_PRICE_MAX_EXTENSION_INTERVALS = 4 # Default: up to 4 intervals (1 hour) per side
|
DEFAULT_BEST_PRICE_MAX_EXTENSION_INTERVALS = 4 # Default: up to 4 intervals (1 hour) per side
|
||||||
|
DEFAULT_BEST_PRICE_GEOMETRIC_FLEX = 0 # Default: 0% (disabled); positive int % (e.g. 10 = 10%)
|
||||||
DEFAULT_PEAK_PRICE_EXTEND_TO_VERY_EXPENSIVE = False # Default: disabled (opt-in feature)
|
DEFAULT_PEAK_PRICE_EXTEND_TO_VERY_EXPENSIVE = False # Default: disabled (opt-in feature)
|
||||||
DEFAULT_PEAK_PRICE_MAX_EXTENSION_INTERVALS = 4 # Default: up to 4 intervals (1 hour) per side
|
DEFAULT_PEAK_PRICE_MAX_EXTENSION_INTERVALS = 4 # Default: up to 4 intervals (1 hour) per side
|
||||||
|
DEFAULT_PEAK_PRICE_GEOMETRIC_FLEX = 0 # Default: 0% (disabled); positive int % (e.g. 10 = 10%)
|
||||||
|
|
||||||
# Validation limits (used in GUI schemas and server-side validation)
|
# Validation limits (used in GUI schemas and server-side validation)
|
||||||
# These ensure consistency between frontend and backend validation
|
# These ensure consistency between frontend and backend validation
|
||||||
|
|
@ -148,6 +152,7 @@ MAX_GAP_COUNT = 8 # Maximum gap count for level filtering (GUI slider limit)
|
||||||
MAX_MIN_PERIODS = 10 # Maximum number of minimum periods per day (GUI slider limit)
|
MAX_MIN_PERIODS = 10 # Maximum number of minimum periods per day (GUI slider limit)
|
||||||
MAX_RELAXATION_ATTEMPTS = 12 # Maximum relaxation attempts (GUI slider limit)
|
MAX_RELAXATION_ATTEMPTS = 12 # Maximum relaxation attempts (GUI slider limit)
|
||||||
MAX_EXTENSION_INTERVALS = 12 # Maximum extension intervals per side (GUI slider limit = 3 hours)
|
MAX_EXTENSION_INTERVALS = 12 # Maximum extension intervals per side (GUI slider limit = 3 hours)
|
||||||
|
MAX_GEOMETRIC_FLEX = 25 # Maximum geometric flex bonus percentage (GUI slider limit)
|
||||||
MIN_PERIOD_LENGTH = 15 # Minimum period length in minutes (1 quarter hour)
|
MIN_PERIOD_LENGTH = 15 # Minimum period length in minutes (1 quarter hour)
|
||||||
MAX_MIN_PERIOD_LENGTH = 180 # Maximum for minimum period length setting (3 hours - realistic for required minimum)
|
MAX_MIN_PERIOD_LENGTH = 180 # Maximum for minimum period length setting (3 hours - realistic for required minimum)
|
||||||
|
|
||||||
|
|
@ -420,8 +425,10 @@ def get_default_options(currency_code: str | None) -> dict[str, Any]:
|
||||||
"extension_settings": {
|
"extension_settings": {
|
||||||
CONF_BEST_PRICE_EXTEND_TO_VERY_CHEAP: DEFAULT_BEST_PRICE_EXTEND_TO_VERY_CHEAP,
|
CONF_BEST_PRICE_EXTEND_TO_VERY_CHEAP: DEFAULT_BEST_PRICE_EXTEND_TO_VERY_CHEAP,
|
||||||
CONF_BEST_PRICE_MAX_EXTENSION_INTERVALS: DEFAULT_BEST_PRICE_MAX_EXTENSION_INTERVALS,
|
CONF_BEST_PRICE_MAX_EXTENSION_INTERVALS: DEFAULT_BEST_PRICE_MAX_EXTENSION_INTERVALS,
|
||||||
|
CONF_BEST_PRICE_GEOMETRIC_FLEX: DEFAULT_BEST_PRICE_GEOMETRIC_FLEX,
|
||||||
CONF_PEAK_PRICE_EXTEND_TO_VERY_EXPENSIVE: DEFAULT_PEAK_PRICE_EXTEND_TO_VERY_EXPENSIVE,
|
CONF_PEAK_PRICE_EXTEND_TO_VERY_EXPENSIVE: DEFAULT_PEAK_PRICE_EXTEND_TO_VERY_EXPENSIVE,
|
||||||
CONF_PEAK_PRICE_MAX_EXTENSION_INTERVALS: DEFAULT_PEAK_PRICE_MAX_EXTENSION_INTERVALS,
|
CONF_PEAK_PRICE_MAX_EXTENSION_INTERVALS: DEFAULT_PEAK_PRICE_MAX_EXTENSION_INTERVALS,
|
||||||
|
CONF_PEAK_PRICE_GEOMETRIC_FLEX: DEFAULT_PEAK_PRICE_GEOMETRIC_FLEX,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -875,9 +875,11 @@ class TibberPricesDataUpdateCoordinator(DataUpdateCoordinator[dict[str, Any]]):
|
||||||
"""Get threshold percentages from config options."""
|
"""Get threshold percentages from config options."""
|
||||||
return self._data_transformer.get_threshold_percentages()
|
return self._data_transformer.get_threshold_percentages()
|
||||||
|
|
||||||
def _calculate_periods_for_price_info(self, price_info: dict[str, Any]) -> dict[str, Any]:
|
def _calculate_periods_for_price_info(
|
||||||
|
self, price_info: dict[str, Any], day_patterns: dict[str, Any] | None = None
|
||||||
|
) -> dict[str, Any]:
|
||||||
"""Calculate periods (best price and peak price) for the given price info."""
|
"""Calculate periods (best price and peak price) for the given price info."""
|
||||||
return self._period_calculator.calculate_periods_for_price_info(price_info)
|
return self._period_calculator.calculate_periods_for_price_info(price_info, day_patterns)
|
||||||
|
|
||||||
def _transform_data(self, raw_data: dict[str, Any]) -> dict[str, Any]:
|
def _transform_data(self, raw_data: dict[str, Any]) -> dict[str, Any]:
|
||||||
"""Transform raw data for main entry (aggregated view of all homes)."""
|
"""Transform raw data for main entry (aggregated view of all homes)."""
|
||||||
|
|
|
||||||
|
|
@ -28,7 +28,7 @@ class TibberPricesDataTransformer:
|
||||||
self,
|
self,
|
||||||
config_entry: ConfigEntry,
|
config_entry: ConfigEntry,
|
||||||
log_prefix: str,
|
log_prefix: str,
|
||||||
calculate_periods_fn: Callable[[dict[str, Any]], dict[str, Any]],
|
calculate_periods_fn: Callable[[dict[str, Any], dict[str, Any] | None], dict[str, Any]],
|
||||||
time: TibberPricesTimeService,
|
time: TibberPricesTimeService,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Initialize the data transformer."""
|
"""Initialize the data transformer."""
|
||||||
|
|
@ -271,16 +271,19 @@ class TibberPricesDataTransformer:
|
||||||
"currency": currency,
|
"currency": currency,
|
||||||
}
|
}
|
||||||
|
|
||||||
# Calculate periods (best price and peak price)
|
|
||||||
if "priceInfo" in transformed_data:
|
|
||||||
transformed_data["pricePeriods"] = self._calculate_periods_fn(transformed_data["priceInfo"])
|
|
||||||
|
|
||||||
# Detect day patterns (yesterday / today / tomorrow)
|
# Detect day patterns (yesterday / today / tomorrow)
|
||||||
|
# IMPORTANT: Must be computed BEFORE pricePeriods so geometric flex can use pattern data
|
||||||
transformed_data["dayPatterns"] = detect_day_patterns(
|
transformed_data["dayPatterns"] = detect_day_patterns(
|
||||||
transformed_data["priceInfo"],
|
transformed_data["priceInfo"],
|
||||||
time=self.time,
|
time=self.time,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# Calculate periods (best price and peak price)
|
||||||
|
if "priceInfo" in transformed_data:
|
||||||
|
transformed_data["pricePeriods"] = self._calculate_periods_fn(
|
||||||
|
transformed_data["priceInfo"], transformed_data.get("dayPatterns")
|
||||||
|
)
|
||||||
|
|
||||||
# Cache the transformed data
|
# Cache the transformed data
|
||||||
self._cached_transformed_data = transformed_data
|
self._cached_transformed_data = transformed_data
|
||||||
self._last_transformation_config = self._get_current_transformation_config()
|
self._last_transformation_config = self._get_current_transformation_config()
|
||||||
|
|
|
||||||
|
|
@ -38,6 +38,7 @@ def calculate_periods(
|
||||||
*,
|
*,
|
||||||
config: TibberPricesPeriodConfig,
|
config: TibberPricesPeriodConfig,
|
||||||
time: TibberPricesTimeService,
|
time: TibberPricesTimeService,
|
||||||
|
day_patterns_by_date: dict | None = None,
|
||||||
) -> dict[str, Any]:
|
) -> dict[str, Any]:
|
||||||
"""
|
"""
|
||||||
Calculate price periods (best or peak) from price data.
|
Calculate price periods (best or peak) from price data.
|
||||||
|
|
@ -59,6 +60,7 @@ def calculate_periods(
|
||||||
config: Period configuration containing reverse_sort, flex, min_distance_from_avg,
|
config: Period configuration containing reverse_sort, flex, min_distance_from_avg,
|
||||||
min_period_length, threshold_low, and threshold_high.
|
min_period_length, threshold_low, and threshold_high.
|
||||||
time: TibberPricesTimeService instance (required).
|
time: TibberPricesTimeService instance (required).
|
||||||
|
day_patterns_by_date: Optional dict mapping date → day pattern dict for geometric flex bonus.
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
Dict with:
|
Dict with:
|
||||||
|
|
@ -156,6 +158,8 @@ def calculate_periods(
|
||||||
"intervals_by_day": intervals_by_day, # Needed for day volatility calculation
|
"intervals_by_day": intervals_by_day, # Needed for day volatility calculation
|
||||||
"flex": flex,
|
"flex": flex,
|
||||||
"min_distance_from_avg": min_distance_from_avg,
|
"min_distance_from_avg": min_distance_from_avg,
|
||||||
|
"geometric_extra_flex": config.geometric_extra_flex, # Extra flex for geometric zone
|
||||||
|
"day_patterns_by_date": day_patterns_by_date, # Pattern data keyed by date (may be None)
|
||||||
}
|
}
|
||||||
raw_periods = build_periods(
|
raw_periods = build_periods(
|
||||||
all_prices_smoothed, # Use smoothed prices for period formation
|
all_prices_smoothed, # Use smoothed prices for period formation
|
||||||
|
|
|
||||||
|
|
@ -11,9 +11,11 @@ See docs/development/period-calculation-theory.md for detailed explanation.
|
||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
from typing import TYPE_CHECKING
|
from typing import TYPE_CHECKING, Any
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
|
from datetime import datetime
|
||||||
|
|
||||||
from .types import TibberPricesIntervalCriteria
|
from .types import TibberPricesIntervalCriteria
|
||||||
|
|
||||||
from custom_components.tibber_prices.const import PRICE_LEVEL_MAPPING
|
from custom_components.tibber_prices.const import PRICE_LEVEL_MAPPING
|
||||||
|
|
@ -241,3 +243,54 @@ def check_interval_criteria(
|
||||||
meets_min_distance = price <= min_distance_threshold
|
meets_min_distance = price <= min_distance_threshold
|
||||||
|
|
||||||
return in_flex, meets_min_distance
|
return in_flex, meets_min_distance
|
||||||
|
|
||||||
|
|
||||||
|
def compute_geometric_flex_bonus(
|
||||||
|
interval_time: datetime,
|
||||||
|
day_pattern: dict[str, Any] | None,
|
||||||
|
*,
|
||||||
|
extra_flex: float,
|
||||||
|
reverse_sort: bool,
|
||||||
|
) -> float:
|
||||||
|
"""
|
||||||
|
Return extra flex if interval falls within the valley/peak geometric zone.
|
||||||
|
|
||||||
|
For best price (reverse_sort=False): widens flex inside the VALLEY zone
|
||||||
|
defined by [valley_start, valley_end] knee points.
|
||||||
|
For peak price (reverse_sort=True): widens flex inside the PEAK zone
|
||||||
|
defined by [peak_start, peak_end] knee points.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
interval_time: Timezone-aware datetime of the interval's start.
|
||||||
|
day_pattern: DayPatternDict for the interval's calendar day, or None.
|
||||||
|
extra_flex: Additional flex to add (decimal, e.g. 0.10 for 10%).
|
||||||
|
reverse_sort: True for peak price, False for best price.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
``extra_flex`` if the interval is inside the geometric zone, else ``0.0``.
|
||||||
|
|
||||||
|
"""
|
||||||
|
if not day_pattern or extra_flex <= 0:
|
||||||
|
return 0.0
|
||||||
|
|
||||||
|
pattern = day_pattern.get("pattern", "")
|
||||||
|
|
||||||
|
if reverse_sort:
|
||||||
|
# Peak price: expand inside PEAK (Λ-shape) zone
|
||||||
|
if pattern != "peak":
|
||||||
|
return 0.0
|
||||||
|
zone_start = day_pattern.get("peak_start")
|
||||||
|
zone_end = day_pattern.get("peak_end")
|
||||||
|
else:
|
||||||
|
# Best price: expand inside VALLEY (V/U-shape) zone
|
||||||
|
if pattern != "valley":
|
||||||
|
return 0.0
|
||||||
|
zone_start = day_pattern.get("valley_start")
|
||||||
|
zone_end = day_pattern.get("valley_end")
|
||||||
|
|
||||||
|
if zone_start is None or zone_end is None:
|
||||||
|
return 0.0
|
||||||
|
|
||||||
|
if zone_start <= interval_time <= zone_end:
|
||||||
|
return extra_flex
|
||||||
|
return 0.0
|
||||||
|
|
|
||||||
|
|
@ -14,6 +14,7 @@ if TYPE_CHECKING:
|
||||||
from .level_filtering import (
|
from .level_filtering import (
|
||||||
apply_level_filter,
|
apply_level_filter,
|
||||||
check_interval_criteria,
|
check_interval_criteria,
|
||||||
|
compute_geometric_flex_bonus,
|
||||||
)
|
)
|
||||||
from .types import TibberPricesIntervalCriteria
|
from .types import TibberPricesIntervalCriteria
|
||||||
|
|
||||||
|
|
@ -83,6 +84,8 @@ def build_periods( # noqa: PLR0913, PLR0915, PLR0912 - Complex period building
|
||||||
avg_prices = price_context["avg_prices"]
|
avg_prices = price_context["avg_prices"]
|
||||||
flex = price_context["flex"]
|
flex = price_context["flex"]
|
||||||
min_distance_from_avg = price_context["min_distance_from_avg"]
|
min_distance_from_avg = price_context["min_distance_from_avg"]
|
||||||
|
geometric_extra_flex: float = float(price_context.get("geometric_extra_flex", 0.0))
|
||||||
|
day_patterns_by_date: dict[date, dict[str, Any]] | None = price_context.get("day_patterns_by_date")
|
||||||
|
|
||||||
# Calculate level_order if level_filter is active
|
# Calculate level_order if level_filter is active
|
||||||
level_order = None
|
level_order = None
|
||||||
|
|
@ -147,7 +150,20 @@ def build_periods( # noqa: PLR0913, PLR0915, PLR0912 - Complex period building
|
||||||
|
|
||||||
# Check flex and minimum distance criteria (using smoothed price and interval's own day reference)
|
# Check flex and minimum distance criteria (using smoothed price and interval's own day reference)
|
||||||
criteria = criteria_by_day[ref_date]
|
criteria = criteria_by_day[ref_date]
|
||||||
in_flex, meets_min_distance = check_interval_criteria(price_for_criteria, criteria)
|
|
||||||
|
# Compute geometric flex bonus if pattern-aware expansion is enabled
|
||||||
|
geo_bonus = 0.0
|
||||||
|
if geometric_extra_flex > 0 and day_patterns_by_date is not None:
|
||||||
|
day_pattern_for_date = day_patterns_by_date.get(ref_date)
|
||||||
|
geo_bonus = compute_geometric_flex_bonus(
|
||||||
|
starts_at,
|
||||||
|
day_pattern_for_date,
|
||||||
|
extra_flex=geometric_extra_flex,
|
||||||
|
reverse_sort=reverse_sort,
|
||||||
|
)
|
||||||
|
|
||||||
|
effective_criteria = criteria._replace(flex=criteria.flex + geo_bonus) if geo_bonus > 0 else criteria
|
||||||
|
in_flex, meets_min_distance = check_interval_criteria(price_for_criteria, effective_criteria)
|
||||||
|
|
||||||
# Track why intervals are filtered
|
# Track why intervals are filtered
|
||||||
if not in_flex:
|
if not in_flex:
|
||||||
|
|
@ -159,7 +175,7 @@ def build_periods( # noqa: PLR0913, PLR0915, PLR0912 - Complex period building
|
||||||
smoothing_was_impactful = False
|
smoothing_was_impactful = False
|
||||||
if price_data.get("_smoothed", False):
|
if price_data.get("_smoothed", False):
|
||||||
# Check if original price would have passed the same criteria
|
# Check if original price would have passed the same criteria
|
||||||
in_flex_original, meets_min_distance_original = check_interval_criteria(price_original, criteria)
|
in_flex_original, meets_min_distance_original = check_interval_criteria(price_original, effective_criteria)
|
||||||
# Smoothing was impactful if original would have failed but smoothed passed
|
# Smoothing was impactful if original would have failed but smoothed passed
|
||||||
smoothing_was_impactful = (in_flex and meets_min_distance) and not (
|
smoothing_was_impactful = (in_flex and meets_min_distance) and not (
|
||||||
in_flex_original and meets_min_distance_original
|
in_flex_original and meets_min_distance_original
|
||||||
|
|
@ -184,6 +200,7 @@ def build_periods( # noqa: PLR0913, PLR0915, PLR0912 - Complex period building
|
||||||
# Only True if smoothing changed whether the interval qualified for period inclusion
|
# Only True if smoothing changed whether the interval qualified for period inclusion
|
||||||
"smoothing_was_impactful": smoothing_was_impactful,
|
"smoothing_was_impactful": smoothing_was_impactful,
|
||||||
"is_level_gap": is_level_gap, # Track if kept due to level gap tolerance
|
"is_level_gap": is_level_gap, # Track if kept due to level gap tolerance
|
||||||
|
"geometric_bonus_applied": geo_bonus > 0, # True if interval is in geometric zone
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
elif current_period:
|
elif current_period:
|
||||||
|
|
|
||||||
|
|
@ -220,6 +220,17 @@ def build_period_summary_dict(
|
||||||
return summary
|
return summary
|
||||||
|
|
||||||
|
|
||||||
|
def _add_interval_flag_counts(summary: dict, period: list[dict]) -> None:
|
||||||
|
"""Add optional interval flag counts to period summary."""
|
||||||
|
if (count := sum(1 for i in period if i.get("smoothing_was_impactful", False))) > 0:
|
||||||
|
summary["period_interval_smoothed_count"] = count
|
||||||
|
if (count := sum(1 for i in period if i.get("is_level_gap", False))) > 0:
|
||||||
|
summary["period_interval_level_gap_count"] = count
|
||||||
|
if (count := sum(1 for i in period if i.get("geometric_bonus_applied", False))) > 0:
|
||||||
|
summary["geometric_extension_active"] = True
|
||||||
|
summary["geometric_extension_intervals"] = count
|
||||||
|
|
||||||
|
|
||||||
def extract_period_summaries(
|
def extract_period_summaries(
|
||||||
periods: list[list[dict]],
|
periods: list[list[dict]],
|
||||||
all_prices: list[dict],
|
all_prices: list[dict],
|
||||||
|
|
@ -328,12 +339,6 @@ def extract_period_summaries(
|
||||||
).lower()
|
).lower()
|
||||||
rating_difference_pct = calculate_aggregated_rating_difference(period_price_data)
|
rating_difference_pct = calculate_aggregated_rating_difference(period_price_data)
|
||||||
|
|
||||||
# Count how many intervals in this period benefited from smoothing (i.e., would have been excluded)
|
|
||||||
smoothed_impactful_count = sum(1 for interval in period if interval.get("smoothing_was_impactful", False))
|
|
||||||
|
|
||||||
# Count how many intervals were kept due to level filter gap tolerance
|
|
||||||
level_gap_count = sum(1 for interval in period if interval.get("is_level_gap", False))
|
|
||||||
|
|
||||||
# Build period data and statistics objects
|
# Build period data and statistics objects
|
||||||
period_data = TibberPricesPeriodData(
|
period_data = TibberPricesPeriodData(
|
||||||
start_time=start_time,
|
start_time=start_time,
|
||||||
|
|
@ -363,13 +368,8 @@ def extract_period_summaries(
|
||||||
period_data, stats, reverse_sort=thresholds.reverse_sort, price_context=price_context
|
period_data, stats, reverse_sort=thresholds.reverse_sort, price_context=price_context
|
||||||
)
|
)
|
||||||
|
|
||||||
# Add smoothing information if any intervals benefited from smoothing
|
# Add optional interval flag counts (smoothing, level gaps, geometric extension)
|
||||||
if smoothed_impactful_count > 0:
|
_add_interval_flag_counts(summary, period)
|
||||||
summary["period_interval_smoothed_count"] = smoothed_impactful_count
|
|
||||||
|
|
||||||
# Add level gap tolerance information if any intervals were kept as gaps
|
|
||||||
if level_gap_count > 0:
|
|
||||||
summary["period_interval_level_gap_count"] = level_gap_count
|
|
||||||
|
|
||||||
summaries.append(summary)
|
summaries.append(summary)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -526,6 +526,7 @@ def calculate_periods_with_relaxation( # noqa: PLR0912, PLR0913, PLR0915 - Per-
|
||||||
should_show_callback: Callable[[str | None], bool],
|
should_show_callback: Callable[[str | None], bool],
|
||||||
time: TibberPricesTimeService,
|
time: TibberPricesTimeService,
|
||||||
config_entry: Any, # ConfigEntry type
|
config_entry: Any, # ConfigEntry type
|
||||||
|
day_patterns_by_date: dict | None = None,
|
||||||
) -> dict[str, Any]:
|
) -> dict[str, Any]:
|
||||||
"""
|
"""
|
||||||
Calculate periods with optional per-day filter relaxation.
|
Calculate periods with optional per-day filter relaxation.
|
||||||
|
|
@ -552,6 +553,8 @@ def calculate_periods_with_relaxation( # noqa: PLR0912, PLR0913, PLR0915 - Per-
|
||||||
to use original configured filter values.
|
to use original configured filter values.
|
||||||
time: TibberPricesTimeService instance (required).
|
time: TibberPricesTimeService instance (required).
|
||||||
config_entry: Config entry to get display unit configuration.
|
config_entry: Config entry to get display unit configuration.
|
||||||
|
day_patterns_by_date: Optional dict mapping date → day pattern dict. Used for
|
||||||
|
geometric flex bonus in period detection. Passed through to calculate_periods().
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
Dict with same format as calculate_periods() output:
|
Dict with same format as calculate_periods() output:
|
||||||
|
|
@ -709,7 +712,7 @@ def calculate_periods_with_relaxation( # noqa: PLR0912, PLR0913, PLR0915 - Per-
|
||||||
# === BASELINE CALCULATION (process ALL prices together, including yesterday) ===
|
# === BASELINE CALCULATION (process ALL prices together, including yesterday) ===
|
||||||
# Periods that ended before yesterday will be filtered out later by filter_periods_by_end_date()
|
# Periods that ended before yesterday will be filtered out later by filter_periods_by_end_date()
|
||||||
# This keeps yesterday/today/tomorrow periods in the cache
|
# This keeps yesterday/today/tomorrow periods in the cache
|
||||||
baseline_result = calculate_periods(all_prices, config=config, time=time)
|
baseline_result = calculate_periods(all_prices, config=config, time=time, day_patterns_by_date=day_patterns_by_date)
|
||||||
all_periods = baseline_result["periods"]
|
all_periods = baseline_result["periods"]
|
||||||
|
|
||||||
# Count periods per day for min_periods check
|
# Count periods per day for min_periods check
|
||||||
|
|
@ -765,6 +768,7 @@ def calculate_periods_with_relaxation( # noqa: PLR0912, PLR0913, PLR0915 - Per-
|
||||||
baseline_periods=all_periods,
|
baseline_periods=all_periods,
|
||||||
time=time,
|
time=time,
|
||||||
config_entry=config_entry,
|
config_entry=config_entry,
|
||||||
|
day_patterns_by_date=day_patterns_by_date,
|
||||||
)
|
)
|
||||||
|
|
||||||
all_periods = relaxed_result["periods"]
|
all_periods = relaxed_result["periods"]
|
||||||
|
|
@ -865,6 +869,7 @@ def relax_all_prices( # noqa: PLR0913 - Comprehensive filter relaxation require
|
||||||
*,
|
*,
|
||||||
time: TibberPricesTimeService,
|
time: TibberPricesTimeService,
|
||||||
config_entry: Any, # ConfigEntry type
|
config_entry: Any, # ConfigEntry type
|
||||||
|
day_patterns_by_date: dict | None = None,
|
||||||
) -> tuple[dict[str, Any], dict[str, Any]]:
|
) -> tuple[dict[str, Any], dict[str, Any]]:
|
||||||
"""
|
"""
|
||||||
Relax filters for all prices until min_periods per day is reached.
|
Relax filters for all prices until min_periods per day is reached.
|
||||||
|
|
@ -883,6 +888,8 @@ def relax_all_prices( # noqa: PLR0913 - Comprehensive filter relaxation require
|
||||||
baseline_periods: Baseline periods (before relaxation).
|
baseline_periods: Baseline periods (before relaxation).
|
||||||
time: TibberPricesTimeService instance.
|
time: TibberPricesTimeService instance.
|
||||||
config_entry: Config entry to get display unit configuration.
|
config_entry: Config entry to get display unit configuration.
|
||||||
|
day_patterns_by_date: Optional dict mapping date → day pattern dict. Used for
|
||||||
|
geometric flex bonus in period detection. Passed through to calculate_periods().
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
Tuple of (result_dict, metadata_dict)
|
Tuple of (result_dict, metadata_dict)
|
||||||
|
|
@ -947,7 +954,9 @@ def relax_all_prices( # noqa: PLR0913 - Comprehensive filter relaxation require
|
||||||
)
|
)
|
||||||
|
|
||||||
# Process ALL prices together (allows midnight crossing)
|
# Process ALL prices together (allows midnight crossing)
|
||||||
result = calculate_periods(all_prices, config=relaxed_config, time=time)
|
result = calculate_periods(
|
||||||
|
all_prices, config=relaxed_config, time=time, day_patterns_by_date=day_patterns_by_date
|
||||||
|
)
|
||||||
new_periods = result["periods"]
|
new_periods = result["periods"]
|
||||||
|
|
||||||
_LOGGER_DETAILS.debug(
|
_LOGGER_DETAILS.debug(
|
||||||
|
|
|
||||||
|
|
@ -58,6 +58,7 @@ class TibberPricesPeriodConfig(NamedTuple):
|
||||||
gap_count: int = 0 # Number of allowed consecutive deviating intervals
|
gap_count: int = 0 # Number of allowed consecutive deviating intervals
|
||||||
extend_to_extreme: bool = False # Extend periods into adjacent VERY_CHEAP/VERY_EXPENSIVE intervals
|
extend_to_extreme: bool = False # Extend periods into adjacent VERY_CHEAP/VERY_EXPENSIVE intervals
|
||||||
max_extension_intervals: int = 0 # Max intervals this extension may add per side (0 = disabled)
|
max_extension_intervals: int = 0 # Max intervals this extension may add per side (0 = disabled)
|
||||||
|
geometric_extra_flex: float = 0.0 # Extra flex (decimal) for intervals inside the valley/peak zone (0.0 = disabled)
|
||||||
|
|
||||||
|
|
||||||
class TibberPricesPeriodData(NamedTuple):
|
class TibberPricesPeriodData(NamedTuple):
|
||||||
|
|
|
||||||
|
|
@ -8,6 +8,7 @@ gap tolerance, and coordination of the period_handlers calculation functions.
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import logging
|
import logging
|
||||||
|
from datetime import date, timedelta
|
||||||
from typing import TYPE_CHECKING, Any
|
from typing import TYPE_CHECKING, Any
|
||||||
|
|
||||||
from custom_components.tibber_prices import const as _const
|
from custom_components.tibber_prices import const as _const
|
||||||
|
|
@ -260,6 +261,25 @@ class TibberPricesPeriodCalculator:
|
||||||
config["extend_to_extreme"] = extend_to_extreme
|
config["extend_to_extreme"] = extend_to_extreme
|
||||||
config["max_extension_intervals"] = max_extension_intervals
|
config["max_extension_intervals"] = max_extension_intervals
|
||||||
|
|
||||||
|
# Geometric flex bonus (intervals inside valley/peak zone get extra flex)
|
||||||
|
if reverse_sort:
|
||||||
|
geometric_flex_pct = int(
|
||||||
|
self._get_option(
|
||||||
|
_const.CONF_PEAK_PRICE_GEOMETRIC_FLEX,
|
||||||
|
"extension_settings",
|
||||||
|
_const.DEFAULT_PEAK_PRICE_GEOMETRIC_FLEX,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
geometric_flex_pct = int(
|
||||||
|
self._get_option(
|
||||||
|
_const.CONF_BEST_PRICE_GEOMETRIC_FLEX,
|
||||||
|
"extension_settings",
|
||||||
|
_const.DEFAULT_BEST_PRICE_GEOMETRIC_FLEX,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
config["geometric_extra_flex"] = geometric_flex_pct / 100
|
||||||
|
|
||||||
# Cache the result
|
# Cache the result
|
||||||
self._config_cache[cache_key] = config
|
self._config_cache[cache_key] = config
|
||||||
self._config_cache_valid = True
|
self._config_cache_valid = True
|
||||||
|
|
@ -633,6 +653,7 @@ class TibberPricesPeriodCalculator:
|
||||||
def calculate_periods_for_price_info(
|
def calculate_periods_for_price_info(
|
||||||
self,
|
self,
|
||||||
price_info: dict[str, Any],
|
price_info: dict[str, Any],
|
||||||
|
day_patterns: dict[str, Any] | None = None,
|
||||||
) -> dict[str, Any]:
|
) -> dict[str, Any]:
|
||||||
"""
|
"""
|
||||||
Calculate periods (best price and peak price) for the given price info.
|
Calculate periods (best price and peak price) for the given price info.
|
||||||
|
|
@ -657,6 +678,19 @@ class TibberPricesPeriodCalculator:
|
||||||
coordinator_data = {"priceInfo": price_info}
|
coordinator_data = {"priceInfo": price_info}
|
||||||
all_prices = get_intervals_for_day_offsets(coordinator_data, [-2, -1, 0, 1])
|
all_prices = get_intervals_for_day_offsets(coordinator_data, [-2, -1, 0, 1])
|
||||||
|
|
||||||
|
# Convert day_patterns (keyed by "yesterday"/"today"/"tomorrow") to date-keyed dict
|
||||||
|
# Needed for geometric valley/peak zone flex bonus in period calculation
|
||||||
|
today_date = self.time.now().date()
|
||||||
|
day_patterns_by_date: dict[date, dict[str, Any]] | None = (
|
||||||
|
{
|
||||||
|
today_date + timedelta(days=ofs): pat
|
||||||
|
for ofs, lbl in ((-1, "yesterday"), (0, "today"), (1, "tomorrow"))
|
||||||
|
if (pat := day_patterns.get(lbl)) is not None
|
||||||
|
}
|
||||||
|
if day_patterns
|
||||||
|
else None
|
||||||
|
)
|
||||||
|
|
||||||
# Get rating thresholds from config (flat in options, not in sections)
|
# Get rating thresholds from config (flat in options, not in sections)
|
||||||
# CRITICAL: Price rating thresholds are stored FLAT in options (no sections)
|
# CRITICAL: Price rating thresholds are stored FLAT in options (no sections)
|
||||||
threshold_low = self.config_entry.options.get(
|
threshold_low = self.config_entry.options.get(
|
||||||
|
|
@ -739,6 +773,7 @@ class TibberPricesPeriodCalculator:
|
||||||
gap_count=gap_count_best,
|
gap_count=gap_count_best,
|
||||||
extend_to_extreme=best_config["extend_to_extreme"],
|
extend_to_extreme=best_config["extend_to_extreme"],
|
||||||
max_extension_intervals=best_config["max_extension_intervals"],
|
max_extension_intervals=best_config["max_extension_intervals"],
|
||||||
|
geometric_extra_flex=best_config["geometric_extra_flex"],
|
||||||
)
|
)
|
||||||
best_periods = calculate_periods_with_relaxation(
|
best_periods = calculate_periods_with_relaxation(
|
||||||
all_prices,
|
all_prices,
|
||||||
|
|
@ -753,6 +788,7 @@ class TibberPricesPeriodCalculator:
|
||||||
),
|
),
|
||||||
time=self.time,
|
time=self.time,
|
||||||
config_entry=self.config_entry,
|
config_entry=self.config_entry,
|
||||||
|
day_patterns_by_date=day_patterns_by_date,
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
best_periods = {
|
best_periods = {
|
||||||
|
|
@ -822,6 +858,7 @@ class TibberPricesPeriodCalculator:
|
||||||
gap_count=gap_count_peak,
|
gap_count=gap_count_peak,
|
||||||
extend_to_extreme=peak_config["extend_to_extreme"],
|
extend_to_extreme=peak_config["extend_to_extreme"],
|
||||||
max_extension_intervals=peak_config["max_extension_intervals"],
|
max_extension_intervals=peak_config["max_extension_intervals"],
|
||||||
|
geometric_extra_flex=peak_config["geometric_extra_flex"],
|
||||||
)
|
)
|
||||||
peak_periods = calculate_periods_with_relaxation(
|
peak_periods = calculate_periods_with_relaxation(
|
||||||
all_prices,
|
all_prices,
|
||||||
|
|
@ -836,6 +873,7 @@ class TibberPricesPeriodCalculator:
|
||||||
),
|
),
|
||||||
time=self.time,
|
time=self.time,
|
||||||
config_entry=self.config_entry,
|
config_entry=self.config_entry,
|
||||||
|
day_patterns_by_date=day_patterns_by_date,
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
peak_periods = {
|
peak_periods = {
|
||||||
|
|
|
||||||
|
|
@ -244,11 +244,13 @@
|
||||||
"description": "Erkannte Bestpreisperioden optional an beiden Rändern erweitern, um angrenzende sehr günstige Intervalle aufzunehmen.",
|
"description": "Erkannte Bestpreisperioden optional an beiden Rändern erweitern, um angrenzende sehr günstige Intervalle aufzunehmen.",
|
||||||
"data": {
|
"data": {
|
||||||
"best_price_extend_to_very_cheap": "Auf sehr günstige Intervalle erweitern",
|
"best_price_extend_to_very_cheap": "Auf sehr günstige Intervalle erweitern",
|
||||||
"best_price_max_extension_intervals": "Maximale Erweiterungsintervalle"
|
"best_price_max_extension_intervals": "Maximale Erweiterungsintervalle",
|
||||||
|
"best_price_geometric_flex": "Geometrischer Flex-Bonus"
|
||||||
},
|
},
|
||||||
"data_description": {
|
"data_description": {
|
||||||
"best_price_extend_to_very_cheap": "Wenn aktiviert, erweitern sich erkannte Bestpreisperioden nach außen, um angrenzende Intervalle mit dem Preisniveau 'Sehr günstig' aufzunehmen. So werden extrem günstige Intervalle an den Rändern erkannter Perioden besser erfasst.",
|
"best_price_extend_to_very_cheap": "Wenn aktiviert, erweitern sich erkannte Bestpreisperioden nach außen, um angrenzende Intervalle mit dem Preisniveau 'Sehr günstig' aufzunehmen. So werden extrem günstige Intervalle an den Rändern erkannter Perioden besser erfasst.",
|
||||||
"best_price_max_extension_intervals": "Maximale Anzahl zusätzlicher Intervalle pro Seite (linker und rechter Rand). Jedes Intervall dauert 15 Minuten. Beispiel: 4 Intervalle = bis zu 1 Stunde Erweiterung pro Rand. Standard: 4"
|
"best_price_max_extension_intervals": "Maximale Anzahl zusätzlicher Intervalle pro Seite (linker und rechter Rand). Jedes Intervall dauert 15 Minuten. Beispiel: 4 Intervalle = bis zu 1 Stunde Erweiterung pro Rand. Standard: 4",
|
||||||
|
"best_price_geometric_flex": "Zusätzlicher Flex-Prozentsatz für Intervalle, die in ein erkanntes Preistal (V-Form) fallen. Wenn für den Tag ein Tal-Muster erkannt wird, erhalten Intervalle innerhalb der Talzone diese zusätzliche Toleranz, damit der Periodendetektor sie eher einschließt. 0 = deaktiviert. Standard: 0"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
@ -303,11 +305,13 @@
|
||||||
"description": "Erkannte Spitzenpreisperioden optional an beiden Rändern erweitern, um angrenzende sehr teure Intervalle aufzunehmen.",
|
"description": "Erkannte Spitzenpreisperioden optional an beiden Rändern erweitern, um angrenzende sehr teure Intervalle aufzunehmen.",
|
||||||
"data": {
|
"data": {
|
||||||
"peak_price_extend_to_very_expensive": "Auf sehr teure Intervalle erweitern",
|
"peak_price_extend_to_very_expensive": "Auf sehr teure Intervalle erweitern",
|
||||||
"peak_price_max_extension_intervals": "Maximale Erweiterungsintervalle"
|
"peak_price_max_extension_intervals": "Maximale Erweiterungsintervalle",
|
||||||
|
"peak_price_geometric_flex": "Geometrischer Flex-Bonus"
|
||||||
},
|
},
|
||||||
"data_description": {
|
"data_description": {
|
||||||
"peak_price_extend_to_very_expensive": "Wenn aktiviert, erweitern sich erkannte Spitzenpreisperioden nach außen, um angrenzende Intervalle mit dem Preisniveau 'Sehr teuer' aufzunehmen. So werden extrem teure Intervalle an den Rändern erkannter Perioden besser erfasst.",
|
"peak_price_extend_to_very_expensive": "Wenn aktiviert, erweitern sich erkannte Spitzenpreisperioden nach außen, um angrenzende Intervalle mit dem Preisniveau 'Sehr teuer' aufzunehmen. So werden extrem teure Intervalle an den Rändern erkannter Perioden besser erfasst.",
|
||||||
"peak_price_max_extension_intervals": "Maximale Anzahl zusätzlicher Intervalle pro Seite (linker und rechter Rand). Jedes Intervall dauert 15 Minuten. Beispiel: 4 Intervalle = bis zu 1 Stunde Erweiterung pro Rand. Standard: 4"
|
"peak_price_max_extension_intervals": "Maximale Anzahl zusätzlicher Intervalle pro Seite (linker und rechter Rand). Jedes Intervall dauert 15 Minuten. Beispiel: 4 Intervalle = bis zu 1 Stunde Erweiterung pro Rand. Standard: 4",
|
||||||
|
"peak_price_geometric_flex": "Zusätzlicher Flex-Prozentsatz für Intervalle, die in einen erkannten Preisplateau (Λ-Form) fallen. Wenn für den Tag ein Gipfel-Muster erkannt wird, erhalten Intervalle innerhalb der Gipfelzone diese zusätzliche Toleranz, damit der Periodendetektor sie eher einschließt. 0 = deaktiviert. Standard: 0"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -255,11 +255,13 @@
|
||||||
"description": "Optionally extend detected best price periods at both edges to absorb adjacent very cheap intervals.",
|
"description": "Optionally extend detected best price periods at both edges to absorb adjacent very cheap intervals.",
|
||||||
"data": {
|
"data": {
|
||||||
"best_price_extend_to_very_cheap": "Extend to Very Cheap Intervals",
|
"best_price_extend_to_very_cheap": "Extend to Very Cheap Intervals",
|
||||||
"best_price_max_extension_intervals": "Maximum Extension Intervals"
|
"best_price_max_extension_intervals": "Maximum Extension Intervals",
|
||||||
|
"best_price_geometric_flex": "Geometric Flex Bonus"
|
||||||
},
|
},
|
||||||
"data_description": {
|
"data_description": {
|
||||||
"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_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_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"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
@ -314,11 +316,13 @@
|
||||||
"description": "Optionally extend detected peak price periods at both edges to absorb adjacent very expensive intervals.",
|
"description": "Optionally extend detected peak price periods at both edges to absorb adjacent very expensive intervals.",
|
||||||
"data": {
|
"data": {
|
||||||
"peak_price_extend_to_very_expensive": "Extend to Very Expensive Intervals",
|
"peak_price_extend_to_very_expensive": "Extend to Very Expensive Intervals",
|
||||||
"peak_price_max_extension_intervals": "Maximum Extension Intervals"
|
"peak_price_max_extension_intervals": "Maximum Extension Intervals",
|
||||||
|
"peak_price_geometric_flex": "Geometric Flex Bonus"
|
||||||
},
|
},
|
||||||
"data_description": {
|
"data_description": {
|
||||||
"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_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_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"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -244,11 +244,13 @@
|
||||||
"description": "Utvid eventuelt oppdagede bestprisperioder ved begge ender for å inkludere tilstøtende svært billige intervaller.",
|
"description": "Utvid eventuelt oppdagede bestprisperioder ved begge ender for å inkludere tilstøtende svært billige intervaller.",
|
||||||
"data": {
|
"data": {
|
||||||
"best_price_extend_to_very_cheap": "Utvid til svært billige intervaller",
|
"best_price_extend_to_very_cheap": "Utvid til svært billige intervaller",
|
||||||
"best_price_max_extension_intervals": "Maksimale utvidelsesintervaller"
|
"best_price_max_extension_intervals": "Maksimale utvidelsesintervaller",
|
||||||
|
"best_price_geometric_flex": "Geometrisk fleksbonus"
|
||||||
},
|
},
|
||||||
"data_description": {
|
"data_description": {
|
||||||
"best_price_extend_to_very_cheap": "Når aktivert, utvider oppdagede bestprisperioder seg utover for å inkludere tilstøtende intervaller med prisnivået 'Svært billig'. Dette fanger opp ekstremt billige intervaller ved kantene av oppdagede perioder.",
|
"best_price_extend_to_very_cheap": "Når aktivert, utvider oppdagede bestprisperioder seg utover for å inkludere tilstøtende intervaller med prisnivået 'Svært billig'. Dette fanger opp ekstremt billige intervaller ved kantene av oppdagede perioder.",
|
||||||
"best_price_max_extension_intervals": "Maksimalt antall ekstra intervaller per side (venstre og høyre kant). Hvert intervall er 15 minutter. Eksempel: 4 intervaller = opptil 1 times utvidelse per kant. Standard: 4"
|
"best_price_max_extension_intervals": "Maksimalt antall ekstra intervaller per side (venstre og høyre kant). Hvert intervall er 15 minutter. Eksempel: 4 intervaller = opptil 1 times utvidelse per kant. Standard: 4",
|
||||||
|
"best_price_geometric_flex": "Ekstra fleksprosent for intervaller som faller innenfor en oppdaget prisdal (V-form). Når et dal-mønster oppdages for dagen, får intervaller innen dalsonen denne ekstra toleransen, slik at periodevarslingssystemet er mer tilbøyelig til å inkludere dem. 0 = deaktivert. Standard: 0"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
@ -303,11 +305,13 @@
|
||||||
"description": "Utvid eventuelt oppdagede topprisperioder ved begge ender for å inkludere tilstøtende svært dyre intervaller.",
|
"description": "Utvid eventuelt oppdagede topprisperioder ved begge ender for å inkludere tilstøtende svært dyre intervaller.",
|
||||||
"data": {
|
"data": {
|
||||||
"peak_price_extend_to_very_expensive": "Utvid til svært dyre intervaller",
|
"peak_price_extend_to_very_expensive": "Utvid til svært dyre intervaller",
|
||||||
"peak_price_max_extension_intervals": "Maksimale utvidelsesintervaller"
|
"peak_price_max_extension_intervals": "Maksimale utvidelsesintervaller",
|
||||||
|
"peak_price_geometric_flex": "Geometrisk fleksbonus"
|
||||||
},
|
},
|
||||||
"data_description": {
|
"data_description": {
|
||||||
"peak_price_extend_to_very_expensive": "Når aktivert, utvider oppdagede topprisperioder seg utover for å inkludere tilstøtende intervaller med prisnivået 'Svært dyrt'. Dette fanger opp ekstremt dyre intervaller ved kantene av oppdagede perioder.",
|
"peak_price_extend_to_very_expensive": "Når aktivert, utvider oppdagede topprisperioder seg utover for å inkludere tilstøtende intervaller med prisnivået 'Svært dyrt'. Dette fanger opp ekstremt dyre intervaller ved kantene av oppdagede perioder.",
|
||||||
"peak_price_max_extension_intervals": "Maksimalt antall ekstra intervaller per side (venstre og høyre kant). Hvert intervall er 15 minutter. Eksempel: 4 intervaller = opptil 1 times utvidelse per kant. Standard: 4"
|
"peak_price_max_extension_intervals": "Maksimalt antall ekstra intervaller per side (venstre og høyre kant). Hvert intervall er 15 minutter. Eksempel: 4 intervaller = opptil 1 times utvidelse per kant. Standard: 4",
|
||||||
|
"peak_price_geometric_flex": "Ekstra fleksprosent for intervaller som faller innenfor en oppdaget pristopp (Λ-form). Når et topp-mønster oppdages for dagen, får intervaller innen toppsonene denne ekstra toleransen, slik at periodevarslingssystemet er mer tilbøyelig til å inkludere dem. 0 = deaktivert. Standard: 0"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -244,11 +244,13 @@
|
||||||
"description": "Breid gedetecteerde beste-prijsperioden eventueel uit aan beide randen om aangrenzende zeer goedkope intervallen op te nemen.",
|
"description": "Breid gedetecteerde beste-prijsperioden eventueel uit aan beide randen om aangrenzende zeer goedkope intervallen op te nemen.",
|
||||||
"data": {
|
"data": {
|
||||||
"best_price_extend_to_very_cheap": "Uitbreiden met zeer goedkope intervallen",
|
"best_price_extend_to_very_cheap": "Uitbreiden met zeer goedkope intervallen",
|
||||||
"best_price_max_extension_intervals": "Maximale uitbreidingsintervallen"
|
"best_price_max_extension_intervals": "Maximale uitbreidingsintervallen",
|
||||||
|
"best_price_geometric_flex": "Geometrische Flex Bonus"
|
||||||
},
|
},
|
||||||
"data_description": {
|
"data_description": {
|
||||||
"best_price_extend_to_very_cheap": "Indien ingeschakeld, breiden gedetecteerde beste-prijsperioden zich uit aan de randen om aangrenzende intervallen met prijsniveau 'Zeer goedkoop' op te nemen. Dit vangt extreem goedkope intervallen op aan de randen van gedetecteerde perioden.",
|
"best_price_extend_to_very_cheap": "Indien ingeschakeld, breiden gedetecteerde beste-prijsperioden zich uit aan de randen om aangrenzende intervallen met prijsniveau 'Zeer goedkoop' op te nemen. Dit vangt extreem goedkope intervallen op aan de randen van gedetecteerde perioden.",
|
||||||
"best_price_max_extension_intervals": "Maximaal aantal extra intervallen per kant (linker en rechter rand). Elk interval is 15 minuten. Voorbeeld: 4 intervallen = maximaal 1 uur uitbreiding per rand. Standaard: 4"
|
"best_price_max_extension_intervals": "Maximaal aantal extra intervallen per kant (linker en rechter rand). Elk interval is 15 minuten. Voorbeeld: 4 intervallen = maximaal 1 uur uitbreiding per rand. Standaard: 4",
|
||||||
|
"best_price_geometric_flex": "Extra flex percentage voor intervallen die binnen een gedetecteerde prijsdal (V-vorm) vallen. Wanneer een dal-patroon wordt gedetecteerd voor de dag, krijgen intervallen binnen de dalzone deze extra tolerantie, waardoor de periodedetector ze eerder opneemt. 0 = uitgeschakeld. Standaard: 0"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
@ -303,11 +305,13 @@
|
||||||
"description": "Breid gedetecteerde piekprijsperioden eventueel uit aan beide randen om aangrenzende zeer dure intervallen op te nemen.",
|
"description": "Breid gedetecteerde piekprijsperioden eventueel uit aan beide randen om aangrenzende zeer dure intervallen op te nemen.",
|
||||||
"data": {
|
"data": {
|
||||||
"peak_price_extend_to_very_expensive": "Uitbreiden met zeer dure intervallen",
|
"peak_price_extend_to_very_expensive": "Uitbreiden met zeer dure intervallen",
|
||||||
"peak_price_max_extension_intervals": "Maximale uitbreidingsintervallen"
|
"peak_price_max_extension_intervals": "Maximale uitbreidingsintervallen",
|
||||||
|
"peak_price_geometric_flex": "Geometrische Flex Bonus"
|
||||||
},
|
},
|
||||||
"data_description": {
|
"data_description": {
|
||||||
"peak_price_extend_to_very_expensive": "Indien ingeschakeld, breiden gedetecteerde piekprijsperioden zich uit aan de randen om aangrenzende intervallen met prijsniveau 'Zeer duur' op te nemen. Dit vangt extreem dure intervallen op aan de randen van gedetecteerde perioden.",
|
"peak_price_extend_to_very_expensive": "Indien ingeschakeld, breiden gedetecteerde piekprijsperioden zich uit aan de randen om aangrenzende intervallen met prijsniveau 'Zeer duur' op te nemen. Dit vangt extreem dure intervallen op aan de randen van gedetecteerde perioden.",
|
||||||
"peak_price_max_extension_intervals": "Maximaal aantal extra intervallen per kant (linker en rechter rand). Elk interval is 15 minuten. Voorbeeld: 4 intervallen = maximaal 1 uur uitbreiding per rand. Standaard: 4"
|
"peak_price_max_extension_intervals": "Maximaal aantal extra intervallen per kant (linker en rechter rand). Elk interval is 15 minuten. Voorbeeld: 4 intervallen = maximaal 1 uur uitbreiding per rand. Standaard: 4",
|
||||||
|
"peak_price_geometric_flex": "Extra flex percentage voor intervallen die binnen een gedetecteerde prijspiek (Λ-vorm) vallen. Wanneer een piek-patroon wordt gedetecteerd voor de dag, krijgen intervallen binnen de piekzone deze extra tolerantie, waardoor de periodedetector ze eerder opneemt. 0 = uitgeschakeld. Standaard: 0"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -244,11 +244,13 @@
|
||||||
"description": "Utvidga eventuellt hittade bästa-prisperioder vid båda ändarna för att inkludera angränsande mycket billiga intervall.",
|
"description": "Utvidga eventuellt hittade bästa-prisperioder vid båda ändarna för att inkludera angränsande mycket billiga intervall.",
|
||||||
"data": {
|
"data": {
|
||||||
"best_price_extend_to_very_cheap": "Utvidga till mycket billiga intervall",
|
"best_price_extend_to_very_cheap": "Utvidga till mycket billiga intervall",
|
||||||
"best_price_max_extension_intervals": "Maximalt antal utvidgningsintervall"
|
"best_price_max_extension_intervals": "Maximalt antal utvidgningsintervall",
|
||||||
|
"best_price_geometric_flex": "Geometrisk flexbonus"
|
||||||
},
|
},
|
||||||
"data_description": {
|
"data_description": {
|
||||||
"best_price_extend_to_very_cheap": "När aktiverat utvidgas hittade bästa-prisperioder utåt för att inkludera angränsande intervall med prisnivån 'Mycket billig'. Detta fångar upp extremt billiga intervall vid kanterna av hittade perioder.",
|
"best_price_extend_to_very_cheap": "När aktiverat utvidgas hittade bästa-prisperioder utåt för att inkludera angränsande intervall med prisnivån 'Mycket billig'. Detta fångar upp extremt billiga intervall vid kanterna av hittade perioder.",
|
||||||
"best_price_max_extension_intervals": "Maximalt antal extra intervall per sida (vänster och höger kant). Varje intervall är 15 minuter. Exempel: 4 intervall = upp till 1 timmes utvidgning per kant. Standard: 4"
|
"best_price_max_extension_intervals": "Maximalt antal extra intervall per sida (vänster och höger kant). Varje intervall är 15 minuter. Exempel: 4 intervall = upp till 1 timmes utvidgning per kant. Standard: 4",
|
||||||
|
"best_price_geometric_flex": "Extra flexprocentandel för intervall som faller inom en detekterad prisdal (V-form). När ett dalmönster detekteras för dagen får intervall inom dalzonen denna extra tolerans, vilket gör att perioddektorn är mer benägen att inkludera dem. 0 = inaktiverad. Standard: 0"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
@ -303,11 +305,13 @@
|
||||||
"description": "Utvidga eventuellt hittade topprisperioder vid båda ändarna för att inkludera angränsande mycket dyra intervall.",
|
"description": "Utvidga eventuellt hittade topprisperioder vid båda ändarna för att inkludera angränsande mycket dyra intervall.",
|
||||||
"data": {
|
"data": {
|
||||||
"peak_price_extend_to_very_expensive": "Utvidga till mycket dyra intervall",
|
"peak_price_extend_to_very_expensive": "Utvidga till mycket dyra intervall",
|
||||||
"peak_price_max_extension_intervals": "Maximalt antal utvidgningsintervall"
|
"peak_price_max_extension_intervals": "Maximalt antal utvidgningsintervall",
|
||||||
|
"peak_price_geometric_flex": "Geometrisk flexbonus"
|
||||||
},
|
},
|
||||||
"data_description": {
|
"data_description": {
|
||||||
"peak_price_extend_to_very_expensive": "När aktiverat utvidgas hittade topprisperioder utåt för att inkludera angränsande intervall med prisnivån 'Mycket dyr'. Detta fångar upp extremt dyra intervall vid kanterna av hittade perioder.",
|
"peak_price_extend_to_very_expensive": "När aktiverat utvidgas hittade topprisperioder utåt för att inkludera angränsande intervall med prisnivån 'Mycket dyr'. Detta fångar upp extremt dyra intervall vid kanterna av hittade perioder.",
|
||||||
"peak_price_max_extension_intervals": "Maximalt antal extra intervall per sida (vänster och höger kant). Varje intervall är 15 minuter. Exempel: 4 intervall = upp till 1 timmes utvidgning per kant. Standard: 4"
|
"peak_price_max_extension_intervals": "Maximalt antal extra intervall per sida (vänster och höger kant). Varje intervall är 15 minuter. Exempel: 4 intervall = upp till 1 timmes utvidgning per kant. Standard: 4",
|
||||||
|
"peak_price_geometric_flex": "Extra flexprocentandel för intervall som faller inom en detekterad prispeak (Λ-form). När ett peak-mönster detekteras för dagen får intervall inom peakzonen denna extra tolerans, vilket gör att perioddetektor är mer benägen att inkludera dem. 0 = inaktiverad. Standard: 0"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue