feat(services): add rolling window options with auto-zoom for ApexCharts

Added two new rolling window options for get_apexcharts_yaml service to provide
flexible dynamic chart visualization:

- rolling_window: Fixed 48h window that automatically shifts between
  yesterday+today and today+tomorrow based on data availability
- rolling_window_autozoom: Same as rolling_window but with progressive zoom-in
  (2h lookback + remaining time until midnight, updates every 15min)

Implementation changes:
- Updated service schema validation to accept new day options
- Added entity mapping patterns for both rolling modes
- Implemented minute-based graph_span calculation with quarter-hour alignment
- Added config-template-card integration for dynamic span updates
- Used current_interval_price sensor as 15-minute update trigger
- Unified data loading: both rolling modes omit day parameter for dynamic selection
- Applied ternary operator pattern for cleaner day_param logic
- Made grid lines more subtle (borderColor #f5f5f5, strokeDashArray 0)

Translation updates:
- Added selector options in all 5 languages (de, en, nb, nl, sv)
- Updated field descriptions to include default behavior and new options
- Documented that rolling window is default when day parameter omitted

Documentation updates:
- Updated user docs (actions.md, automation-examples.md) with new options
- Added detailed explanation of day parameter options
- Included examples for both rolling_window and rolling_window_autozoom modes

Impact: Users can now create auto-adapting ApexCharts that show 48h rolling
windows with optional progressive zoom throughout the day. Requires
config-template-card for dynamic behavior.
This commit is contained in:
Julian Pawlowski 2025-12-04 14:39:00 +00:00
parent 1386407df8
commit c9a7dcdae7
9 changed files with 197 additions and 147 deletions

View file

@ -1,54 +1,30 @@
get_price: get_price:
name: Get Price Data
description: >-
Fetch price data for a specific time range with automatic routing. Development and testing service for the price_info_for_range API function. Automatically uses PRICE_INFO, PRICE_INFO_RANGE, or both based on the time range boundary.
fields: fields:
entry_id: entry_id:
name: Entry ID
description: The config entry ID for the Tibber integration.
required: true required: true
example: "1234567890abcdef" example: "1234567890abcdef"
selector: selector:
config_entry: config_entry:
integration: tibber_prices integration: tibber_prices
start_time: start_time:
name: Start Time
description: Start of the time range (inclusive, timezone-aware).
required: true required: true
example: "2025-11-01T00:00:00+01:00" example: "2025-11-01T00:00:00+01:00"
selector: selector:
datetime: datetime:
end_time: end_time:
name: End Time
description: End of the time range (exclusive, timezone-aware).
required: true required: true
example: "2025-11-02T00:00:00+01:00" example: "2025-11-02T00:00:00+01:00"
selector: selector:
datetime: datetime:
get_apexcharts_yaml: get_apexcharts_yaml:
name: Get ApexCharts Card YAML
description: >-
⚠️ IMPORTANT: This service generates a BASIC EXAMPLE configuration for ApexCharts Card as a starting point. It is NOT a complete solution for all ApexCharts features.
This integration is primarily a DATA PROVIDER. The generated YAML demonstrates how to use the `get_chartdata` service to fetch price data. Due to the segmented nature of our data (different time periods per series) and the use of Home Assistant's service API instead of entity attributes, many advanced ApexCharts features (like in_header, certain transformations) are not compatible or require manual customization.
You are welcome to customize the generated YAML for your specific needs, but please understand that comprehensive ApexCharts configuration support is beyond the scope of this integration. Community contributions with improved configurations are always appreciated - if you find a better setup that works, please share it so everyone can benefit!
For direct data access to build your own charts, use the `get_chartdata` service instead.
fields: fields:
entry_id: entry_id:
name: Entry ID
description: The config entry ID for the Tibber integration.
required: true required: true
example: "1234567890abcdef" example: "1234567890abcdef"
selector: selector:
config_entry: config_entry:
integration: tibber_prices integration: tibber_prices
day: day:
name: Day
description: >-
Which day to visualize (yesterday, today, or tomorrow). If not specified, returns a rolling 2-day window: today+tomorrow (when tomorrow data is available) or yesterday+today (when tomorrow data is not yet available).
required: false required: false
example: today example: today
selector: selector:
@ -57,11 +33,10 @@ get_apexcharts_yaml:
- yesterday - yesterday
- today - today
- tomorrow - tomorrow
- rolling_window
- rolling_window_autozoom
translation_key: day translation_key: day
level_type: level_type:
name: Level Type
description: >-
Select which price level classification to visualize: 'rating_level' (LOW/NORMAL/HIGH based on your configured thresholds) or 'level' (Tibber API levels: VERY_CHEAP/CHEAP/NORMAL/EXPENSIVE/VERY_EXPENSIVE).
required: false required: false
default: rating_level default: rating_level
example: rating_level example: rating_level
@ -72,24 +47,16 @@ get_apexcharts_yaml:
- level - level
translation_key: level_type translation_key: level_type
highlight_best_price: highlight_best_price:
name: Highlight Best Price Periods
description: >-
Add a semi-transparent green overlay to highlight the best price periods on the chart. This makes it easy to visually identify the optimal times for energy consumption.
required: false required: false
default: true default: true
example: true example: true
selector: selector:
boolean: boolean:
get_chartdata: get_chartdata:
name: Get Chart Data
description: >-
Returns price data in a chart-friendly format compatible with Tibber Core output. Works with ha-price-timeline-card, ApexCharts, Plotly, Mini Graph Card, and History Graph. Field names and structure are configurable.
fields: fields:
general: general:
fields: fields:
entry_id: entry_id:
name: Entry ID
description: The config entry ID for the Tibber integration.
required: true required: true
example: "1234567890abcdef" example: "1234567890abcdef"
selector: selector:
@ -99,9 +66,6 @@ get_chartdata:
collapsed: true collapsed: true
fields: fields:
day: day:
name: Day
description: >-
Which day(s) to fetch prices for. You can select multiple days. If not specified, returns a rolling 2-day window: today+tomorrow (when tomorrow data is available) or yesterday+today (when tomorrow data is not yet available). This provides continuous chart display without gaps.
required: false required: false
selector: selector:
select: select:
@ -112,9 +76,6 @@ get_chartdata:
multiple: true multiple: true
translation_key: day translation_key: day
resolution: resolution:
name: Resolution
description: >-
Time resolution for the returned data. Options: 'interval' (default, 15-minute intervals, 96 points per day), 'hourly' (hourly averages, 24 points per day).
required: false required: false
default: interval default: interval
example: hourly example: hourly
@ -128,9 +89,6 @@ get_chartdata:
collapsed: true collapsed: true
fields: fields:
level_filter: level_filter:
name: Level Filter
description: >-
Filter intervals to include only specific Tibber price levels (very_cheap, cheap, normal, expensive, very_expensive). If not specified, all levels are included.
required: false required: false
selector: selector:
select: select:
@ -143,9 +101,6 @@ get_chartdata:
multiple: true multiple: true
translation_key: level_filter translation_key: level_filter
rating_level_filter: rating_level_filter:
name: Rating Level Filter
description: >-
Filter intervals to include only specific rating levels (low, normal, high). If not specified, all rating levels are included.
required: false required: false
selector: selector:
select: select:
@ -156,9 +111,6 @@ get_chartdata:
multiple: true multiple: true
translation_key: rating_level_filter translation_key: rating_level_filter
period_filter: period_filter:
name: Period Filter
description: >-
Filter intervals to include only those within Best Price or Peak Price periods. Options: 'best_price' (only intervals in Best Price periods), 'peak_price' (only intervals in Peak Price periods). If not specified, all intervals are included. This uses the precomputed period data from binary sensors (binary_sensor.best_price_period / binary_sensor.peak_price_period).
required: false required: false
selector: selector:
select: select:
@ -170,18 +122,12 @@ get_chartdata:
collapsed: true collapsed: true
fields: fields:
minor_currency: minor_currency:
name: Minor Currency
description: >-
Return prices in minor currency units (cents for EUR, øre for NOK/SEK) instead of major currency units. Disabled by default.
required: false required: false
default: false default: false
example: true example: true
selector: selector:
boolean: boolean:
round_decimals: round_decimals:
name: Round Decimals
description: >-
Number of decimal places to round prices to (0-10). If not specified, uses default precision (4 decimals for major currency, 2 for minor currency).
required: false required: false
example: 2 example: 2
selector: selector:
@ -190,12 +136,6 @@ get_chartdata:
max: 10 max: 10
mode: box mode: box
insert_nulls: insert_nulls:
name: Insert NULL Values
description: >-
NULL insertion mode for filtered data.
• none (default): only matching intervals
• segments: add NULLs at segment boundaries (clean gaps)
• all: NULL for every non-matching timestamp
required: false required: false
default: none default: none
selector: selector:
@ -206,19 +146,11 @@ get_chartdata:
- all - all
translation_key: insert_nulls translation_key: insert_nulls
connect_segments: connect_segments:
name: Connect Segments
description: >-
Only with insert_nulls='segments'. Adds boundary points to visually connect segments (stepline).
• Downward boundary: lower price at end of current segment
• Upward boundary: hold previous price before the gap
required: false required: false
default: false default: false
selector: selector:
boolean: boolean:
add_trailing_null: add_trailing_null:
name: Add Trailing Null Point
description: >-
Add a final data point with null values (except timestamp) at the end. Some chart libraries need this to prevent extrapolation/interpolation to the viewport edge when using stepline rendering. Leave disabled unless your chart requires it.
required: false required: false
default: false default: false
selector: selector:
@ -227,11 +159,6 @@ get_chartdata:
collapsed: true collapsed: true
fields: fields:
output_format: output_format:
name: Output Format
description: >-
Output format.
• array_of_objects (default): objects with configurable field names
• array_of_arrays: [timestamp, price] tuples
required: false required: false
default: array_of_objects default: array_of_objects
example: array_of_objects example: array_of_objects
@ -242,9 +169,6 @@ get_chartdata:
- array_of_arrays - array_of_arrays
translation_key: output_format translation_key: output_format
data_key: data_key:
name: Data Key
description: >-
Custom name for the top-level data key in the response. Defaults to "data" if not specified. For ApexCharts compatibility with array_of_arrays, use "points".
required: false required: false
example: prices example: prices
selector: selector:
@ -253,73 +177,46 @@ get_chartdata:
collapsed: true collapsed: true
fields: fields:
include_level: include_level:
name: Include Level
description: >-
Include Tibber price level (VERY_CHEAP … VERY_EXPENSIVE).
required: false required: false
default: false default: false
example: true example: true
selector: selector:
boolean: boolean:
include_rating_level: include_rating_level:
name: Include Rating Level
description: >-
Include rating level (LOW/NORMAL/HIGH) based on configured thresholds.
required: false required: false
default: false default: false
example: true example: true
selector: selector:
boolean: boolean:
include_average: include_average:
name: Include Average
description: >-
Include daily average price.
required: false required: false
default: false default: false
selector: selector:
boolean: boolean:
start_time_field: start_time_field:
name: Start Time Field Name
description: >-
Custom name for the start time field in the output. Defaults to "start_time" if not specified.
required: false required: false
example: time example: time
selector: selector:
text: text:
end_time_field: end_time_field:
name: End Time Field Name
description: >-
Custom name for the end time field in the output. Defaults to "end_time" if not specified. Only used with period_filter.
required: false required: false
example: end example: end
selector: selector:
text: text:
price_field: price_field:
name: Price Field Name
description: >-
Custom name for the price field in the output. Defaults to "price_per_kwh" if not specified.
required: false required: false
example: price example: price
selector: selector:
text: text:
level_field: level_field:
name: Level Field Name
description: >-
Custom name for the level field in the output. Defaults to "level" if not specified. Only used when include_level is enabled.
required: false required: false
selector: selector:
text: text:
rating_level_field: rating_level_field:
name: Rating Level Field Name
description: >-
Custom name for the rating_level field in the output. Defaults to "rating_level" if not specified. Only used when include_rating_level is enabled.
required: false required: false
selector: selector:
text: text:
average_field: average_field:
name: Average Field Name
description: >-
Custom name for the average field in the output. Defaults to "average" if not specified. Only used when include_average is enabled.
required: false required: false
selector: selector:
text: text:
@ -327,22 +224,12 @@ get_chartdata:
collapsed: true collapsed: true
fields: fields:
array_fields: array_fields:
name: Array Fields
description: >-
Choose extra fields to include using {field} syntax, comma-separated.
Available: start_time, price_per_kwh, level, rating_level, average.
Empty = default (timestamp + price).
required: false required: false
selector: selector:
text: text:
refresh_user_data: refresh_user_data:
name: Refresh User Data
description: >-
Forces a refresh of the user data (homes, profile information) from the Tibber API. This can be useful after making changes to your Tibber account or when troubleshooting connectivity issues.
fields: fields:
entry_id: entry_id:
name: Entry ID
description: The config entry ID for the Tibber integration.
required: true required: true
example: "1234567890abcdef" example: "1234567890abcdef"
selector: selector:

View file

@ -59,7 +59,7 @@ ATTR_ENTRY_ID: Final = "entry_id"
APEXCHARTS_SERVICE_SCHEMA = vol.Schema( APEXCHARTS_SERVICE_SCHEMA = vol.Schema(
{ {
vol.Required(ATTR_ENTRY_ID): cv.string, vol.Required(ATTR_ENTRY_ID): cv.string,
vol.Optional("day"): vol.In(["yesterday", "today", "tomorrow"]), 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("level_type", default="rating_level"): vol.In(["rating_level", "level"]),
vol.Optional("highlight_best_price", default=True): cv.boolean, vol.Optional("highlight_best_price", default=True): cv.boolean,
} }
@ -92,6 +92,7 @@ def _build_entity_map(
# Define mapping patterns for each combination of level_type and day # Define mapping patterns for each combination of level_type and day
# Note: Match by entity key (in unique_id), not entity_id (user can rename) # Note: Match by entity key (in unique_id), not entity_id (user can rename)
# Note: For "yesterday", we use "today" sensors as they show current state # Note: For "yesterday", we use "today" sensors as they show current state
# Note: For "yesterday_today_tomorrow" and "today_tomorrow", we use "today" sensors (dynamic windows)
pattern_map = { pattern_map = {
("rating_level", "today"): [ ("rating_level", "today"): [
("lowest_price_today", [PRICE_RATING_LOW]), ("lowest_price_today", [PRICE_RATING_LOW]),
@ -108,6 +109,16 @@ def _build_entity_map(
("average_price_tomorrow", [PRICE_RATING_NORMAL]), ("average_price_tomorrow", [PRICE_RATING_NORMAL]),
("highest_price_tomorrow", [PRICE_RATING_HIGH]), ("highest_price_tomorrow", [PRICE_RATING_HIGH]),
], ],
("rating_level", "rolling_window"): [
("lowest_price_today", [PRICE_RATING_LOW]),
("average_price_today", [PRICE_RATING_NORMAL]),
("highest_price_today", [PRICE_RATING_HIGH]),
],
("rating_level", "rolling_window_autozoom"): [
("lowest_price_today", [PRICE_RATING_LOW]),
("average_price_today", [PRICE_RATING_NORMAL]),
("highest_price_today", [PRICE_RATING_HIGH]),
],
("level", "today"): [ ("level", "today"): [
("lowest_price_today", [PRICE_LEVEL_VERY_CHEAP, PRICE_LEVEL_CHEAP]), ("lowest_price_today", [PRICE_LEVEL_VERY_CHEAP, PRICE_LEVEL_CHEAP]),
("average_price_today", [PRICE_LEVEL_NORMAL]), ("average_price_today", [PRICE_LEVEL_NORMAL]),
@ -123,6 +134,16 @@ def _build_entity_map(
("average_price_tomorrow", [PRICE_LEVEL_NORMAL]), ("average_price_tomorrow", [PRICE_LEVEL_NORMAL]),
("highest_price_tomorrow", [PRICE_LEVEL_EXPENSIVE, PRICE_LEVEL_VERY_EXPENSIVE]), ("highest_price_tomorrow", [PRICE_LEVEL_EXPENSIVE, PRICE_LEVEL_VERY_EXPENSIVE]),
], ],
("level", "rolling_window"): [
("lowest_price_today", [PRICE_LEVEL_VERY_CHEAP, PRICE_LEVEL_CHEAP]),
("average_price_today", [PRICE_LEVEL_NORMAL]),
("highest_price_today", [PRICE_LEVEL_EXPENSIVE, PRICE_LEVEL_VERY_EXPENSIVE]),
],
("level", "rolling_window_autozoom"): [
("lowest_price_today", [PRICE_LEVEL_VERY_CHEAP, PRICE_LEVEL_CHEAP]),
("average_price_today", [PRICE_LEVEL_NORMAL]),
("highest_price_today", [PRICE_LEVEL_EXPENSIVE, PRICE_LEVEL_VERY_EXPENSIVE]),
],
} }
patterns = pattern_map.get((level_type, day), []) patterns = pattern_map.get((level_type, day), [])
@ -237,7 +258,8 @@ async def handle_apexcharts_yaml(call: ServiceCall) -> dict[str, Any]: # noqa:
filter_param = f"level_filter: ['{level_key}']" filter_param = f"level_filter: ['{level_key}']"
# Conditionally include day parameter (omit for rolling window mode) # Conditionally include day parameter (omit for rolling window mode)
day_param = f"day: ['{day}'], " if day else "" # For rolling_window and rolling_window_autozoom, omit day parameter (dynamic selection)
day_param = "" if day in ("rolling_window", "rolling_window_autozoom", None) else f"day: ['{day}'], "
data_generator = ( data_generator = (
f"const response = await hass.callWS({{ " f"const response = await hass.callWS({{ "
@ -282,7 +304,8 @@ async def handle_apexcharts_yaml(call: ServiceCall) -> dict[str, Any]: # noqa:
# Create vertical highlight bands using separate Y-axis (0-1 range) # Create vertical highlight bands using separate Y-axis (0-1 range)
# This creates a semi-transparent overlay from bottom to top without affecting price scale # This creates a semi-transparent overlay from bottom to top without affecting price scale
# Conditionally include day parameter (omit for rolling window mode) # Conditionally include day parameter (omit for rolling window mode)
day_param = f"day: ['{day}'], " if day else "" # For rolling_window and rolling_window_autozoom, omit day parameter (dynamic selection)
day_param = "" if day in ("rolling_window", "rolling_window_autozoom", None) else f"day: ['{day}'], "
# Store original prices for tooltip, but map to 1 for full-height overlay # Store original prices for tooltip, but map to 1 for full-height overlay
# We use a custom tooltip formatter to show the real price # We use a custom tooltip formatter to show the real price
@ -326,13 +349,13 @@ async def handle_apexcharts_yaml(call: ServiceCall) -> dict[str, Any]: # noqa:
"Price Phases Daily Progress" if level_type == "rating_level" else "Price Level" "Price Phases Daily Progress" if level_type == "rating_level" else "Price Level"
) )
# Add translated day to title (only if day parameter was provided) # Add translated day to title (only for fixed day views, not for dynamic modes)
if day: if day and day not in ("rolling_window", "rolling_window_autozoom"):
day_translated = get_translation(["selector", "day", "options", day], user_language) or day.capitalize() day_translated = get_translation(["selector", "day", "options", day], user_language) or day.capitalize()
title = f"{title} - {day_translated}" title = f"{title} - {day_translated}"
# Configure span based on selected day # Configure span based on selected day
# For rolling window mode, use config-template-card for dynamic offset # For rolling window modes, use config-template-card for dynamic config
if day == "yesterday": if day == "yesterday":
span_config = {"start": "day", "offset": "-1d"} span_config = {"start": "day", "offset": "-1d"}
graph_span_value = None graph_span_value = None
@ -341,11 +364,22 @@ async def handle_apexcharts_yaml(call: ServiceCall) -> dict[str, Any]: # noqa:
span_config = {"start": "day", "offset": "+1d"} span_config = {"start": "day", "offset": "+1d"}
graph_span_value = None graph_span_value = None
use_template = False use_template = False
elif day == "rolling_window":
# Rolling 48h window: yesterday+today OR today+tomorrow (shifts at 13:00)
span_config = None # Will be set in template
graph_span_value = "48h"
use_template = True
elif day == "rolling_window_autozoom":
# Rolling 48h window with auto-zoom: yesterday+today OR today+tomorrow (shifts at 13:00)
# Auto-zooms based on current time (2h lookback + remaining time)
span_config = None # Will be set in template
graph_span_value = None # Will be set in template
use_template = True
elif day: # today (explicit) elif day: # today (explicit)
span_config = {"start": "day"} span_config = {"start": "day"}
graph_span_value = None graph_span_value = None
use_template = False use_template = False
else: # Rolling window mode (None) else: # Rolling window mode (None - same as rolling_window)
# Use config-template-card to dynamically set offset based on data availability # Use config-template-card to dynamically set offset based on data availability
span_config = None # Will be set in template span_config = None # Will be set in template
graph_span_value = "48h" graph_span_value = "48h"
@ -390,8 +424,8 @@ async def handle_apexcharts_yaml(call: ServiceCall) -> dict[str, Any]: # noqa:
}, },
"grid": { "grid": {
"show": True, "show": True,
"borderColor": "#40475D", "borderColor": "#f5f5f5",
"strokeDashArray": 4, "strokeDashArray": 0,
"xaxis": {"lines": {"show": True}}, "xaxis": {"lines": {"show": True}},
"yaxis": {"lines": {"show": True}}, "yaxis": {"lines": {"show": True}},
}, },
@ -420,11 +454,8 @@ async def handle_apexcharts_yaml(call: ServiceCall) -> dict[str, Any]: # noqa:
"series": series, "series": series,
} }
# For rolling window mode, wrap in config-template-card for dynamic offset # For rolling window mode and today_tomorrow, wrap in config-template-card for dynamic config
if use_template: if use_template:
# Add graph_span to base config (48h window)
result["graph_span"] = graph_span_value
# Find tomorrow_data_available binary sensor # Find tomorrow_data_available binary sensor
tomorrow_data_sensor = next( tomorrow_data_sensor = next(
( (
@ -438,6 +469,81 @@ async def handle_apexcharts_yaml(call: ServiceCall) -> dict[str, Any]: # noqa:
) )
if tomorrow_data_sensor: if tomorrow_data_sensor:
if day == "rolling_window_autozoom":
# rolling_window_autozoom mode: Dynamic graph_span with auto-zoom
# Shows last 120 min (8 intervals) + remaining minutes until end of time window
# Auto-zooms every 15 minutes when current interval completes
# When tomorrow data arrives after 13:00, extends to show tomorrow too
#
# Key principle: graph_span must always be divisible by 15 (full intervals)
# The current (running) interval stays included until it completes
#
# Calculation:
# 1. Round current time UP to next quarter-hour (include running interval)
# 2. Calculate minutes from end of running interval to midnight
# 3. Round to ensure full 15-minute intervals
# 4. Add 120min lookback (always 8 intervals)
# 5. If tomorrow data available: add 1440min (96 intervals)
#
# Example timeline (without tomorrow data):
# 08:00 → next quarter: 08:15 → to midnight: 945min → span: 120+945 = 1065min (71 intervals)
# 08:07 → next quarter: 08:15 → to midnight: 945min → span: 120+945 = 1065min (stays same)
# 08:15 → next quarter: 08:30 → to midnight: 930min → span: 120+930 = 1050min (70 intervals)
# 14:23 → next quarter: 14:30 → to midnight: 570min → span: 120+570 = 690min (46 intervals)
#
# After 13:00 with tomorrow data:
# 14:00 → next quarter: 14:15 → to midnight: 585min → span: 120+585+1440 = 2145min (143 intervals)
# 14:15 → next quarter: 14:30 → to midnight: 570min → span: 120+570+1440 = 2130min (142 intervals)
template_graph_span = (
f"const now = new Date(); "
f"const currentMinute = now.getMinutes(); "
f"const nextQuarterMinute = Math.ceil(currentMinute / 15) * 15; "
f"const currentIntervalEnd = new Date(now); "
f"if (nextQuarterMinute === 60) {{ "
f" currentIntervalEnd.setHours(now.getHours() + 1, 0, 0, 0); "
f"}} else {{ "
f" currentIntervalEnd.setMinutes(nextQuarterMinute, 0, 0); "
f"}} "
f"const midnight = new Date(now.getFullYear(), now.getMonth(), now.getDate() + 1, 0, 0, 0); "
f"const minutesFromIntervalEndToMidnight = Math.ceil((midnight - currentIntervalEnd) / 60000); "
f"const minutesRounded = Math.ceil(minutesFromIntervalEndToMidnight / 15) * 15; "
f"const lookback = 120; "
f"const hasTomorrowData = states['{tomorrow_data_sensor}'].state === 'on'; "
f"const totalMinutes = lookback + minutesRounded + (hasTomorrowData ? 1440 : 0); "
f"totalMinutes + 'min';"
)
# Find current_interval_price sensor for 15-minute update trigger
current_price_sensor = next(
(
entity.entity_id
for entity in entity_registry.entities.values()
if entity.config_entry_id == entry_id
and entity.unique_id
and entity.unique_id.endswith("_current_interval_price")
),
None,
)
trigger_entities = [tomorrow_data_sensor]
if current_price_sensor:
trigger_entities.append(current_price_sensor)
return {
"type": "custom:config-template-card",
"variables": {
"v_graph_span": template_graph_span,
},
"entities": trigger_entities,
"card": {
**result,
"span": {"start": "minute", "offset": "-120min"},
"graph_span": "${v_graph_span}",
},
}
# Rolling window modes (day is None or rolling_window): Dynamic offset
# Add graph_span to base config (48h window)
result["graph_span"] = graph_span_value
# Wrap in config-template-card with dynamic offset calculation # Wrap in config-template-card with dynamic offset calculation
# Template checks if tomorrow data is available (binary sensor state) # Template checks if tomorrow data is available (binary sensor state)
# If 'on' (tomorrow data available) → offset +1d (show today+tomorrow) # If 'on' (tomorrow data available) → offset +1d (show today+tomorrow)
@ -458,8 +564,15 @@ async def handle_apexcharts_yaml(call: ServiceCall) -> dict[str, Any]: # noqa:
}, },
} }
# Fallback if sensor not found: just use +1d offset # Fallback if sensor not found
if day == "rolling_window_autozoom":
# Fallback: show today with 24h span
result["span"] = {"start": "day"}
result["graph_span"] = "24h"
else:
# Rolling window fallback (rolling_window or None): just use +1d offset
result["span"] = {"end": "day", "offset": "+1d"} result["span"] = {"end": "day", "offset": "+1d"}
result["graph_span"] = "48h"
return result return result
# Add span for fixed-day views # Add span for fixed-day views

View file

@ -861,11 +861,15 @@
}, },
"day": { "day": {
"name": "Tag", "name": "Tag",
"description": "Welcher Tag visualisiert werden soll (gestern, heute oder morgen). Falls nicht angegeben, wird ein rollierendes 2-Tage-Fenster zurückgegeben: heute+morgen (wenn Daten für morgen verfügbar sind) oder gestern+heute (wenn Daten für morgen noch nicht verfügbar sind)." "description": "Welcher Tag visualisiert werden soll (Standard: Rollierendes Fenster). Feste Tag-Optionen (Gestern/Heute/Morgen) zeigen 24h-Fenster ohne zusätzliche Abhängigkeiten. Dynamische Optionen benötigen config-template-card: Rollierendes Fenster zeigt ein festes 48h-Fenster, das automatisch zwischen gestern+heute und heute+morgen wechselt basierend auf Datenverfügbarkeit. Rollierendes Fenster (Auto-Zoom) verhält sich gleich, zoomt aber zusätzlich automatisch rein (2h Rückblick + verbleibende Zeit bis Mitternacht, graph_span verringert sich alle 15 Minuten)."
}, },
"level_type": { "level_type": {
"name": "Stufen-Typ", "name": "Stufen-Typ",
"description": "Wähle, welche Preisstufen-Klassifizierung visualisiert werden soll: 'rating_level' (niedrig/normal/hoch basierend auf deinen konfigurierten Schwellenwerten) oder 'level' (Tibber-API-Stufen: sehr günstig/günstig/normal/teuer/sehr teuer)." "description": "Wähle, welche Preisstufen-Klassifizierung visualisiert werden soll: 'rating_level' (niedrig/normal/hoch basierend auf deinen konfigurierten Schwellenwerten) oder 'level' (Tibber-API-Stufen: sehr günstig/günstig/normal/teuer/sehr teuer)."
},
"highlight_best_price": {
"name": "Bestpreis-Zeiträume hervorheben",
"description": "Füge eine halbtransparente grüne Überlagerung hinzu, um die Bestpreis-Zeiträume im Diagramm hervorzuheben. Dies erleichtert die visuelle Identifizierung der optimalen Zeiten für den Energieverbrauch."
} }
} }
}, },
@ -1018,7 +1022,9 @@
"options": { "options": {
"yesterday": "Gestern", "yesterday": "Gestern",
"today": "Heute", "today": "Heute",
"tomorrow": "Morgen" "tomorrow": "Morgen",
"rolling_window": "Rollierendes Fenster",
"rolling_window_autozoom": "Rollierendes Fenster (Auto-Zoom)"
} }
}, },
"resolution": { "resolution": {

View file

@ -857,11 +857,15 @@
}, },
"day": { "day": {
"name": "Day", "name": "Day",
"description": "Which day to visualize (yesterday, today, or tomorrow). If not specified, returns a rolling 2-day window: today+tomorrow (when tomorrow data is available) or yesterday+today (when tomorrow data is not yet available)." "description": "Which day to visualize (default: Rolling Window). Fixed day options (Yesterday/Today/Tomorrow) show 24h spans without additional dependencies. Dynamic options require config-template-card: Rolling Window displays a fixed 48h window that automatically shifts between yesterday+today and today+tomorrow based on data availability. Rolling Window (Auto-Zoom) behaves the same but additionally auto-zooms in (2h lookback + remaining time until midnight, graph_span decreases every 15 minutes)."
}, },
"level_type": { "level_type": {
"name": "Level Type", "name": "Level Type",
"description": "Select which price level classification to visualize: 'rating_level' (low/normal/high based on your configured thresholds) or 'level' (Tibber API levels: very cheap/cheap/normal/expensive/very expensive)." "description": "Select which price level classification to visualize: 'rating_level' (low/normal/high based on your configured thresholds) or 'level' (Tibber API levels: very cheap/cheap/normal/expensive/very expensive)."
},
"highlight_best_price": {
"name": "Highlight Best Price Periods",
"description": "Add a semi-transparent green overlay to highlight the best price periods on the chart. This makes it easy to visually identify the optimal times for energy consumption."
} }
} }
}, },
@ -1014,7 +1018,9 @@
"options": { "options": {
"yesterday": "Yesterday", "yesterday": "Yesterday",
"today": "Today", "today": "Today",
"tomorrow": "Tomorrow" "tomorrow": "Tomorrow",
"rolling_window": "Rolling Window",
"rolling_window_autozoom": "Rolling Window (Auto-Zoom)"
} }
}, },
"resolution": { "resolution": {

View file

@ -857,11 +857,15 @@
}, },
"day": { "day": {
"name": "Dag", "name": "Dag",
"description": "Hvilken dag som skal visualiseres (i går, i dag eller i morgen). Hvis ikke angitt, returneres et rullende 2-dagers vindu: i dag+i morgen (når data for i morgen er tilgjengelig) eller i går+i dag (når data for i morgen ikke er tilgjengelig ennå)." "description": "Hvilken dag som skal visualiseres (standard: Rullerende vindu). Faste dagalternativer (I går/I dag/I morgen) viser 24t-spenn uten ekstra avhengigheter. Dynamiske alternativer krever config-template-card: Rullerende vindu lager et fast 48t-vindu som automatisk skifter mellom i går+i dag og i dag+i morgen basert på datatilgjengelighet. Rullerende vindu (Auto-Zoom) oppfører seg likt, men zoomer i tillegg automatisk inn (2t tilbakeblikk + gjenværende tid til midnatt, graph_span reduseres hvert 15. minutt)."
}, },
"level_type": { "level_type": {
"name": "Nivåtype", "name": "Nivåtype",
"description": "Velg hvilken prisnivåklassifisering som skal visualiseres: 'rating_level' (lav/normal/høy basert på dine konfigurerte terskelverdier) eller 'level' (Tibber API-nivåer: veldig billig/billig/normal/dyr/veldig dyr)." "description": "Velg hvilken prisnivåklassifisering som skal visualiseres: 'rating_level' (lav/normal/høy basert på dine konfigurerte terskelverdier) eller 'level' (Tibber API-nivåer: veldig billig/billig/normal/dyr/veldig dyr)."
},
"highlight_best_price": {
"name": "Fremhev beste prisperioder",
"description": "Legg til et halvgjennomsiktig grønt overlegg for å fremheve de beste prisperiodene i diagrammet. Dette gjør det enkelt å visuelt identifisere de optimale tidene for energiforbruk."
} }
} }
}, },
@ -1014,7 +1018,9 @@
"options": { "options": {
"yesterday": "I går", "yesterday": "I går",
"today": "I dag", "today": "I dag",
"tomorrow": "I morgen" "tomorrow": "I morgen",
"rolling_window": "Rullerende vindu",
"rolling_window_autozoom": "Rullerende vindu (Auto-Zoom)"
} }
}, },
"resolution": { "resolution": {

View file

@ -857,11 +857,15 @@
}, },
"day": { "day": {
"name": "Dag", "name": "Dag",
"description": "Welke dag gevisualiseerd moet worden (gisteren, vandaag of morgen). Indien niet opgegeven, wordt een rollend 2-dagen venster geretourneerd: vandaag+morgen (wanneer gegevens voor morgen beschikbaar zijn) of gisteren+vandaag (wanneer gegevens voor morgen nog niet beschikbaar zijn)." "description": "Welke dag gevisualiseerd moet worden (standaard: Rollend venster). Vaste dagopties (Gisteren/Vandaag/Morgen) tonen 24u-vensters zonder extra afhankelijkheden. Dynamische opties vereisen config-template-card: Rollend venster creëert een vast 48u-venster dat automatisch wisselt tussen gisteren+vandaag en vandaag+morgen op basis van databeschikbaarheid. Rollend venster (Auto-Zoom) gedraagt zich hetzelfde maar zoomt bovendien automatisch in (2u terugkijken + resterende tijd tot middernacht, graph_span neemt elke 15 minuten af)."
}, },
"level_type": { "level_type": {
"name": "Niveautype", "name": "Niveautype",
"description": "Selecteer welke prijsniveauclassificatie gevisualiseerd moet worden: 'rating_level' (laag/normaal/hoog op basis van jouw geconfigureerde drempelwaarden) of 'level' (Tibber API-niveaus: zeer goedkoop/goedkoop/normaal/duur/zeer duur)." "description": "Selecteer welke prijsniveauclassificatie gevisualiseerd moet worden: 'rating_level' (laag/normaal/hoog op basis van jouw geconfigureerde drempelwaarden) of 'level' (Tibber API-niveaus: zeer goedkoop/goedkoop/normaal/duur/zeer duur)."
},
"highlight_best_price": {
"name": "Beste prijsperiodes markeren",
"description": "Voeg een halfdo0rzichtige groene overlay toe om de beste prijsperiodes in de grafiek te markeren. Dit maakt het gemakkelijk om visueel de optimale tijden voor energieverbruik te identificeren."
} }
} }
}, },
@ -1014,7 +1018,9 @@
"options": { "options": {
"yesterday": "Gisteren", "yesterday": "Gisteren",
"today": "Vandaag", "today": "Vandaag",
"tomorrow": "Morgen" "tomorrow": "Morgen",
"rolling_window": "Rollend venster",
"rolling_window_autozoom": "Rollend venster (Auto-Zoom)"
} }
}, },
"resolution": { "resolution": {

View file

@ -857,11 +857,15 @@
}, },
"day": { "day": {
"name": "Dag", "name": "Dag",
"description": "Vilken dag som ska visualiseras (igår, idag eller imorgon). Om inte angivet returneras ett rullande 2-dagarsfönster: idag+imorgon (när data för imorgon är tillgänglig) eller igår+idag (när data för imorgon inte är tillgänglig ännu)." "description": "Vilken dag som ska visualiseras (standard: Rullande fönster). Fasta dagalternativ (Igår/Idag/Imorgon) visar 24t-spann utan extra beroenden. Dynamiska alternativ kräver config-template-card: Rullande fönster skapar ett fast 48t-fönster som automatiskt växlar mellan igår+idag och idag+imorgon baserat på datatillgänglighet. Rullande fönster (Auto-Zoom) beter sig likadant men zoomar dessutom automatiskt in (2t tillbakablick + återstående tid till midnatt, graph_span minskar varje kvart)."
}, },
"level_type": { "level_type": {
"name": "Nivåtyp", "name": "Nivåtyp",
"description": "Välj vilken prisnivåklassificering som ska visualiseras: 'rating_level' (låg/normal/hög baserat på dina konfigurerade tröskelvärden) eller 'level' (Tibber API-nivåer: mycket billig/billig/normal/dyr/mycket dyr)." "description": "Välj vilken prisnivåklassificering som ska visualiseras: 'rating_level' (låg/normal/hög baserat på dina konfigurerade tröskelvärden) eller 'level' (Tibber API-nivåer: mycket billig/billig/normal/dyr/mycket dyr)."
},
"highlight_best_price": {
"name": "Markera bästa prisperioder",
"description": "Lägg till ett halvgenomskinligt grönt överlägg för att markera de bästa prisperioderna i diagrammet. Detta gör det enkelt att visuellt identifiera de optimala tiderna för energiförbrukning."
} }
} }
}, },
@ -1014,7 +1018,9 @@
"options": { "options": {
"yesterday": "Igår", "yesterday": "Igår",
"today": "Idag", "today": "Idag",
"tomorrow": "Imorgon" "tomorrow": "Imorgon",
"rolling_window": "Rullande fönster",
"rolling_window_autozoom": "Rullande fönster (Auto-Zoom)"
} }
}, },
"resolution": { "resolution": {

View file

@ -132,15 +132,17 @@ For detailed parameter descriptions, open **Developer Tools → Actions** (the U
service: tibber_prices.get_apexcharts_yaml service: tibber_prices.get_apexcharts_yaml
data: data:
entry_id: YOUR_ENTRY_ID entry_id: YOUR_ENTRY_ID
day: today # Optional: omit for rolling 48h window (requires config-template-card) day: today # Optional: yesterday, today, tomorrow, rolling_window, rolling_window_autozoom
response_variable: apexcharts_config response_variable: apexcharts_config
``` ```
**Rolling Window Mode:** When omitting the `day` parameter, the action generates a dynamic 48-hour rolling window that automatically shows: **Day Parameter Options:**
- Today + Tomorrow (when tomorrow data is available)
- Yesterday + Today (when tomorrow data is not yet available)
This mode requires the Config Template Card to dynamically adjust the time window based on data availability. - **Fixed days** (`yesterday`, `today`, `tomorrow`): Static 24-hour views, no additional dependencies
- **Rolling Window** (default when omitted or `rolling_window`): Dynamic 48-hour window that automatically shifts between yesterday+today and today+tomorrow based on data availability
- **Rolling Window (Auto-Zoom)** (`rolling_window_autozoom`): Same as rolling window, but additionally zooms in progressively (2h lookback + remaining time until midnight, graph span decreases every 15 minutes)
**Note:** Rolling window modes require [Config Template Card](https://github.com/iantrich/config-template-card) for dynamic behavior.
Use the response in Lovelace dashboards by copying the generated YAML. Use the response in Lovelace dashboards by copying the generated YAML.

View file

@ -285,7 +285,7 @@ For a dynamic chart that automatically adapts to data availability:
service: tibber_prices.get_apexcharts_yaml service: tibber_prices.get_apexcharts_yaml
data: data:
entry_id: YOUR_ENTRY_ID entry_id: YOUR_ENTRY_ID
# Omit 'day' parameter for rolling window day: rolling_window # Or omit for same behavior (default)
level_type: rating_level level_type: rating_level
response_variable: apexcharts_config response_variable: apexcharts_config
``` ```
@ -293,8 +293,26 @@ response_variable: apexcharts_config
**Behavior:** **Behavior:**
- **When tomorrow data available** (typically after ~13:00): Shows today + tomorrow - **When tomorrow data available** (typically after ~13:00): Shows today + tomorrow
- **When tomorrow data not available**: Shows yesterday + today - **When tomorrow data not available**: Shows yesterday + today
- **Fixed 48h span:** Always shows full 48 hours
**Note:** Rolling window mode requires Config Template Card to dynamically adjust the time range. **Auto-Zoom Variant:**
For progressive zoom-in throughout the day:
```yaml
service: tibber_prices.get_apexcharts_yaml
data:
entry_id: YOUR_ENTRY_ID
day: rolling_window_autozoom
level_type: rating_level
response_variable: apexcharts_config
```
- Same data loading as rolling window
- **Progressive zoom:** Graph span starts at ~26h in the morning and decreases to ~14h by midnight
- **Updates every 15 minutes:** Always shows 2h lookback + remaining time until midnight
**Note:** Rolling window modes require Config Template Card to dynamically adjust the time range.
### Features ### Features