mirror of
https://github.com/jpawlowski/hass.tibber_prices.git
synced 2026-03-30 05:13:40 +00:00
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.
This commit is contained in:
parent
cf8d9ba8e8
commit
e156dfb061
11 changed files with 222 additions and 34 deletions
|
|
@ -40,9 +40,9 @@ get_apexcharts_yaml:
|
||||||
integration: tibber_prices
|
integration: tibber_prices
|
||||||
day:
|
day:
|
||||||
name: 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
|
required: false
|
||||||
default: today
|
|
||||||
example: today
|
example: today
|
||||||
selector:
|
selector:
|
||||||
select:
|
select:
|
||||||
|
|
@ -90,7 +90,8 @@ get_chartdata:
|
||||||
# === DATA SELECTION ===
|
# === DATA SELECTION ===
|
||||||
day:
|
day:
|
||||||
name: 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
|
required: false
|
||||||
selector:
|
selector:
|
||||||
select:
|
select:
|
||||||
|
|
|
||||||
|
|
@ -36,6 +36,7 @@ from custom_components.tibber_prices.const import (
|
||||||
get_translation,
|
get_translation,
|
||||||
)
|
)
|
||||||
from homeassistant.exceptions import ServiceValidationError
|
from homeassistant.exceptions import ServiceValidationError
|
||||||
|
from homeassistant.helpers import config_validation as cv
|
||||||
from homeassistant.helpers.entity_registry import (
|
from homeassistant.helpers.entity_registry import (
|
||||||
EntityRegistry,
|
EntityRegistry,
|
||||||
)
|
)
|
||||||
|
|
@ -55,12 +56,12 @@ ATTR_DAY: Final = "day"
|
||||||
ATTR_ENTRY_ID: Final = "entry_id"
|
ATTR_ENTRY_ID: Final = "entry_id"
|
||||||
|
|
||||||
# Service schema
|
# Service schema
|
||||||
APEXCHARTS_SERVICE_SCHEMA: Final = vol.Schema(
|
APEXCHARTS_SERVICE_SCHEMA = vol.Schema(
|
||||||
{
|
{
|
||||||
vol.Required(ATTR_ENTRY_ID): str,
|
vol.Required(ATTR_ENTRY_ID): cv.string,
|
||||||
vol.Optional("day", default="today"): vol.In(["yesterday", "today", "tomorrow"]),
|
vol.Optional("day"): vol.In(["yesterday", "today", "tomorrow"]),
|
||||||
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): 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.
|
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")
|
raise ServiceValidationError(translation_domain=DOMAIN, translation_key="missing_entry_id")
|
||||||
entry_id: str = str(entry_id_raw)
|
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")
|
level_type = call.data.get("level_type", "rating_level")
|
||||||
highlight_best_price = call.data.get("highlight_best_price", True)
|
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)
|
entity_registry = async_get_entity_registry(hass)
|
||||||
|
|
||||||
# Build entity mapping based on level_type and day for clickable states
|
# 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":
|
if level_type == "rating_level":
|
||||||
series_levels = [
|
series_levels = [
|
||||||
|
|
@ -234,13 +236,16 @@ async def handle_apexcharts_yaml(call: ServiceCall) -> dict[str, Any]:
|
||||||
else:
|
else:
|
||||||
filter_param = f"level_filter: ['{level_key}']"
|
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 = (
|
data_generator = (
|
||||||
f"const response = await hass.callWS({{ "
|
f"const response = await hass.callWS({{ "
|
||||||
f"type: 'call_service', "
|
f"type: 'call_service', "
|
||||||
f"domain: 'tibber_prices', "
|
f"domain: 'tibber_prices', "
|
||||||
f"service: 'get_chartdata', "
|
f"service: 'get_chartdata', "
|
||||||
f"return_response: true, "
|
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"output_format: 'array_of_arrays', insert_nulls: 'segments', minor_currency: true, "
|
||||||
f"connect_segments: true }} }}); "
|
f"connect_segments: true }} }}); "
|
||||||
f"return response.response.data;"
|
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:
|
if highlight_best_price and entity_map:
|
||||||
# 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)
|
||||||
|
day_param = f"day: ['{day}'], " if day else ""
|
||||||
|
|
||||||
best_price_generator = (
|
best_price_generator = (
|
||||||
f"const response = await hass.callWS({{ "
|
f"const response = await hass.callWS({{ "
|
||||||
f"type: 'call_service', "
|
f"type: 'call_service', "
|
||||||
f"domain: 'tibber_prices', "
|
f"domain: 'tibber_prices', "
|
||||||
f"service: 'get_chartdata', "
|
f"service: 'get_chartdata', "
|
||||||
f"return_response: true, "
|
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"period_filter: 'best_price', "
|
||||||
f"output_format: 'array_of_arrays', minor_currency: true }} }}); "
|
f"output_format: 'array_of_arrays', minor_currency: true }} }}); "
|
||||||
f"return response.response.data.map(point => [point[0], 1]);"
|
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"
|
"Price Phases Daily Progress" if level_type == "rating_level" else "Price Level"
|
||||||
)
|
)
|
||||||
|
|
||||||
# Add translated day to title
|
# 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()
|
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
|
||||||
if day == "yesterday":
|
if day == "yesterday":
|
||||||
span_config = {"start": "day", "offset": "-1d"}
|
span_config = {"start": "day", "offset": "-1d"}
|
||||||
|
graph_span_value = None
|
||||||
|
use_template = False
|
||||||
elif day == "tomorrow":
|
elif day == "tomorrow":
|
||||||
span_config = {"start": "day", "offset": "+1d"}
|
span_config = {"start": "day", "offset": "+1d"}
|
||||||
else: # today
|
graph_span_value = None
|
||||||
|
use_template = False
|
||||||
|
elif day: # today (explicit)
|
||||||
span_config = {"start": "day"}
|
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",
|
"type": "custom:apexcharts-card",
|
||||||
"update_interval": "5m",
|
"update_interval": "5m",
|
||||||
"span": span_config,
|
|
||||||
"header": {
|
"header": {
|
||||||
"show": True,
|
"show": True,
|
||||||
"title": title,
|
"title": title,
|
||||||
|
|
@ -392,3 +412,55 @@ async def handle_apexcharts_yaml(call: ServiceCall) -> dict[str, Any]:
|
||||||
},
|
},
|
||||||
"series": series,
|
"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
|
||||||
|
|
|
||||||
|
|
@ -48,7 +48,7 @@ from custom_components.tibber_prices.coordinator.helpers import (
|
||||||
from homeassistant.exceptions import ServiceValidationError
|
from homeassistant.exceptions import ServiceValidationError
|
||||||
|
|
||||||
from .formatters import aggregate_hourly_exact, get_period_data, normalize_level_filter, normalize_rating_level_filter
|
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:
|
if TYPE_CHECKING:
|
||||||
from homeassistant.core import ServiceCall
|
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
|
Supports both 15-minute intervals and hourly aggregation, with optional filtering by
|
||||||
price level, rating level, or period (best_price/peak_price).
|
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.
|
See services.yaml for detailed parameter documentation.
|
||||||
|
|
||||||
Args:
|
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")
|
raise ServiceValidationError(translation_domain=DOMAIN, translation_key="missing_entry_id")
|
||||||
entry_id: str = str(entry_id_raw)
|
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)
|
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:
|
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
|
# Convert single string to list for uniform processing
|
||||||
elif isinstance(days_raw, str):
|
elif isinstance(days_raw, str):
|
||||||
days = [days_raw]
|
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:
|
if average_field in array_fields_template:
|
||||||
include_average = True
|
include_average = True
|
||||||
|
|
||||||
_, coordinator, _ = get_entry_and_data(hass, entry_id)
|
|
||||||
|
|
||||||
# Get thresholds from config for rating aggregation
|
# Get thresholds from config for rating aggregation
|
||||||
threshold_low = coordinator.config_entry.options.get(
|
threshold_low = coordinator.config_entry.options.get(
|
||||||
CONF_PRICE_RATING_THRESHOLD_LOW, DEFAULT_PRICE_RATING_THRESHOLD_LOW
|
CONF_PRICE_RATING_THRESHOLD_LOW, DEFAULT_PRICE_RATING_THRESHOLD_LOW
|
||||||
|
|
|
||||||
|
|
@ -19,9 +19,11 @@ from __future__ import annotations
|
||||||
from typing import TYPE_CHECKING, Any
|
from typing import TYPE_CHECKING, Any
|
||||||
|
|
||||||
from custom_components.tibber_prices.const import DOMAIN
|
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
|
from homeassistant.exceptions import ServiceValidationError
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
|
from custom_components.tibber_prices.coordinator import TibberPricesDataUpdateCoordinator
|
||||||
from homeassistant.core import HomeAssistant
|
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
|
coordinator = entry.runtime_data.coordinator
|
||||||
data = coordinator.data or {}
|
data = coordinator.data or {}
|
||||||
return entry, coordinator, data
|
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
|
||||||
|
|
|
||||||
|
|
@ -799,7 +799,7 @@
|
||||||
},
|
},
|
||||||
"day": {
|
"day": {
|
||||||
"name": "Tag",
|
"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": {
|
"level_type": {
|
||||||
"name": "Stufen-Typ",
|
"name": "Stufen-Typ",
|
||||||
|
|
@ -817,7 +817,7 @@
|
||||||
},
|
},
|
||||||
"day": {
|
"day": {
|
||||||
"name": "Tag",
|
"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": {
|
"resolution": {
|
||||||
"name": "Auflösung",
|
"name": "Auflösung",
|
||||||
|
|
|
||||||
|
|
@ -795,7 +795,7 @@
|
||||||
},
|
},
|
||||||
"day": {
|
"day": {
|
||||||
"name": "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": {
|
"level_type": {
|
||||||
"name": "Level Type",
|
"name": "Level Type",
|
||||||
|
|
@ -813,7 +813,7 @@
|
||||||
},
|
},
|
||||||
"day": {
|
"day": {
|
||||||
"name": "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": {
|
"resolution": {
|
||||||
"name": "Resolution",
|
"name": "Resolution",
|
||||||
|
|
|
||||||
|
|
@ -795,7 +795,7 @@
|
||||||
},
|
},
|
||||||
"day": {
|
"day": {
|
||||||
"name": "Dag",
|
"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": {
|
"level_type": {
|
||||||
"name": "Nivåtype",
|
"name": "Nivåtype",
|
||||||
|
|
@ -813,7 +813,7 @@
|
||||||
},
|
},
|
||||||
"day": {
|
"day": {
|
||||||
"name": "Dag",
|
"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": {
|
"resolution": {
|
||||||
"name": "Oppløsning",
|
"name": "Oppløsning",
|
||||||
|
|
|
||||||
|
|
@ -795,7 +795,7 @@
|
||||||
},
|
},
|
||||||
"day": {
|
"day": {
|
||||||
"name": "Dag",
|
"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": {
|
"level_type": {
|
||||||
"name": "Niveautype",
|
"name": "Niveautype",
|
||||||
|
|
@ -813,7 +813,7 @@
|
||||||
},
|
},
|
||||||
"day": {
|
"day": {
|
||||||
"name": "Dag",
|
"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": {
|
"resolution": {
|
||||||
"name": "Resolutie",
|
"name": "Resolutie",
|
||||||
|
|
|
||||||
|
|
@ -795,7 +795,7 @@
|
||||||
},
|
},
|
||||||
"day": {
|
"day": {
|
||||||
"name": "Dag",
|
"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": {
|
"level_type": {
|
||||||
"name": "Nivåtyp",
|
"name": "Nivåtyp",
|
||||||
|
|
@ -813,7 +813,7 @@
|
||||||
},
|
},
|
||||||
"day": {
|
"day": {
|
||||||
"name": "Dag",
|
"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": {
|
"resolution": {
|
||||||
"name": "Upplösning",
|
"name": "Upplösning",
|
||||||
|
|
|
||||||
|
|
@ -241,4 +241,60 @@ Coming soon...
|
||||||
|
|
||||||
## ApexCharts Cards
|
## 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
|
||||||
|
|
|
||||||
|
|
@ -57,6 +57,25 @@ response_variable: chart_data
|
||||||
| `minor_currency` | Return prices in ct/øre instead of EUR/NOK | `false` |
|
| `minor_currency` | Return prices in ct/øre instead of EUR/NOK | `false` |
|
||||||
| `round_decimals` | Decimal places (0-10) | 4 (major) or 2 (minor) |
|
| `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:**
|
**Period Filter Example:**
|
||||||
|
|
||||||
Get best price periods as summaries instead of intervals:
|
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.
|
**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:**
|
**Quick Example:**
|
||||||
|
|
||||||
```yaml
|
```yaml
|
||||||
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)
|
||||||
response_variable: apexcharts_config
|
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.
|
Use the response in Lovelace dashboards by copying the generated YAML.
|
||||||
|
|
||||||
**Documentation:** See Developer Tools → Services for parameter details.
|
**Documentation:** See Developer Tools → Services for parameter details.
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue