hass.tibber_prices/custom_components/tibber_prices/sensor/chart_metadata.py
Julian Pawlowski f92fc3b444 refactor(services): remove gradient_stop, use fixed 50% gradient
Implementation flaw discovered: gradient_stop calculated as
`(avg - min) / (max - min)` for combined data produces one value
applied to ALL series. Each series (VERY_CHEAP, NORMAL, VERY_EXPENSIVE)
has different min/max ranges, so the same gradient stop position
represents a different absolute price in each series.

Example failure case:
- VERY_CHEAP: 10-20 ct → 50% at 15 ct (below overall avg!)
- VERY_EXPENSIVE: 40-50 ct → 50% at 45 ct (above overall avg!)

Conclusion: Gradient shows middle of each series range, not average
price position.

Solution: Fixed 50% gradient purely for visual appeal. Semantic
information provided by:
- Series colors (CHEAP/NORMAL/EXPENSIVE)
- Grid lines (implicitly show average)
- Dynamic Y-axis bounds (optimal scaling via chart_metadata sensor)

Changes:
- sensor/chart_metadata.py: Remove gradient_stop extraction
- services/get_apexcharts_yaml.py: Fixed gradient at [50, 100]
- custom_translations/*.json: Remove gradient_stop references

Impact: Honest visualization with no false semantic signals. Feature
was never released, clean removal without migration.
2025-12-05 20:51:30 +00:00

142 lines
4.6 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", {})
# 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 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