From 779e22a84e111d2df65ce037a6084f91b86fe256 Mon Sep 17 00:00:00 2001 From: Julian Pawlowski Date: Sun, 12 Apr 2026 09:50:31 +0000 Subject: [PATCH] fix(periods): replace redundant total attributes with per-day counts MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Removed periods_found_total and replaced with period_count_today / period_count_tomorrow. The old attribute counted all periods including yesterday (coordinator scope), causing a discrepancy vs. the displayed list (sensor scope, today+tomorrow only). Renamed periods_total → period_count_total for naming consistency with the new per-day attributes. Recalculate period_position / period_count_total / periods_remaining after the today+tomorrow filter so all three navigation attributes reflect the filtered scope. period_count_tomorrow is always present (0 when no tomorrow data or no periods found), enabling automations without default(0) guards. Removed internal periods_found key from relaxation metadata — it was only consumed by add_calculation_summary_attributes which is now removed. BREAKING CHANGE: periods_found_total removed (replace with period_count_today + period_count_tomorrow). periods_total renamed to period_count_total. Impact: Period navigation attributes (position/total/remaining) now correctly reflect today+tomorrow scope. Per-day counts allow automations to distinguish "2 periods today, 0 tomorrow" from "1+1". --- .../tibber_prices/binary_sensor/attributes.py | 37 +++++++++++-------- .../tibber_prices/binary_sensor/core.py | 3 +- .../tibber_prices/binary_sensor/types.py | 6 +-- .../period_handlers/period_overlap.py | 4 +- .../period_handlers/period_statistics.py | 2 +- .../coordinator/period_handlers/relaxation.py | 5 --- .../tibber_prices/sensor/core.py | 2 +- .../docs/period-calculation-theory.md | 4 +- docs/developer/docs/recorder-optimization.md | 6 ++- docs/user/docs/period-calculation.md | 19 +++++----- docs/user/docs/period-relaxation.md | 2 +- 11 files changed, 46 insertions(+), 44 deletions(-) diff --git a/custom_components/tibber_prices/binary_sensor/attributes.py b/custom_components/tibber_prices/binary_sensor/attributes.py index 1820501..ae56eca 100644 --- a/custom_components/tibber_prices/binary_sensor/attributes.py +++ b/custom_components/tibber_prices/binary_sensor/attributes.py @@ -119,6 +119,19 @@ def get_price_intervals_attributes( if not filtered_periods: return build_no_periods_result(time=time) + # Recalculate position metadata after filtering (coordinator stamped values include yesterday) + # Use shallow copies so coordinator dicts are not mutated + total_filtered = len(filtered_periods) + filtered_periods = [ + period + | { + "period_position": i, + "period_count_total": total_filtered, + "periods_remaining": total_filtered - i, + } + for i, period in enumerate(filtered_periods, 1) + ] + # Find current or next period based on current time current_period = None @@ -251,8 +264,8 @@ def add_detail_attributes(attributes: dict, current_period: dict) -> None: attributes["period_interval_count"] = current_period["period_interval_count"] if "period_position" in current_period: attributes["period_position"] = current_period["period_position"] - if "periods_total" in current_period: - attributes["periods_total"] = current_period["periods_total"] + if "period_count_total" in current_period: + attributes["period_count_total"] = current_period["period_count_total"] if "periods_remaining" in current_period: attributes["periods_remaining"] = current_period["periods_remaining"] @@ -298,9 +311,8 @@ def add_period_count_attributes( count_tomorrow += 1 _ = now # used for clarity only - if count_today > 0 or count_tomorrow > 0: - attributes["period_count_today"] = count_today - attributes["period_count_tomorrow"] = count_tomorrow + attributes["period_count_today"] = count_today + attributes["period_count_tomorrow"] = count_tomorrow def add_relaxation_attributes(attributes: dict, current_period: dict) -> None: @@ -324,13 +336,11 @@ def add_calculation_summary_attributes(attributes: dict, period_metadata: dict) """ Add calculation summary attributes (priority 7). - Provides diagnostic visibility into the period calculation: how many periods - were requested vs. found, whether any flat days triggered adaptive min_periods, - and whether relaxation could not satisfy all days. + Provides diagnostic visibility into the period calculation: whether any flat days + triggered adaptive min_periods, and whether relaxation could not satisfy all days. Only adds non-default/interesting values to avoid clutter: - min_periods_configured: always added (useful reference for automations) - - periods_found_total: always added - flat_days_detected: only when > 0 (explains why fewer periods than configured) - relaxation_incomplete: only when True (diagnostic flag for troubleshooting) @@ -342,9 +352,6 @@ def add_calculation_summary_attributes(attributes: dict, period_metadata: dict) if "min_periods_requested" in relaxation_meta: attributes["min_periods_configured"] = relaxation_meta["min_periods_requested"] - if "periods_found" in relaxation_meta: - attributes["periods_found_total"] = relaxation_meta["periods_found"] - flat_days = relaxation_meta.get("flat_days_detected", 0) if flat_days > 0: attributes["flat_days_detected"] = flat_days @@ -414,10 +421,10 @@ def build_final_attributes_simple( 2. Core decision attributes (level, rating_level, rating_difference_%) 3. Price statistics (price_mean, price_median, price_min, price_max, price_spread, volatility) 4. Price differences (period_price_diff_from_daily_min, period_price_diff_from_daily_min_%) - 5. Detail information (period_interval_count, period_position, periods_total, periods_remaining) + 5. Detail information (period_interval_count, period_position, period_count_total, periods_remaining) 6. Relaxation information (relaxation_active, relaxation_level, relaxation_threshold_original_%, relaxation_threshold_applied_%) - only if current period was relaxed - 7. Calculation summary (min_periods_configured, periods_found_total, flat_days_detected, + 7. Calculation summary (min_periods_configured, flat_days_detected, relaxation_incomplete) - diagnostic info about the overall calculation 8. Meta information (periods list) @@ -464,7 +471,7 @@ def build_final_attributes_simple( # 6. Relaxation information (only if current period was relaxed) add_relaxation_attributes(attributes, current_period) - # 7. Calculation summary (diagnostic: min_periods_configured, periods_found_total, etc.) + # 7. Calculation summary (diagnostic: min_periods_configured, flat_days_detected, etc.) if period_metadata: add_calculation_summary_attributes(attributes, period_metadata) diff --git a/custom_components/tibber_prices/binary_sensor/core.py b/custom_components/tibber_prices/binary_sensor/core.py index 1b1beb2..fe3ca45 100644 --- a/custom_components/tibber_prices/binary_sensor/core.py +++ b/custom_components/tibber_prices/binary_sensor/core.py @@ -64,7 +64,6 @@ class TibberPricesBinarySensor(TibberPricesEntity, BinarySensorEntity, RestoreEn "relaxation_threshold_applied_%", # Calculation Summary (diagnostic, changes daily → not useful in history) "min_periods_configured", - "periods_found_total", "flat_days_detected", "relaxation_incomplete", # Redundant/Derived @@ -73,7 +72,7 @@ class TibberPricesBinarySensor(TibberPricesEntity, BinarySensorEntity, RestoreEn "rating_difference_%", "period_price_diff_from_daily_min", "period_price_diff_from_daily_min_%", - "periods_total", + "period_count_total", "periods_remaining", } ) diff --git a/custom_components/tibber_prices/binary_sensor/types.py b/custom_components/tibber_prices/binary_sensor/types.py index f2f3c9b..5b297ed 100644 --- a/custom_components/tibber_prices/binary_sensor/types.py +++ b/custom_components/tibber_prices/binary_sensor/types.py @@ -104,7 +104,7 @@ class PeriodSummary(TypedDict, total=False): # Detail information (priority 5) period_interval_count: int # Number of intervals in period period_position: int # Period position (1-based) - periods_total: int # Total number of periods + period_count_total: int # Total number of periods periods_remaining: int # Remaining periods after this one # Relaxation information (priority 6 - only if period was relaxed) @@ -125,7 +125,7 @@ class PeriodAttributes(BaseAttributes, total=False): 2. Core decision attributes (level, rating_level, rating_difference_%) 3. Price statistics (price_mean, price_median, price_min, price_max, price_spread, volatility) 4. Price comparison (period_price_diff_from_daily_min, period_price_diff_from_daily_min_%) - 5. Detail information (period_interval_count, period_position, periods_total, periods_remaining) + 5. Detail information (period_interval_count, period_position, period_count_total, periods_remaining) 6. Relaxation information (only if period was relaxed) 7. Meta information (periods list) """ @@ -155,7 +155,7 @@ class PeriodAttributes(BaseAttributes, total=False): # Detail information (priority 5) period_interval_count: int # Number of intervals in current/next period period_position: int # Period position (1-based) - periods_total: int # Total number of periods found + period_count_total: int # Total number of periods found periods_remaining: int # Remaining periods after current/next one # Relaxation information (priority 6 - only if period was relaxed) diff --git a/custom_components/tibber_prices/coordinator/period_handlers/period_overlap.py b/custom_components/tibber_prices/coordinator/period_handlers/period_overlap.py index ed7a993..5490023 100644 --- a/custom_components/tibber_prices/coordinator/period_handlers/period_overlap.py +++ b/custom_components/tibber_prices/coordinator/period_handlers/period_overlap.py @@ -56,7 +56,7 @@ def recalculate_period_metadata(periods: list[dict], *, time: TibberPricesTimeSe """ Recalculate period metadata after merging periods. - Updates period_position, periods_total, and periods_remaining for all periods + Updates period_position, period_count_total, and periods_remaining for all periods based on chronological order. This must be called after resolve_period_overlaps() to ensure metadata @@ -78,7 +78,7 @@ def recalculate_period_metadata(periods: list[dict], *, time: TibberPricesTimeSe for position, period in enumerate(periods, 1): period["period_position"] = position - period["periods_total"] = total_periods + period["period_count_total"] = total_periods period["periods_remaining"] = total_periods - position diff --git a/custom_components/tibber_prices/coordinator/period_handlers/period_statistics.py b/custom_components/tibber_prices/coordinator/period_handlers/period_statistics.py index 963af4c..8ce6f37 100644 --- a/custom_components/tibber_prices/coordinator/period_handlers/period_statistics.py +++ b/custom_components/tibber_prices/coordinator/period_handlers/period_statistics.py @@ -178,7 +178,7 @@ def build_period_summary_dict( # 5. Detail information (additional context) "period_interval_count": period_data.period_length, "period_position": period_data.period_idx, - "periods_total": period_data.total_periods, + "period_count_total": period_data.total_periods, "periods_remaining": period_data.total_periods - period_data.period_idx, } diff --git a/custom_components/tibber_prices/coordinator/period_handlers/relaxation.py b/custom_components/tibber_prices/coordinator/period_handlers/relaxation.py index c1475f5..131a839 100644 --- a/custom_components/tibber_prices/coordinator/period_handlers/relaxation.py +++ b/custom_components/tibber_prices/coordinator/period_handlers/relaxation.py @@ -639,7 +639,6 @@ def calculate_periods_with_relaxation( # noqa: PLR0912, PLR0913, PLR0915 - Per- "relaxation_active": False, "relaxation_attempted": False, "min_periods_requested": min_periods if enable_relaxation else 0, - "periods_found": 0, }, }, "reference_data": {}, @@ -840,8 +839,6 @@ def calculate_periods_with_relaxation( # noqa: PLR0912, PLR0913, PLR0915 - Per- final_result = baseline_result.copy() final_result["periods"] = all_periods - total_periods = len(all_periods) - # Add relaxation info to metadata if "metadata" not in final_result: final_result["metadata"] = {} @@ -849,7 +846,6 @@ def calculate_periods_with_relaxation( # noqa: PLR0912, PLR0913, PLR0915 - Per- "relaxation_active": relaxation_was_needed, "relaxation_attempted": relaxation_was_needed, "min_periods_requested": min_periods, - "periods_found": total_periods, "phases_used": list(set(all_phases_used)), # Unique phases used across all days "days_processed": total_days, "days_meeting_requirement": days_meeting_requirement, @@ -1023,5 +1019,4 @@ def relax_all_prices( # noqa: PLR0913 - Comprehensive filter relaxation require return final_result, { "phases_used": phases_used, - "periods_found": len(existing_periods), } diff --git a/custom_components/tibber_prices/sensor/core.py b/custom_components/tibber_prices/sensor/core.py index 4477b3b..3ea0410 100644 --- a/custom_components/tibber_prices/sensor/core.py +++ b/custom_components/tibber_prices/sensor/core.py @@ -172,7 +172,7 @@ class TibberPricesSensor(TibberPricesEntity, RestoreSensor): "rating_difference_%", "period_price_diff_from_daily_min", "period_price_diff_from_daily_min_%", - "periods_total", + "period_count_total", "periods_remaining", } ) diff --git a/docs/developer/docs/period-calculation-theory.md b/docs/developer/docs/period-calculation-theory.md index 5dcda87..cea1f69 100644 --- a/docs/developer/docs/period-calculation-theory.md +++ b/docs/developer/docs/period-calculation-theory.md @@ -834,8 +834,7 @@ INFO: Day 2025-11-11: Baseline satisfied (1 period, effective minimum is 1) **Sensor Attributes:** ```yaml min_periods_configured: 2 # User's setting -periods_found_total: 1 # Actual result -flat_days_detected: 1 # Explains the difference +flat_days_detected: 1 # Explains why only 1 period found ``` **Why not for Peak Price?** @@ -957,7 +956,6 @@ When debugging period calculation issues: | Attribute | Type | When shown | Meaning | |---|---|---|---| | `min_periods_configured` | int | Always | User's configured target per day | -| `periods_found_total` | int | Always | Actual periods found across all days | | `flat_days_detected` | int | Only when > 0 | Days where CV ≤ 10% reduced target to 1 | | `relaxation_incomplete` | bool | Only when true | Relaxation exhausted, target not reached | | `relaxation_active` | bool | Only when true | This specific period needed relaxed filters | diff --git a/docs/developer/docs/recorder-optimization.md b/docs/developer/docs/recorder-optimization.md index 50cc494..e602f7d 100644 --- a/docs/developer/docs/recorder-optimization.md +++ b/docs/developer/docs/recorder-optimization.md @@ -137,7 +137,7 @@ class TibberPricesSensor(TibberPricesEntity, SensorEntity): ### 7. Redundant/Derived Data -**Attributes:** `price_spread`, `volatility`, `diff_%`, `rating_difference_%`, `period_price_diff_from_daily_min`, `period_price_diff_from_daily_min_%`, `periods_total`, `periods_remaining` +**Attributes:** `price_spread`, `volatility`, `diff_%`, `rating_difference_%`, `period_price_diff_from_daily_min`, `period_price_diff_from_daily_min_%`, `period_count_total`, `periods_remaining` **Reason:** - Can be calculated from other attributes @@ -146,7 +146,7 @@ class TibberPricesSensor(TibberPricesEntity, SensorEntity): **Impact:** ~100-200 bytes saved per state change -**Example:** `price_spread = price_max - price_min` (both are recorded, so spread can be calculated) +**Example:** `price_spread = price_max - price_min` (both are recorded, so spread can be calculated). `periods_remaining = period_count_total - period_position` (both components are recorded). ## Attributes That ARE Recorded @@ -166,6 +166,8 @@ These attributes **remain in history** because they provide essential analytical ### Period Data - `start`, `end`, `duration_minutes` - Core period timing - `price_mean`, `price_median`, `price_min`, `price_max` - Core price statistics +- `period_position` - Position of current period in the day's sequence +- `period_count_today`, `period_count_tomorrow` - How many periods per day (useful in automations) ### High-Level Status - `relaxation_active` - Whether relaxation was used (boolean, useful for analyzing when periods needed relaxation) diff --git a/docs/user/docs/period-calculation.md b/docs/user/docs/period-calculation.md index cc09f31..75cb8e4 100644 --- a/docs/user/docs/period-calculation.md +++ b/docs/user/docs/period-calculation.md @@ -523,7 +523,7 @@ This is **expected behavior** on days with very uniform electricity prices. When ```yaml min_periods_configured: 2 -periods_found_total: 1 +period_count_today: 1 flat_days_detected: 1 # Uniform prices today → 1 period is the right answer ``` @@ -656,7 +656,8 @@ relaxation_level: "price_diff_18.0%+level_any" # Found at 18% flex, level filte # Calculation summary (always shown – diagnostic overview of this calculation run): min_periods_configured: 2 # What you configured as target -periods_found_total: 3 # What was actually found across all days +period_count_today: 2 # How many periods are scheduled today +period_count_tomorrow: 2 # How many periods are scheduled tomorrow (when data available) # Optional (only shown when relevant): period_interval_smoothed_count: 2 # Number of price spikes smoothed @@ -669,7 +670,7 @@ relaxation_incomplete: true # Some days couldn't reach the configured ta #### What the diagnostic attributes mean -**`min_periods_configured` / `periods_found_total`** +**`min_periods_configured` / `period_count_today`** These two values together quickly show whether the calculation achieved its goal: @@ -678,18 +679,18 @@ These two values together quickly show whether the calculation achieved its goal ```yaml min_periods_configured: 2 # You asked for 2 periods per day -periods_found_total: 6 # 3 days × 2 periods = fully satisfied ✅ +period_count_today: 2 # ✅ Today: target reached +period_count_tomorrow: 2 # ✅ Tomorrow: target reached ``` ```yaml min_periods_configured: 2 -periods_found_total: 5 # 3 days, but one day got only 1 period +period_count_today: 1 # ⚠️ Today: only 1 period found +period_count_tomorrow: 2 # ✅ Tomorrow: target reached ``` -Note that `periods_found_total` counts **all periods across today and tomorrow** – so 4 on a two-day view means 2 per day on average. - **`flat_days_detected`** This is the most important diagnostic for days with very uniform prices (e.g. sunny spring/summer days with high solar generation): @@ -699,7 +700,7 @@ This is the most important diagnostic for days with very uniform prices (e.g. su ```yaml min_periods_configured: 2 -periods_found_total: 1 +period_count_today: 1 flat_days_detected: 1 # ← This explains why you got 1 instead of 2 ``` @@ -718,7 +719,7 @@ This flag appears when even after all relaxation attempts, at least one day coul ```yaml min_periods_configured: 2 -periods_found_total: 1 +period_count_today: 1 relaxation_incomplete: true # ← Relaxation tried everything, still short ``` diff --git a/docs/user/docs/period-relaxation.md b/docs/user/docs/period-relaxation.md index f2b47f6..af74b8a 100644 --- a/docs/user/docs/period-relaxation.md +++ b/docs/user/docs/period-relaxation.md @@ -130,7 +130,7 @@ Check the period sensor attributes to understand what happened: relaxation_active: true # This day needed relaxation relaxation_level: "price_diff_18.0%+level_any" # Found at 18% flex, level filter removed min_periods_configured: 2 # Your target -periods_found_total: 3 # What was actually found +period_count_today: 3 # What was actually found today ```