mirror of
https://github.com/jpawlowski/hass.tibber_prices.git
synced 2026-05-28 18:43:40 +00:00
feat(best_price,peak_price): add optional extension to VERY_CHEAP/VERY_EXPENSIVE intervals
After period detection, optionally walk left/right from each period boundary to absorb adjacent VERY_CHEAP (best price) or VERY_EXPENSIVE (peak price) intervals (step 7.5 in the pipeline). New constants: CONF_BEST_PRICE_EXTEND_TO_VERY_CHEAP, CONF_BEST_PRICE_MAX_EXTENSION_INTERVALS, CONF_PEAK_PRICE_EXTEND_TO_VERY_EXPENSIVE, CONF_PEAK_PRICE_MAX_EXTENSION_INTERVALS. Defaults: off / 4 intervals (1 hour per side). Hard maximum: 12 intervals (3 hours). Config stored under "extension_settings" section, reflected in period hash for correct cache invalidation. New module: coordinator/period_handlers/shape_extension.py handles the boundary walk, stat recalculation, and extension_intervals_added bookkeeping. Impact: Users can opt-in to wider best/peak price windows that include extreme-level adjacent intervals, reducing missed very cheap/expensive slots at period edges.
This commit is contained in:
parent
447dc907e6
commit
b7f1efce1f
12 changed files with 10006 additions and 9485 deletions
|
|
@ -12,7 +12,9 @@ import voluptuous as vol
|
|||
from custom_components.tibber_prices.const import (
|
||||
BEST_PRICE_MAX_LEVEL_OPTIONS,
|
||||
CONF_AVERAGE_SENSOR_DISPLAY,
|
||||
CONF_BEST_PRICE_EXTEND_TO_VERY_CHEAP,
|
||||
CONF_BEST_PRICE_FLEX,
|
||||
CONF_BEST_PRICE_MAX_EXTENSION_INTERVALS,
|
||||
CONF_BEST_PRICE_MAX_LEVEL,
|
||||
CONF_BEST_PRICE_MAX_LEVEL_GAP_COUNT,
|
||||
CONF_BEST_PRICE_MIN_DISTANCE_FROM_AVG,
|
||||
|
|
@ -23,7 +25,9 @@ from custom_components.tibber_prices.const import (
|
|||
CONF_EXTENDED_DESCRIPTIONS,
|
||||
CONF_MIN_PERIODS_BEST,
|
||||
CONF_MIN_PERIODS_PEAK,
|
||||
CONF_PEAK_PRICE_EXTEND_TO_VERY_EXPENSIVE,
|
||||
CONF_PEAK_PRICE_FLEX,
|
||||
CONF_PEAK_PRICE_MAX_EXTENSION_INTERVALS,
|
||||
CONF_PEAK_PRICE_MAX_LEVEL_GAP_COUNT,
|
||||
CONF_PEAK_PRICE_MIN_DISTANCE_FROM_AVG,
|
||||
CONF_PEAK_PRICE_MIN_LEVEL,
|
||||
|
|
@ -49,7 +53,9 @@ from custom_components.tibber_prices.const import (
|
|||
CONF_VOLATILITY_THRESHOLD_MODERATE,
|
||||
CONF_VOLATILITY_THRESHOLD_VERY_HIGH,
|
||||
DEFAULT_AVERAGE_SENSOR_DISPLAY,
|
||||
DEFAULT_BEST_PRICE_EXTEND_TO_VERY_CHEAP,
|
||||
DEFAULT_BEST_PRICE_FLEX,
|
||||
DEFAULT_BEST_PRICE_MAX_EXTENSION_INTERVALS,
|
||||
DEFAULT_BEST_PRICE_MAX_LEVEL,
|
||||
DEFAULT_BEST_PRICE_MAX_LEVEL_GAP_COUNT,
|
||||
DEFAULT_BEST_PRICE_MIN_DISTANCE_FROM_AVG,
|
||||
|
|
@ -59,7 +65,9 @@ from custom_components.tibber_prices.const import (
|
|||
DEFAULT_EXTENDED_DESCRIPTIONS,
|
||||
DEFAULT_MIN_PERIODS_BEST,
|
||||
DEFAULT_MIN_PERIODS_PEAK,
|
||||
DEFAULT_PEAK_PRICE_EXTEND_TO_VERY_EXPENSIVE,
|
||||
DEFAULT_PEAK_PRICE_FLEX,
|
||||
DEFAULT_PEAK_PRICE_MAX_EXTENSION_INTERVALS,
|
||||
DEFAULT_PEAK_PRICE_MAX_LEVEL_GAP_COUNT,
|
||||
DEFAULT_PEAK_PRICE_MIN_DISTANCE_FROM_AVG,
|
||||
DEFAULT_PEAK_PRICE_MIN_LEVEL,
|
||||
|
|
@ -86,6 +94,7 @@ from custom_components.tibber_prices.const import (
|
|||
DEFAULT_VOLATILITY_THRESHOLD_VERY_HIGH,
|
||||
DISPLAY_MODE_BASE,
|
||||
DISPLAY_MODE_SUBUNIT,
|
||||
MAX_EXTENSION_INTERVALS,
|
||||
MAX_GAP_COUNT,
|
||||
MAX_MIN_PERIOD_LENGTH,
|
||||
MAX_MIN_PERIODS,
|
||||
|
|
@ -618,6 +627,7 @@ def get_best_price_schema(
|
|||
period_settings = options.get("period_settings", {})
|
||||
flexibility_settings = options.get("flexibility_settings", {})
|
||||
relaxation_settings = options.get("relaxation_and_target_periods", {})
|
||||
extension_settings = options.get("extension_settings", {})
|
||||
|
||||
# Get current values for override display
|
||||
min_period_length = int(
|
||||
|
|
@ -633,6 +643,12 @@ def get_best_price_schema(
|
|||
enable_min_periods = relaxation_settings.get(CONF_ENABLE_MIN_PERIODS_BEST, DEFAULT_ENABLE_MIN_PERIODS_BEST)
|
||||
min_periods = int(relaxation_settings.get(CONF_MIN_PERIODS_BEST, DEFAULT_MIN_PERIODS_BEST))
|
||||
relaxation_attempts = int(relaxation_settings.get(CONF_RELAXATION_ATTEMPTS_BEST, DEFAULT_RELAXATION_ATTEMPTS_BEST))
|
||||
extend_to_very_cheap = bool(
|
||||
extension_settings.get(CONF_BEST_PRICE_EXTEND_TO_VERY_CHEAP, DEFAULT_BEST_PRICE_EXTEND_TO_VERY_CHEAP)
|
||||
)
|
||||
max_extension_intervals_best = int(
|
||||
extension_settings.get(CONF_BEST_PRICE_MAX_EXTENSION_INTERVALS, DEFAULT_BEST_PRICE_MAX_EXTENSION_INTERVALS)
|
||||
)
|
||||
|
||||
# Build section schemas with optional override warnings
|
||||
period_warning = get_section_override_warning("best_price", "period_settings", overrides, translations) or {}
|
||||
|
|
@ -754,6 +770,28 @@ def get_best_price_schema(
|
|||
vol.Schema(relaxation_fields),
|
||||
{"collapsed": True},
|
||||
),
|
||||
vol.Required("extension_settings"): section(
|
||||
vol.Schema(
|
||||
{
|
||||
vol.Optional(
|
||||
CONF_BEST_PRICE_EXTEND_TO_VERY_CHEAP,
|
||||
default=extend_to_very_cheap,
|
||||
): BooleanSelector(selector.BooleanSelectorConfig()),
|
||||
vol.Optional(
|
||||
CONF_BEST_PRICE_MAX_EXTENSION_INTERVALS,
|
||||
default=max_extension_intervals_best,
|
||||
): NumberSelector(
|
||||
NumberSelectorConfig(
|
||||
min=1,
|
||||
max=MAX_EXTENSION_INTERVALS,
|
||||
step=1,
|
||||
mode=NumberSelectorMode.SLIDER,
|
||||
)
|
||||
),
|
||||
}
|
||||
),
|
||||
{"collapsed": True},
|
||||
),
|
||||
}
|
||||
)
|
||||
|
||||
|
|
@ -779,6 +817,7 @@ def get_peak_price_schema(
|
|||
period_settings = options.get("period_settings", {})
|
||||
flexibility_settings = options.get("flexibility_settings", {})
|
||||
relaxation_settings = options.get("relaxation_and_target_periods", {})
|
||||
extension_settings = options.get("extension_settings", {})
|
||||
|
||||
# Get current values for override display
|
||||
min_period_length = int(
|
||||
|
|
@ -794,6 +833,12 @@ def get_peak_price_schema(
|
|||
enable_min_periods = relaxation_settings.get(CONF_ENABLE_MIN_PERIODS_PEAK, DEFAULT_ENABLE_MIN_PERIODS_PEAK)
|
||||
min_periods = int(relaxation_settings.get(CONF_MIN_PERIODS_PEAK, DEFAULT_MIN_PERIODS_PEAK))
|
||||
relaxation_attempts = int(relaxation_settings.get(CONF_RELAXATION_ATTEMPTS_PEAK, DEFAULT_RELAXATION_ATTEMPTS_PEAK))
|
||||
extend_to_very_expensive = bool(
|
||||
extension_settings.get(CONF_PEAK_PRICE_EXTEND_TO_VERY_EXPENSIVE, DEFAULT_PEAK_PRICE_EXTEND_TO_VERY_EXPENSIVE)
|
||||
)
|
||||
max_extension_intervals_peak = int(
|
||||
extension_settings.get(CONF_PEAK_PRICE_MAX_EXTENSION_INTERVALS, DEFAULT_PEAK_PRICE_MAX_EXTENSION_INTERVALS)
|
||||
)
|
||||
|
||||
# Build section schemas with optional override warnings
|
||||
period_warning = get_section_override_warning("peak_price", "period_settings", overrides, translations) or {}
|
||||
|
|
@ -915,6 +960,28 @@ def get_peak_price_schema(
|
|||
vol.Schema(relaxation_fields),
|
||||
{"collapsed": True},
|
||||
),
|
||||
vol.Required("extension_settings"): section(
|
||||
vol.Schema(
|
||||
{
|
||||
vol.Optional(
|
||||
CONF_PEAK_PRICE_EXTEND_TO_VERY_EXPENSIVE,
|
||||
default=extend_to_very_expensive,
|
||||
): BooleanSelector(selector.BooleanSelectorConfig()),
|
||||
vol.Optional(
|
||||
CONF_PEAK_PRICE_MAX_EXTENSION_INTERVALS,
|
||||
default=max_extension_intervals_peak,
|
||||
): NumberSelector(
|
||||
NumberSelectorConfig(
|
||||
min=1,
|
||||
max=MAX_EXTENSION_INTERVALS,
|
||||
step=1,
|
||||
mode=NumberSelectorMode.SLIDER,
|
||||
)
|
||||
),
|
||||
}
|
||||
),
|
||||
{"collapsed": True},
|
||||
),
|
||||
}
|
||||
)
|
||||
|
||||
|
|
|
|||
|
|
@ -68,6 +68,10 @@ CONF_RELAXATION_ATTEMPTS_BEST = "relaxation_attempts_best"
|
|||
CONF_ENABLE_MIN_PERIODS_PEAK = "enable_min_periods_peak"
|
||||
CONF_MIN_PERIODS_PEAK = "min_periods_peak"
|
||||
CONF_RELAXATION_ATTEMPTS_PEAK = "relaxation_attempts_peak"
|
||||
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_PEAK_PRICE_EXTEND_TO_VERY_EXPENSIVE = "peak_price_extend_to_very_expensive"
|
||||
CONF_PEAK_PRICE_MAX_EXTENSION_INTERVALS = "peak_price_max_extension_intervals"
|
||||
|
||||
ATTRIBUTION = "Data provided by Tibber"
|
||||
|
||||
|
|
@ -131,6 +135,10 @@ DEFAULT_RELAXATION_ATTEMPTS_BEST = 11 # Default: 11 steps allows escalation fro
|
|||
DEFAULT_ENABLE_MIN_PERIODS_PEAK = True # Default: minimum periods feature enabled for peak price
|
||||
DEFAULT_MIN_PERIODS_PEAK = 2 # Default: require at least 2 peak price periods (when enabled)
|
||||
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_MAX_EXTENSION_INTERVALS = 4 # Default: up to 4 intervals (1 hour) per side
|
||||
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
|
||||
|
||||
# Validation limits (used in GUI schemas and server-side validation)
|
||||
# These ensure consistency between frontend and backend validation
|
||||
|
|
@ -139,6 +147,7 @@ MAX_DISTANCE_PERCENTAGE = 50 # Maximum distance from average percentage (GUI sl
|
|||
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_RELAXATION_ATTEMPTS = 12 # Maximum relaxation attempts (GUI slider limit)
|
||||
MAX_EXTENSION_INTERVALS = 12 # Maximum extension intervals per side (GUI slider limit = 3 hours)
|
||||
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)
|
||||
|
||||
|
|
@ -407,6 +416,13 @@ def get_default_options(currency_code: str | None) -> dict[str, Any]:
|
|||
CONF_MIN_PERIODS_PEAK: DEFAULT_MIN_PERIODS_PEAK,
|
||||
CONF_RELAXATION_ATTEMPTS_PEAK: DEFAULT_RELAXATION_ATTEMPTS_PEAK,
|
||||
},
|
||||
# Nested section: Extension settings (shared by best/peak price)
|
||||
"extension_settings": {
|
||||
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_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,
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -28,6 +28,9 @@ from .outlier_filtering import filter_price_outliers
|
|||
# Re-export relaxation
|
||||
from .relaxation import calculate_periods_with_relaxation
|
||||
|
||||
# Re-export shape extension
|
||||
from .shape_extension import extend_periods_for_shape
|
||||
|
||||
# Re-export constants and types
|
||||
from .types import (
|
||||
ALL_DAY_PATTERNS,
|
||||
|
|
@ -80,5 +83,6 @@ __all__ = [
|
|||
"calculate_periods",
|
||||
"calculate_periods_with_relaxation",
|
||||
"detect_day_patterns",
|
||||
"extend_periods_for_shape",
|
||||
"filter_price_outliers",
|
||||
]
|
||||
|
|
|
|||
|
|
@ -25,6 +25,7 @@ from .period_building import (
|
|||
from .period_statistics import (
|
||||
extract_period_summaries,
|
||||
)
|
||||
from .shape_extension import extend_periods_for_shape
|
||||
from .types import TibberPricesThresholdConfig
|
||||
|
||||
# Flex limits to prevent degenerate behavior (see docs/development/period-calculation-theory.md)
|
||||
|
|
@ -209,6 +210,20 @@ def calculate_periods(
|
|||
time=time,
|
||||
)
|
||||
|
||||
# Step 7.5: Extend periods into adjacent VERY_CHEAP / VERY_EXPENSIVE intervals
|
||||
# This is an opt-in feature (disabled by default) that adds contiguous
|
||||
# extreme-level intervals on each side of an already-found period.
|
||||
if config.extend_to_extreme and config.max_extension_intervals > 0:
|
||||
period_summaries = extend_periods_for_shape(
|
||||
period_summaries,
|
||||
all_prices_sorted,
|
||||
price_context,
|
||||
reverse_sort=reverse_sort,
|
||||
max_extension_intervals=config.max_extension_intervals,
|
||||
thresholds=thresholds,
|
||||
time=time,
|
||||
)
|
||||
|
||||
# Step 8: Cross-day extension for late-night periods
|
||||
# If a best-price period ends near midnight and tomorrow has continued low prices,
|
||||
# extend the period across midnight to give users the full cheap window
|
||||
|
|
|
|||
|
|
@ -0,0 +1,258 @@
|
|||
"""
|
||||
Shape-based period extension: extend periods into adjacent VERY_CHEAP/VERY_EXPENSIVE intervals.
|
||||
|
||||
After periods are identified by the core algorithm, this module optionally extends
|
||||
each period's boundaries to include any directly-adjacent intervals that carry the
|
||||
most extreme price level relevant to the period type:
|
||||
|
||||
- Best price periods → extend into VERY_CHEAP neighbouring intervals
|
||||
- Peak price periods → extend into VERY_EXPENSIVE neighbouring intervals
|
||||
|
||||
Extension is purely additive and opt-in (disabled by default). It does not affect
|
||||
the core period-finding logic; periods that would not normally be found are not
|
||||
created by this step.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import statistics
|
||||
from datetime import timedelta
|
||||
from typing import TYPE_CHECKING, Any
|
||||
|
||||
from custom_components.tibber_prices.const import (
|
||||
PRICE_LEVEL_VERY_CHEAP,
|
||||
PRICE_LEVEL_VERY_EXPENSIVE,
|
||||
)
|
||||
from custom_components.tibber_prices.utils.price import (
|
||||
aggregate_period_levels,
|
||||
aggregate_period_ratings,
|
||||
)
|
||||
|
||||
from .period_statistics import (
|
||||
calculate_aggregated_rating_difference,
|
||||
calculate_period_price_diff,
|
||||
calculate_period_price_statistics,
|
||||
)
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from datetime import datetime
|
||||
|
||||
from custom_components.tibber_prices.coordinator.time_service import TibberPricesTimeService
|
||||
|
||||
from .types import TibberPricesThresholdConfig
|
||||
|
||||
_INTERVAL_DURATION = timedelta(minutes=15)
|
||||
|
||||
|
||||
def extend_periods_for_shape( # noqa: PLR0913 - Extension requires all context params
|
||||
periods: list[dict[str, Any]],
|
||||
all_prices: list[dict[str, Any]],
|
||||
price_context: dict[str, Any],
|
||||
*,
|
||||
reverse_sort: bool,
|
||||
max_extension_intervals: int,
|
||||
thresholds: TibberPricesThresholdConfig,
|
||||
time: TibberPricesTimeService,
|
||||
) -> list[dict[str, Any]]:
|
||||
"""
|
||||
Extend each period into adjacent VERY_CHEAP or VERY_EXPENSIVE intervals.
|
||||
|
||||
For best price periods (reverse_sort=False): extend into VERY_CHEAP neighbours.
|
||||
For peak price periods (reverse_sort=True): extend into VERY_EXPENSIVE neighbours.
|
||||
|
||||
Only intervals that are directly contiguous with the period and carry the
|
||||
target level are added. At most *max_extension_intervals* are consumed on
|
||||
each side independently. Period statistics are fully recalculated after
|
||||
any extension.
|
||||
|
||||
Args:
|
||||
periods: Period summary dicts from ``extract_period_summaries``.
|
||||
all_prices: All enriched price intervals (yesterday + today + tomorrow).
|
||||
price_context: Dict with ``ref_prices`` and ``avg_prices`` per calendar day.
|
||||
reverse_sort: ``True`` for peak price, ``False`` for best price.
|
||||
max_extension_intervals: Maximum extra intervals that may be added per side.
|
||||
thresholds: Threshold configuration for level / rating aggregation.
|
||||
time: Time-service instance used to resolve ``startsAt`` timestamps.
|
||||
|
||||
Returns:
|
||||
Updated list of period dicts, potentially with extended boundaries and
|
||||
recalculated statistics. Unmodified periods are returned as-is.
|
||||
|
||||
"""
|
||||
if not periods or max_extension_intervals <= 0:
|
||||
return periods
|
||||
|
||||
target_level = PRICE_LEVEL_VERY_EXPENSIVE if reverse_sort else PRICE_LEVEL_VERY_CHEAP
|
||||
|
||||
# Build a lookup dict: local datetime → full interval dict
|
||||
interval_index: dict[datetime, dict[str, Any]] = {}
|
||||
for iv in all_prices:
|
||||
t = time.get_interval_time(iv)
|
||||
if t is not None:
|
||||
interval_index[t] = iv
|
||||
|
||||
return [
|
||||
_extend_period_edges(
|
||||
period,
|
||||
interval_index,
|
||||
target_level=target_level,
|
||||
max_intervals=max_extension_intervals,
|
||||
thresholds=thresholds,
|
||||
price_context=price_context,
|
||||
)
|
||||
for period in periods
|
||||
]
|
||||
|
||||
|
||||
# ── private helpers ────────────────────────────────────────────────────────────
|
||||
|
||||
|
||||
def _extend_period_edges( # noqa: PLR0913, PLR0912, PLR0915 - Period edge extension requires many args, branches, and statements
|
||||
period: dict[str, Any],
|
||||
interval_index: dict[datetime, dict[str, Any]],
|
||||
*,
|
||||
target_level: str,
|
||||
max_intervals: int,
|
||||
thresholds: TibberPricesThresholdConfig,
|
||||
price_context: dict[str, Any],
|
||||
) -> dict[str, Any]:
|
||||
"""
|
||||
Consume adjacent target-level intervals on both edges of a period.
|
||||
|
||||
The original period dict is never mutated; a new dict is returned.
|
||||
If no extension is possible, the original dict is returned unchanged.
|
||||
|
||||
Args:
|
||||
period: Period summary dict with ``start`` and ``end`` datetime keys.
|
||||
interval_index: Lookup map of ``{starts_at_datetime: interval_dict}``.
|
||||
target_level: ``"VERY_CHEAP"`` or ``"VERY_EXPENSIVE"``.
|
||||
max_intervals: Maximum intervals that may be added on each side.
|
||||
thresholds: Threshold config for aggregation helpers.
|
||||
price_context: Reference prices / averages per calendar day.
|
||||
|
||||
Returns:
|
||||
Extended (or original) period summary dict.
|
||||
|
||||
"""
|
||||
start: datetime = period["start"]
|
||||
end: datetime = period["end"]
|
||||
# ``end`` is the exclusive boundary: the last included interval starts at
|
||||
# ``end - _INTERVAL_DURATION``.
|
||||
|
||||
# ── walk LEFT (earlier than period start) ─────────────────────────────────
|
||||
left_additions: list[dict[str, Any]] = []
|
||||
cursor = start - _INTERVAL_DURATION
|
||||
for _ in range(max_intervals):
|
||||
iv = interval_index.get(cursor)
|
||||
if iv is None or iv.get("level") != target_level:
|
||||
break
|
||||
left_additions.insert(0, iv)
|
||||
cursor -= _INTERVAL_DURATION
|
||||
|
||||
# ── walk RIGHT (later than period end) ────────────────────────────────────
|
||||
right_additions: list[dict[str, Any]] = []
|
||||
cursor = end # first interval AFTER the period
|
||||
for _ in range(max_intervals):
|
||||
iv = interval_index.get(cursor)
|
||||
if iv is None or iv.get("level") != target_level:
|
||||
break
|
||||
right_additions.append(iv)
|
||||
cursor += _INTERVAL_DURATION
|
||||
|
||||
total_added = len(left_additions) + len(right_additions)
|
||||
if total_added == 0:
|
||||
return period
|
||||
|
||||
# ── rebuild full interval list for the extended period ────────────────────
|
||||
original_intervals = _collect_original_intervals(start, end, interval_index)
|
||||
all_period_intervals = left_additions + original_intervals + right_additions
|
||||
|
||||
# ── recalculate boundaries ────────────────────────────────────────────────
|
||||
new_start = start - _INTERVAL_DURATION * len(left_additions)
|
||||
new_end = end + _INTERVAL_DURATION * len(right_additions)
|
||||
new_duration_minutes = int((new_end - new_start).total_seconds() // 60)
|
||||
new_interval_count = len(all_period_intervals)
|
||||
|
||||
# ── recalculate price statistics ──────────────────────────────────────────
|
||||
price_stats = calculate_period_price_statistics(all_period_intervals)
|
||||
period_price_diff, period_price_diff_pct = calculate_period_price_diff(
|
||||
price_stats["price_mean"], new_start, price_context
|
||||
)
|
||||
rating_diff_pct = calculate_aggregated_rating_difference(all_period_intervals)
|
||||
|
||||
# ── recalculate level / rating aggregates ─────────────────────────────────
|
||||
new_level = aggregate_period_levels(all_period_intervals)
|
||||
new_rating: str | None = None
|
||||
if thresholds.threshold_low is not None and thresholds.threshold_high is not None:
|
||||
new_rating, _ = aggregate_period_ratings(
|
||||
all_period_intervals,
|
||||
thresholds.threshold_low,
|
||||
thresholds.threshold_high,
|
||||
)
|
||||
|
||||
# ── recalculate volatility (coefficient of variation) ────────────────────
|
||||
prices_for_vol = [float(p["total"]) for p in all_period_intervals if "total" in p]
|
||||
cv_pct: float | None = None
|
||||
if len(prices_for_vol) >= 2: # noqa: PLR2004
|
||||
mean_p = statistics.mean(prices_for_vol)
|
||||
if mean_p > 0:
|
||||
cv_pct = round(statistics.stdev(prices_for_vol) / mean_p * 100, 1)
|
||||
|
||||
# ── assemble updated period dict (keep structural fields, update statistics) ─
|
||||
reverse_sort = target_level == PRICE_LEVEL_VERY_EXPENSIVE
|
||||
updated: dict[str, Any] = {
|
||||
**period,
|
||||
# Time fields
|
||||
"start": new_start,
|
||||
"end": new_end,
|
||||
"duration_minutes": new_duration_minutes,
|
||||
# Core decision attributes
|
||||
"level": new_level,
|
||||
"rating_level": new_rating,
|
||||
"rating_difference_%": rating_diff_pct,
|
||||
# Price statistics
|
||||
"price_mean": price_stats["price_mean"],
|
||||
"price_median": price_stats["price_median"],
|
||||
"price_min": price_stats["price_min"],
|
||||
"price_max": price_stats["price_max"],
|
||||
"price_spread": price_stats["price_spread"],
|
||||
"price_coefficient_variation_%": cv_pct,
|
||||
# Detail
|
||||
"period_interval_count": new_interval_count,
|
||||
# Extension metadata
|
||||
"extension_intervals_added": total_added,
|
||||
}
|
||||
|
||||
# Refresh period price diff (replaces old value from base period)
|
||||
if reverse_sort:
|
||||
updated.pop("period_price_diff_from_daily_min", None)
|
||||
updated.pop("period_price_diff_from_daily_min_%", None)
|
||||
if period_price_diff is not None:
|
||||
updated["period_price_diff_from_daily_max"] = period_price_diff
|
||||
if period_price_diff_pct is not None:
|
||||
updated["period_price_diff_from_daily_max_%"] = period_price_diff_pct
|
||||
else:
|
||||
updated.pop("period_price_diff_from_daily_max", None)
|
||||
updated.pop("period_price_diff_from_daily_max_%", None)
|
||||
if period_price_diff is not None:
|
||||
updated["period_price_diff_from_daily_min"] = period_price_diff
|
||||
if period_price_diff_pct is not None:
|
||||
updated["period_price_diff_from_daily_min_%"] = period_price_diff_pct
|
||||
|
||||
return updated
|
||||
|
||||
|
||||
def _collect_original_intervals(
|
||||
start: datetime,
|
||||
end: datetime,
|
||||
interval_index: dict[datetime, dict[str, Any]],
|
||||
) -> list[dict[str, Any]]:
|
||||
"""Reconstruct the ordered interval list for an existing period from the index."""
|
||||
result: list[dict[str, Any]] = []
|
||||
cursor = start
|
||||
while cursor < end:
|
||||
iv = interval_index.get(cursor)
|
||||
if iv is not None:
|
||||
result.append(iv)
|
||||
cursor += _INTERVAL_DURATION
|
||||
return result
|
||||
|
|
@ -56,6 +56,8 @@ class TibberPricesPeriodConfig(NamedTuple):
|
|||
threshold_volatility_very_high: float = DEFAULT_VOLATILITY_THRESHOLD_VERY_HIGH
|
||||
level_filter: str | None = None # "any", "cheap", "expensive", etc. or None
|
||||
gap_count: int = 0 # Number of allowed consecutive deviating 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)
|
||||
|
||||
|
||||
class TibberPricesPeriodData(NamedTuple):
|
||||
|
|
|
|||
|
|
@ -225,6 +225,41 @@ class TibberPricesPeriodCalculator:
|
|||
"min_period_length": int(min_period_length),
|
||||
}
|
||||
|
||||
# Extension settings (stored in 'extension_settings' nested section)
|
||||
if reverse_sort:
|
||||
extend_to_extreme = bool(
|
||||
self._get_option(
|
||||
_const.CONF_PEAK_PRICE_EXTEND_TO_VERY_EXPENSIVE,
|
||||
"extension_settings",
|
||||
_const.DEFAULT_PEAK_PRICE_EXTEND_TO_VERY_EXPENSIVE,
|
||||
)
|
||||
)
|
||||
max_extension_intervals = int(
|
||||
self._get_option(
|
||||
_const.CONF_PEAK_PRICE_MAX_EXTENSION_INTERVALS,
|
||||
"extension_settings",
|
||||
_const.DEFAULT_PEAK_PRICE_MAX_EXTENSION_INTERVALS,
|
||||
)
|
||||
)
|
||||
else:
|
||||
extend_to_extreme = bool(
|
||||
self._get_option(
|
||||
_const.CONF_BEST_PRICE_EXTEND_TO_VERY_CHEAP,
|
||||
"extension_settings",
|
||||
_const.DEFAULT_BEST_PRICE_EXTEND_TO_VERY_CHEAP,
|
||||
)
|
||||
)
|
||||
max_extension_intervals = int(
|
||||
self._get_option(
|
||||
_const.CONF_BEST_PRICE_MAX_EXTENSION_INTERVALS,
|
||||
"extension_settings",
|
||||
_const.DEFAULT_BEST_PRICE_MAX_EXTENSION_INTERVALS,
|
||||
)
|
||||
)
|
||||
|
||||
config["extend_to_extreme"] = extend_to_extreme
|
||||
config["max_extension_intervals"] = max_extension_intervals
|
||||
|
||||
# Cache the result
|
||||
self._config_cache[cache_key] = config
|
||||
self._config_cache_valid = True
|
||||
|
|
@ -702,6 +737,8 @@ class TibberPricesPeriodCalculator:
|
|||
threshold_volatility_very_high=threshold_volatility_very_high,
|
||||
level_filter=max_level_best,
|
||||
gap_count=gap_count_best,
|
||||
extend_to_extreme=best_config["extend_to_extreme"],
|
||||
max_extension_intervals=best_config["max_extension_intervals"],
|
||||
)
|
||||
best_periods = calculate_periods_with_relaxation(
|
||||
all_prices,
|
||||
|
|
@ -783,6 +820,8 @@ class TibberPricesPeriodCalculator:
|
|||
threshold_volatility_very_high=threshold_volatility_very_high,
|
||||
level_filter=min_level_peak,
|
||||
gap_count=gap_count_peak,
|
||||
extend_to_extreme=peak_config["extend_to_extreme"],
|
||||
max_extension_intervals=peak_config["max_extension_intervals"],
|
||||
)
|
||||
peak_periods = calculate_periods_with_relaxation(
|
||||
all_prices,
|
||||
|
|
|
|||
|
|
@ -238,6 +238,18 @@
|
|||
"min_periods_best": "Mindestanzahl von Bestpreis-Zeiträumen pro Tag, die angestrebt werden sollen. Filter werden schrittweise gelockert, um diese Anzahl zu erreichen. Nur aktiv wenn 'Mindestanzahl anstreben' aktiviert ist. Standard: 1",
|
||||
"relaxation_attempts_best": "Wie viele Flexibilitätsstufen (Versuche) zu versuchen sind, bevor aufgegeben wird. Jeder Versuch führt alle Filterkombinationen auf der neuen Flexibilitätsstufe aus. Mehr Versuche erhöhen die Chance, zusätzliche Zeiträume zu finden, kosten aber mehr Verarbeitungszeit."
|
||||
}
|
||||
},
|
||||
"extension_settings": {
|
||||
"name": "Periodenrand-Erweiterung",
|
||||
"description": "Erkannte Bestpreisperioden optional an beiden Rändern erweitern, um angrenzende sehr günstige Intervalle aufzunehmen.",
|
||||
"data": {
|
||||
"best_price_extend_to_very_cheap": "Auf sehr günstige Intervalle erweitern",
|
||||
"best_price_max_extension_intervals": "Maximale Erweiterungsintervalle"
|
||||
},
|
||||
"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_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"
|
||||
}
|
||||
}
|
||||
},
|
||||
"submit": "↩ Speichern & Zurück"
|
||||
|
|
@ -285,6 +297,18 @@
|
|||
"min_periods_peak": "Mindestanzahl an Spitzenpreis-Zeiträumen, die pro Tag angestrebt werden. Filter werden schrittweise gelockert, um diese Anzahl zu erreichen. Nur aktiv, wenn 'Mindestanzahl Zeiträume anstreben' aktiviert ist. Standard: 1",
|
||||
"relaxation_attempts_peak": "Wie viele Flex-Stufen (Versuche) nacheinander ausprobiert werden, bevor aufgegeben wird. Jeder Versuch testet alle Filterkombinationen auf der neuen Flex-Stufe. Mehr Versuche erhöhen die Chance auf zusätzliche Spitzenpreis-Zeiträume, benötigen aber etwas mehr Rechenzeit."
|
||||
}
|
||||
},
|
||||
"extension_settings": {
|
||||
"name": "Periodenrand-Erweiterung",
|
||||
"description": "Erkannte Spitzenpreisperioden optional an beiden Rändern erweitern, um angrenzende sehr teure Intervalle aufzunehmen.",
|
||||
"data": {
|
||||
"peak_price_extend_to_very_expensive": "Auf sehr teure Intervalle erweitern",
|
||||
"peak_price_max_extension_intervals": "Maximale Erweiterungsintervalle"
|
||||
},
|
||||
"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_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"
|
||||
}
|
||||
}
|
||||
},
|
||||
"submit": "↩ Speichern & Zurück"
|
||||
|
|
|
|||
|
|
@ -249,6 +249,18 @@
|
|||
"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 'Achieve Minimum Count' is enabled. Default: 1",
|
||||
"relaxation_attempts_best": "How many flex levels (attempts) to try before giving up. Each attempt runs all filter combinations at the new flex level. More attempts increase the chance of finding additional periods at the cost of longer processing time."
|
||||
}
|
||||
},
|
||||
"extension_settings": {
|
||||
"name": "Period Edge Extension",
|
||||
"description": "Optionally extend detected best price periods at both edges to absorb adjacent very cheap intervals.",
|
||||
"data": {
|
||||
"best_price_extend_to_very_cheap": "Extend to Very Cheap Intervals",
|
||||
"best_price_max_extension_intervals": "Maximum Extension Intervals"
|
||||
},
|
||||
"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_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"
|
||||
}
|
||||
}
|
||||
},
|
||||
"submit": "↩ Save & Back"
|
||||
|
|
@ -296,6 +308,18 @@
|
|||
"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 'Achieve Minimum Count' is enabled. Default: 1",
|
||||
"relaxation_attempts_peak": "How many flex levels (attempts) to try before giving up. Each attempt runs all filter combinations at the new flex level. More attempts increase the chance of finding additional peak periods at the cost of longer processing time."
|
||||
}
|
||||
},
|
||||
"extension_settings": {
|
||||
"name": "Period Edge Extension",
|
||||
"description": "Optionally extend detected peak price periods at both edges to absorb adjacent very expensive intervals.",
|
||||
"data": {
|
||||
"peak_price_extend_to_very_expensive": "Extend to Very Expensive Intervals",
|
||||
"peak_price_max_extension_intervals": "Maximum Extension Intervals"
|
||||
},
|
||||
"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_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"
|
||||
}
|
||||
}
|
||||
},
|
||||
"submit": "↩ Save & Back"
|
||||
|
|
|
|||
|
|
@ -238,6 +238,18 @@
|
|||
"min_periods_best": "Minimumsantall beste pris-perioder å sikte på per dag. Filtre vil bli lempet steg for steg for å forsøke å oppnå dette antallet. Kun aktiv når 'Oppnå minimumsantall' er aktivert. Standard: 1",
|
||||
"relaxation_attempts_best": "Hvor mange fleksnivåer (forsøk) å prøve før man gir opp. Hvert forsøk kjører alle filterkombinasjoner på det nye fleksnivået. Flere forsøk øker sjansen for å finne flere perioder på bekostning av lengre behandlingstid."
|
||||
}
|
||||
},
|
||||
"extension_settings": {
|
||||
"name": "Utvidelse av perioderender",
|
||||
"description": "Utvid eventuelt oppdagede bestprisperioder ved begge ender for å inkludere tilstøtende svært billige intervaller.",
|
||||
"data": {
|
||||
"best_price_extend_to_very_cheap": "Utvid til svært billige intervaller",
|
||||
"best_price_max_extension_intervals": "Maksimale utvidelsesintervaller"
|
||||
},
|
||||
"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_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"
|
||||
}
|
||||
}
|
||||
},
|
||||
"submit": "↩ Lagre & tilbake"
|
||||
|
|
@ -285,6 +297,18 @@
|
|||
"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_attempts_peak": "Hvor mange fleksnivåer (forsøk) som testes før vi gir opp. Hvert forsøk kjører alle filterkombinasjoner på det nye fleksnivået. Flere forsøk øker sjansen for ekstra toppprisperioder, men tar litt lengre tid."
|
||||
}
|
||||
},
|
||||
"extension_settings": {
|
||||
"name": "Utvidelse av perioderender",
|
||||
"description": "Utvid eventuelt oppdagede topprisperioder ved begge ender for å inkludere tilstøtende svært dyre intervaller.",
|
||||
"data": {
|
||||
"peak_price_extend_to_very_expensive": "Utvid til svært dyre intervaller",
|
||||
"peak_price_max_extension_intervals": "Maksimale utvidelsesintervaller"
|
||||
},
|
||||
"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_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"
|
||||
}
|
||||
}
|
||||
},
|
||||
"submit": "↩ Lagre & tilbake"
|
||||
|
|
|
|||
|
|
@ -238,6 +238,18 @@
|
|||
"min_periods_best": "Minimaal aantal beste prijsperiodes om per dag na te streven. Filters worden stapsgewijs versoepeld om dit aantal te proberen te bereiken. Alleen actief wanneer 'Bereik Minimum Aantal' is ingeschakeld. Standaard: 1",
|
||||
"relaxation_attempts_best": "Hoeveel flex niveaus (pogingen) te proberen voordat opgegeven wordt. Elke poging voert alle filtercombinaties uit op het nieuwe flex niveau. Meer pogingen verhogen de kans om extra periodes te vinden ten koste van langere verwerkingstijd."
|
||||
}
|
||||
},
|
||||
"extension_settings": {
|
||||
"name": "Uitbreiding aan perioderand",
|
||||
"description": "Breid gedetecteerde beste-prijsperioden eventueel uit aan beide randen om aangrenzende zeer goedkope intervallen op te nemen.",
|
||||
"data": {
|
||||
"best_price_extend_to_very_cheap": "Uitbreiden met zeer goedkope intervallen",
|
||||
"best_price_max_extension_intervals": "Maximale uitbreidingsintervallen"
|
||||
},
|
||||
"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_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"
|
||||
}
|
||||
}
|
||||
},
|
||||
"submit": "↩ Opslaan & Terug"
|
||||
|
|
@ -285,6 +297,18 @@
|
|||
"min_periods_peak": "Minimaal aantal piekprijs periodes om per dag na te streven. Filters worden stapsgewijs versoepeld om dit aantal te proberen te bereiken. Alleen actief wanneer 'Bereik Minimum Aantal' is ingeschakeld. Standaard: 1",
|
||||
"relaxation_attempts_peak": "Hoeveel flex niveaus (pogingen) te proberen voordat opgegeven wordt. Elke poging voert alle filtercombinaties uit op het nieuwe flex niveau. Meer pogingen verhogen de kans om extra piekperiodes te vinden ten koste van langere verwerkingstijd."
|
||||
}
|
||||
},
|
||||
"extension_settings": {
|
||||
"name": "Uitbreiding aan perioderand",
|
||||
"description": "Breid gedetecteerde piekprijsperioden eventueel uit aan beide randen om aangrenzende zeer dure intervallen op te nemen.",
|
||||
"data": {
|
||||
"peak_price_extend_to_very_expensive": "Uitbreiden met zeer dure intervallen",
|
||||
"peak_price_max_extension_intervals": "Maximale uitbreidingsintervallen"
|
||||
},
|
||||
"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_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"
|
||||
}
|
||||
}
|
||||
},
|
||||
"submit": "↩ Opslaan & Terug"
|
||||
|
|
|
|||
|
|
@ -238,6 +238,18 @@
|
|||
"min_periods_best": "Minsta antal bästa prisperioder att sikta på per dag. Filter kommer att relaxeras steg för steg för att försöka uppnå detta antal. Endast aktiv när 'Uppnå Minimiantal' är aktiverad. Standard: 1",
|
||||
"relaxation_attempts_best": "Hur många flexnivåer (försök) att prova innan man ger upp. Varje försök kör alla filterkombinationer på den nya flexnivån. Fler försök ökar chansen att hitta ytterligare perioder på bekostnad av längre behandlingstid."
|
||||
}
|
||||
},
|
||||
"extension_settings": {
|
||||
"name": "Utvidgning av periodändarna",
|
||||
"description": "Utvidga eventuellt hittade bästa-prisperioder vid båda ändarna för att inkludera angränsande mycket billiga intervall.",
|
||||
"data": {
|
||||
"best_price_extend_to_very_cheap": "Utvidga till mycket billiga intervall",
|
||||
"best_price_max_extension_intervals": "Maximalt antal utvidgningsintervall"
|
||||
},
|
||||
"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_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"
|
||||
}
|
||||
}
|
||||
},
|
||||
"submit": "↩ Spara & tillbaka"
|
||||
|
|
@ -285,6 +297,18 @@
|
|||
"min_periods_peak": "Minsta antal topprisperioder att sikta på per dag. Filter kommer att relaxeras steg för steg för att försöka uppnå detta antal. Endast aktiv när 'Uppnå Minimiantal' är aktiverad. Standard: 1",
|
||||
"relaxation_attempts_peak": "Hur många flexnivåer (försök) att prova innan man ger upp. Varje försök kör alla filterkombinationer på den nya flexnivån. Fler försök ökar chansen att hitta ytterligare toppperioder på bekostnad av längre behandlingstid."
|
||||
}
|
||||
},
|
||||
"extension_settings": {
|
||||
"name": "Utvidgning av periodändarna",
|
||||
"description": "Utvidga eventuellt hittade topprisperioder vid båda ändarna för att inkludera angränsande mycket dyra intervall.",
|
||||
"data": {
|
||||
"peak_price_extend_to_very_expensive": "Utvidga till mycket dyra intervall",
|
||||
"peak_price_max_extension_intervals": "Maximalt antal utvidgningsintervall"
|
||||
},
|
||||
"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_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"
|
||||
}
|
||||
}
|
||||
},
|
||||
"submit": "↩ Spara & tillbaka"
|
||||
|
|
|
|||
Loading…
Reference in a new issue