mirror of
https://github.com/jpawlowski/hass.tibber_prices.git
synced 2026-04-07 08:03:40 +00:00
feat(binary_sensor): expose period calculation diagnostics as attributes
Add calculation summary attributes to best_price_period and peak_price_period binary sensors for diagnostic transparency. New attributes (all excluded from recorder history): - min_periods_configured: User's configured target per day (always shown) - periods_found_total: Actual periods found across all days (always shown) - flat_days_detected: Days where CV <= 10% reduced target to 1 (only when > 0) - relaxation_incomplete: Some days couldn't reach the target (only when true) These attributes explain observed behavior without requiring users to read logs: seeing "flat_days_detected: 1" alongside "min_periods_configured: 2, periods_found_total: 1" immediately explains why the count is lower than configured on uniform-price days. Implementation: - _compute_day_effective_min() now returns (dict, int) tuple to propagate the flat day count through to the metadata dict - flat_days_detected added to metadata["relaxation"] in calculate_periods_with_relaxation() - build_final_attributes_simple() gains optional period_metadata parameter - New add_calculation_summary_attributes() function handles the rendering - Attributes are shown even when no period is currently active Updated recorder-optimization.md: attribute counts + clarified that timestamp is correctly excluded (entity native_value is recorded separately by HA as the entity state itself). Impact: Users can understand why they received fewer periods than configured without enabling debug logging.
This commit is contained in:
parent
1e1c8d5299
commit
3a25bd260e
3 changed files with 75 additions and 20 deletions
|
|
@ -138,8 +138,13 @@ def get_price_intervals_attributes(
|
|||
current_period = period
|
||||
break
|
||||
|
||||
# Extract calculation metadata for diagnostic attributes
|
||||
period_metadata = period_data.get("metadata", {})
|
||||
|
||||
# Build final attributes (use filtered_periods for display)
|
||||
return build_final_attributes_simple(current_period, filtered_periods, time=time, config_entry=config_entry)
|
||||
return build_final_attributes_simple(
|
||||
current_period, filtered_periods, time=time, config_entry=config_entry, period_metadata=period_metadata
|
||||
)
|
||||
|
||||
|
||||
def build_no_periods_result(*, time: TibberPricesTimeService) -> dict:
|
||||
|
|
@ -269,6 +274,39 @@ def add_relaxation_attributes(attributes: dict, current_period: dict) -> None:
|
|||
attributes["relaxation_threshold_applied_%"] = current_period["relaxation_threshold_applied_%"]
|
||||
|
||||
|
||||
def add_calculation_summary_attributes(attributes: dict, period_metadata: dict) -> None:
|
||||
"""
|
||||
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.
|
||||
|
||||
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)
|
||||
|
||||
"""
|
||||
relaxation_meta = period_metadata.get("relaxation", {})
|
||||
if not relaxation_meta:
|
||||
return
|
||||
|
||||
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
|
||||
|
||||
if relaxation_meta.get("relaxation_incomplete"):
|
||||
attributes["relaxation_incomplete"] = True
|
||||
|
||||
|
||||
def _convert_periods_to_display_units(period_summaries: list[dict], factor: int) -> list[dict]:
|
||||
"""
|
||||
Convert price values in periods array to display units.
|
||||
|
|
@ -314,6 +352,7 @@ def build_final_attributes_simple(
|
|||
*,
|
||||
time: TibberPricesTimeService,
|
||||
config_entry: TibberPricesConfigEntry,
|
||||
period_metadata: dict | None = None,
|
||||
) -> dict:
|
||||
"""
|
||||
Build the final attributes dictionary from coordinator's period summaries.
|
||||
|
|
@ -331,14 +370,17 @@ def build_final_attributes_simple(
|
|||
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)
|
||||
6. Relaxation information (relaxation_active, relaxation_level, relaxation_threshold_original_%,
|
||||
relaxation_threshold_applied_%) - only if period was relaxed
|
||||
7. Meta information (periods list)
|
||||
relaxation_threshold_applied_%) - only if current period was relaxed
|
||||
7. Calculation summary (min_periods_configured, periods_found_total, flat_days_detected,
|
||||
relaxation_incomplete) - diagnostic info about the overall calculation
|
||||
8. Meta information (periods list)
|
||||
|
||||
Args:
|
||||
current_period: The current or next period (already complete from coordinator)
|
||||
period_summaries: All period summaries from coordinator
|
||||
time: TibberPricesTimeService instance (required)
|
||||
config_entry: Config entry for display unit configuration
|
||||
period_metadata: Metadata from coordinator's period calculation (relaxation diagnostics)
|
||||
|
||||
Returns:
|
||||
Complete attributes dict with all fields
|
||||
|
|
@ -370,19 +412,27 @@ def build_final_attributes_simple(
|
|||
# 5. Detail information
|
||||
add_detail_attributes(attributes, current_period)
|
||||
|
||||
# 6. Relaxation information (only if period was relaxed)
|
||||
# 6. Relaxation information (only if current period was relaxed)
|
||||
add_relaxation_attributes(attributes, current_period)
|
||||
|
||||
# 7. Meta information (periods array - prices converted to display units)
|
||||
# 7. Calculation summary (diagnostic: min_periods_configured, periods_found_total, etc.)
|
||||
if period_metadata:
|
||||
add_calculation_summary_attributes(attributes, period_metadata)
|
||||
|
||||
# 8. Meta information (periods array - prices converted to display units)
|
||||
attributes["periods"] = _convert_periods_to_display_units(period_summaries, factor)
|
||||
|
||||
return attributes
|
||||
|
||||
# No current/next period found - return all periods with timestamp (prices converted)
|
||||
return {
|
||||
# Still add calculation summary so diagnostics are accessible even between periods
|
||||
result: dict = {
|
||||
"timestamp": timestamp,
|
||||
"periods": _convert_periods_to_display_units(period_summaries, factor),
|
||||
}
|
||||
if period_metadata:
|
||||
add_calculation_summary_attributes(result, period_metadata)
|
||||
result["periods"] = _convert_periods_to_display_units(period_summaries, factor)
|
||||
return result
|
||||
|
||||
|
||||
async def build_async_extra_state_attributes( # noqa: PLR0913
|
||||
|
|
|
|||
|
|
@ -54,10 +54,15 @@ class TibberPricesBinarySensor(TibberPricesEntity, BinarySensorEntity, RestoreEn
|
|||
"rating_value",
|
||||
"level_id",
|
||||
"rating_id",
|
||||
# Relaxation Details
|
||||
# Relaxation Details (per-period)
|
||||
"relaxation_level",
|
||||
"relaxation_threshold_original_%",
|
||||
"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
|
||||
"price_spread",
|
||||
"volatility",
|
||||
|
|
|
|||
|
|
@ -111,13 +111,14 @@ class TibberPricesSensor(TibberPricesEntity, SensorEntity):
|
|||
|
||||
### 5. Temporary/Time-Bound Data
|
||||
|
||||
**Attributes:** `next_api_poll`, `next_midnight_turnover`, `last_api_fetch`, `last_cache_update`, `last_turnover`, `last_error`, `error`
|
||||
**Attributes:** `timestamp`, `next_api_poll`, `next_midnight_turnover`, `last_api_fetch`, `last_cache_update`, `last_turnover`, `last_error`, `error`
|
||||
|
||||
**Reason:**
|
||||
- Only relevant at moment of reading
|
||||
- Won't be valid after some time
|
||||
- `timestamp` is the rounded-quarter reference time used at the moment of the state write — it's stale as soon as the next update fires and has no analytical value in history
|
||||
- `next_api_poll`, `next_midnight_turnover` etc. are only relevant at the moment of reading; they're superseded by the next update
|
||||
- Similar to `entity_picture` in HA core image entities
|
||||
- Superseded by next update
|
||||
|
||||
**Note:** The entity's `native_value` (the actual price/state) is always recorded by HA as the entity state itself — independently of `_unrecorded_attributes`. So excluding `timestamp` does not create a gap in the time-series; the state row already carries the recording timestamp.
|
||||
|
||||
**Impact:** ~200-400 bytes saved per state change
|
||||
|
||||
|
|
@ -152,8 +153,7 @@ class TibberPricesSensor(TibberPricesEntity, SensorEntity):
|
|||
These attributes **remain in history** because they provide essential analytical value:
|
||||
|
||||
### Time-Series Core
|
||||
- `timestamp` - Critical for time-series analysis (ALWAYS FIRST)
|
||||
- All price values - Core sensor states
|
||||
- All price values - Core sensor states (the entity's `native_value` is always recorded separately)
|
||||
|
||||
### Diagnostics & Tracking
|
||||
- `cache_age_minutes` - Numeric value for diagnostics tracking over time
|
||||
|
|
@ -208,11 +208,11 @@ For a typical installation with:
|
|||
|
||||
- **Sensor Platform**: `custom_components/tibber_prices/sensor/core.py`
|
||||
- Class: `TibberPricesSensor`
|
||||
- 47 attributes excluded
|
||||
- 46 attributes excluded
|
||||
|
||||
- **Binary Sensor Platform**: `custom_components/tibber_prices/binary_sensor/core.py`
|
||||
- Class: `TibberPricesBinarySensor`
|
||||
- 30 attributes excluded
|
||||
- 29 attributes excluded
|
||||
|
||||
## When to Update _unrecorded_attributes
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue