From e156dfb06132d305ad0aa42b044cea267a948f56 Mon Sep 17 00:00:00 2001 From: Julian Pawlowski <75446+jpawlowski@users.noreply.github.com> Date: Mon, 1 Dec 2025 23:46:09 +0000 Subject: [PATCH] feat(services): add rolling 48h window support to chart services Add dynamic rolling window mode to get_chartdata and get_apexcharts_yaml services that automatically adapts to data availability. When 'day' parameter is omitted, services return 48-hour window: - With tomorrow data (after ~13:00): today + tomorrow - Without tomorrow data: yesterday + today Changes: - Implement rolling window logic in get_chartdata using has_tomorrow_data() - Generate config-template-card wrapper in get_apexcharts_yaml for dynamic ApexCharts span.offset based on tomorrow_data_available binary sensor - Update service descriptions in services.yaml - Add rolling window descriptions to all translations (de, en, nb, nl, sv) - Document rolling window mode in docs/user/services.md - Add ApexCharts examples with prerequisites in docs/user/automation-examples.md BREAKING CHANGE: get_apexcharts_yaml rolling window mode requires config-template-card in addition to apexcharts-card for dynamic offset calculation. Impact: Users can create auto-adapting 48h price charts without manual day selection. Fixed day views (day: today/yesterday/tomorrow) still work with apexcharts-card only. --- custom_components/tibber_prices/services.yaml | 7 +- .../services/get_apexcharts_yaml.py | 102 +++++++++++++++--- .../tibber_prices/services/get_chartdata.py | 18 +++- .../tibber_prices/services/helpers.py | 21 ++++ .../tibber_prices/translations/de.json | 4 +- .../tibber_prices/translations/en.json | 4 +- .../tibber_prices/translations/nb.json | 4 +- .../tibber_prices/translations/nl.json | 4 +- .../tibber_prices/translations/sv.json | 4 +- docs/user/automation-examples.md | 58 +++++++++- docs/user/services.md | 30 ++++++ 11 files changed, 222 insertions(+), 34 deletions(-) diff --git a/custom_components/tibber_prices/services.yaml b/custom_components/tibber_prices/services.yaml index 24f4e2b..cbe8673 100644 --- a/custom_components/tibber_prices/services.yaml +++ b/custom_components/tibber_prices/services.yaml @@ -40,9 +40,9 @@ get_apexcharts_yaml: integration: tibber_prices day: name: Day - description: Which day to visualize (yesterday, today, or tomorrow). + 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 - default: today example: today selector: select: @@ -90,7 +90,8 @@ get_chartdata: # === DATA SELECTION === day: name: Day - description: Which day(s) to fetch prices for. You can select multiple days. If not specified, returns all available data (today + tomorrow if available). + 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 selector: select: diff --git a/custom_components/tibber_prices/services/get_apexcharts_yaml.py b/custom_components/tibber_prices/services/get_apexcharts_yaml.py index 5e536da..b8fe515 100644 --- a/custom_components/tibber_prices/services/get_apexcharts_yaml.py +++ b/custom_components/tibber_prices/services/get_apexcharts_yaml.py @@ -36,6 +36,7 @@ from custom_components.tibber_prices.const import ( get_translation, ) from homeassistant.exceptions import ServiceValidationError +from homeassistant.helpers import config_validation as cv from homeassistant.helpers.entity_registry import ( EntityRegistry, ) @@ -55,12 +56,12 @@ ATTR_DAY: Final = "day" ATTR_ENTRY_ID: Final = "entry_id" # Service schema -APEXCHARTS_SERVICE_SCHEMA: Final = vol.Schema( +APEXCHARTS_SERVICE_SCHEMA = vol.Schema( { - vol.Required(ATTR_ENTRY_ID): str, - vol.Optional("day", default="today"): vol.In(["yesterday", "today", "tomorrow"]), + vol.Required(ATTR_ENTRY_ID): cv.string, + vol.Optional("day"): vol.In(["yesterday", "today", "tomorrow"]), vol.Optional("level_type", default="rating_level"): vol.In(["rating_level", "level"]), - vol.Optional("highlight_best_price", default=True): bool, + vol.Optional("highlight_best_price", default=True): cv.boolean, } ) @@ -158,7 +159,7 @@ def _get_current_price_entity(entity_registry: EntityRegistry, entry_id: str) -> ) -async def handle_apexcharts_yaml(call: ServiceCall) -> dict[str, Any]: +async def handle_apexcharts_yaml(call: ServiceCall) -> dict[str, Any]: # noqa: PLR0912, PLR0915 """ Return YAML snippet for ApexCharts card. @@ -186,7 +187,7 @@ async def handle_apexcharts_yaml(call: ServiceCall) -> dict[str, Any]: raise ServiceValidationError(translation_domain=DOMAIN, translation_key="missing_entry_id") entry_id: str = str(entry_id_raw) - day = call.data.get("day", "today") + 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) @@ -203,7 +204,8 @@ async def handle_apexcharts_yaml(call: ServiceCall) -> dict[str, Any]: entity_registry = async_get_entity_registry(hass) # Build entity mapping based on level_type and day for clickable states - entity_map = _build_entity_map(entity_registry, entry_id, level_type, day) + # When day is None, use "today" as fallback for entity mapping + entity_map = _build_entity_map(entity_registry, entry_id, level_type, day or "today") if level_type == "rating_level": series_levels = [ @@ -234,13 +236,16 @@ async def handle_apexcharts_yaml(call: ServiceCall) -> dict[str, Any]: else: filter_param = f"level_filter: ['{level_key}']" + # Conditionally include day parameter (omit for rolling window mode) + day_param = f"day: ['{day}'], " if day else "" + data_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: ['{day}'], {filter_param}, " + f"service_data: {{ entry_id: '{entry_id}', {day_param}{filter_param}, " f"output_format: 'array_of_arrays', insert_nulls: 'segments', minor_currency: true, " f"connect_segments: true }} }}); " f"return response.response.data;" @@ -276,13 +281,16 @@ async def handle_apexcharts_yaml(call: ServiceCall) -> dict[str, Any]: if highlight_best_price and entity_map: # 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 + # Conditionally include day parameter (omit for rolling window mode) + day_param = f"day: ['{day}'], " if day else "" + best_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: ['{day}'], " + f"service_data: {{ entry_id: '{entry_id}', {day_param}" f"period_filter: 'best_price', " f"output_format: 'array_of_arrays', minor_currency: true }} }}); " f"return response.response.data.map(point => [point[0], 1]);" @@ -311,22 +319,34 @@ async def handle_apexcharts_yaml(call: ServiceCall) -> dict[str, Any]: "Price Phases Daily Progress" if level_type == "rating_level" else "Price Level" ) - # Add translated day to title - day_translated = get_translation(["selector", "day", "options", day], user_language) or day.capitalize() - title = f"{title} - {day_translated}" + # Add translated day to title (only if day parameter was provided) + if day: + day_translated = get_translation(["selector", "day", "options", day], user_language) or day.capitalize() + title = f"{title} - {day_translated}" # Configure span based on selected day + # For rolling window mode, use config-template-card for dynamic offset if day == "yesterday": span_config = {"start": "day", "offset": "-1d"} + graph_span_value = None + use_template = False elif day == "tomorrow": span_config = {"start": "day", "offset": "+1d"} - else: # today + graph_span_value = None + use_template = False + elif day: # today (explicit) span_config = {"start": "day"} + graph_span_value = None + use_template = False + else: # Rolling window mode (None) + # Use config-template-card to dynamically set offset based on data availability + span_config = None # Will be set in template + graph_span_value = "48h" + use_template = True - return { + result = { "type": "custom:apexcharts-card", "update_interval": "5m", - "span": span_config, "header": { "show": True, "title": title, @@ -392,3 +412,55 @@ async def handle_apexcharts_yaml(call: ServiceCall) -> dict[str, Any]: }, "series": series, } + + # For rolling window mode, wrap in config-template-card for dynamic offset + if use_template: + # Add graph_span to base config (48h window) + result["graph_span"] = graph_span_value + + # Find tomorrow_data_available binary sensor + tomorrow_data_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("_tomorrow_data_available") + ), + None, + ) + + if tomorrow_data_sensor: + # Wrap in config-template-card with dynamic offset calculation + # Template checks if tomorrow data is available (binary sensor state) + # If 'on' (tomorrow data available) → offset +1d (show today+tomorrow) + # If 'off' (no tomorrow data) → offset +0d (show yesterday+today) + template_value = f"states['{tomorrow_data_sensor}'].state === 'on' ? '+1d' : '+0d'" + return { + "type": "custom:config-template-card", + "variables": { + "v_offset": template_value, + }, + "entities": [tomorrow_data_sensor], + "card": { + **result, + "span": { + "end": "day", + "offset": "${v_offset}", + }, + }, + } + + # Fallback if sensor not found: just use +1d offset + result["span"] = {"end": "day", "offset": "+1d"} + return result + + # Add span for fixed-day views + if span_config: + result["span"] = span_config + + # Add graph_span if needed + if graph_span_value: + result["graph_span"] = graph_span_value + + return result diff --git a/custom_components/tibber_prices/services/get_chartdata.py b/custom_components/tibber_prices/services/get_chartdata.py index 7aa3901..d078836 100644 --- a/custom_components/tibber_prices/services/get_chartdata.py +++ b/custom_components/tibber_prices/services/get_chartdata.py @@ -48,7 +48,7 @@ from custom_components.tibber_prices.coordinator.helpers import ( from homeassistant.exceptions import ServiceValidationError from .formatters import aggregate_hourly_exact, get_period_data, normalize_level_filter, normalize_rating_level_filter -from .helpers import get_entry_and_data +from .helpers import get_entry_and_data, has_tomorrow_data if TYPE_CHECKING: from homeassistant.core import ServiceCall @@ -114,6 +114,11 @@ async def handle_chartdata(call: ServiceCall) -> dict[str, Any]: # noqa: PLR091 Supports both 15-minute intervals and hourly aggregation, with optional filtering by price level, rating level, or period (best_price/peak_price). + Default behavior (no day parameter): + - Returns rolling 2-day window for continuous chart display + - If tomorrow data available: today + tomorrow + - If tomorrow data NOT available: yesterday + today + See services.yaml for detailed parameter documentation. Args: @@ -132,10 +137,15 @@ async def handle_chartdata(call: ServiceCall) -> dict[str, Any]: # noqa: PLR091 raise ServiceValidationError(translation_domain=DOMAIN, translation_key="missing_entry_id") entry_id: str = str(entry_id_raw) + # Get coordinator to check data availability + _, coordinator, _ = get_entry_and_data(hass, entry_id) + days_raw = call.data.get(ATTR_DAY) - # If no day specified, return all available data (today + tomorrow) + # If no day specified, use rolling 2-day window: + # - If tomorrow data available: today + tomorrow + # - If tomorrow data NOT available: yesterday + today if days_raw is None: - days = ["today", "tomorrow"] + days = ["today", "tomorrow"] if has_tomorrow_data(coordinator) else ["yesterday", "today"] # Convert single string to list for uniform processing elif isinstance(days_raw, str): days = [days_raw] @@ -174,8 +184,6 @@ async def handle_chartdata(call: ServiceCall) -> dict[str, Any]: # noqa: PLR091 if average_field in array_fields_template: include_average = True - _, coordinator, _ = get_entry_and_data(hass, entry_id) - # Get thresholds from config for rating aggregation threshold_low = coordinator.config_entry.options.get( CONF_PRICE_RATING_THRESHOLD_LOW, DEFAULT_PRICE_RATING_THRESHOLD_LOW diff --git a/custom_components/tibber_prices/services/helpers.py b/custom_components/tibber_prices/services/helpers.py index ab06651..38c54b7 100644 --- a/custom_components/tibber_prices/services/helpers.py +++ b/custom_components/tibber_prices/services/helpers.py @@ -19,9 +19,11 @@ from __future__ import annotations from typing import TYPE_CHECKING, Any from custom_components.tibber_prices.const import DOMAIN +from custom_components.tibber_prices.coordinator.helpers import get_intervals_for_day_offsets from homeassistant.exceptions import ServiceValidationError if TYPE_CHECKING: + from custom_components.tibber_prices.coordinator import TibberPricesDataUpdateCoordinator from homeassistant.core import HomeAssistant @@ -51,3 +53,22 @@ def get_entry_and_data(hass: HomeAssistant, entry_id: str) -> tuple[Any, Any, di coordinator = entry.runtime_data.coordinator data = coordinator.data or {} return entry, coordinator, data + + +def has_tomorrow_data(coordinator: TibberPricesDataUpdateCoordinator) -> bool: + """ + Check if tomorrow's price data is available in coordinator. + + Uses get_intervals_for_day_offsets() to automatically determine tomorrow + based on current date. + + Args: + coordinator: TibberPricesDataUpdateCoordinator instance + + Returns: + True if tomorrow's data exists (at least one interval), False otherwise + + """ + coordinator_data = coordinator.data or {} + tomorrow_intervals = get_intervals_for_day_offsets(coordinator_data, [1]) + return len(tomorrow_intervals) > 0 diff --git a/custom_components/tibber_prices/translations/de.json b/custom_components/tibber_prices/translations/de.json index 839dd35..b6f59e5 100644 --- a/custom_components/tibber_prices/translations/de.json +++ b/custom_components/tibber_prices/translations/de.json @@ -799,7 +799,7 @@ }, "day": { "name": "Tag", - "description": "Welcher Tag visualisiert werden soll (gestern, heute oder morgen)." + "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)." }, "level_type": { "name": "Stufen-Typ", @@ -817,7 +817,7 @@ }, "day": { "name": "Tag", - "description": "Für welche(n) Tag(e) sollen Preise abgerufen werden. Du kannst mehrere Tage auswählen. Falls nicht angegeben, werden alle verfügbaren Daten zurückgegeben (heute + morgen falls verfügbar)." + "description": "Für welche(n) Tag(e) sollen Preise abgerufen werden. Du kannst mehrere Tage auswählen. Falls nicht angegeben, wird ein rollierendes 2-Tages-Fenster zurückgegeben: heute+morgen (wenn Morgendaten verfügbar) oder gestern+heute (wenn Morgendaten noch nicht verfügbar). Dies ermöglicht eine kontinuierliche Diagrammanzeige ohne Lücken." }, "resolution": { "name": "Auflösung", diff --git a/custom_components/tibber_prices/translations/en.json b/custom_components/tibber_prices/translations/en.json index 236c7bf..ba6a597 100644 --- a/custom_components/tibber_prices/translations/en.json +++ b/custom_components/tibber_prices/translations/en.json @@ -795,7 +795,7 @@ }, "day": { "name": "Day", - "description": "Which day to visualize (yesterday, today, or tomorrow)." + "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)." }, "level_type": { "name": "Level Type", @@ -813,7 +813,7 @@ }, "day": { "name": "Day", - "description": "Which day(s) to fetch prices for. You can select multiple days. If not specified, returns all available data (today + tomorrow if available)." + "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." }, "resolution": { "name": "Resolution", diff --git a/custom_components/tibber_prices/translations/nb.json b/custom_components/tibber_prices/translations/nb.json index 211e1d7..0e0fd72 100644 --- a/custom_components/tibber_prices/translations/nb.json +++ b/custom_components/tibber_prices/translations/nb.json @@ -795,7 +795,7 @@ }, "day": { "name": "Dag", - "description": "Hvilken dag som skal visualiseres (i går, i dag eller i morgen)." + "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å)." }, "level_type": { "name": "Nivåtype", @@ -813,7 +813,7 @@ }, "day": { "name": "Dag", - "description": "Hvilken dag(er) skal det hentes priser for. Du kan velge flere dager. Hvis ikke angitt, returneres alle tilgjengelige data (i dag + i morgen hvis tilgjengelig)." + "description": "Hvilken dag(er) skal det hentes priser for. Du kan velge flere dager. Hvis ikke angitt, returneres et rullerende 2-dagers vindu: i dag+i morgen (når morgendagens data er tilgjengelig) eller i går+i dag (når morgendagens data ikke er tilgjengelig ennå). Dette gir kontinuerlig diagramvisning uten hull." }, "resolution": { "name": "Oppløsning", diff --git a/custom_components/tibber_prices/translations/nl.json b/custom_components/tibber_prices/translations/nl.json index c9adb79..d62d96f 100644 --- a/custom_components/tibber_prices/translations/nl.json +++ b/custom_components/tibber_prices/translations/nl.json @@ -795,7 +795,7 @@ }, "day": { "name": "Dag", - "description": "Welke dag gevisualiseerd moet worden (gisteren, vandaag of morgen)." + "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)." }, "level_type": { "name": "Niveautype", @@ -813,7 +813,7 @@ }, "day": { "name": "Dag", - "description": "Voor welke dag(en) moeten prijzen worden opgehaald. Je kunt meerdere dagen selecteren. Als niet opgegeven, worden alle beschikbare gegevens geretourneerd (vandaag + morgen indien beschikbaar)." + "description": "Voor welke dag(en) moeten prijzen worden opgehaald. Je kunt meerdere dagen selecteren. Als niet opgegeven, wordt een rollend 2-daags venster geretourneerd: vandaag+morgen (wanneer morgengegevens beschikbaar zijn) of gisteren+vandaag (wanneer morgengegevens nog niet beschikbaar zijn). Dit zorgt voor een continue grafiekweergave zonder hiaten." }, "resolution": { "name": "Resolutie", diff --git a/custom_components/tibber_prices/translations/sv.json b/custom_components/tibber_prices/translations/sv.json index a581d51..fc97f1b 100644 --- a/custom_components/tibber_prices/translations/sv.json +++ b/custom_components/tibber_prices/translations/sv.json @@ -795,7 +795,7 @@ }, "day": { "name": "Dag", - "description": "Vilken dag som ska visualiseras (igår, idag eller imorgon)." + "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)." }, "level_type": { "name": "Nivåtyp", @@ -813,7 +813,7 @@ }, "day": { "name": "Dag", - "description": "Vilken dag(ar) ska priser hämtas för. Du kan välja flera dagar. Om inte angivet, returneras alla tillgängliga data (idag + imorgon om tillgängligt)." + "description": "Vilken dag(ar) ska priser hämtas för. Du kan välja flera dagar. Om inte angivet, returneras ett rullande 2-dagars fönster: idag+imorgon (när morgondagens data är tillgänglig) eller igår+idag (när morgondagens data inte är tillgänglig ännu). Detta ger kontinuerlig diagramvisning utan luckor." }, "resolution": { "name": "Upplösning", diff --git a/docs/user/automation-examples.md b/docs/user/automation-examples.md index 6776c34..8ab71f8 100644 --- a/docs/user/automation-examples.md +++ b/docs/user/automation-examples.md @@ -241,4 +241,60 @@ Coming soon... ## ApexCharts Cards -Coming soon... +The `tibber_prices.get_apexcharts_yaml` service generates complete ApexCharts card configurations for visualizing electricity prices. + +### Prerequisites + +**Required:** +- [ApexCharts Card](https://github.com/RomRider/apexcharts-card) - Install via HACS + +**Optional (for rolling window mode):** +- [Config Template Card](https://github.com/iantrich/config-template-card) - Install via HACS + +### Installation + +1. Open HACS → Frontend +2. Search for "ApexCharts Card" and install +3. (Optional) Search for "Config Template Card" and install if you want rolling window mode + +### Example: Fixed Day View + +```yaml +# Generate configuration via automation/script +service: tibber_prices.get_apexcharts_yaml +data: + entry_id: YOUR_ENTRY_ID + day: today # or "yesterday", "tomorrow" + level_type: rating_level # or "level" for 5-level view +response_variable: apexcharts_config +``` + +Then copy the generated YAML into your Lovelace dashboard. + +### Example: Rolling 48h Window + +For a dynamic chart that automatically adapts to data availability: + +```yaml +service: tibber_prices.get_apexcharts_yaml +data: + entry_id: YOUR_ENTRY_ID + # Omit 'day' parameter for rolling window + level_type: rating_level +response_variable: apexcharts_config +``` + +**Behavior:** +- **When tomorrow data available** (typically after ~13:00): Shows today + tomorrow +- **When tomorrow data not available**: Shows yesterday + today + +**Note:** Rolling window mode requires Config Template Card to dynamically adjust the time range. + +### Features + +- Color-coded price levels/ratings (green = cheap, yellow = normal, red = expensive) +- Best price period highlighting (semi-transparent green overlay) +- Automatic NULL insertion for clean gaps +- Translated labels based on your Home Assistant language +- Interactive zoom and pan +- Live marker showing current time diff --git a/docs/user/services.md b/docs/user/services.md index bc23c74..fac441f 100644 --- a/docs/user/services.md +++ b/docs/user/services.md @@ -57,6 +57,25 @@ response_variable: chart_data | `minor_currency` | Return prices in ct/øre instead of EUR/NOK | `false` | | `round_decimals` | Decimal places (0-10) | 4 (major) or 2 (minor) | +**Rolling Window Mode:** + +Omit the `day` parameter to get a dynamic 48-hour rolling window that automatically adapts to data availability: + +```yaml +service: tibber_prices.get_chartdata +data: + entry_id: YOUR_ENTRY_ID + # Omit 'day' for rolling window + output_format: array_of_objects +response_variable: chart_data +``` + +**Behavior:** +- **When tomorrow data available** (typically after ~13:00): Returns today + tomorrow +- **When tomorrow data not available**: Returns yesterday + today + +This is useful for charts that should always show a 48-hour window without manual day selection. + **Period Filter Example:** Get best price periods as summaries instead of intervals: @@ -93,15 +112,26 @@ For detailed parameter descriptions, see the service definition in **Developer T **Purpose:** Generates complete ApexCharts card YAML configuration for visualizing electricity prices. +**Prerequisites:** +- [ApexCharts Card](https://github.com/RomRider/apexcharts-card) (required for all configurations) +- [Config Template Card](https://github.com/iantrich/config-template-card) (required only for rolling window mode without `day` parameter) + **Quick Example:** ```yaml service: tibber_prices.get_apexcharts_yaml data: entry_id: YOUR_ENTRY_ID + day: today # Optional: omit for rolling 48h window (requires config-template-card) response_variable: apexcharts_config ``` +**Rolling Window Mode:** When omitting the `day` parameter, the service generates a dynamic 48-hour rolling window that automatically shows: +- 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. + Use the response in Lovelace dashboards by copying the generated YAML. **Documentation:** See Developer Tools → Services for parameter details.