mirror of
https://github.com/jpawlowski/hass.tibber_prices.git
synced 2026-05-28 18:43:40 +00:00
refactor(services): enhance validation for service parameters and error messages
Improved validation logic for service parameters in find_cheapest_hours, find_cheapest_schedule, and chartdata services. Added checks for unique task names, ensured that segment durations do not exceed total duration, and clarified error messages for better user understanding. Impact: Users will receive clearer error messages and improved validation when using the services, leading to a more robust experience.
This commit is contained in:
parent
9042ea6efb
commit
729bf307ca
9 changed files with 232 additions and 11 deletions
|
|
@ -202,7 +202,7 @@ def _build_found_response( # noqa: PLR0913
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
async def _handle_find_hours(
|
async def _handle_find_hours( # noqa: PLR0915
|
||||||
call: ServiceCall,
|
call: ServiceCall,
|
||||||
*,
|
*,
|
||||||
reverse: bool = False,
|
reverse: bool = False,
|
||||||
|
|
@ -258,6 +258,15 @@ async def _handle_find_hours(
|
||||||
min_segment_intervals = min_segment_minutes // INTERVAL_MINUTES
|
min_segment_intervals = min_segment_minutes // INTERVAL_MINUTES
|
||||||
|
|
||||||
# Validate parameter combinations
|
# Validate parameter combinations
|
||||||
|
if min_segment_minutes > total_minutes:
|
||||||
|
raise ServiceValidationError(
|
||||||
|
translation_domain=DOMAIN,
|
||||||
|
translation_key="min_segment_exceeds_duration",
|
||||||
|
translation_placeholders={
|
||||||
|
"min_segment_minutes": str(min_segment_minutes),
|
||||||
|
"duration_minutes": str(total_minutes),
|
||||||
|
},
|
||||||
|
)
|
||||||
validate_price_level_range(min_price_level, max_price_level)
|
validate_price_level_range(min_price_level, max_price_level)
|
||||||
validate_power_profile_length(power_profile, total_intervals)
|
validate_power_profile_length(power_profile, total_intervals)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -226,6 +226,16 @@ async def handle_find_cheapest_schedule(call: ServiceCall) -> ServiceResponse:
|
||||||
include_comparison_details: bool = call.data.get("include_comparison_details", False)
|
include_comparison_details: bool = call.data.get("include_comparison_details", False)
|
||||||
level_filter_active = min_price_level is not None or max_price_level is not None
|
level_filter_active = min_price_level is not None or max_price_level is not None
|
||||||
|
|
||||||
|
# Validate task names are unique (before any expensive operations)
|
||||||
|
task_names = [t["name"] for t in tasks_raw]
|
||||||
|
duplicate_names = sorted({n for n in task_names if task_names.count(n) > 1})
|
||||||
|
if duplicate_names:
|
||||||
|
raise ServiceValidationError(
|
||||||
|
translation_domain=DOMAIN,
|
||||||
|
translation_key="duplicate_task_names",
|
||||||
|
translation_placeholders={"names": ", ".join(duplicate_names)},
|
||||||
|
)
|
||||||
|
|
||||||
# Round gap up to nearest quarter interval
|
# Round gap up to nearest quarter interval
|
||||||
gap_intervals = math.ceil(gap_minutes / INTERVAL_MINUTES) if gap_minutes > 0 else 0
|
gap_intervals = math.ceil(gap_minutes / INTERVAL_MINUTES) if gap_minutes > 0 else 0
|
||||||
|
|
||||||
|
|
@ -269,6 +279,21 @@ async def handle_find_cheapest_schedule(call: ServiceCall) -> ServiceResponse:
|
||||||
# Validate parameter combinations
|
# Validate parameter combinations
|
||||||
validate_price_level_range(min_price_level, max_price_level)
|
validate_price_level_range(min_price_level, max_price_level)
|
||||||
|
|
||||||
|
# Validate that total task time + gaps fits within the search window
|
||||||
|
window_minutes = int((search_end - search_start).total_seconds() / 60)
|
||||||
|
total_task_minutes = sum(t["duration_minutes"] for t in tasks)
|
||||||
|
total_gap_minutes = gap_intervals * INTERVAL_MINUTES * max(0, len(tasks) - 1)
|
||||||
|
required_minutes = total_task_minutes + total_gap_minutes
|
||||||
|
if required_minutes > window_minutes:
|
||||||
|
raise ServiceValidationError(
|
||||||
|
translation_domain=DOMAIN,
|
||||||
|
translation_key="tasks_exceed_search_window",
|
||||||
|
translation_placeholders={
|
||||||
|
"total_minutes": str(required_minutes),
|
||||||
|
"window_minutes": str(window_minutes),
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
_LOGGER.info(
|
_LOGGER.info(
|
||||||
"%s called: %d tasks, gap=%dmin, range=%s to %s",
|
"%s called: %d tasks, gap=%dmin, range=%s to %s",
|
||||||
service_label,
|
service_label,
|
||||||
|
|
|
||||||
|
|
@ -409,6 +409,13 @@ async def handle_chartdata(call: ServiceCall) -> dict[str, Any]: # noqa: PLR091
|
||||||
translation_placeholders={"mode": insert_nulls},
|
translation_placeholders={"mode": insert_nulls},
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# insert_nulls="all" is only implemented for level/rating filters, not period_filter
|
||||||
|
if insert_nulls == "all" and period_filter and not (level_filter or rating_level_filter):
|
||||||
|
raise ServiceValidationError(
|
||||||
|
translation_domain=DOMAIN,
|
||||||
|
translation_key="insert_nulls_all_with_period_filter",
|
||||||
|
)
|
||||||
|
|
||||||
# connect_segments requires insert_nulls="segments" (with a filter)
|
# connect_segments requires insert_nulls="segments" (with a filter)
|
||||||
if connect_segments and insert_nulls != "segments":
|
if connect_segments and insert_nulls != "segments":
|
||||||
raise ServiceValidationError(
|
raise ServiceValidationError(
|
||||||
|
|
@ -416,6 +423,13 @@ async def handle_chartdata(call: ServiceCall) -> dict[str, Any]: # noqa: PLR091
|
||||||
translation_key="connect_segments_requires_segments_mode",
|
translation_key="connect_segments_requires_segments_mode",
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# connect_segments is not applicable to period_filter (periods are already contiguous)
|
||||||
|
if connect_segments and period_filter:
|
||||||
|
raise ServiceValidationError(
|
||||||
|
translation_domain=DOMAIN,
|
||||||
|
translation_key="connect_segments_with_period_filter",
|
||||||
|
)
|
||||||
|
|
||||||
# array_fields is only meaningful with array_of_arrays format
|
# array_fields is only meaningful with array_of_arrays format
|
||||||
if call.data.get("array_fields") and output_format != "array_of_arrays":
|
if call.data.get("array_fields") and output_format != "array_of_arrays":
|
||||||
raise ServiceValidationError(
|
raise ServiceValidationError(
|
||||||
|
|
@ -676,6 +690,58 @@ async def handle_chartdata(call: ServiceCall) -> dict[str, Any]: # noqa: PLR091
|
||||||
|
|
||||||
chart_data.append(data_point)
|
chart_data.append(data_point)
|
||||||
|
|
||||||
|
elif insert_nulls == "segments" and period_filter is not None and period_timestamps is not None:
|
||||||
|
# Mode 'segments' with period_filter: Insert NULL separators at period boundaries
|
||||||
|
# so that separate best/peak price periods appear as distinct areas in the chart,
|
||||||
|
# rather than one continuous area spanning the gaps between periods.
|
||||||
|
prev_start_time: Any | None = None
|
||||||
|
|
||||||
|
for interval in all_prices:
|
||||||
|
start_time = interval.get("startsAt")
|
||||||
|
price = interval.get("total")
|
||||||
|
|
||||||
|
if start_time is None or price is None:
|
||||||
|
continue
|
||||||
|
|
||||||
|
start_str = start_time.isoformat() if hasattr(start_time, "isoformat") else start_time
|
||||||
|
|
||||||
|
if start_str not in period_timestamps:
|
||||||
|
# Leaving a period — close the previous segment with a NULL
|
||||||
|
if prev_start_time is not None:
|
||||||
|
chart_data.append({start_time_field: start_str, price_field: None})
|
||||||
|
prev_start_time = None
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Still inside a period — check for a temporal gap (new disjoint period starting)
|
||||||
|
if prev_start_time is not None:
|
||||||
|
interval_duration = coordinator.time.get_interval_duration()
|
||||||
|
expected = prev_start_time + interval_duration
|
||||||
|
if start_time != expected:
|
||||||
|
prev_str = (
|
||||||
|
prev_start_time.isoformat() if hasattr(prev_start_time, "isoformat") else prev_start_time
|
||||||
|
)
|
||||||
|
chart_data.append({start_time_field: prev_str, price_field: None})
|
||||||
|
|
||||||
|
converted_price = round(price * 100, 2) if subunit_currency else round(price, 4)
|
||||||
|
if round_decimals is not None:
|
||||||
|
converted_price = round(converted_price, round_decimals)
|
||||||
|
|
||||||
|
data_point: dict = {
|
||||||
|
start_time_field: start_str,
|
||||||
|
price_field: converted_price,
|
||||||
|
}
|
||||||
|
if include_level and "level" in interval:
|
||||||
|
data_point[level_field] = interval["level"]
|
||||||
|
if include_rating_level and "rating_level" in interval:
|
||||||
|
data_point[rating_level_field] = interval["rating_level"]
|
||||||
|
day_key = _get_day_key_for_interval(start_time)
|
||||||
|
if include_average and day_key and day_key in day_averages:
|
||||||
|
data_point[average_field] = day_averages[day_key]
|
||||||
|
_add_energy_tax_fields(data_point, interval, converted_price)
|
||||||
|
|
||||||
|
chart_data.append(data_point)
|
||||||
|
prev_start_time = start_time
|
||||||
|
|
||||||
elif insert_nulls == "segments" and (level_filter or rating_level_filter):
|
elif insert_nulls == "segments" and (level_filter or rating_level_filter):
|
||||||
# Mode 'segments': Add NULL points at segment boundaries for clean gaps
|
# Mode 'segments': Add NULL points at segment boundaries for clean gaps
|
||||||
# Process ALL intervals as one continuous list - no special midnight handling needed
|
# Process ALL intervals as one continuous list - no special midnight handling needed
|
||||||
|
|
|
||||||
|
|
@ -95,6 +95,18 @@ def validate_search_params(call_data: dict[str, Any]) -> None:
|
||||||
translation_placeholders={"params": ", ".join(sorted(conflicting))},
|
translation_placeholders={"params": ", ".join(sorted(conflicting))},
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# search_start and search_start_time are mutually exclusive start-time specifications
|
||||||
|
if "search_start" in call_data and "search_start_time" in call_data:
|
||||||
|
raise ServiceValidationError(
|
||||||
|
translation_domain=DOMAIN,
|
||||||
|
translation_key="start_time_conflict",
|
||||||
|
)
|
||||||
|
if "search_end" in call_data and "search_end_time" in call_data:
|
||||||
|
raise ServiceValidationError(
|
||||||
|
translation_domain=DOMAIN,
|
||||||
|
translation_key="end_time_conflict",
|
||||||
|
)
|
||||||
|
|
||||||
# day_offset without matching time parameter is meaningless
|
# day_offset without matching time parameter is meaningless
|
||||||
# Schema defaults provide 0, but user explicitly setting non-zero without time is an error.
|
# Schema defaults provide 0, but user explicitly setting non-zero without time is an error.
|
||||||
# We detect explicit usage by checking for non-default values when time is absent.
|
# We detect explicit usage by checking for non-default values when time is absent.
|
||||||
|
|
@ -488,6 +500,10 @@ def resolve_search_range(
|
||||||
raise ServiceValidationError(
|
raise ServiceValidationError(
|
||||||
translation_domain=DOMAIN,
|
translation_domain=DOMAIN,
|
||||||
translation_key="end_before_start",
|
translation_key="end_before_start",
|
||||||
|
translation_placeholders={
|
||||||
|
"search_start": search_start.strftime("%Y-%m-%d %H:%M %z"),
|
||||||
|
"search_end": search_end.strftime("%Y-%m-%d %H:%M %z"),
|
||||||
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
return search_start, search_end
|
return search_start, search_end
|
||||||
|
|
|
||||||
|
|
@ -1175,7 +1175,7 @@
|
||||||
"message": "Zeitzone des Zuhauses konnte nicht ermittelt werden. Bitte überprüfe die Konfiguration in deinem Tibber-Konto."
|
"message": "Zeitzone des Zuhauses konnte nicht ermittelt werden. Bitte überprüfe die Konfiguration in deinem Tibber-Konto."
|
||||||
},
|
},
|
||||||
"end_before_start": {
|
"end_before_start": {
|
||||||
"message": "Endzeit muss nach der Startzeit liegen."
|
"message": "Der Endzeitpunkt ({search_end}) muss nach dem Startzeitpunkt ({search_start}) liegen. Überprüfe die Zeit-Parameter und eventuelle Day-Offsets."
|
||||||
},
|
},
|
||||||
"price_fetch_failed": {
|
"price_fetch_failed": {
|
||||||
"message": "Preisdaten konnten nicht von der Tibber-API abgerufen werden. Bitte versuche es später erneut."
|
"message": "Preisdaten konnten nicht von der Tibber-API abgerufen werden. Bitte versuche es später erneut."
|
||||||
|
|
@ -1208,7 +1208,28 @@
|
||||||
"message": "array_fields kann nur mit output_format: array_of_arrays verwendet werden. Ändere das Ausgabeformat oder entferne array_fields."
|
"message": "array_fields kann nur mit output_format: array_of_arrays verwendet werden. Ändere das Ausgabeformat oder entferne array_fields."
|
||||||
},
|
},
|
||||||
"invalid_array_fields": {
|
"invalid_array_fields": {
|
||||||
"message": "Ungültige array_fields-Vorlage. Verwende Feldnamen in geschweiften Klammern, z.B. '{start_time}, {price_per_kwh}, {level}'."
|
"message": "Der Wert '{template}' für array_fields ist ungültig. Feldnamen müssen in geschweifte Klammern eingeschlossen sein, z.B. '{start_time}, {price_per_kwh}, {level}'."
|
||||||
|
},
|
||||||
|
"min_segment_exceeds_duration": {
|
||||||
|
"message": "min_segment_duration ({min_segment_minutes} Min.) darf die Gesamtdauer ({duration_minutes} Min.) nicht überschreiten. Reduziere min_segment_duration oder erhöhe duration."
|
||||||
|
},
|
||||||
|
"start_time_conflict": {
|
||||||
|
"message": "search_start und search_start_time definieren beide den Startzeitpunkt — verwende nur einen. Nutze search_start für ein genaues Datum/Uhrzeit oder search_start_time für eine Tageszeit."
|
||||||
|
},
|
||||||
|
"end_time_conflict": {
|
||||||
|
"message": "search_end und search_end_time definieren beide den Endzeitpunkt — verwende nur einen. Nutze search_end für ein genaues Datum/Uhrzeit oder search_end_time für eine Tageszeit."
|
||||||
|
},
|
||||||
|
"insert_nulls_all_with_period_filter": {
|
||||||
|
"message": "insert_nulls: all wird mit period_filter nicht unterstützt. Verwende stattdessen insert_nulls: segments — das fügt Lücken zwischen einzelnen Perioden im Diagramm ein."
|
||||||
|
},
|
||||||
|
"connect_segments_with_period_filter": {
|
||||||
|
"message": "connect_segments kann nicht zusammen mit period_filter verwendet werden. Perioden sind bereits zusammenhängend — connect_segments wirkt nur bei level_filter oder rating_level_filter."
|
||||||
|
},
|
||||||
|
"duplicate_task_names": {
|
||||||
|
"message": "Aufgabennamen müssen eindeutig sein. Doppelt vergeben: {names}. Vergib unterschiedliche Namen, damit die Ergebnisse den richtigen Aufgaben zugeordnet werden können."
|
||||||
|
},
|
||||||
|
"tasks_exceed_search_window": {
|
||||||
|
"message": "Die Gesamtdauer aller Aufgaben inklusive Pausen ({total_minutes} Min.) überschreitet das Suchfenster ({window_minutes} Min.). Reduziere die Aufgabendauern, verringere gap_minutes oder erweitere den Suchzeitraum."
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"services": {
|
"services": {
|
||||||
|
|
|
||||||
|
|
@ -1175,7 +1175,7 @@
|
||||||
"message": "Could not determine the home timezone. Please verify the home configuration in your Tibber account."
|
"message": "Could not determine the home timezone. Please verify the home configuration in your Tibber account."
|
||||||
},
|
},
|
||||||
"end_before_start": {
|
"end_before_start": {
|
||||||
"message": "End time must be after start time."
|
"message": "End time ({search_end}) must be after start time ({search_start}). Check your time parameters and any day offsets."
|
||||||
},
|
},
|
||||||
"price_fetch_failed": {
|
"price_fetch_failed": {
|
||||||
"message": "Failed to fetch price data from the Tibber API. Please try again later."
|
"message": "Failed to fetch price data from the Tibber API. Please try again later."
|
||||||
|
|
@ -1208,7 +1208,28 @@
|
||||||
"message": "array_fields can only be used with output_format: array_of_arrays. Change the output format or remove array_fields."
|
"message": "array_fields can only be used with output_format: array_of_arrays. Change the output format or remove array_fields."
|
||||||
},
|
},
|
||||||
"invalid_array_fields": {
|
"invalid_array_fields": {
|
||||||
"message": "Invalid array_fields template. Use field names in curly braces, e.g. '{start_time}, {price_per_kwh}, {level}'."
|
"message": "The array_fields value '{template}' is invalid. Field names must be wrapped in curly braces, e.g. '{start_time}, {price_per_kwh}, {level}'."
|
||||||
|
},
|
||||||
|
"min_segment_exceeds_duration": {
|
||||||
|
"message": "min_segment_duration ({min_segment_minutes} min) cannot exceed the total duration ({duration_minutes} min). Reduce min_segment_duration or increase duration."
|
||||||
|
},
|
||||||
|
"start_time_conflict": {
|
||||||
|
"message": "search_start and search_start_time both specify the start time — use only one. Choose search_start for an exact datetime or search_start_time for a time of day."
|
||||||
|
},
|
||||||
|
"end_time_conflict": {
|
||||||
|
"message": "search_end and search_end_time both specify the end time — use only one. Choose search_end for an exact datetime or search_end_time for a time of day."
|
||||||
|
},
|
||||||
|
"insert_nulls_all_with_period_filter": {
|
||||||
|
"message": "insert_nulls: all is not supported with period_filter. Use insert_nulls: segments instead — this adds gaps between separate periods in the chart."
|
||||||
|
},
|
||||||
|
"connect_segments_with_period_filter": {
|
||||||
|
"message": "connect_segments cannot be used with period_filter. Periods are already contiguous — connect_segments only has effect with level_filter or rating_level_filter."
|
||||||
|
},
|
||||||
|
"duplicate_task_names": {
|
||||||
|
"message": "Task names must be unique. Duplicate: {names}. Give each task a different name so the results can be matched to the correct task."
|
||||||
|
},
|
||||||
|
"tasks_exceed_search_window": {
|
||||||
|
"message": "Total task time including gaps ({total_minutes} min) exceeds the search window ({window_minutes} min). Reduce task durations, lower gap_minutes, or extend the search range."
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"services": {
|
"services": {
|
||||||
|
|
|
||||||
|
|
@ -1175,7 +1175,7 @@
|
||||||
"message": "Kunne ikke bestemme hjemmets tidssone. Vennligst sjekk hjemmekonfigurasjonen i Tibber-kontoen din."
|
"message": "Kunne ikke bestemme hjemmets tidssone. Vennligst sjekk hjemmekonfigurasjonen i Tibber-kontoen din."
|
||||||
},
|
},
|
||||||
"end_before_start": {
|
"end_before_start": {
|
||||||
"message": "Sluttid må være etter starttid."
|
"message": "Sluttidspunktet ({search_end}) må være etter starttidspunktet ({search_start}). Sjekk tid-parameterne og eventuelle day-offsets."
|
||||||
},
|
},
|
||||||
"price_fetch_failed": {
|
"price_fetch_failed": {
|
||||||
"message": "Kunne ikke hente prisdata fra Tibber API. Vennligst prøv igjen senere."
|
"message": "Kunne ikke hente prisdata fra Tibber API. Vennligst prøv igjen senere."
|
||||||
|
|
@ -1208,7 +1208,28 @@
|
||||||
"message": "array_fields kan kun brukes med output_format: array_of_arrays. Endre utdataformatet eller fjern array_fields."
|
"message": "array_fields kan kun brukes med output_format: array_of_arrays. Endre utdataformatet eller fjern array_fields."
|
||||||
},
|
},
|
||||||
"invalid_array_fields": {
|
"invalid_array_fields": {
|
||||||
"message": "Ugyldig array_fields-mal. Bruk feltnavn i krøllparenteser, f.eks. '{start_time}, {price_per_kwh}, {level}'."
|
"message": "Verdien '{template}' for array_fields er ugyldig. Feltnavn må omsluttes av krøllparenteser, f.eks. '{start_time}, {price_per_kwh}, {level}'."
|
||||||
|
},
|
||||||
|
"min_segment_exceeds_duration": {
|
||||||
|
"message": "min_segment_duration ({min_segment_minutes} min) kan ikke overstige total varighet ({duration_minutes} min). Reduser min_segment_duration eller øk duration."
|
||||||
|
},
|
||||||
|
"start_time_conflict": {
|
||||||
|
"message": "search_start og search_start_time angir begge starttidspunktet — bruk bare én. Velg search_start for eksakt dato/klokkeslett eller search_start_time for et tidspunkt på dagen."
|
||||||
|
},
|
||||||
|
"end_time_conflict": {
|
||||||
|
"message": "search_end og search_end_time angir begge sluttidspunktet — bruk bare én. Velg search_end for eksakt dato/klokkeslett eller search_end_time for et tidspunkt på dagen."
|
||||||
|
},
|
||||||
|
"insert_nulls_all_with_period_filter": {
|
||||||
|
"message": "insert_nulls: all støttes ikke med period_filter. Bruk insert_nulls: segments i stedet — dette legger til tomrom mellom separate perioder i diagrammet."
|
||||||
|
},
|
||||||
|
"connect_segments_with_period_filter": {
|
||||||
|
"message": "connect_segments kan ikke brukes med period_filter. Perioder er allerede sammenhengende — connect_segments har bare effekt med level_filter eller rating_level_filter."
|
||||||
|
},
|
||||||
|
"duplicate_task_names": {
|
||||||
|
"message": "Oppgavenavn må være unike. Duplikat: {names}. Gi hver oppgave et unikt navn slik at resultatene kan matches til riktig oppgave."
|
||||||
|
},
|
||||||
|
"tasks_exceed_search_window": {
|
||||||
|
"message": "Total oppgavetid inkludert pauser ({total_minutes} min) overstiger søkevinduet ({window_minutes} min). Reduser oppgavevarighetene, senk gap_minutes, eller utvid søkeperioden."
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"services": {
|
"services": {
|
||||||
|
|
|
||||||
|
|
@ -1175,7 +1175,7 @@
|
||||||
"message": "Kon de tijdzone van het huis niet bepalen. Controleer de huisconfiguratie in je Tibber-account."
|
"message": "Kon de tijdzone van het huis niet bepalen. Controleer de huisconfiguratie in je Tibber-account."
|
||||||
},
|
},
|
||||||
"end_before_start": {
|
"end_before_start": {
|
||||||
"message": "Eindtijd moet na de starttijd liggen."
|
"message": "Het eindtijdstip ({search_end}) moet na het starttijdstip ({search_start}) liggen. Controleer je tijdparameters en eventuele day-offsets."
|
||||||
},
|
},
|
||||||
"price_fetch_failed": {
|
"price_fetch_failed": {
|
||||||
"message": "Kon prijsgegevens niet ophalen bij de Tibber API. Probeer het later opnieuw."
|
"message": "Kon prijsgegevens niet ophalen bij de Tibber API. Probeer het later opnieuw."
|
||||||
|
|
@ -1208,7 +1208,28 @@
|
||||||
"message": "array_fields kan alleen gebruikt worden met output_format: array_of_arrays. Wijzig het uitvoerformaat of verwijder array_fields."
|
"message": "array_fields kan alleen gebruikt worden met output_format: array_of_arrays. Wijzig het uitvoerformaat of verwijder array_fields."
|
||||||
},
|
},
|
||||||
"invalid_array_fields": {
|
"invalid_array_fields": {
|
||||||
"message": "Ongeldig array_fields-sjabloon. Gebruik veldnamen tussen accolades, bijv. '{start_time}, {price_per_kwh}, {level}'."
|
"message": "De waarde '{template}' voor array_fields is ongeldig. Veldnamen moeten tussen accolades staan, bijv. '{start_time}, {price_per_kwh}, {level}'."
|
||||||
|
},
|
||||||
|
"min_segment_exceeds_duration": {
|
||||||
|
"message": "min_segment_duration ({min_segment_minutes} min) mag de totale duur ({duration_minutes} min) niet overschrijden. Verklein min_segment_duration of vergroot duration."
|
||||||
|
},
|
||||||
|
"start_time_conflict": {
|
||||||
|
"message": "search_start en search_start_time specificeren beide het starttijdstip — gebruik slechts één van beide. Kies search_start voor een exacte datum/tijd of search_start_time voor een tijdstip op de dag."
|
||||||
|
},
|
||||||
|
"end_time_conflict": {
|
||||||
|
"message": "search_end en search_end_time specificeren beide het eindtijdstip — gebruik slechts één van beide. Kies search_end voor een exacte datum/tijd of search_end_time voor een tijdstip op de dag."
|
||||||
|
},
|
||||||
|
"insert_nulls_all_with_period_filter": {
|
||||||
|
"message": "insert_nulls: all wordt niet ondersteund met period_filter. Gebruik insert_nulls: segments — dit voegt lege ruimtes in tussen afzonderlijke perioden in de grafiek."
|
||||||
|
},
|
||||||
|
"connect_segments_with_period_filter": {
|
||||||
|
"message": "connect_segments kan niet worden gebruikt met period_filter. Perioden zijn al aaneengesloten — connect_segments heeft alleen effect met level_filter of rating_level_filter."
|
||||||
|
},
|
||||||
|
"duplicate_task_names": {
|
||||||
|
"message": "Taaknamen moeten uniek zijn. Duplicaat: {names}. Geef elke taak een andere naam zodat de resultaten aan de juiste taak gekoppeld kunnen worden."
|
||||||
|
},
|
||||||
|
"tasks_exceed_search_window": {
|
||||||
|
"message": "De totale taaktijd inclusief pauzes ({total_minutes} min) overschrijdt het zoekvenster ({window_minutes} min). Verklein de taakduur, verlaag gap_minutes of vergroot het zoekbereik."
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"services": {
|
"services": {
|
||||||
|
|
|
||||||
|
|
@ -1175,7 +1175,7 @@
|
||||||
"message": "Kunde inte fastställa hemmets tidszon. Kontrollera hemkonfigurationen i ditt Tibber-konto."
|
"message": "Kunde inte fastställa hemmets tidszon. Kontrollera hemkonfigurationen i ditt Tibber-konto."
|
||||||
},
|
},
|
||||||
"end_before_start": {
|
"end_before_start": {
|
||||||
"message": "Sluttid måste vara efter starttid."
|
"message": "Sluttidpunkten ({search_end}) måste vara efter starttidpunkten ({search_start}). Kontrollera tidsparametrarna och eventuella day-offsets."
|
||||||
},
|
},
|
||||||
"price_fetch_failed": {
|
"price_fetch_failed": {
|
||||||
"message": "Kunde inte hämta prisdata från Tibber API. Försök igen senare."
|
"message": "Kunde inte hämta prisdata från Tibber API. Försök igen senare."
|
||||||
|
|
@ -1208,7 +1208,28 @@
|
||||||
"message": "array_fields kan bara användas med output_format: array_of_arrays. Ändra utdataformatet eller ta bort array_fields."
|
"message": "array_fields kan bara användas med output_format: array_of_arrays. Ändra utdataformatet eller ta bort array_fields."
|
||||||
},
|
},
|
||||||
"invalid_array_fields": {
|
"invalid_array_fields": {
|
||||||
"message": "Ogiltig array_fields-mall. Använd fältnamn inom klammerparenteser, t.ex. '{start_time}, {price_per_kwh}, {level}'."
|
"message": "Värdet '{template}' för array_fields är ogiltigt. Fältnamn måste omges av klammerparenteser, t.ex. '{start_time}, {price_per_kwh}, {level}'."
|
||||||
|
},
|
||||||
|
"min_segment_exceeds_duration": {
|
||||||
|
"message": "min_segment_duration ({min_segment_minutes} min) får inte överstiga den totala varaktigheten ({duration_minutes} min). Minska min_segment_duration eller öka duration."
|
||||||
|
},
|
||||||
|
"start_time_conflict": {
|
||||||
|
"message": "search_start och search_start_time anger båda starttidpunkten — använd bara en. Välj search_start för exakt datum/tid eller search_start_time för en tid på dagen."
|
||||||
|
},
|
||||||
|
"end_time_conflict": {
|
||||||
|
"message": "search_end och search_end_time anger båda sluttidpunkten — använd bara en. Välj search_end för exakt datum/tid eller search_end_time för en tid på dagen."
|
||||||
|
},
|
||||||
|
"insert_nulls_all_with_period_filter": {
|
||||||
|
"message": "insert_nulls: all stöds inte med period_filter. Använd insert_nulls: segments istället — det lägger till tomrum mellan separata perioder i diagrammet."
|
||||||
|
},
|
||||||
|
"connect_segments_with_period_filter": {
|
||||||
|
"message": "connect_segments kan inte användas med period_filter. Perioder är redan sammanhängande — connect_segments har bara effekt med level_filter eller rating_level_filter."
|
||||||
|
},
|
||||||
|
"duplicate_task_names": {
|
||||||
|
"message": "Uppgiftsnamn måste vara unika. Duplikat: {names}. Ge varje uppgift ett unikt namn så att resultaten kan matchas till rätt uppgift."
|
||||||
|
},
|
||||||
|
"tasks_exceed_search_window": {
|
||||||
|
"message": "Total uppgiftstid inklusive pauser ({total_minutes} min) överstiger sökfönstret ({window_minutes} min). Minska uppgiftslängderna, sänk gap_minutes eller utöka sökintervallet."
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"services": {
|
"services": {
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue