feat(services): add peak price overlay toggle to ApexCharts YAML

Added `highlight_peak_price` (default: false) to `get_apexcharts_yaml` service
and implemented a subtle red overlay analogous to best price periods using
`period_filter: 'peak_price'`. Tooltips now dynamically exclude overlay
series to prevent overlay tooltips.

Impact: Users can visualize peak-price periods in ApexCharts cards
when desired, with default opt-out behavior.
This commit is contained in:
Julian Pawlowski 2025-12-26 00:07:28 +00:00
parent c6b34984fa
commit 665fac10fc
7 changed files with 65 additions and 3 deletions

View file

@ -2,7 +2,8 @@
"apexcharts": {
"title_rating_level": "Preisphasen Tagesverlauf",
"title_level": "Preisniveau",
"best_price_period_name": "Beste Preisperiode",
"best_price_period_name": "Bestpreis-Zeitraum",
"peak_price_period_name": "Spitzenpreis-Zeitraum",
"notification": {
"metadata_sensor_unavailable": {
"title": "Tibber Prices: ApexCharts YAML mit eingeschränkter Funktionalität generiert",

View file

@ -3,6 +3,7 @@
"title_rating_level": "Price Phases Daily Progress",
"title_level": "Price Level",
"best_price_period_name": "Best Price Period",
"peak_price_period_name": "Peak Price Period",
"notification": {
"metadata_sensor_unavailable": {
"title": "Tibber Prices: ApexCharts YAML Generated with Limited Functionality",

View file

@ -3,6 +3,7 @@
"title_rating_level": "Prisfaser dagsfremdrift",
"title_level": "Prisnivå",
"best_price_period_name": "Beste prisperiode",
"peak_price_period_name": "Toppprisperiode",
"notification": {
"metadata_sensor_unavailable": {
"title": "Tibber Prices: ApexCharts YAML generert med begrenset funksjonalitet",

View file

@ -3,6 +3,7 @@
"title_rating_level": "Prijsfasen dagverloop",
"title_level": "Prijsniveau",
"best_price_period_name": "Beste prijsperiode",
"peak_price_period_name": "Piekprijsperiode",
"notification": {
"metadata_sensor_unavailable": {
"title": "Tibber Prices: ApexCharts YAML gegenereerd met beperkte functionaliteit",

View file

@ -3,6 +3,7 @@
"title_rating_level": "Prisfaser dagsprogress",
"title_level": "Prisnivå",
"best_price_period_name": "Bästa prisperiod",
"peak_price_period_name": "Toppprisperiod",
"notification": {
"metadata_sensor_unavailable": {
"title": "Tibber Prices: ApexCharts YAML genererad med begränsad funktionalitet",

View file

@ -52,6 +52,12 @@ get_apexcharts_yaml:
example: true
selector:
boolean:
highlight_peak_price:
required: false
default: false
example: false
selector:
boolean:
get_chartdata:
fields:
general:

View file

@ -64,6 +64,7 @@ APEXCHARTS_SERVICE_SCHEMA = vol.Schema(
vol.Optional("day"): vol.In(["yesterday", "today", "tomorrow", "rolling_window", "rolling_window_autozoom"]),
vol.Optional("level_type", default="rating_level"): vol.In(["rating_level", "level"]),
vol.Optional("highlight_best_price", default=True): cv.boolean,
vol.Optional("highlight_peak_price", default=False): cv.boolean,
}
)
@ -296,6 +297,7 @@ async def handle_apexcharts_yaml(call: ServiceCall) -> dict[str, Any]: # noqa:
day = call.data.get("day") # Can be None (rolling window mode)
level_type = call.data.get("level_type", "rating_level")
highlight_best_price = call.data.get("highlight_best_price", True)
highlight_peak_price = call.data.get("highlight_peak_price", False)
# Get user's language from hass config
user_language = hass.config.language or "en"
@ -333,8 +335,13 @@ async def handle_apexcharts_yaml(call: ServiceCall) -> dict[str, Any]: # noqa:
]
series = []
# Get translated name for best price periods (needed for layer)
# Get translated names for overlays (best/peak)
best_price_name = get_translation(["apexcharts", "best_price_period_name"], user_language) or "Best Price Period"
peak_price_name = get_translation(["apexcharts", "peak_price_period_name"], user_language) or "Peak Price Period"
# Track overlays added for tooltip index calculation later
best_overlay_added = False
peak_overlay_added = False
# Add best price period highlight overlay FIRST (so it renders behind all other series)
if highlight_best_price and entity_map:
@ -379,6 +386,45 @@ async def handle_apexcharts_yaml(call: ServiceCall) -> dict[str, Any]: # noqa:
"stroke_width": 0,
}
)
best_overlay_added = True
# Add peak price period highlight overlay (renders behind series as well)
if highlight_peak_price and entity_map:
# Conditionally include day parameter (omit for rolling window mode)
day_param = "" if day in ("rolling_window", "rolling_window_autozoom", None) else f"day: ['{day}'], "
subunit_param = "true" if use_subunit else "false"
peak_price_generator = (
f"const response = await hass.callWS({{ "
f"type: 'call_service', "
f"domain: 'tibber_prices', "
f"service: 'get_chartdata', "
f"return_response: true, "
f"service_data: {{ entry_id: '{entry_id}', {day_param}"
f"period_filter: 'peak_price', "
f"output_format: 'array_of_arrays', insert_nulls: 'segments', subunit_currency: {subunit_param} }} }}); "
f"const originalData = response.response.data; "
f"return originalData.map((point, i) => {{ "
f"const result = [point[0], point[1] === null ? null : 1]; "
f"result.originalPrice = point[1]; "
f"return result; "
f"}});"
)
peak_price_entity = next(iter(entity_map.values()))
series.append(
{
"entity": peak_price_entity,
"name": peak_price_name,
"type": "area",
"color": "rgba(231, 76, 60, 0.06)", # Subtle red overlay for peak price
"yaxis_id": "highlight",
"show": {"legend_value": False, "in_header": False, "in_legend": False},
"data_generator": peak_price_generator,
"stroke_width": 0,
}
)
peak_overlay_added = True
# Only create series for levels that have a matching entity (filter out missing levels)
for level_key, color in series_levels:
@ -547,7 +593,8 @@ async def handle_apexcharts_yaml(call: ServiceCall) -> dict[str, Any]: # noqa:
"tooltip": {
"enabled": True,
"shared": True, # Combine tooltips from all series at same x-value
"enabledOnSeries": [1, 2, 3, 4, 5] if highlight_best_price else [0, 1, 2, 3, 4],
# enabledOnSeries will be set dynamically below based on overlays
"enabledOnSeries": [],
"marker": {
"show": False,
},
@ -584,6 +631,10 @@ async def handle_apexcharts_yaml(call: ServiceCall) -> dict[str, Any]: # noqa:
"series": series,
}
# Dynamically set tooltip enabledOnSeries to exclude overlay indices
overlay_count = (1 if best_overlay_added else 0) + (1 if peak_overlay_added else 0)
result["apex_config"]["tooltip"]["enabledOnSeries"] = list(range(overlay_count, len(series)))
# For rolling window mode and today_tomorrow, wrap in config-template-card for dynamic config
if use_template:
# Find tomorrow_data_available binary sensor