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
|
current_period = period
|
||||||
break
|
break
|
||||||
|
|
||||||
|
# Extract calculation metadata for diagnostic attributes
|
||||||
|
period_metadata = period_data.get("metadata", {})
|
||||||
|
|
||||||
# Build final attributes (use filtered_periods for display)
|
# 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:
|
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_%"]
|
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]:
|
def _convert_periods_to_display_units(period_summaries: list[dict], factor: int) -> list[dict]:
|
||||||
"""
|
"""
|
||||||
Convert price values in periods array to display units.
|
Convert price values in periods array to display units.
|
||||||
|
|
@ -314,6 +352,7 @@ def build_final_attributes_simple(
|
||||||
*,
|
*,
|
||||||
time: TibberPricesTimeService,
|
time: TibberPricesTimeService,
|
||||||
config_entry: TibberPricesConfigEntry,
|
config_entry: TibberPricesConfigEntry,
|
||||||
|
period_metadata: dict | None = None,
|
||||||
) -> dict:
|
) -> dict:
|
||||||
"""
|
"""
|
||||||
Build the final attributes dictionary from coordinator's period summaries.
|
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_%)
|
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, periods_total, periods_remaining)
|
||||||
6. Relaxation information (relaxation_active, relaxation_level, relaxation_threshold_original_%,
|
6. Relaxation information (relaxation_active, relaxation_level, relaxation_threshold_original_%,
|
||||||
relaxation_threshold_applied_%) - only if period was relaxed
|
relaxation_threshold_applied_%) - only if current period was relaxed
|
||||||
7. Meta information (periods list)
|
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:
|
Args:
|
||||||
current_period: The current or next period (already complete from coordinator)
|
current_period: The current or next period (already complete from coordinator)
|
||||||
period_summaries: All period summaries from coordinator
|
period_summaries: All period summaries from coordinator
|
||||||
time: TibberPricesTimeService instance (required)
|
time: TibberPricesTimeService instance (required)
|
||||||
config_entry: Config entry for display unit configuration
|
config_entry: Config entry for display unit configuration
|
||||||
|
period_metadata: Metadata from coordinator's period calculation (relaxation diagnostics)
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
Complete attributes dict with all fields
|
Complete attributes dict with all fields
|
||||||
|
|
@ -370,19 +412,27 @@ def build_final_attributes_simple(
|
||||||
# 5. Detail information
|
# 5. Detail information
|
||||||
add_detail_attributes(attributes, current_period)
|
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)
|
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)
|
attributes["periods"] = _convert_periods_to_display_units(period_summaries, factor)
|
||||||
|
|
||||||
return attributes
|
return attributes
|
||||||
|
|
||||||
# No current/next period found - return all periods with timestamp (prices converted)
|
# 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,
|
"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
|
async def build_async_extra_state_attributes( # noqa: PLR0913
|
||||||
|
|
|
||||||
|
|
@ -54,10 +54,15 @@ class TibberPricesBinarySensor(TibberPricesEntity, BinarySensorEntity, RestoreEn
|
||||||
"rating_value",
|
"rating_value",
|
||||||
"level_id",
|
"level_id",
|
||||||
"rating_id",
|
"rating_id",
|
||||||
# Relaxation Details
|
# Relaxation Details (per-period)
|
||||||
"relaxation_level",
|
"relaxation_level",
|
||||||
"relaxation_threshold_original_%",
|
"relaxation_threshold_original_%",
|
||||||
"relaxation_threshold_applied_%",
|
"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
|
# Redundant/Derived
|
||||||
"price_spread",
|
"price_spread",
|
||||||
"volatility",
|
"volatility",
|
||||||
|
|
|
||||||
|
|
@ -111,13 +111,14 @@ class TibberPricesSensor(TibberPricesEntity, SensorEntity):
|
||||||
|
|
||||||
### 5. Temporary/Time-Bound Data
|
### 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:**
|
**Reason:**
|
||||||
- Only relevant at moment of reading
|
- `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
|
||||||
- Won't be valid after some time
|
- `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
|
- 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
|
**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:
|
These attributes **remain in history** because they provide essential analytical value:
|
||||||
|
|
||||||
### Time-Series Core
|
### Time-Series Core
|
||||||
- `timestamp` - Critical for time-series analysis (ALWAYS FIRST)
|
- All price values - Core sensor states (the entity's `native_value` is always recorded separately)
|
||||||
- All price values - Core sensor states
|
|
||||||
|
|
||||||
### Diagnostics & Tracking
|
### Diagnostics & Tracking
|
||||||
- `cache_age_minutes` - Numeric value for diagnostics tracking over time
|
- `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`
|
- **Sensor Platform**: `custom_components/tibber_prices/sensor/core.py`
|
||||||
- Class: `TibberPricesSensor`
|
- Class: `TibberPricesSensor`
|
||||||
- 47 attributes excluded
|
- 46 attributes excluded
|
||||||
|
|
||||||
- **Binary Sensor Platform**: `custom_components/tibber_prices/binary_sensor/core.py`
|
- **Binary Sensor Platform**: `custom_components/tibber_prices/binary_sensor/core.py`
|
||||||
- Class: `TibberPricesBinarySensor`
|
- Class: `TibberPricesBinarySensor`
|
||||||
- 30 attributes excluded
|
- 29 attributes excluded
|
||||||
|
|
||||||
## When to Update _unrecorded_attributes
|
## When to Update _unrecorded_attributes
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue