hass.tibber_prices/custom_components/tibber_prices/sensor/chart_metadata.py
Julian Pawlowski 6922e52368 feat(sensors): add chart_metadata sensor for lightweight chart configuration
Implemented new chart_metadata diagnostic sensor that provides essential
chart configuration values (yaxis_min, yaxis_max, gradient_stop) as
attributes, enabling dynamic chart configuration without requiring
async service calls in templates.

Sensor implementation:
- New chart_metadata.py module with metadata-only service calls
- Automatically calls get_chartdata with metadata="only" parameter
- Refreshes on coordinator updates (new price data or user data)
- State values: "pending", "ready", "error"
- Enabled by default (critical for chart features)

ApexCharts YAML generator integration:
- Checks for chart_metadata sensor availability before generation
- Uses template variables to read sensor attributes dynamically
- Fallback to fixed values (gradient_stop=50%) if sensor unavailable
- Creates separate notifications for two independent issues:
  1. Chart metadata sensor disabled (reduced functionality warning)
  2. Required custom cards missing (YAML won't work warning)
- Both notifications explain YAML generation context and provide
  complete fix instructions with regeneration requirement

Configuration:
- Supports configuration.yaml: tibber_prices.chart_metadata_config
- Optional parameters: day, minor_currency, resolution
- Defaults to minor_currency=True for ApexCharts compatibility

Translation additions:
- Entity name and state translations (all 5 languages)
- Notification messages for sensor unavailable and missing cards
- best_price_period_name for tooltip formatter

Binary sensor improvements:
- tomorrow_data_available now enabled by default (critical for automations)
- data_lifecycle_status now enabled by default (critical for debugging)

Impact: Users get dynamic chart configuration with optimized Y-axis scaling
and gradient positioning without manual calculations. ApexCharts YAML
generation now provides clear, actionable notifications when issues occur,
ensuring users understand why functionality is limited and how to fix it.
2025-12-05 20:30:54 +00:00

149 lines
5 KiB
Python

"""Chart metadata export functionality for Tibber Prices sensors."""
from __future__ import annotations
from typing import TYPE_CHECKING
from custom_components.tibber_prices.const import DATA_CHART_METADATA_CONFIG, DOMAIN
if TYPE_CHECKING:
from datetime import datetime
from custom_components.tibber_prices.coordinator import TibberPricesDataUpdateCoordinator
from custom_components.tibber_prices.data import TibberPricesConfigEntry
from homeassistant.core import HomeAssistant
async def call_chartdata_service_for_metadata_async(
hass: HomeAssistant,
coordinator: TibberPricesDataUpdateCoordinator,
config_entry: TibberPricesConfigEntry,
) -> tuple[dict | None, str | None]:
"""
Call get_chartdata service with configuration from configuration.yaml for metadata (async).
Returns:
Tuple of (response, error_message).
If successful: (response_dict, None)
If failed: (None, error_string)
"""
# Get configuration from hass.data (loaded from configuration.yaml)
domain_data = hass.data.get(DOMAIN, {})
chart_metadata_config = domain_data.get(DATA_CHART_METADATA_CONFIG, {})
# Use chart_metadata_config directly (already a dict from async_setup)
service_params = dict(chart_metadata_config) if chart_metadata_config else {}
# Add required entry_id parameter
service_params["entry_id"] = config_entry.entry_id
# Force metadata to "only" - this sensor ONLY provides metadata
service_params["metadata"] = "only"
# Default to minor_currency=True for ApexCharts compatibility (can be overridden in configuration.yaml)
if "minor_currency" not in service_params:
service_params["minor_currency"] = True
# Call get_chartdata service using official HA service system
try:
response = await hass.services.async_call(
DOMAIN,
"get_chartdata",
service_params,
blocking=True,
return_response=True,
)
except Exception as ex:
coordinator.logger.exception("Chart metadata service call failed")
return None, str(ex)
else:
return response, None
def get_chart_metadata_state(
chart_metadata_response: dict | None,
chart_metadata_error: str | None,
) -> str | None:
"""
Return state for chart_metadata sensor.
Args:
chart_metadata_response: Last service response (or None)
chart_metadata_error: Last error message (or None)
Returns:
"error" if error occurred
"ready" if metadata available
"pending" if no data yet
"""
if chart_metadata_error:
return "error"
if chart_metadata_response:
return "ready"
return "pending"
def build_chart_metadata_attributes(
chart_metadata_response: dict | None,
chart_metadata_last_update: datetime | None,
chart_metadata_error: str | None,
) -> dict[str, object] | None:
"""
Return chart metadata from last service call as attributes.
Attribute order: timestamp, error (if any), metadata fields (at the end).
Args:
chart_metadata_response: Last service response (should contain "metadata" key)
chart_metadata_last_update: Timestamp of last update
chart_metadata_error: Error message if service call failed
Returns:
Dict with timestamp, optional error, and metadata fields.
"""
# Build base attributes with timestamp FIRST
attributes: dict[str, object] = {
"timestamp": chart_metadata_last_update,
}
# Add error message if service call failed
if chart_metadata_error:
attributes["error"] = chart_metadata_error
if not chart_metadata_response:
# No data - only timestamp (and error if present)
return attributes
# Extract metadata from response (get_chartdata returns {"metadata": {...}})
metadata = chart_metadata_response.get("metadata", {})
# Extract the fields we care about for charts
# These are the universal chart metadata fields useful for any chart card
if metadata:
yaxis_suggested = metadata.get("yaxis_suggested", {})
price_stats = metadata.get("price_stats", {})
combined_stats = price_stats.get("combined", {})
# Add yaxis bounds (useful for all chart cards)
if "min" in yaxis_suggested:
attributes["yaxis_min"] = yaxis_suggested["min"]
if "max" in yaxis_suggested:
attributes["yaxis_max"] = yaxis_suggested["max"]
# Add gradient stop position (useful for gradient-based charts)
if "avg_position" in combined_stats:
avg_position = combined_stats["avg_position"]
attributes["gradient_stop"] = round(avg_position * 100)
# Add currency info (useful for labeling)
if "currency" in metadata:
attributes["currency"] = metadata["currency"]
# Add resolution info (interval duration in minutes)
if "resolution" in metadata:
attributes["resolution"] = metadata["resolution"]
return attributes