mirror of
https://github.com/jpawlowski/hass.tibber_prices.git
synced 2026-05-28 18:43:40 +00:00
fix(periods): replace redundant total attributes with per-day counts
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".
This commit is contained in:
parent
9e1ba10f0b
commit
779e22a84e
11 changed files with 46 additions and 44 deletions
|
|
@ -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,7 +311,6 @@ 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
|
||||
|
||||
|
|
@ -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)
|
||||
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
}
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
}
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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 |
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
```
|
||||
|
||||
</details>
|
||||
|
||||
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
|
||||
```
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
```
|
||||
|
||||
</details>
|
||||
|
|
|
|||
Loading…
Reference in a new issue