mirror of
https://github.com/jpawlowski/hass.tibber_prices.git
synced 2026-03-29 21:03:40 +00:00
feat(services): add hourly resolution option for chart data services
Add resolution parameter to get_chartdata and get_apexcharts_yaml services, allowing users to choose between original 15-minute intervals or aggregated hourly values for chart visualization. Implementation uses rolling 5-interval window aggregation (-2, -1, 0, +1, +2 around :00 of each hour = 60 minutes total), matching the sensor rolling hour methodology. Respects user's CONF_AVERAGE_SENSOR_DISPLAY setting for mean vs median calculation. Changes: - formatters.py: Add aggregate_to_hourly() function preserving original field names (startsAt, total, level, rating_level) for unified processing - get_chartdata.py: Pre-aggregate data before processing when resolution is 'hourly', enabling same code path for filters/insert_nulls/connect_segments - get_apexcharts_yaml.py: Add resolution parameter, pass to all 4 get_chartdata service calls in generated JavaScript - services.yaml: Add resolution field with interval/hourly selector - icons.json: Add section icons for get_apexcharts_yaml fields - translations: Add highlight_peak_price and resolution field translations for all 5 languages (en, de, sv, nb, nl) Impact: Users can now generate cleaner charts with 24 hourly data points instead of 96 quarter-hourly intervals. The unified processing approach ensures all chart features (filters, null insertion, segment connection) work identically for both resolutions.
This commit is contained in:
parent
1b22ce3f2a
commit
2f36c73c18
10 changed files with 183 additions and 34 deletions
|
|
@ -16,7 +16,15 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"get_apexcharts_yaml": {
|
"get_apexcharts_yaml": {
|
||||||
"service": "mdi:chart-line"
|
"service": "mdi:chart-line",
|
||||||
|
"sections": {
|
||||||
|
"entry_id": "mdi:identifier",
|
||||||
|
"day": "mdi:calendar-range",
|
||||||
|
"level_type": "mdi:format-list-bulleted-type",
|
||||||
|
"resolution": "mdi:timer-sand",
|
||||||
|
"highlight_best_price": "mdi:battery-charging-low",
|
||||||
|
"highlight_peak_price": "mdi:battery-alert"
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"refresh_user_data": {
|
"refresh_user_data": {
|
||||||
"service": "mdi:refresh"
|
"service": "mdi:refresh"
|
||||||
|
|
|
||||||
|
|
@ -46,6 +46,16 @@ get_apexcharts_yaml:
|
||||||
- rating_level
|
- rating_level
|
||||||
- level
|
- level
|
||||||
translation_key: level_type
|
translation_key: level_type
|
||||||
|
resolution:
|
||||||
|
required: false
|
||||||
|
default: interval
|
||||||
|
example: interval
|
||||||
|
selector:
|
||||||
|
select:
|
||||||
|
options:
|
||||||
|
- interval
|
||||||
|
- hourly
|
||||||
|
translation_key: resolution
|
||||||
highlight_best_price:
|
highlight_best_price:
|
||||||
required: false
|
required: false
|
||||||
default: true
|
default: true
|
||||||
|
|
|
||||||
|
|
@ -24,6 +24,8 @@ from datetime import datetime, time
|
||||||
from typing import Any
|
from typing import Any
|
||||||
|
|
||||||
from custom_components.tibber_prices.const import (
|
from custom_components.tibber_prices.const import (
|
||||||
|
CONF_AVERAGE_SENSOR_DISPLAY,
|
||||||
|
DEFAULT_AVERAGE_SENSOR_DISPLAY,
|
||||||
DEFAULT_PRICE_RATING_THRESHOLD_HIGH,
|
DEFAULT_PRICE_RATING_THRESHOLD_HIGH,
|
||||||
DEFAULT_PRICE_RATING_THRESHOLD_LOW,
|
DEFAULT_PRICE_RATING_THRESHOLD_LOW,
|
||||||
get_translation,
|
get_translation,
|
||||||
|
|
@ -32,6 +34,7 @@ from custom_components.tibber_prices.coordinator.helpers import (
|
||||||
get_intervals_for_day_offsets,
|
get_intervals_for_day_offsets,
|
||||||
)
|
)
|
||||||
from custom_components.tibber_prices.sensor.helpers import aggregate_level_data, aggregate_rating_data
|
from custom_components.tibber_prices.sensor.helpers import aggregate_level_data, aggregate_rating_data
|
||||||
|
from custom_components.tibber_prices.utils.average import calculate_mean, calculate_median
|
||||||
|
|
||||||
|
|
||||||
def normalize_level_filter(value: list[str] | None) -> list[str] | None:
|
def normalize_level_filter(value: list[str] | None) -> list[str] | None:
|
||||||
|
|
@ -48,6 +51,99 @@ def normalize_rating_level_filter(value: list[str] | None) -> list[str] | None:
|
||||||
return [v.upper() for v in value]
|
return [v.upper() for v in value]
|
||||||
|
|
||||||
|
|
||||||
|
def aggregate_to_hourly( # noqa: PLR0912
|
||||||
|
intervals: list[dict],
|
||||||
|
coordinator: Any,
|
||||||
|
threshold_low: float = DEFAULT_PRICE_RATING_THRESHOLD_LOW,
|
||||||
|
threshold_high: float = DEFAULT_PRICE_RATING_THRESHOLD_HIGH,
|
||||||
|
) -> list[dict]:
|
||||||
|
"""
|
||||||
|
Aggregate 15-minute intervals to hourly using rolling 5-interval window.
|
||||||
|
|
||||||
|
Preserves original field names (startsAt, total, level, rating_level) so the
|
||||||
|
aggregated data can be processed by the same code path as interval data.
|
||||||
|
|
||||||
|
Uses the same methodology as sensor rolling hour calculations:
|
||||||
|
- 5-interval window: 2 before + center + 2 after (60 minutes total)
|
||||||
|
- Center interval is at :00 of each hour
|
||||||
|
- Respects user's CONF_AVERAGE_SENSOR_DISPLAY setting (mean vs median)
|
||||||
|
|
||||||
|
Example for 10:00 data point:
|
||||||
|
- Window includes: 09:30, 09:45, 10:00, 10:15, 10:30
|
||||||
|
|
||||||
|
Args:
|
||||||
|
intervals: List of 15-minute price intervals with startsAt, total, level, rating_level
|
||||||
|
coordinator: Data update coordinator instance
|
||||||
|
threshold_low: Rating level threshold (low/normal boundary)
|
||||||
|
threshold_high: Rating level threshold (normal/high boundary)
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
List of hourly data points with same structure as input (startsAt, total, level, rating_level)
|
||||||
|
|
||||||
|
"""
|
||||||
|
if not intervals:
|
||||||
|
return []
|
||||||
|
|
||||||
|
# Get user's average display preference (mean or median)
|
||||||
|
average_display = coordinator.config_entry.options.get(CONF_AVERAGE_SENSOR_DISPLAY, DEFAULT_AVERAGE_SENSOR_DISPLAY)
|
||||||
|
use_median = average_display == "median"
|
||||||
|
|
||||||
|
hourly_data = []
|
||||||
|
|
||||||
|
# Iterate through all intervals, only process those at :00
|
||||||
|
for i, interval in enumerate(intervals):
|
||||||
|
start_time = interval.get("startsAt")
|
||||||
|
|
||||||
|
if not start_time:
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Check if this is the start of an hour (:00)
|
||||||
|
if start_time.minute != 0:
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Collect 5-interval rolling window: -2, -1, 0, +1, +2
|
||||||
|
window_prices: list[float] = []
|
||||||
|
window_intervals: list[dict] = []
|
||||||
|
|
||||||
|
for offset in range(-2, 3): # -2, -1, 0, +1, +2
|
||||||
|
target_idx = i + offset
|
||||||
|
if 0 <= target_idx < len(intervals):
|
||||||
|
target_interval = intervals[target_idx]
|
||||||
|
price = target_interval.get("total")
|
||||||
|
if price is not None:
|
||||||
|
window_prices.append(price)
|
||||||
|
window_intervals.append(target_interval)
|
||||||
|
|
||||||
|
# Calculate aggregated price based on user preference
|
||||||
|
if window_prices:
|
||||||
|
aggregated_price = calculate_median(window_prices) if use_median else calculate_mean(window_prices)
|
||||||
|
|
||||||
|
if aggregated_price is None:
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Build data point with original field names
|
||||||
|
data_point: dict[str, Any] = {
|
||||||
|
"startsAt": start_time,
|
||||||
|
"total": aggregated_price,
|
||||||
|
}
|
||||||
|
|
||||||
|
# Add aggregated level
|
||||||
|
if window_intervals:
|
||||||
|
aggregated_level = aggregate_level_data(window_intervals)
|
||||||
|
if aggregated_level:
|
||||||
|
data_point["level"] = aggregated_level.upper()
|
||||||
|
|
||||||
|
# Add aggregated rating_level
|
||||||
|
if window_intervals:
|
||||||
|
aggregated_rating = aggregate_rating_data(window_intervals, threshold_low, threshold_high)
|
||||||
|
if aggregated_rating:
|
||||||
|
data_point["rating_level"] = aggregated_rating.upper()
|
||||||
|
|
||||||
|
hourly_data.append(data_point)
|
||||||
|
|
||||||
|
return hourly_data
|
||||||
|
|
||||||
|
|
||||||
def aggregate_hourly_exact( # noqa: PLR0913, PLR0912, PLR0915
|
def aggregate_hourly_exact( # noqa: PLR0913, PLR0912, PLR0915
|
||||||
intervals: list[dict],
|
intervals: list[dict],
|
||||||
start_time_field: str,
|
start_time_field: str,
|
||||||
|
|
|
||||||
|
|
@ -63,6 +63,7 @@ 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", "rolling_window", "rolling_window_autozoom"]),
|
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("resolution", default="interval"): vol.In(["interval", "hourly"]),
|
||||||
vol.Optional("highlight_best_price", default=True): cv.boolean,
|
vol.Optional("highlight_best_price", default=True): cv.boolean,
|
||||||
vol.Optional("highlight_peak_price", default=False): cv.boolean,
|
vol.Optional("highlight_peak_price", default=False): cv.boolean,
|
||||||
}
|
}
|
||||||
|
|
@ -296,6 +297,7 @@ async def handle_apexcharts_yaml(call: ServiceCall) -> dict[str, Any]: # noqa:
|
||||||
|
|
||||||
day = call.data.get("day") # Can be None (rolling window mode)
|
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")
|
||||||
|
resolution = call.data.get("resolution", "interval")
|
||||||
highlight_best_price = call.data.get("highlight_best_price", True)
|
highlight_best_price = call.data.get("highlight_best_price", True)
|
||||||
highlight_peak_price = call.data.get("highlight_peak_price", False)
|
highlight_peak_price = call.data.get("highlight_peak_price", False)
|
||||||
|
|
||||||
|
|
@ -361,7 +363,7 @@ async def handle_apexcharts_yaml(call: ServiceCall) -> dict[str, Any]: # noqa:
|
||||||
f"service: 'get_chartdata', "
|
f"service: 'get_chartdata', "
|
||||||
f"return_response: true, "
|
f"return_response: true, "
|
||||||
f"service_data: {{ entry_id: '{entry_id}', {day_param}"
|
f"service_data: {{ entry_id: '{entry_id}', {day_param}"
|
||||||
f"period_filter: 'best_price', "
|
f"period_filter: 'best_price', resolution: '{resolution}', "
|
||||||
f"output_format: 'array_of_arrays', insert_nulls: 'segments', subunit_currency: {subunit_param} }} }}); "
|
f"output_format: 'array_of_arrays', insert_nulls: 'segments', subunit_currency: {subunit_param} }} }}); "
|
||||||
f"const originalData = response.response.data; "
|
f"const originalData = response.response.data; "
|
||||||
f"return originalData.map((point, i) => {{ "
|
f"return originalData.map((point, i) => {{ "
|
||||||
|
|
@ -400,7 +402,7 @@ async def handle_apexcharts_yaml(call: ServiceCall) -> dict[str, Any]: # noqa:
|
||||||
f"service: 'get_chartdata', "
|
f"service: 'get_chartdata', "
|
||||||
f"return_response: true, "
|
f"return_response: true, "
|
||||||
f"service_data: {{ entry_id: '{entry_id}', {day_param}"
|
f"service_data: {{ entry_id: '{entry_id}', {day_param}"
|
||||||
f"period_filter: 'peak_price', "
|
f"period_filter: 'peak_price', resolution: '{resolution}', "
|
||||||
f"output_format: 'array_of_arrays', insert_nulls: 'segments', subunit_currency: {subunit_param} }} }}); "
|
f"output_format: 'array_of_arrays', insert_nulls: 'segments', subunit_currency: {subunit_param} }} }}); "
|
||||||
f"const originalData = response.response.data; "
|
f"const originalData = response.response.data; "
|
||||||
f"return originalData.map((point, i) => {{ "
|
f"return originalData.map((point, i) => {{ "
|
||||||
|
|
@ -455,7 +457,7 @@ async def handle_apexcharts_yaml(call: ServiceCall) -> dict[str, Any]: # noqa:
|
||||||
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_param}{filter_param}, "
|
f"service_data: {{ entry_id: '{entry_id}', {day_param}{filter_param}, resolution: '{resolution}', "
|
||||||
f"output_format: 'array_of_arrays', insert_nulls: 'segments', subunit_currency: {subunit_param}, "
|
f"output_format: 'array_of_arrays', insert_nulls: 'segments', subunit_currency: {subunit_param}, "
|
||||||
f"connect_segments: true }} }}); "
|
f"connect_segments: true }} }}); "
|
||||||
f"return response.response.data;"
|
f"return response.response.data;"
|
||||||
|
|
@ -468,7 +470,7 @@ async def handle_apexcharts_yaml(call: ServiceCall) -> dict[str, Any]: # noqa:
|
||||||
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_param}{filter_param}, "
|
f"service_data: {{ entry_id: '{entry_id}', {day_param}{filter_param}, resolution: '{resolution}', "
|
||||||
f"output_format: 'array_of_arrays', insert_nulls: 'segments', subunit_currency: {subunit_param}, "
|
f"output_format: 'array_of_arrays', insert_nulls: 'segments', subunit_currency: {subunit_param}, "
|
||||||
f"connect_segments: true }} }}); "
|
f"connect_segments: true }} }}); "
|
||||||
f"return response.response.data;"
|
f"return response.response.data;"
|
||||||
|
|
|
||||||
|
|
@ -54,7 +54,12 @@ 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_to_hourly,
|
||||||
|
get_period_data,
|
||||||
|
normalize_level_filter,
|
||||||
|
normalize_rating_level_filter,
|
||||||
|
)
|
||||||
from .helpers import get_entry_and_data, has_tomorrow_data
|
from .helpers import get_entry_and_data, has_tomorrow_data
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
|
|
@ -529,6 +534,19 @@ async def handle_chartdata(call: ServiceCall) -> dict[str, Any]: # noqa: PLR091
|
||||||
day_offsets = [{"yesterday": -1, "today": 0, "tomorrow": 1}[day] for day in days]
|
day_offsets = [{"yesterday": -1, "today": 0, "tomorrow": 1}[day] for day in days]
|
||||||
all_prices = get_intervals_for_day_offsets(coordinator.data, day_offsets)
|
all_prices = get_intervals_for_day_offsets(coordinator.data, day_offsets)
|
||||||
|
|
||||||
|
# For hourly resolution, aggregate BEFORE processing
|
||||||
|
# This keeps the same data format (startsAt, total, level, rating_level)
|
||||||
|
# so all subsequent code (filters, insert_nulls, etc.) works unchanged
|
||||||
|
if resolution == "hourly":
|
||||||
|
all_prices = aggregate_to_hourly(
|
||||||
|
all_prices,
|
||||||
|
coordinator=coordinator,
|
||||||
|
threshold_low=threshold_low,
|
||||||
|
threshold_high=threshold_high,
|
||||||
|
)
|
||||||
|
# Also update all_timestamps for insert_nulls='all' mode
|
||||||
|
all_timestamps = sorted({interval["startsAt"] for interval in all_prices if interval.get("startsAt")})
|
||||||
|
|
||||||
# Helper to get day key from interval timestamp for average lookup
|
# Helper to get day key from interval timestamp for average lookup
|
||||||
def _get_day_key_for_interval(interval_start: Any) -> str | None:
|
def _get_day_key_for_interval(interval_start: Any) -> str | None:
|
||||||
"""Determine which day key (yesterday/today/tomorrow) an interval belongs to."""
|
"""Determine which day key (yesterday/today/tomorrow) an interval belongs to."""
|
||||||
|
|
@ -537,8 +555,9 @@ async def handle_chartdata(call: ServiceCall) -> dict[str, Any]: # noqa: PLR091
|
||||||
# Use pre-built mapping from actual interval data (TimeService-compatible)
|
# Use pre-built mapping from actual interval data (TimeService-compatible)
|
||||||
return date_to_day_key.get(interval_start.date())
|
return date_to_day_key.get(interval_start.date())
|
||||||
|
|
||||||
if resolution == "interval":
|
# Process price data - same logic handles both interval and hourly resolution
|
||||||
# Original 15-minute intervals
|
# (hourly data was already aggregated above, but has the same format)
|
||||||
|
if resolution in ("interval", "hourly"):
|
||||||
if insert_nulls == "all" and (level_filter or rating_level_filter):
|
if insert_nulls == "all" and (level_filter or rating_level_filter):
|
||||||
# Mode 'all': Insert NULL for all timestamps where filter doesn't match
|
# Mode 'all': Insert NULL for all timestamps where filter doesn't match
|
||||||
# Build a map of timestamp -> interval for quick lookup
|
# Build a map of timestamp -> interval for quick lookup
|
||||||
|
|
@ -865,32 +884,6 @@ async def handle_chartdata(call: ServiceCall) -> dict[str, Any]: # noqa: PLR091
|
||||||
|
|
||||||
chart_data.append(data_point)
|
chart_data.append(data_point)
|
||||||
|
|
||||||
elif resolution == "hourly":
|
|
||||||
# Hourly averages (4 intervals per hour: :00, :15, :30, :45)
|
|
||||||
# Process all intervals together for hourly aggregation
|
|
||||||
chart_data.extend(
|
|
||||||
aggregate_hourly_exact(
|
|
||||||
all_prices,
|
|
||||||
start_time_field,
|
|
||||||
price_field,
|
|
||||||
coordinator=coordinator,
|
|
||||||
use_subunit_currency=subunit_currency,
|
|
||||||
round_decimals=round_decimals,
|
|
||||||
include_level=include_level,
|
|
||||||
include_rating_level=include_rating_level,
|
|
||||||
level_filter=level_filter,
|
|
||||||
rating_level_filter=rating_level_filter,
|
|
||||||
include_average=include_average,
|
|
||||||
level_field=level_field,
|
|
||||||
rating_level_field=rating_level_field,
|
|
||||||
average_field=average_field,
|
|
||||||
day_average=None, # Not used when processing all days together
|
|
||||||
threshold_low=threshold_low,
|
|
||||||
period_timestamps=period_timestamps,
|
|
||||||
threshold_high=threshold_high,
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
# Remove trailing null values ONLY for insert_nulls='segments' mode.
|
# Remove trailing null values ONLY for insert_nulls='segments' mode.
|
||||||
# For 'all' mode, trailing nulls are intentional (show no-match until end of day).
|
# For 'all' mode, trailing nulls are intentional (show no-match until end of day).
|
||||||
# For 'segments' mode, trailing nulls cause ApexCharts header to show "N/A".
|
# For 'segments' mode, trailing nulls cause ApexCharts header to show "N/A".
|
||||||
|
|
|
||||||
|
|
@ -948,6 +948,14 @@
|
||||||
"highlight_best_price": {
|
"highlight_best_price": {
|
||||||
"name": "Bestpreis-Zeiträume hervorheben",
|
"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."
|
"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."
|
||||||
|
},
|
||||||
|
"highlight_peak_price": {
|
||||||
|
"name": "Spitzenpreis-Zeiträume hervorheben",
|
||||||
|
"description": "Füge eine halbtransparente rote Überlagerung hinzu, um die Spitzenpreis-Zeiträume im Diagramm hervorzuheben. Dies erleichtert die visuelle Identifizierung der Zeiten, in denen Energie am teuersten ist."
|
||||||
|
},
|
||||||
|
"resolution": {
|
||||||
|
"name": "Auflösung",
|
||||||
|
"description": "Zeitauflösung für die Diagrammdaten. 'interval' (Standard): Originale 15-Minuten-Intervalle (96 Punkte pro Tag). 'hourly': Aggregierte Stundenwerte mit einem rollierenden 60-Minuten-Fenster (24 Punkte pro Tag) für ein übersichtlicheres Diagramm."
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -948,6 +948,14 @@
|
||||||
"highlight_best_price": {
|
"highlight_best_price": {
|
||||||
"name": "Highlight Best Price Periods",
|
"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."
|
"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."
|
||||||
|
},
|
||||||
|
"highlight_peak_price": {
|
||||||
|
"name": "Highlight Peak Price Periods",
|
||||||
|
"description": "Add a semi-transparent red overlay to highlight the peak price periods on the chart. This makes it easy to visually identify times when energy is most expensive."
|
||||||
|
},
|
||||||
|
"resolution": {
|
||||||
|
"name": "Resolution",
|
||||||
|
"description": "Time resolution for the chart data. 'interval' (default): Original 15-minute intervals (96 points per day). 'hourly': Aggregated hourly values using a rolling 60-minute window (24 points per day) for a cleaner, less cluttered chart."
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -948,6 +948,14 @@
|
||||||
"highlight_best_price": {
|
"highlight_best_price": {
|
||||||
"name": "Fremhev beste prisperioder",
|
"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."
|
"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."
|
||||||
|
},
|
||||||
|
"highlight_peak_price": {
|
||||||
|
"name": "Fremhev høyeste prisperioder",
|
||||||
|
"description": "Legg til et halvgjennomsiktig rødt overlegg for å fremheve de høyeste prisperiodene i diagrammet. Dette gjør det enkelt å visuelt identifisere tidene når energi er dyrest."
|
||||||
|
},
|
||||||
|
"resolution": {
|
||||||
|
"name": "Oppløsning",
|
||||||
|
"description": "Tidsoppløsning for diagramdata. 'interval' (standard): Opprinnelige 15-minutters intervaller (96 punkter per dag). 'hourly': Aggregerte timeverdier med et rullende 60-minutters vindu (24 punkter per dag) for et ryddigere og mindre rotete diagram."
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -948,6 +948,14 @@
|
||||||
"highlight_best_price": {
|
"highlight_best_price": {
|
||||||
"name": "Beste prijsperiodes markeren",
|
"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."
|
"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."
|
||||||
|
},
|
||||||
|
"highlight_peak_price": {
|
||||||
|
"name": "Piekprijsperiodes markeren",
|
||||||
|
"description": "Voeg een halfdoorzichtige rode overlay toe om de piekprijsperiodes in de grafiek te markeren. Dit maakt het gemakkelijk om visueel de tijden te identificeren wanneer energie het duurst is."
|
||||||
|
},
|
||||||
|
"resolution": {
|
||||||
|
"name": "Resolutie",
|
||||||
|
"description": "Tijdresolutie voor de grafiekdata. 'interval' (standaard): Originele 15-minutenintervallen (96 punten per dag). 'hourly': Geaggregeerde uurwaarden met een rollend 60-minutenvenster (24 punten per dag) voor een overzichtelijkere grafiek."
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -948,6 +948,14 @@
|
||||||
"highlight_best_price": {
|
"highlight_best_price": {
|
||||||
"name": "Markera bästa prisperioder",
|
"name": "Markera bästa prisperioder",
|
||||||
"description": "Lägg till ett halvtransparent grönt överlag 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."
|
"description": "Lägg till ett halvtransparent grönt överlag 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."
|
||||||
|
},
|
||||||
|
"highlight_peak_price": {
|
||||||
|
"name": "Markera högsta prisperioder",
|
||||||
|
"description": "Lägg till ett halvtransparent rött överlag för att markera de högsta prisperioderna i diagrammet. Detta gör det enkelt att visuellt identifiera tiderna när energi är som dyrast."
|
||||||
|
},
|
||||||
|
"resolution": {
|
||||||
|
"name": "Upplösning",
|
||||||
|
"description": "Tidsupplösning för diagramdata. 'interval' (standard): Ursprungliga 15-minutersintervall (96 punkter per dag). 'hourly': Aggregerade timvärden med ett rullande 60-minutersfönster (24 punkter per dag) för ett renare och mindre rörigt diagram."
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue