hass.tibber_prices/docs/user/period-calculation.md

23 KiB
Raw Blame History

Period Calculation

A detailed explanation of how Best Price and Peak Price periods are calculated and how you can influence the calculation with configuration options.

Table of Contents


Overview

What are Price Periods?

The integration automatically calculates Best Price Periods (cheap time windows) and Peak Price Periods (expensive time windows) for each day. These periods help you:

  • Best Price: Shift electricity consumption to cheap times (e.g., charge electric car, run dishwasher, washing machine, heat pump water heater)
  • Peak Price: Avoid high consumption during expensive times (e.g., reduce heating temporarily, defer non-essential loads)

Basic Principle

The calculation happens in multiple steps:

  1. Period Identification: Find contiguous time ranges within a flexibility range of the daily MIN/MAX prices
  2. Hard Filters: Apply non-configurable filters (periods must be below/above average)
  3. Length Filter: Remove periods that are too short
  4. Quality Filter: Ensure meaningful distance from average
  5. Optional Filters: Apply volatility and level filters if configured

All steps except #2 can be influenced by configuration options.


Calculation Flow

Step 1: Data Preparation

What happens:

  • Fetch all price intervals for today (96 x 15-minute intervals = 24 hours)
  • Calculate daily MIN, MAX, and AVG prices
  • Calculate trailing 24h average for each interval

Example:

Today: 96 intervals from 00:00 to 23:59
Daily MIN: 18.0 ct/kWh
Daily MAX: 35.0 ct/kWh
Daily AVG: 26.5 ct/kWh

Step 2: Period Identification (Flexibility)

What happens:

  • Search for contiguous intervals within a flexibility range of the daily extreme prices
  • Best Price: Includes intervals within flexibility range of the day's MINIMUM price
  • Peak Price: Includes intervals within flexibility range of the day's MAXIMUM price

Configuration:

  • best_price_flex (default: 15%) - How much more expensive than the daily MIN can an interval be?
  • peak_price_flex (default: -15%) - How much less expensive than the daily MAX can an interval be?

Example (Best Price with 15% flexibility):

Daily prices: 18.0 ct (min), 35.0 ct (max), 26.5 ct (avg)
Flexibility: 15%
Reference: Daily MIN = 18.0 ct (not average!)
Threshold: 18.0 + (18.0 × 0.15) = 20.7 ct/kWh

Intervals that cost ≤ 20.7 ct/kWh are grouped into periods:
00:00-00:15: 18.5 ct ✓ │
00:15-00:30: 18.0 ct ✓ ├─ Period 1 (1h)
00:30-00:45: 19.8 ct ✓ │
00:45-01:00: 20.2 ct ✓ │
01:00-01:15: 21.5 ct ✗   (exceeds flexibility threshold, period ends)

Why compare to MIN/MAX instead of average?

  • Creates periods around the best/worst price opportunities of the day
  • More predictable behavior: flexibility directly controls how far from the extreme prices you go
  • Prevents marking mediocre prices as "best" just because the daily average is high

Note: The flexibility check (vs MIN/MAX) and the minimum distance check (vs AVG in Step 4) work together to ensure periods are both close to extremes AND meaningfully different from average.

Step 3: Minimum Period Length

What happens:

  • Periods that are too short are discarded (not practical to use)

Configuration:

  • best_price_min_period_length (default: 60 minutes)
  • peak_price_min_period_length (default: 60 minutes)

Example:

Found periods:
- 00:00-01:00 (60 min) ✓ Keep
- 03:00-03:30 (30 min) ✗ Discard (too short)
- 14:00-15:15 (75 min) ✓ Keep

Step 4: Minimum Distance from Average

What happens:

  • This is a SEPARATE filter from flexibility (Step 2)
  • Each interval must be sufficiently far from the daily average
  • Prevents marking "almost normal" prices as "Best/Peak" on days with small price spread
  • Implicitly ensures intervals are below/above average (since distance > 0% by default)

Configuration:

  • best_price_min_distance_from_avg (default: 2%) - Minimum distance below average
  • peak_price_min_distance_from_avg (default: 2%) - Minimum distance above average

Example (Best Price):

Daily prices: 18.0 ct (min), 35.0 ct (max), 26.5 ct (avg)
Flexibility: 15%
Minimum distance: 2%

Flexibility threshold (from Step 2): 18.0 × 1.15 = 20.7 ct
Average distance threshold: 26.5 × 0.98 = 25.97 ct

Final check for each interval - BOTH conditions must pass:
1. Price ≤ flexibility threshold? (20.7 ct) ← vs MIN
2. AND price ≤ average distance threshold? (25.97 ct) ← vs AVG

Interval at 19.5 ct:
  ✓ Meets flexibility (19.5 ≤ 20.7)
  ✓ Meets min distance (19.5 ≤ 25.97)
  → ACCEPTED (both conditions met)

Interval at 22.0 ct:
  ✗ Fails flexibility (22.0 > 20.7)
  ✓ Meets min distance (22.0 ≤ 25.97)
  → REJECTED (flexibility condition failed)

Interval at 26.0 ct (hypothetical):
  ✗ Fails flexibility (26.0 > 20.7)
  ✗ Fails min distance (26.0 > 25.97)
  → REJECTED (both conditions failed)

Why this matters:

  • On days with small price variation, flexibility alone might include intervals that are barely below average
  • The minimum distance filter ensures you're actually getting meaningful savings
  • With default 2%, intervals must be at least 2% below average (which also ensures they're below average)

Step 5: Filter Application

What happens:

  • Apply optional filters (volatility, price level)
  • See Filter Pipeline for details

Configuration Options in Detail

Best Price Period Settings

Option Default Description Acts in Step
best_price_flex 15% How much more expensive than the daily MIN can an interval be? 2 (Identification)
best_price_min_period_length 60 min Minimum length of a period 3 (Length filter)
best_price_min_distance_from_avg 2% Minimum distance below daily average (separate from flexibility) 4 (Quality filter)
best_price_min_volatility LOW Minimum volatility within the period (optional) 5 (Volatility filter)
best_price_max_level ANY Maximum price level (optional, e.g., only CHEAP or better) 5 (Level filter)
best_price_max_level_gap_count 0 Tolerance for level deviations (see Gap Tolerance) 5 (Level filter)
enable_min_periods_best Off Enables relaxation mechanism - (Relaxation)
min_periods_best 2 Minimum number of periods to achieve - (Relaxation)
relaxation_step_best 25% Step size for filter relaxation - (Relaxation)

Peak Price Period Settings

Option Default Description Acts in Step
peak_price_flex -15% How much less expensive than the daily MAX can an interval be? 2 (Identification)
peak_price_min_period_length 60 min Minimum length of a period 3 (Length filter)
peak_price_min_distance_from_avg 2% Minimum distance above daily average (separate from flexibility) 4 (Quality filter)
peak_price_min_volatility LOW Minimum volatility within the period (optional) 5 (Volatility filter)
peak_price_min_level ANY Minimum price level (optional, e.g., only EXPENSIVE or higher) 5 (Level filter)
peak_price_max_level_gap_count 0 Tolerance for level deviations (see Gap Tolerance) 5 (Level filter)
enable_min_periods_peak Off Enables relaxation mechanism - (Relaxation)
min_periods_peak 2 Minimum number of periods to achieve - (Relaxation)
relaxation_step_peak 25% Step size for filter relaxation - (Relaxation)

Filter Pipeline

After basic period identification (Steps 1-4), two optional additional filters can be applied:

Volatility Filter

Purpose: Only show periods when the price spread within the period is large enough.

Use case:

  • Best Price: "I only want to optimize when it's really worth it" (high volatility)
  • Peak Price: "Only warn me about large price swings" (high volatility)

How it works:

Period: 00:00-01:00
Intervals: 20.5 | 19.8 | 21.0 | 20.2 ct/kWh
Min: 19.8 ct, Max: 21.0 ct
Volatility (spread): 21.0 - 19.8 = 1.2 ct/kWh

Volatility thresholds:
- LOW: < 5.0 ct   → This period: LOW
- MODERATE: 5-15 ct
- HIGH: 15-30 ct
- VERY_HIGH: ≥ 30 ct

best_price_min_volatility = "MODERATE" (5 ct)
→ Period is REJECTED (1.2 ct < 5.0 ct)

Configuration:

  • best_price_min_volatility: low | moderate | high | very_high
  • peak_price_min_volatility: low | moderate | high | very_high

Default: low (filter disabled, all periods shown)

Level Filter (Price Level)

Purpose: Only show periods that are actually cheap/expensive in absolute terms, not just relative to the daily average.

Use case:

  • Best Price: "Only show best price when there's at least one CHEAP interval" (not just "less expensive than usual today")
  • Peak Price: "Only show peak price when there's at least one EXPENSIVE interval" (not just "more expensive than average")

Price levels (from Tibber API):

  • VERY_CHEAP (-2)
  • CHEAP (-1)
  • NORMAL (0)
  • EXPENSIVE (+1)
  • VERY_EXPENSIVE (+2)

How it works (Best Price example):

best_price_max_level = "CHEAP"

Period: 00:00-01:00
Intervals with levels:
  00:00: 20.5 ct → CHEAP ✓
  00:15: 19.8 ct → VERY_CHEAP ✓
  00:30: 21.0 ct → NORMAL ✗
  00:45: 20.2 ct → CHEAP ✓

Filter logic (without gap tolerance):
  → Does the period have at least ONE interval with level ≤ CHEAP?
  → YES (three intervals are CHEAP or better)
  → Period is KEPT

But: One NORMAL interval in the middle!
  → Without gap tolerance: Period is split into two parts
  → With gap tolerance: Period stays together (see next section)

Configuration:

  • best_price_max_level: any | very_cheap | cheap | normal | expensive
  • peak_price_min_level: any | expensive | normal | cheap | very_cheap

Default: any (filter disabled)


Gap Tolerance for Level Filters

Problem Without Gap Tolerance

When you activate a level filter (e.g., best_price_max_level = "CHEAP"), periods are strictly filtered:

Period: 00:00-02:00 (2 hours)
Intervals:
  00:00-01:30: CHEAP, CHEAP, CHEAP, CHEAP, CHEAP, CHEAP
  01:30-01:45: NORMAL  ← A single deviating interval!
  01:45-02:00: CHEAP

Without gap tolerance:
  → Period is split into TWO periods:
    1. 00:00-01:30 (1.5h)
    2. 01:45-02:00 (0.25h) ✗ too short, discarded!
  → Result: Only 1.5h best price instead of 2h

Solution: Gap Tolerance

Gap tolerance allows a configurable number of intervals that deviate by exactly one level step from the required level.

How it works:

  1. "Gap" definition: An interval that deviates by exactly 1 level step

    Best Price filter: CHEAP (-1)
    NORMAL (0) is +1 step → GAP ✓
    EXPENSIVE (+1) is +2 steps → NOT A GAP, too far away
    
  2. Gap counting: Max X gaps allowed per period (configurable: 0-8)

  3. Minimum distance between gaps: Gaps must not be too close together

    Dynamic formula: max(2, (interval_count / max_gaps) / 2)
    
    Example: 16 intervals, max 2 gaps allowed
    → Minimum distance: max(2, (16/2)/2) = max(2, 4) = 4 intervals
    
    CHEAP, CHEAP, CHEAP, CHEAP, NORMAL, CHEAP, CHEAP, CHEAP, NORMAL, CHEAP
             ↑                    GAP1           ↑            GAP2
             └─────── 4 intervals ──────────────┘
    → OK, minimum distance maintained
    
  4. 25% cap: Maximum 25% of a period's intervals can be gaps

    Period: 12 intervals, user configured 5 gaps
    → Effective: min(5, 12/4) = min(5, 3) = 3 gaps allowed
    
  5. Minimum period length: Gap tolerance only applies to periods ≥ 1.5h (6 intervals)

    Period < 1.5h: Strict filtering (0 tolerance)
    Period ≥ 1.5h: Gap tolerance as configured
    

Gap Cluster Splitting

If a period would still be rejected despite gap tolerance (too many gaps or too dense), the integration tries to intelligently split it:

Period: 00:00-04:00 (16 intervals)
CHEAP, CHEAP, CHEAP, NORMAL, NORMAL, NORMAL, CHEAP, CHEAP, ..., CHEAP
                      └─ Gap cluster (3×) ─┘

Gap cluster = 2+ consecutive deviating intervals

→ Splitting at gap cluster:
  1. 00:00-00:45 (3 intervals) ✗ too short
  2. 01:30-04:00 (10 intervals) ✓ kept

→ Result: 2.5h best price instead of complete rejection

Configuration

Best Price:

best_price_max_level: "cheap"           # Enable level filter
best_price_max_level_gap_count: 2       # Allow 2 NORMAL intervals per period

Peak Price:

peak_price_min_level: "expensive"       # Enable level filter
peak_price_max_level_gap_count: 1       # Allow 1 NORMAL interval per period

Default: 0 (no tolerance, strict filtering)

Example Scenarios

Scenario 1: Conservative (0 gaps)

best_price_max_level: "cheap"
best_price_max_level_gap_count: 0  # Default

Behavior:

  • Every interval MUST be CHEAP or better
  • A single NORMAL interval → period is split

Good for: Users who want absolute price guarantees

Scenario 2: Moderate (2-3 gaps)

best_price_max_level: "cheap"
best_price_max_level_gap_count: 2

Behavior:

  • Up to 2 NORMAL intervals per period tolerated
  • Minimum distance between gaps dynamically calculated
  • 25% cap protects against too many gaps

Good for: Most users - balance between quality and period length

Scenario 3: Aggressive (5-8 gaps)

best_price_max_level: "cheap"
best_price_max_level_gap_count: 5

Behavior:

  • Up to 5 NORMAL intervals (but max 25% of period)
  • Longer periods possible
  • Quality may suffer (more "not-quite-so-cheap" intervals)

Good for: Users with flexible devices that need long run times


Relaxation Mechanism

If too few periods are found despite all filters, the integration can automatically gradually relax filters.

When is Relaxation Applied?

Only when both conditions are met:

  1. enable_min_periods_best/peak is enabled
  2. Fewer than min_periods_best/peak periods found

Relaxation Levels

The integration tries to relax filters in this order:

Level 1: Relax Flexibility

Original: best_price_flex = 15%
Step 1: 15% + (15% × 0.25) = 18.75%
Step 2: 18.75% + (18.75% × 0.25) = 23.44%
Step 3: ...

Calculation: new_flexibility = old_flexibility × (1 + relaxation_step / 100)

Important: This increases the flexibility percentage, which allows intervals further from the daily MIN/MAX to be included. For best price, this means accepting intervals more expensive than the original flexibility threshold.

Level 2: Disable Volatility Filter

If flexibility relaxation isn't enough:
  → best_price_min_volatility = "any" (filter off)

Level 3: Disable All Filters

If still too few periods:
  → Volatility = "any"
  → Level filter = "any"
  → Only flexibility and minimum length active

Relaxation Status

The sensors show the relaxation status as an attribute:

Best Price Period:  # sensor.tibber_home_best_price_period
  state: "on"
  attributes:
    relaxation_level: "volatility_any"  # Volatility filter was disabled

Possible values:

  • none - No relaxation, normal filters
  • volatility_any - Volatility filter disabled
  • all_filters_off - All optional filters disabled

Example Configuration

# Best Price: Try to find at least 2 periods
enable_min_periods_best: true
min_periods_best: 2
relaxation_step_best: 25  # 25% per step

best_price_flex: 15
best_price_min_volatility: "moderate"

Process on a day with little price spread:

  1. Try with 15% flex + MODERATE volatility → 0 periods
  2. Relax to 18.75% flex → 1 period
  3. Relax to 23.44% flex → 1 period (still < 2)
  4. Disable volatility filter → 2 periods ✓

Result: User sees 2 periods with relaxation_level: "volatility_any"


Practical Examples

Example 1: Standard Configuration (Best Price)

Configuration:

best_price_flex: 15
best_price_min_period_length: 60
best_price_min_distance_from_avg: 2
best_price_min_volatility: "low"  # Filter disabled
best_price_max_level: "any"  # Filter disabled

Daily prices:

MIN: 18.0 ct/kWh
MAX: 32.0 ct/kWh
AVG: 25.0 ct/kWh

00:00-02:00: 18-20 ct (cheap)
06:00-08:00: 28-30 ct (expensive)
12:00-14:00: 24-26 ct (normal)
18:00-20:00: 19-21 ct (cheap)

Calculation:

  1. Flexibility threshold: 18.0 × 1.15 = 20.7 ct (vs MIN, not average!)
  2. Minimum distance threshold: 25.0 × 0.98 = 24.5 ct (vs AVG)
  3. Both conditions: Price ≤ 20.7 ct AND Price ≤ 24.5 ct

Result:

  • ✓ 00:00-02:00 (18-20 ct, all ≤ 20.7 and all ≤ 24.5)
  • ✗ 06:00-08:00 (too expensive)
  • ✗ 12:00-14:00 (24-26 ct, exceeds flexibility threshold of 20.7 ct)
  • ✓ 18:00-20:00 (19-21 ct, all ≤ 20.7 and all ≤ 24.5)

2 Best Price periods found!

Example 2: Strict Level Filter Without Gap Tolerance

Configuration:

best_price_flex: 15
best_price_max_level: "cheap"
best_price_max_level_gap_count: 0  # No tolerance

Period candidate:

00:00-02:00:
  00:00-01:30: CHEAP, CHEAP, CHEAP, CHEAP, CHEAP, CHEAP
  01:30-01:45: NORMAL  ← Deviation!
  01:45-02:00: CHEAP

Result:

  • ✗ Period is split into 00:00-01:30 and 01:45-02:00
  • ✗ 01:45-02:00 too short (15 min < 60 min) → discarded
  • ✓ Only 00:00-01:30 (1.5h) remains

Example 3: Level Filter With Gap Tolerance

Configuration:

best_price_flex: 15
best_price_max_level: "cheap"
best_price_max_level_gap_count: 2  # 2 gaps allowed

Period candidate (same as above):

00:00-02:00:
  00:00-01:30: CHEAP, CHEAP, CHEAP, CHEAP, CHEAP, CHEAP
  01:30-01:45: NORMAL  ← Gap (1 of 2 allowed)
  01:45-02:00: CHEAP

Gap tolerance check:

  • Gaps found: 1 (NORMAL)
  • Max allowed: 2
  • 25% cap: min(2, 8/4) = 2 (8 intervals)
  • Minimum distance: N/A (only 1 gap)

Result:

  • ✓ Period stays as a whole: 00:00-02:00 (2h)
  • 1 NORMAL interval is tolerated

Example 4: Gap Cluster Gets Split

Configuration:

best_price_flex: 15
best_price_max_level: "cheap"
best_price_max_level_gap_count: 2

Period candidate:

00:00-04:00 (16 intervals):
  00:00-01:00: CHEAP, CHEAP, CHEAP, CHEAP (4)
  01:00-02:00: NORMAL, NORMAL, NORMAL, NORMAL (4) ← Gap cluster!
  02:00-04:00: CHEAP, CHEAP, CHEAP, ..., CHEAP (8)

Gap tolerance check:

  • Gaps found: 4 (too many)
  • Max allowed: 2
  • → Normal check fails

Gap cluster splitting:

  • Detect cluster: 4× consecutive NORMAL intervals
  • Split period at cluster boundaries:
    1. 00:00-01:00 (4 intervals = 60 min) ✓
    2. 02:00-04:00 (8 intervals = 120 min) ✓

Result:

  • ✓ Two separate periods: 00:00-01:00 and 02:00-04:00
  • Total 3h best price (instead of complete rejection)

Example 5: Relaxation in Action

Configuration:

enable_min_periods_best: true
min_periods_best: 2
relaxation_step_best: 25

best_price_flex: 5  # Very strict!
best_price_min_volatility: "high"  # Very strict!

Day with little price spread:

MIN: 23.0 ct/kWh
MAX: 27.0 ct/kWh
AVG: 25.0 ct/kWh
All prices between 23-27 ct (low volatility)

Relaxation process:

  1. Attempt 1: 5% flex + HIGH volatility

    Threshold: 23.0 × 1.05 = 24.15 ct (vs MIN)
    No period meets both conditions
    → 0 periods (< 2 required)
    
  2. Attempt 2: 6.25% flex + HIGH volatility

    Threshold: 23.0 × 1.0625 = 24.44 ct
    Still 0 periods
    
  3. Attempt 3: Disable volatility filter

    6.25% flex + ANY volatility
    → 1 period found (< 2)
    
  4. Attempt 4: 7.81% flex + ANY volatility

    Threshold: 23.0 × 1.0781 = 24.80 ct
    → 2 periods found ✓
    

Result:

  • Sensor shows 2 periods with relaxation_level: "volatility_any"
  • User knows: "Filters were relaxed to reach minimum count"

Troubleshooting

Problem: No Periods Found

Possible causes:

  1. Too strict flexibility

    best_price_flex: 5  # Only allows intervals ≤5% above daily MIN
    

    Solution: Increase to 10-15%

  2. Too strict level filter without gap tolerance

    best_price_max_level: "very_cheap"
    best_price_max_level_gap_count: 0
    

    Solution: Relax level to "cheap" or enable gap tolerance (1-2)

  3. Too high volatility requirement

    best_price_min_volatility: "very_high"
    

    Solution: Reduce to "moderate" or "low"

  4. Too long minimum period length

    best_price_min_period_length: 180  # 3 hours
    

    Solution: Reduce to 60-90 minutes

  5. Day with very small price spread

    MIN: 23 ct, MAX: 27 ct (hardly any differences)
    

    Solution: Enable relaxation mechanism:

    enable_min_periods_best: true
    min_periods_best: 1
    

Problem: Too Many Periods

Solution: Make filters stricter:

best_price_flex: 20  # Reduce to 10-15
best_price_min_volatility: "moderate"  # Require higher volatility
best_price_max_level: "cheap"  # Only truly cheap times

Problem: Periods Are Too Short

Solution: Increase minimum length and use gap tolerance:

best_price_min_period_length: 90  # 1.5 hours
best_price_max_level_gap_count: 2  # Tolerate deviations

Problem: Periods With "Mediocre" Prices

Solution: Increase minimum distance:

best_price_min_distance_from_avg: 5  # Must be 5% below average

Problem: Relaxation Applied Too Aggressively

Solution: Reduce step size:

relaxation_step_best: 10  # Smaller steps (instead of 25)

Or disable relaxation completely:

enable_min_periods_best: false

Problem: Gap Tolerance Not Working As Expected

Possible causes:

  1. Period too short (< 1.5h)

    Gap tolerance only applies to periods ≥ 1.5h
    

    Solution: Reduce best_price_min_period_length or adjust flexibility

  2. 25% cap limiting effective gaps

    Period: 8 intervals, configured 4 gaps
    → Effective: min(4, 8/4) = 2 gaps
    

    Solution: Accept limitation or relax level filter

  3. Gaps too close together

    Minimum distance between gaps not maintained
    

    Solution: Increase gap count or accept splitting


Further Documentation


Questions or feedback? Open an issue on GitHub!