feat(i18n): localize time offset descriptions and config flow strings

Added complete localization support for time offset descriptions:
- Convert hardcoded English strings "(X days ago)" to translatable keys
- Add time_units translations (day/days, hour/hours, minute/minutes, ago, now)
- Support singular/plural forms in all 5 languages (de, en, nb, nl, sv)
- German: Proper Dativ case "Tagen" with preposition "vor"
- Compact format for mixed offsets: "7 Tagen - 02:30"

Config flow improvements:
- Replace hardcoded "Enter new API token" with translated "Add new Tibber account API token"
- Use get_translation() for account_choice dropdown labels
- Fix SelectOptionDict usage (no mixing with translation_key parameter)
- Convert days slider from float to int (prevents "2.0 Tage" display)
- DurationSelector: default {"hours": 0, "minutes": 0} to fix validation errors

Translation keys added:
- selector.account_choice.options.new_token
- time_units (day, days, hour, hours, minute, minutes, ago, now)
- config.step.time_offset_description guidance text

Impact: Config flow works fully translated in all 5 languages with proper grammar.
This commit is contained in:
Julian Pawlowski 2025-11-25 20:39:58 +00:00
parent bab72ac341
commit 2449c28a88
7 changed files with 1072 additions and 180 deletions

View file

@ -1,126 +1,306 @@
"""Subentry config flow for adding additional Tibber homes."""
"""Subentry config flow for creating time-travel views."""
from __future__ import annotations
from typing import Any
from custom_components.tibber_prices.config_flow_handlers.schemas import (
get_select_home_schema,
get_subentry_init_schema,
)
import voluptuous as vol
from custom_components.tibber_prices.const import (
CONF_EXTENDED_DESCRIPTIONS,
DEFAULT_EXTENDED_DESCRIPTIONS,
CONF_VIRTUAL_TIME_OFFSET_DAYS,
CONF_VIRTUAL_TIME_OFFSET_HOURS,
CONF_VIRTUAL_TIME_OFFSET_MINUTES,
DOMAIN,
)
from homeassistant.config_entries import ConfigSubentryFlow, SubentryFlowResult
from homeassistant.helpers.selector import SelectOptionDict
from homeassistant.helpers.selector import (
DurationSelector,
DurationSelectorConfig,
NumberSelector,
NumberSelectorConfig,
NumberSelectorMode,
SelectOptionDict,
SelectSelector,
SelectSelectorConfig,
SelectSelectorMode,
)
class TibberPricesSubentryFlowHandler(ConfigSubentryFlow):
"""Handle subentry flows for tibber_prices."""
"""Handle subentry flows for tibber_prices (time-travel views)."""
def __init__(self) -> None:
"""Initialize the subentry flow handler."""
super().__init__()
self._selected_parent_entry_id: str | None = None
async def async_step_user(self, user_input: dict[str, Any] | None = None) -> SubentryFlowResult:
"""User flow to add a new home."""
parent_entry = self._get_entry()
if not parent_entry or not hasattr(parent_entry, "runtime_data") or not parent_entry.runtime_data:
return self.async_abort(reason="no_parent_entry")
coordinator = parent_entry.runtime_data.coordinator
# Force refresh user data to get latest homes from Tibber API
await coordinator.refresh_user_data()
homes = coordinator.get_user_homes()
if not homes:
return self.async_abort(reason="no_available_homes")
"""Step 1: Select which config entry should get a time-travel subentry."""
errors: dict[str, str] = {}
if user_input is not None:
selected_home_id = user_input["home_id"]
selected_home = next((home for home in homes if home["id"] == selected_home_id), None)
self._selected_parent_entry_id = user_input["parent_entry_id"]
return await self.async_step_time_offset()
if not selected_home:
return self.async_abort(reason="home_not_found")
home_title = self._get_home_title(selected_home)
home_id = selected_home["id"]
return self.async_create_entry(
title=home_title,
data={
"home_id": home_id,
"home_data": selected_home,
},
description=f"Subentry for {home_title}",
description_placeholders={"home_id": home_id},
unique_id=home_id,
)
# Get existing home IDs by checking all entries (parent + subentries)
existing_home_ids = {
entry.data["home_id"]
# Get all main config entries (not subentries)
# Subentries have "_hist_" in their unique_id
main_entries = [
entry
for entry in self.hass.config_entries.async_entries(DOMAIN)
if entry.data.get("home_id")
}
if entry.unique_id and "_hist_" not in entry.unique_id
]
# Also include parent entry's home_id if it exists
if parent_entry.data.get("home_id"):
existing_home_ids.add(parent_entry.data["home_id"])
if not main_entries:
return self.async_abort(reason="no_main_entries")
available_homes = [home for home in homes if home["id"] not in existing_home_ids]
if not available_homes:
return self.async_abort(reason="no_available_homes")
home_options = [
# Build options for entry selection
entry_options = [
SelectOptionDict(
value=home["id"],
label=self._get_home_title(home),
value=entry.entry_id,
label=f"{entry.title} ({entry.data.get('user_login', 'N/A')})",
)
for home in available_homes
for entry in main_entries
]
return self.async_show_form(
step_id="user",
data_schema=get_select_home_schema(home_options),
data_schema=vol.Schema(
{
vol.Required("parent_entry_id"): SelectSelector(
SelectSelectorConfig(
options=entry_options,
mode=SelectSelectorMode.DROPDOWN,
)
),
}
),
description_placeholders={},
errors={},
errors=errors,
)
def _get_home_title(self, home: dict) -> str:
"""Generate a user-friendly title for a home."""
title = home.get("appNickname")
if title and title.strip():
return title.strip()
async def async_step_time_offset(self, user_input: dict[str, Any] | None = None) -> SubentryFlowResult:
"""Step 2: Configure time offset for the time-travel view."""
errors: dict[str, str] = {}
address = home.get("address", {})
if address:
parts = []
if address.get("address1"):
parts.append(address["address1"])
if address.get("city"):
parts.append(address["city"])
if parts:
return ", ".join(parts)
if user_input is not None:
# Extract values (convert days to int to avoid float from slider)
offset_days = int(user_input.get(CONF_VIRTUAL_TIME_OFFSET_DAYS, 0))
return home.get("id", "Unknown Home")
# DurationSelector returns dict with 'hours', 'minutes', and 'seconds' keys
# We normalize to minute precision (ignore seconds)
time_offset = user_input.get("time_offset", {})
offset_hours = -abs(int(time_offset.get("hours", 0))) # Always negative for historical data
offset_minutes = -abs(int(time_offset.get("minutes", 0))) # Always negative for historical data
# Note: Seconds are ignored - we only support minute-level precision
# Validate that at least one offset is negative (historical data only)
if offset_days >= 0 and offset_hours >= 0 and offset_minutes >= 0:
errors["base"] = "no_time_offset"
if not errors:
# Get parent entry
if not self._selected_parent_entry_id:
return self.async_abort(reason="parent_entry_not_found")
parent_entry = self.hass.config_entries.async_get_entry(self._selected_parent_entry_id)
if not parent_entry:
return self.async_abort(reason="parent_entry_not_found")
# Get home data from parent entry
home_id = parent_entry.data.get("home_id")
home_data = parent_entry.data.get("home_data", {})
user_login = parent_entry.data.get("user_login", "N/A")
# Build unique_id with time offset signature
offset_str = f"d{offset_days}h{offset_hours}m{offset_minutes}"
user_id = parent_entry.unique_id.split("_")[0] if parent_entry.unique_id else home_id
unique_id = f"{user_id}_{home_id}_hist_{offset_str}"
# Check if this exact time offset already exists
for entry in self.hass.config_entries.async_entries(DOMAIN):
if entry.unique_id == unique_id:
return self.async_abort(reason="already_configured")
# No duplicate found - create the entry
offset_desc = self._format_offset_description(offset_days, offset_hours, offset_minutes)
subentry_title = f"{parent_entry.title} ({offset_desc})"
return self.async_create_entry(
title=subentry_title,
data={
"home_id": home_id,
"home_data": home_data,
"user_login": user_login,
CONF_VIRTUAL_TIME_OFFSET_DAYS: offset_days,
CONF_VIRTUAL_TIME_OFFSET_HOURS: offset_hours,
CONF_VIRTUAL_TIME_OFFSET_MINUTES: offset_minutes,
},
description=f"Time-travel view: {offset_desc}",
description_placeholders={"offset": offset_desc},
unique_id=unique_id,
)
return self.async_show_form(
step_id="time_offset",
data_schema=vol.Schema(
{
vol.Required(CONF_VIRTUAL_TIME_OFFSET_DAYS, default=0): NumberSelector(
NumberSelectorConfig(
mode=NumberSelectorMode.SLIDER,
min=-374,
max=0,
step=1,
)
),
vol.Optional("time_offset", default={"hours": 0, "minutes": 0}): DurationSelector(
DurationSelectorConfig(
allow_negative=False, # We handle sign automatically
enable_day=False, # Days are handled by the slider above
)
),
}
),
description_placeholders={},
errors=errors,
)
def _format_offset_description(self, days: int, hours: int, minutes: int) -> str:
"""
Format time offset into human-readable description.
Examples:
-7, 0, 0 -> "7 days ago" (English) / "vor 7 Tagen" (German)
0, -2, 0 -> "2 hours ago" (English) / "vor 2 Stunden" (German)
-7, -2, -30 -> "7 days - 02:30" (compact format when time is added)
"""
# Get translations loaded by Home Assistant
standard_translations_key = f"{DOMAIN}_standard_translations_{self.hass.config.language}"
translations = self.hass.data.get(standard_translations_key, {})
time_units = translations.get("config_subentries", {}).get("home", {}).get("time_units", {})
# Fallback to English if translations not available
if not time_units:
time_units = {
"day": "{count} day",
"days": "{count} days",
"hour": "{count} hour",
"hours": "{count} hours",
"minute": "{count} minute",
"minutes": "{count} minutes",
"ago": "{parts} ago",
"now": "now",
}
# Check if we have hours or minutes (need compact format)
has_time = hours != 0 or minutes != 0
if days != 0 and has_time:
# Compact format: "7 days - 02:30"
count = abs(days)
unit_key = "days" if count != 1 else "day"
day_part = time_units[unit_key].format(count=count)
time_part = f"{abs(hours):02d}:{abs(minutes):02d}"
return f"{day_part} - {time_part}"
# Standard format: separate parts with spaces
parts = []
if days != 0:
count = abs(days)
unit_key = "days" if count != 1 else "day"
parts.append(time_units[unit_key].format(count=count))
if hours != 0:
count = abs(hours)
unit_key = "hours" if count != 1 else "hour"
parts.append(time_units[unit_key].format(count=count))
if minutes != 0:
count = abs(minutes)
unit_key = "minutes" if count != 1 else "minute"
parts.append(time_units[unit_key].format(count=count))
if not parts:
return time_units.get("now", "now")
# All offsets should be negative (historical data only)
# Join parts with space and apply "ago" template
return time_units["ago"].format(parts=" ".join(parts))
async def async_step_init(self, user_input: dict | None = None) -> SubentryFlowResult:
"""Manage the options for a subentry."""
"""Manage the options for an existing subentry (time-travel settings)."""
subentry = self._get_reconfigure_subentry()
errors: dict[str, str] = {}
if user_input is not None:
return self.async_update_and_abort(
self._get_entry(),
subentry,
data_updates=user_input,
)
# Extract values (convert days to int to avoid float from slider)
offset_days = int(user_input.get(CONF_VIRTUAL_TIME_OFFSET_DAYS, 0))
extended_descriptions = subentry.data.get(CONF_EXTENDED_DESCRIPTIONS, DEFAULT_EXTENDED_DESCRIPTIONS)
# DurationSelector returns dict with 'hours', 'minutes', and 'seconds' keys
# We normalize to minute precision (ignore seconds)
time_offset = user_input.get("time_offset", {})
offset_hours = -abs(int(time_offset.get("hours", 0))) # Always negative for historical data
offset_minutes = -abs(int(time_offset.get("minutes", 0))) # Always negative for historical data
# Note: Seconds are ignored - we only support minute-level precision
# Validate that at least one offset is negative (historical data only)
if offset_days >= 0 and offset_hours >= 0 and offset_minutes >= 0:
errors["base"] = "no_time_offset"
else:
# Get parent entry to extract home_id and user_id
parent_entry = self._get_entry()
home_id = parent_entry.data.get("home_id")
# Build new unique_id with updated offset signature
offset_str = f"d{offset_days}h{offset_hours}m{offset_minutes}"
user_id = parent_entry.unique_id.split("_")[0] if parent_entry.unique_id else home_id
new_unique_id = f"{user_id}_{home_id}_hist_{offset_str}"
# Generate new title with updated offset description
offset_desc = self._format_offset_description(offset_days, offset_hours, offset_minutes)
# Extract parent title (remove old offset description in parentheses)
parent_title = parent_entry.title.split(" (")[0] if " (" in parent_entry.title else parent_entry.title
new_title = f"{parent_title} ({offset_desc})"
return self.async_update_and_abort(
parent_entry,
subentry,
unique_id=new_unique_id,
title=new_title,
data_updates=user_input,
)
offset_days = subentry.data.get(CONF_VIRTUAL_TIME_OFFSET_DAYS, 0)
offset_hours = subentry.data.get(CONF_VIRTUAL_TIME_OFFSET_HOURS, 0)
offset_minutes = subentry.data.get(CONF_VIRTUAL_TIME_OFFSET_MINUTES, 0)
# Prepare time offset dict for DurationSelector (always positive, we negate on save)
time_offset_dict = {"hours": 0, "minutes": 0} # Default to zeros
if offset_hours != 0:
time_offset_dict["hours"] = abs(offset_hours)
if offset_minutes != 0:
time_offset_dict["minutes"] = abs(offset_minutes)
return self.async_show_form(
step_id="init",
data_schema=get_subentry_init_schema(extended_descriptions=extended_descriptions),
data_schema=vol.Schema(
{
vol.Required(CONF_VIRTUAL_TIME_OFFSET_DAYS, default=offset_days): NumberSelector(
NumberSelectorConfig(
mode=NumberSelectorMode.SLIDER,
min=-374,
max=0,
step=1,
)
),
vol.Optional("time_offset", default=time_offset_dict): DurationSelector(
DurationSelectorConfig(
allow_negative=False, # We handle sign automatically
enable_day=False, # Days are handled by the slider above
)
),
}
),
errors=errors,
)

View file

@ -2,8 +2,11 @@
from __future__ import annotations
from datetime import datetime
from typing import TYPE_CHECKING, Any
import voluptuous as vol
from custom_components.tibber_prices.config_flow_handlers.options_flow import (
TibberPricesOptionsFlowHandler,
)
@ -20,7 +23,7 @@ from custom_components.tibber_prices.config_flow_handlers.validators import (
TibberPricesInvalidAuthError,
validate_api_token,
)
from custom_components.tibber_prices.const import DOMAIN, LOGGER
from custom_components.tibber_prices.const import DOMAIN, LOGGER, get_translation
from homeassistant.config_entries import (
ConfigEntry,
ConfigFlow,
@ -29,13 +32,18 @@ from homeassistant.config_entries import (
)
from homeassistant.const import CONF_ACCESS_TOKEN
from homeassistant.core import callback
from homeassistant.helpers.selector import SelectOptionDict
from homeassistant.helpers.selector import (
SelectOptionDict,
SelectSelector,
SelectSelectorConfig,
SelectSelectorMode,
)
if TYPE_CHECKING:
from homeassistant.config_entries import ConfigSubentryFlow
class TibberPricesFlowHandler(ConfigFlow, domain=DOMAIN):
class TibberPricesConfigFlowHandler(ConfigFlow, domain=DOMAIN):
"""Config flow for tibber_prices."""
VERSION = 1
@ -132,7 +140,110 @@ class TibberPricesFlowHandler(ConfigFlow, domain=DOMAIN):
self,
user_input: dict | None = None,
) -> ConfigFlowResult:
"""Handle a flow initialized by the user. Only ask for access token."""
"""Handle a flow initialized by the user. Choose account or enter new token."""
# Get existing accounts
existing_entries = self.hass.config_entries.async_entries(DOMAIN)
# If there are existing accounts, offer choice
if existing_entries and user_input is None:
return await self.async_step_account_choice()
# Otherwise, go directly to token input
return await self.async_step_new_token(user_input)
async def async_step_account_choice(
self,
user_input: dict | None = None,
) -> ConfigFlowResult:
"""Let user choose between existing account or new token."""
if user_input is not None:
choice = user_input["account_choice"]
if choice == "new_token":
return await self.async_step_new_token()
# User selected an existing account - copy its token
selected_entry_id = choice
selected_entry = next(
(
entry
for entry in self.hass.config_entries.async_entries(DOMAIN)
if entry.entry_id == selected_entry_id
),
None,
)
if not selected_entry:
return self.async_abort(reason="unknown")
# Copy token from selected entry and proceed
access_token = selected_entry.data.get(CONF_ACCESS_TOKEN)
if not access_token:
return self.async_abort(reason="unknown")
return await self.async_step_new_token({CONF_ACCESS_TOKEN: access_token})
# Build options: unique user accounts (grouped by user_id) + "New Token" option
existing_entries = self.hass.config_entries.async_entries(DOMAIN)
# Group entries by user_id to show unique accounts
# Minimum parts in unique_id format: user_id_home_id
min_unique_id_parts = 2
seen_users = {}
for entry in existing_entries:
# Extract user_id from unique_id (format: user_id_home_id or user_id_home_id_sub/hist_...)
unique_id = entry.unique_id
if unique_id:
# Split by underscore and take first part as user_id
parts = unique_id.split("_")
if len(parts) >= min_unique_id_parts:
user_id = parts[0]
if user_id not in seen_users:
seen_users[user_id] = entry
# Build dropdown options from unique user accounts
account_options = [
SelectOptionDict(
value=entry.entry_id,
label=f"{entry.title} ({entry.data.get('user_login', 'N/A')})",
)
for entry in seen_users.values()
]
# Add "new_token" option with translated label
new_token_label = (
get_translation(
["selector", "account_choice", "options", "new_token"],
self.hass.config.language,
)
or "Add new Tibber account API token"
)
account_options.append(
SelectOptionDict(
value="new_token",
label=new_token_label,
)
)
return self.async_show_form(
step_id="account_choice",
data_schema=vol.Schema(
{
vol.Required("account_choice"): SelectSelector(
SelectSelectorConfig(
options=account_options,
mode=SelectSelectorMode.DROPDOWN,
)
),
}
),
)
async def async_step_new_token(
self,
user_input: dict | None = None,
) -> ConfigFlowResult:
"""Handle token input (new or copied from existing account)."""
_errors = {}
if user_input is not None:
try:
@ -159,9 +270,6 @@ class TibberPricesFlowHandler(ConfigFlow, domain=DOMAIN):
LOGGER.debug("Viewer data received: %s", viewer)
await self.async_set_unique_id(unique_id=str(user_id))
self._abort_if_unique_id_configured()
# Store viewer data in the flow for use in the next step
self._viewer = viewer
self._access_token = user_input[CONF_ACCESS_TOKEN]
@ -173,25 +281,94 @@ class TibberPricesFlowHandler(ConfigFlow, domain=DOMAIN):
return await self.async_step_select_home()
return self.async_show_form(
step_id="user",
step_id="new_token",
data_schema=get_user_schema((user_input or {}).get(CONF_ACCESS_TOKEN)),
errors=_errors,
)
async def async_step_select_home(self, user_input: dict | None = None) -> ConfigFlowResult:
async def async_step_select_home(self, user_input: dict | None = None) -> ConfigFlowResult: # noqa: PLR0911
"""Handle home selection during initial setup."""
homes = self._viewer.get("homes", []) if self._viewer else []
if not homes:
return self.async_abort(reason="unknown")
# Filter out already configured homes
configured_home_ids = {
entry.data.get("home_id")
for entry in self.hass.config_entries.async_entries(DOMAIN)
if entry.data.get("home_id")
}
available_homes = [home for home in homes if home["id"] not in configured_home_ids]
# If no homes available, abort
if not available_homes:
return self.async_abort(reason="already_configured")
if user_input is not None:
selected_home_id = user_input["home_id"]
selected_home = next((home for home in homes if home["id"] == selected_home_id), None)
selected_home = next((home for home in available_homes if home["id"] == selected_home_id), None)
if not selected_home:
return self.async_abort(reason="unknown")
# Validate that home has an active or future subscription
subscription_status = self._get_subscription_status(selected_home)
if subscription_status == "none":
return self.async_show_form(
step_id="select_home",
data_schema=get_select_home_schema(
[
SelectOptionDict(
value=home["id"],
label=self._get_home_title_with_status(home),
)
for home in available_homes
]
),
errors={"home_id": "no_active_subscription"},
)
if subscription_status == "expired":
return self.async_show_form(
step_id="select_home",
data_schema=get_select_home_schema(
[
SelectOptionDict(
value=home["id"],
label=self._get_home_title_with_status(home),
)
for home in available_homes
]
),
errors={"home_id": "subscription_expired"},
)
# Set unique_id to user_id + home_id combination
# This allows multiple homes per user account (single-home architecture)
unique_id = f"{self._user_id}_{selected_home_id}"
await self.async_set_unique_id(unique_id)
self._abort_if_unique_id_configured()
# Note: This check is now redundant since we filter available_homes upfront,
# but kept as defensive programming in case of race conditions
for entry in self.hass.config_entries.async_entries(DOMAIN):
if entry.data.get("home_id") == selected_home_id:
return self.async_show_form(
step_id="select_home",
data_schema=get_select_home_schema(
[
SelectOptionDict(
value=home["id"],
label=self._get_home_title(home),
)
for home in available_homes
]
),
errors={"home_id": "home_already_configured"},
)
data = {
CONF_ACCESS_TOKEN: self._access_token or "",
"home_id": selected_home_id,
@ -200,8 +377,11 @@ class TibberPricesFlowHandler(ConfigFlow, domain=DOMAIN):
"user_login": self._user_login or "N/A",
}
# Generate entry title from home address (not appNickname)
entry_title = self._get_entry_title(selected_home)
return self.async_create_entry(
title=self._user_name or "Unknown User",
title=entry_title,
data=data,
description=f"{self._user_login} ({self._user_id})",
)
@ -209,9 +389,9 @@ class TibberPricesFlowHandler(ConfigFlow, domain=DOMAIN):
home_options = [
SelectOptionDict(
value=home["id"],
label=self._get_home_title(home),
label=self._get_home_title_with_status(home),
)
for home in homes
for home in available_homes
]
return self.async_show_form(
@ -234,9 +414,138 @@ class TibberPricesFlowHandler(ConfigFlow, domain=DOMAIN):
return home_ids
@staticmethod
def _get_subscription_status(home: dict) -> str:
"""
Check subscription status of home.
Returns:
- "active": Subscription is currently active
- "future": Subscription exists but starts in the future (validFrom > now)
- "expired": Subscription exists but has ended (validTo < now)
- "none": No subscription exists
"""
subscription = home.get("currentSubscription")
if subscription is None or subscription.get("status") is None:
return "none"
# Check validTo (contract end date)
valid_to = subscription.get("validTo")
if valid_to:
try:
valid_to_dt = datetime.fromisoformat(valid_to)
if valid_to_dt < datetime.now(valid_to_dt.tzinfo):
return "expired"
except (ValueError, AttributeError):
pass # If parsing fails, continue with other checks
# Check validFrom (contract start date)
valid_from = subscription.get("validFrom")
if valid_from:
try:
valid_from_dt = datetime.fromisoformat(valid_from)
if valid_from_dt > datetime.now(valid_from_dt.tzinfo):
return "future"
except (ValueError, AttributeError):
pass # If parsing fails, assume active
return "active"
def _get_home_title_with_status(self, home: dict) -> str:
"""Generate a user-friendly title for a home with subscription status."""
base_title = self._get_home_title(home)
status = self._get_subscription_status(home)
if status == "none":
return f"{base_title} ⚠️ (No active contract)"
if status == "expired":
return f"{base_title} ⚠️ (Contract expired)"
if status == "future":
return f"{base_title} ⚠️ (Contract starts soon)"
return base_title
@staticmethod
def _format_city_name(city: str) -> str:
"""
Format city name to title case.
Converts 'MÜNCHEN' to 'München', handles multi-word cities like
'BAD TÖLZ' -> 'Bad Tölz', and hyphenated cities like
'GARMISCH-PARTENKIRCHEN' -> 'Garmisch-Partenkirchen'.
"""
if not city:
return city
# Split by space and hyphen while preserving delimiters
words = []
current_word = ""
for char in city:
if char in (" ", "-"):
if current_word:
words.append(current_word)
words.append(char) # Preserve delimiter
current_word = ""
else:
current_word += char
if current_word: # Add last word
words.append(current_word)
# Capitalize first letter of each word (not delimiters)
formatted_words = []
for word in words:
if word in (" ", "-"):
formatted_words.append(word)
else:
# Capitalize first letter, lowercase rest
formatted_words.append(word.capitalize())
return "".join(formatted_words)
@staticmethod
def _get_entry_title(home: dict) -> str:
"""
Generate entry title from address (for config entry title).
Uses 'address1, City' format, e.g. 'Pählstraße 6B, München'.
Does NOT use appNickname (that's for _get_home_title).
"""
address = home.get("address", {})
if not address:
# Fallback to home ID if no address
return home.get("id", "Unknown Home")
parts = []
# Always prefer address1
address1 = address.get("address1")
if address1 and address1.strip():
parts.append(address1.strip())
# Format city name (convert MÜNCHEN -> München)
city = address.get("city")
if city and city.strip():
formatted_city = TibberPricesConfigFlowHandler._format_city_name(city.strip())
parts.append(formatted_city)
if parts:
return ", ".join(parts)
# Final fallback
return home.get("id", "Unknown Home")
@staticmethod
def _get_home_title(home: dict) -> str:
"""Generate a user-friendly title for a home."""
"""
Generate a user-friendly title for a home (for dropdown display).
Prefers appNickname, falls back to address.
"""
title = home.get("appNickname")
if title and title.strip():
return title.strip()
@ -247,7 +556,10 @@ class TibberPricesFlowHandler(ConfigFlow, domain=DOMAIN):
if address.get("address1"):
parts.append(address["address1"])
if address.get("city"):
parts.append(address["city"])
# Format city for display too
city = address["city"]
formatted_city = TibberPricesConfigFlowHandler._format_city_name(city)
parts.append(formatted_city)
if parts:
return ", ".join(parts)

View file

@ -1,6 +1,22 @@
{
"config": {
"step": {
"account_choice": {
"title": "Konto wählen",
"description": "Du kannst ein weiteres Zuhause aus einem bestehenden Tibber-Konto hinzufügen oder einen neuen API-Token für ein anderes Konto eingeben.",
"data": {
"account_choice": "Konto"
},
"submit": "Weiter →"
},
"new_token": {
"title": "API-Token eingeben",
"description": "Richte Tibber Preisinformationen & Bewertungen ein.\n\nUm einen API-Zugriffstoken zu generieren, besuche https://developer.tibber.com.",
"data": {
"access_token": "API-Zugriffstoken"
},
"submit": "Token validieren"
},
"user": {
"description": "Richte Tibber Preisinformationen & Bewertungen ein.\n\nUm einen API-Zugriffstoken zu generieren, besuche https://developer.tibber.com.",
"data": {
@ -40,15 +56,24 @@
"cannot_connect": "Verbindung fehlgeschlagen",
"invalid_access_token": "Ungültiges Zugriffstoken",
"missing_homes": "Der neue Zugriffstoken hat keinen Zugriff auf alle konfigurierten Zuhause. Bitte verwende einen Zugriffstoken, der Zugriff auf die gleichen Tibber-Zuhause hat.",
"invalid_yaml_syntax": "Ungültige YAML-Syntax. Bitte prüfe Einrückung, Doppelpunkte und Sonderzeichen.",
"home_already_configured": "Dieses Zuhause ist bereits in einem anderen Eintrag konfiguriert. Jedes Zuhause kann nur einmal konfiguriert werden.",
"no_active_subscription": "Dieses Zuhause hat keinen aktiven Tibber-Vertrag. Nur Häuser mit aktivem Stromvertrag können zu Home Assistant hinzugefügt werden.",
"subscription_expired": "Der Tibber-Vertrag für dieses Zuhause ist abgelaufen. Nur Häuser mit aktivem oder zukünftigem Stromvertrag können zu Home Assistant hinzugefügt werden.",
"future_subscription_warning": "Hinweis: Der Tibber-Vertrag für dieses Zuhause hat noch nicht begonnen. Die Funktionalität ist möglicherweise eingeschränkt, bis der Vertrag aktiv wird.",
"invalid_yaml_syntax": "Ungültige YAML-Syntax. Bitte überprüfe Einrückungen, Doppelpunkte und Sonderzeichen.",
"invalid_yaml_structure": "YAML muss ein Dictionary/Objekt sein (Schlüssel: Wert-Paare), keine Liste oder reiner Text.",
"service_call_failed": "Service-Aufruf-Validierung fehlgeschlagen: {error_detail}"
"service_call_failed": "Service-Aufruf-Validierung fehlgeschlagen: {error_detail}",
"missing_entry_id": "Eintrag-ID wird benötigt, wurde aber nicht bereitgestellt.",
"invalid_entry_id": "Ungültige Eintrag-ID oder Eintrag nicht gefunden.",
"missing_home_id": "Home-ID fehlt in der Konfiguration.",
"user_data_not_available": "Benutzerdaten nicht verfügbar. Bitte aktualisiere zuerst die Benutzerdaten.",
"price_fetch_failed": "Preisdaten konnten nicht abgerufen werden. Bitte prüfe die Logs für Details."
},
"abort": {
"already_configured": "Integration ist bereits konfiguriert",
"entry_not_found": "Tibber Konfigurationseintrag nicht gefunden.",
"setup_complete": "Einrichtung abgeschlossen! Du kannst zusätzliche Optionen für Tibber Preise in den Integrationsoptionen ändern, nachdem du diesen Dialog geschlossen hast.",
"reauth_successful": "Erneute Authentifizierung erfolgreich. Die Integration wurde mit dem neuen Zugriffstoken aktualisiert."
"already_configured": "Alle verfügbaren Tibber-Zuhause sind bereits konfiguriert. Jedes Zuhause kann nur einmal konfiguriert werden.",
"entry_not_found": "Tibber-Konfigurationseintrag nicht gefunden.",
"setup_complete": "Einrichtung abgeschlossen! Du kannst zusätzliche Optionen für Tibber Prices in den Integrationsoptionen nach Schließen dieses Dialogs ändern.",
"reauth_successful": "Neuauthentifizierung erfolgreich. Die Integration wurde mit dem neuen Zugriffstoken aktualisiert."
}
},
"common": {
@ -57,27 +82,59 @@
"config_subentries": {
"home": {
"initiate_flow": {
"user": "Tibber Zuhause hinzufügen"
"user": "Zeitreise-Ansicht erstellen"
},
"title": "Tibber Zuhause hinzufügen",
"title": "Zeitreise-Ansicht erstellen",
"step": {
"user": {
"title": "Tibber Zuhause hinzufügen",
"description": "Wähle ein Zuhause aus, das du zu deiner Tibber-Integration hinzufügen möchtest.",
"title": "Konfigurationseintrag auswählen",
"description": "Wähle den Konfigurationseintrag aus, für den du eine Zeitreise-Ansicht erstellen möchtest.\n\n**Zeitreise-Ansichten** ermöglichen es dir, historische Preisdaten so anzuzeigen, als wären sie die aktuellen Daten. Dies ist nützlich zum Testen von Automatisierungen oder zur Analyse vergangener Preismuster.",
"data": {
"home_id": "Zuhause"
"parent_entry_id": "Konfigurationseintrag"
}
},
"time_offset": {
"title": "Zeitversatz konfigurieren",
"description": "Konfiguriere, wie weit zurück in der Zeit diese Ansicht reisen soll.\n\n**Empfohlen:** Verwende **≥2 Tage** Versatz, um Konflikte mit \"yesterday\"-Entitäten zu vermeiden, die ebenfalls historische Daten bereitstellen.\n\n**Beispiele:**\n• **-7 Tage**: Zeigt Preise von vor 7 Tagen\n• **-2 Tage, 3 Stunden**: Zeigt Preise von vor 2 Tagen und 3 Stunden\n• **-14 Tage**: Zeigt Preise von vor 2 Wochen",
"data": {
"virtual_time_offset_days": "Tage zurück",
"time_offset": "Zusätzlicher Zeitversatz"
},
"data_description": {
"virtual_time_offset_days": "Wie viele Tage in die Vergangenheit reisen. Slider-Bereich: 0 bis 374 Tage (≈1 Jahr). Empfohlen: ≥2 Tage, um Konflikte mit \"yesterday\"-Entitäten zu vermeiden.",
"time_offset": "Optionale Feinabstimmung: Füge Stunden und/oder Minuten zum Tagesversatz hinzu. Die Zeit wird automatisch subtrahiert (weiter zurück reisen). Hinweis: Sekunden werden ignoriert - nur minutengenaue Präzision wird unterstützt."
}
},
"init": {
"title": "Zeitversatz neu konfigurieren",
"description": "Aktualisiere den Zeitversatz für diese Zeitreise-Ansicht.",
"data": {
"virtual_time_offset_days": "Tage zurück",
"time_offset": "Zusätzlicher Zeitversatz"
},
"data_description": {
"virtual_time_offset_days": "Wie viele Tage in die Vergangenheit reisen. Slider-Bereich: 0 bis 374 Tage (≈1 Jahr). Empfohlen: ≥2 Tage, um Konflikte mit \"yesterday\"-Entitäten zu vermeiden.",
"time_offset": "Optionale Feinabstimmung: Füge Stunden und/oder Minuten zum Tagesversatz hinzu. Die Zeit wird automatisch subtrahiert (weiter zurück reisen). Hinweis: Sekunden werden ignoriert - nur minutengenaue Präzision wird unterstützt."
}
}
},
"error": {
"api_error": "Fehler beim Abrufen der Zuhause von der Tibber API"
"no_time_offset": "Mindestens ein Zeitversatzwert muss negativ sein (nur historische Daten)."
},
"abort": {
"no_parent_entry": "Übergeordneter Eintrag nicht gefunden",
"no_access_token": "Kein Zugriffstoken verfügbar",
"home_not_found": "Ausgewähltes Zuhause nicht gefunden",
"api_error": "Fehler beim Abrufen der Zuhause von der Tibber API",
"no_available_homes": "Keine zusätzlichen Zuhause verfügbar. Alle Zuhause von deinem Tibber-Konto wurden bereits hinzugefügt."
"already_configured": "**Eine Zeitreise-Ansicht mit diesem exakten Zeitversatz existiert bereits.**\n\nBitte wähle einen anderen Versatz.",
"no_main_entries": "Keine Hauptkonfigurationseinträge gefunden. Füge zuerst ein Tibber-Zuhause hinzu.",
"parent_entry_not_found": "Ausgewählter Konfigurationseintrag nicht gefunden."
},
"time_units": {
"day": "{count} Tag",
"days": "{count} Tagen",
"hour": "{count} Stunde",
"hours": "{count} Stunden",
"minute": "{count} Minute",
"minutes": "{count} Minuten",
"ago": "vor {parts}",
"now": "jetzt"
}
}
},
@ -723,6 +780,24 @@
}
},
"services": {
"get_price": {
"name": "Preisdaten abrufen",
"description": "Preisdaten für einen bestimmten Zeitraum mit automatischem Routing abrufen. Entwicklungs- und Test-Service für die price_info_for_range API-Funktion. Verwendet automatisch PRICE_INFO, PRICE_INFO_RANGE oder beide basierend auf der Zeitraumgrenze.",
"fields": {
"entry_id": {
"name": "Eintrag-ID",
"description": "Die Konfigurations-Eintrag-ID für die Tibber-Integration."
},
"start_time": {
"name": "Startzeit",
"description": "Start des Zeitraums (inklusive, zeitzonenbewusst)."
},
"end_time": {
"name": "Endzeit",
"description": "Ende des Zeitraums (exklusive, zeitzonenbewusst)."
}
}
},
"get_apexcharts_yaml": {
"name": "ApexCharts-Karten-YAML abrufen",
"description": "Gibt einen fertigen YAML-Schnipsel für eine ApexCharts-Karte zurück, die Tibber-Preise für den ausgewählten Tag visualisiert. Verwende dies, um ganz einfach ein vorkonfiguriertes Diagramm zu deinem Dashboard hinzuzufügen. Das YAML verwendet den get_chartdata-Service für Daten.",
@ -847,6 +922,11 @@
}
},
"selector": {
"account_choice": {
"options": {
"new_token": "Neues Tibber-Konto per API-Token hinzufügen"
}
},
"day": {
"options": {
"yesterday": "Gestern",

View file

@ -1,6 +1,22 @@
{
"config": {
"step": {
"account_choice": {
"title": "Choose Account",
"description": "You can add another home of an existing Tibber account or enter a new API token for a different account.",
"data": {
"account_choice": "Account"
},
"submit": "Continue →"
},
"new_token": {
"title": "Enter API Token",
"description": "Set up Tibber Price Information & Ratings.\n\nTo generate an API access token, visit https://developer.tibber.com.",
"data": {
"access_token": "API access token"
},
"submit": "Validate Token"
},
"user": {
"description": "Set up Tibber Price Information & Ratings.\n\nTo generate an API access token, visit https://developer.tibber.com.",
"data": {
@ -40,12 +56,21 @@
"cannot_connect": "Failed to connect",
"invalid_access_token": "Invalid access token",
"missing_homes": "The new access token does not have access to all configured homes. Please use an access token that has access to the same Tibber homes.",
"home_already_configured": "This home is already configured in another entry. Each home can only be configured once.",
"no_active_subscription": "This home does not have an active Tibber contract. Only homes with active electricity contracts can be added to Home Assistant.",
"subscription_expired": "The Tibber contract for this home has expired. Only homes with active or future electricity contracts can be added to Home Assistant.",
"future_subscription_warning": "Note: This home's Tibber contract has not started yet. Functionality may be limited until the contract becomes active.",
"invalid_yaml_syntax": "Invalid YAML syntax. Please check indentation, colons, and special characters.",
"invalid_yaml_structure": "YAML must be a dictionary/object (key: value pairs), not a list or plain text.",
"service_call_failed": "Service call validation failed: {error_detail}"
"service_call_failed": "Service call validation failed: {error_detail}",
"missing_entry_id": "Entry ID is required but was not provided.",
"invalid_entry_id": "Invalid entry ID or entry not found.",
"missing_home_id": "Home ID is missing from the configuration entry.",
"user_data_not_available": "User data is not available. Please refresh user data first.",
"price_fetch_failed": "Failed to fetch price data. Please check logs for details."
},
"abort": {
"already_configured": "Integration is already configured",
"already_configured": "All available Tibber homes are already configured. Each home can only be configured once.",
"entry_not_found": "Tibber configuration entry not found.",
"setup_complete": "Setup complete! You can change additional options for Tibber Prices in the integration's options after closing this dialog.",
"reauth_successful": "Reauthentication successful. The integration has been updated with the new access token."
@ -57,27 +82,59 @@
"config_subentries": {
"home": {
"initiate_flow": {
"user": "Add Tibber Home"
"user": "Create Time-Travel View"
},
"title": "Add Tibber Home",
"title": "Create Time-Travel View",
"step": {
"user": {
"title": "Add Tibber Home",
"description": "Select a home to add to your Tibber integration.\n\n**Note:** After adding this home, you can add additional homes from the integration's context menu by selecting \"Add Tibber Home\".",
"title": "Select Configuration Entry",
"description": "Select the configuration entry for which you want to create a time-travel view.\n\n**Time-travel views** allow you to see historical price data as if it were the current time. This is useful for testing automations or analyzing past price patterns.",
"data": {
"home_id": "Home"
"parent_entry_id": "Configuration Entry"
}
},
"time_offset": {
"title": "Configure Time Offset",
"description": "Configure how far back in time this view should travel.\n\n**Recommended:** Use **≥2 days** offset to avoid conflicts with \"yesterday\" entities that also provide historical data.\n\n**Examples:**\n• **-7 days**: View prices from 7 days ago\n• **-2 days, 3 hours**: View prices from 2 days and 3 hours ago\n• **-14 days**: View prices from 2 weeks ago",
"data": {
"virtual_time_offset_days": "Days Back",
"time_offset": "Additional Time Offset"
},
"data_description": {
"virtual_time_offset_days": "How many days to travel back in time. Slider range: 0 to 374 days (≈1 year). Recommended: ≥2 days to avoid conflicts with \"yesterday\" entities.",
"time_offset": "Optional fine-tuning: Add hours and/or minutes to the day offset. The time will be automatically subtracted (travel back further). Note: Seconds are ignored - only minute-level precision is supported."
}
},
"init": {
"title": "Reconfigure Time Offset",
"description": "Update the time offset for this time-travel view.",
"data": {
"virtual_time_offset_days": "Days Back",
"time_offset": "Additional Time Offset"
},
"data_description": {
"virtual_time_offset_days": "How many days to travel back in time. Slider range: 0 to 374 days (≈1 year). Recommended: ≥2 days to avoid conflicts with \"yesterday\" entities.",
"time_offset": "Optional fine-tuning: Add hours and/or minutes to the day offset. The time will be automatically subtracted (travel back further). Note: Seconds are ignored - only minute-level precision is supported."
}
}
},
"error": {
"api_error": "Failed to fetch homes from Tibber API"
"no_time_offset": "At least one time offset value must be negative (historical data only)."
},
"abort": {
"no_parent_entry": "Parent entry not found",
"no_access_token": "No access token available",
"home_not_found": "Selected home not found",
"api_error": "Failed to fetch homes from Tibber API",
"no_available_homes": "No additional homes available to add. All homes from your Tibber account have already been added."
"already_configured": "**A time-travel view with this exact time offset already exists.**\n\nPlease choose a different offset.",
"no_main_entries": "No main configuration entries found. Please add a Tibber home first.",
"parent_entry_not_found": "Selected configuration entry not found."
},
"time_units": {
"day": "{count} day",
"days": "{count} days",
"hour": "{count} hour",
"hours": "{count} hours",
"minute": "{count} minute",
"minutes": "{count} minutes",
"ago": "{parts} ago",
"now": "now"
}
}
},
@ -719,6 +776,24 @@
}
},
"services": {
"get_price": {
"name": "Get Price Data",
"description": "Fetch price data for a specific time range with automatic routing. Development and testing service for the price_info_for_range API function. Automatically uses PRICE_INFO, PRICE_INFO_RANGE, or both based on the time range boundary.",
"fields": {
"entry_id": {
"name": "Entry ID",
"description": "The config entry ID for the Tibber integration."
},
"start_time": {
"name": "Start Time",
"description": "Start of the time range (inclusive, timezone-aware)."
},
"end_time": {
"name": "End Time",
"description": "End of the time range (exclusive, timezone-aware)."
}
}
},
"get_apexcharts_yaml": {
"name": "Get ApexCharts Card YAML",
"description": "Returns a ready-to-copy YAML snippet for an ApexCharts card visualizing Tibber Prices for the selected day. Use this to easily add a pre-configured chart to your dashboard. The YAML will use the get_chartdata service for data.",
@ -843,6 +918,11 @@
}
},
"selector": {
"account_choice": {
"options": {
"new_token": "Add new Tibber account API token"
}
},
"day": {
"options": {
"yesterday": "Yesterday",

View file

@ -1,6 +1,22 @@
{
"config": {
"step": {
"account_choice": {
"title": "Velg konto",
"description": "Du kan legge til et nytt hjem fra en eksisterende Tibber-konto eller skrive inn et nytt API-token for en annen konto.",
"data": {
"account_choice": "Konto"
},
"submit": "Fortsett →"
},
"new_token": {
"title": "Skriv inn API-token",
"description": "Sett opp Tibber Prisinformasjon & Vurderinger.\n\nFor å generere et API-tilgangstoken, besøk https://developer.tibber.com.",
"data": {
"access_token": "API-tilgangstoken"
},
"submit": "Valider token"
},
"user": {
"description": "Sett opp Tibber Prisinformasjon & Vurderinger.\n\nFor å generere et API-tilgangstoken, besøk https://developer.tibber.com.",
"data": {
@ -40,15 +56,24 @@
"cannot_connect": "Kunne ikke koble til",
"invalid_access_token": "Ugyldig tilgangstoken",
"missing_homes": "Det nye tilgangstokenet har ikke tilgang til alle konfigurerte hjem. Vennligst bruk et tilgangstoken som har tilgang til de samme Tibber-hjemmene.",
"home_already_configured": "Dette hjemmet er allerede konfigurert i en annen oppføring. Hvert hjem kan kun konfigureres én gang.",
"no_active_subscription": "Dette hjemmet har ikke en aktiv Tibber-kontrakt. Bare hjem med aktive strømkontrakter kan legges til Home Assistant.",
"subscription_expired": "Tibber-kontrakten for dette hjemmet har utløpt. Bare hjem med aktive eller fremtidige strømkontrakter kan legges til Home Assistant.",
"future_subscription_warning": "Merk: Tibber-kontrakten for dette hjemmet har ikke startet ennå. Funksjonaliteten kan være begrenset til kontrakten blir aktiv.",
"invalid_yaml_syntax": "Ugyldig YAML-syntaks. Vennligst sjekk innrykk, kolon og spesialtegn.",
"invalid_yaml_structure": "YAML må være en ordbok/objekt (nøkkel: verdi-par), ikke en liste eller ren tekst.",
"service_call_failed": "Service-kall validering feilet: {error_detail}"
"service_call_failed": "Service-kall validering feilet: {error_detail}",
"missing_entry_id": "Oppførings-ID er påkrevd, men ble ikke oppgitt.",
"invalid_entry_id": "Ugyldig oppførings-ID eller oppføring ikke funnet.",
"missing_home_id": "Hjem-ID mangler fra konfigurasjonsoppføringen.",
"user_data_not_available": "Brukerdata er ikke tilgjengelig. Vennligst oppdater brukerdata først.",
"price_fetch_failed": "Kunne ikke hente prisdata. Vennligst sjekk loggene for detaljer."
},
"abort": {
"already_configured": "Integrasjonen er allerede konfigurert",
"already_configured": "Alle tilgjengelige Tibber-hjem er allerede konfigurert. Hvert hjem kan kun konfigureres én gang.",
"entry_not_found": "Tibber-konfigurasjonsoppføring ikke funnet.",
"setup_complete": "Oppsett fullført! Du kan endre flere innstillinger for Tibber-priser i integrasjonens alternativer etter å ha lukket denne dialogen.",
"reauth_successful": "Autentisering vellykket. Integrasjonen er oppdatert med det nye tilgangstokenet."
"setup_complete": "Oppsett fullført! Du kan endre ytterligere alternativer for Tibber Prices i integrasjonens alternativer etter å ha lukket denne dialogen.",
"reauth_successful": "Ny autentisering vellykket. Integrasjonen har blitt oppdatert med det nye tilgangstokenet."
}
},
"common": {
@ -57,27 +82,59 @@
"config_subentries": {
"home": {
"initiate_flow": {
"user": "Legg til Tibber-hjem"
"user": "Opprett tidsreisevisning"
},
"title": "Legg til Tibber-hjem",
"title": "Opprett tidsreisevisning",
"step": {
"user": {
"title": "Legg til Tibber-hjem",
"description": "Velg et hjem å legge til i din Tibber-integrasjon.\n\n**Merk:** Etter å ha lagt til dette hjemmet, kan du legge til flere hjem fra integrasjonens kontekstmeny ved å velge \"Legg til Tibber-hjem\".",
"title": "Velg konfigurasjonsoppføring",
"description": "Velg konfigurasjonsoppføringen du vil opprette en tidsreisevisning for.\n\n**Tidsreisevisninger** lar deg se historiske prisdata som om det var nåværende tid. Dette er nyttig for å teste automatiseringer eller analysere tidligere prismønstre.",
"data": {
"home_id": "Hjem"
"parent_entry_id": "Konfigurasjonsoppføring"
}
},
"time_offset": {
"title": "Konfigurer tidsforskyvning",
"description": "Konfigurer hvor langt tilbake i tid denne visningen skal reise.\n\n**Anbefalt:** Bruk **≥2 dager** forskyvning for å unngå konflikter med \"yesterday\"-entiteter som også gir historiske data.\n\n**Eksempler:**\n• **-7 dager**: Vis priser fra 7 dager siden\n• **-2 dager, 3 timer**: Vis priser fra 2 dager og 3 timer siden\n• **-14 dager**: Vis priser fra 2 uker siden",
"data": {
"virtual_time_offset_days": "Dager tilbake",
"time_offset": "Ekstra tidsforskyvning"
},
"data_description": {
"virtual_time_offset_days": "Hvor mange dager å reise tilbake i tid. Glidebryter-område: 0 til 374 dager (≈1 år). Anbefalt: ≥2 dager for å unngå konflikter med \"yesterday\"-entiteter.",
"time_offset": "Valgfri finjustering: Legg til timer og/eller minutter til dagesforskyvningen. Tiden trekkes automatisk fra (reis lenger tilbake). Merk: Sekunder ignoreres - kun minuttbasert presisjon støttes."
}
},
"init": {
"title": "Konfigurer tidsforskyvning på nytt",
"description": "Oppdater tidsforskyvningen for denne tidsreisevisningen.",
"data": {
"virtual_time_offset_days": "Dager tilbake",
"time_offset": "Ekstra tidsforskyvning"
},
"data_description": {
"virtual_time_offset_days": "Hvor mange dager å reise tilbake i tid. Glidebryter-område: 0 til 374 dager (≈1 år). Anbefalt: ≥2 dager for å unngå konflikter med \"yesterday\"-entiteter.",
"time_offset": "Valgfri finjustering: Legg til timer og/eller minutter til dagesforskyvningen. Tiden trekkes automatisk fra (reis lenger tilbake). Merk: Sekunder ignoreres - kun minuttbasert presisjon støttes."
}
}
},
"error": {
"api_error": "Kunne ikke hente hjem fra Tibber API"
"no_time_offset": "Minst én tidsforskyvningsverdi må være negativ (kun historiske data)."
},
"abort": {
"no_parent_entry": "Overordnet oppføring ikke funnet",
"no_access_token": "Ingen tilgangstoken tilgjengelig",
"home_not_found": "Valgt hjem ikke funnet",
"api_error": "Kunne ikke hente hjem fra Tibber API",
"no_available_homes": "Ingen flere hjem tilgjengelig for å legge til. Alle hjem fra din Tibber-konto er allerede lagt til."
"already_configured": "**En tidsreisevisning med denne eksakte tidsforskyvningen eksisterer allerede.**\n\nVelg en annen forskyvning.",
"no_main_entries": "Ingen hovedkonfigurasjonsoppføringer funnet. Legg til et Tibber-hjem først.",
"parent_entry_not_found": "Valgt konfigurasjonsoppføring ikke funnet."
},
"time_units": {
"day": "{count} dag",
"days": "{count} dager",
"hour": "{count} time",
"hours": "{count} timer",
"minute": "{count} minutt",
"minutes": "{count} minutter",
"ago": "{parts} siden",
"now": "nå"
}
}
},
@ -719,6 +776,24 @@
}
},
"services": {
"get_price": {
"name": "Hent prisdata",
"description": "Hent prisdata for et spesifikt tidsrom med automatisk ruting. Utviklings- og testtjeneste for price_info_for_range API-funksjonen. Bruker automatisk PRICE_INFO, PRICE_INFO_RANGE eller begge basert på tidsromgrensen.",
"fields": {
"entry_id": {
"name": "Oppførings-ID",
"description": "Konfigurasjonsoppførings-IDen for Tibber-integrasjonen."
},
"start_time": {
"name": "Starttid",
"description": "Start av tidsrommet (inklusiv, tidssonetilpasset)."
},
"end_time": {
"name": "Sluttid",
"description": "Slutt av tidsrommet (eksklusiv, tidssonetilpasset)."
}
}
},
"get_apexcharts_yaml": {
"name": "Hent ApexCharts-kort YAML",
"description": "Returnerer en klar-til-kopier YAML-snippet for et ApexCharts-kort som visualiserer Tibber-priser for den valgte dagen. Bruk dette for å enkelt legge til et forhåndskonfigurert diagram til dashboardet ditt. YAML vil bruke get_chartdata-tjenesten for data.",
@ -843,6 +918,11 @@
}
},
"selector": {
"account_choice": {
"options": {
"new_token": "Legg til ny Tibber-konto med API-token"
}
},
"day": {
"options": {
"yesterday": "I går",

View file

@ -1,6 +1,22 @@
{
"config": {
"step": {
"account_choice": {
"title": "Kies account",
"description": "Je kunt een ander huis van een bestaande Tibber-account toevoegen of een nieuw API-token invoeren voor een ander account.",
"data": {
"account_choice": "Account"
},
"submit": "Doorgaan →"
},
"new_token": {
"title": "Voer API-token in",
"description": "Stel Tibber Prijsinformatie & Beoordelingen in.\n\nOm een API-toegangstoken te genereren, bezoek https://developer.tibber.com.",
"data": {
"access_token": "API-toegangstoken"
},
"submit": "Token valideren"
},
"user": {
"description": "Stel Tibber Prijsinformatie & Beoordelingen in.\n\nOm een API-toegangstoken te genereren, bezoek https://developer.tibber.com.",
"data": {
@ -40,15 +56,24 @@
"cannot_connect": "Verbinding mislukt",
"invalid_access_token": "Ongeldig toegangstoken",
"missing_homes": "Het nieuwe toegangstoken heeft geen toegang tot alle geconfigureerde huizen. Gebruik een toegangstoken dat toegang heeft tot dezelfde Tibber-huizen.",
"invalid_yaml_syntax": "Ongeldige YAML-syntaxis. Controleer inspringen, dubbele punten en speciale tekens.",
"home_already_configured": "Dit huis is al geconfigureerd in een ander item. Elk huis kan slechts één keer worden geconfigureerd.",
"no_active_subscription": "Dit huis heeft geen actief Tibber-contract. Alleen huizen met actieve elektriciteitscontracten kunnen worden toegevoegd aan Home Assistant.",
"subscription_expired": "Het Tibber-contract voor dit huis is verlopen. Alleen huizen met actieve of toekomstige elektriciteitscontracten kunnen worden toegevoegd aan Home Assistant.",
"future_subscription_warning": "Let op: Het Tibber-contract voor dit huis is nog niet begonnen. De functionaliteit kan beperkt zijn totdat het contract actief wordt.",
"invalid_yaml_syntax": "Ongeldige YAML-syntaxis. Controleer inspringingen, dubbele punten en speciale tekens.",
"invalid_yaml_structure": "YAML moet een woordenboek/object zijn (sleutel: waarde-paren), geen lijst of platte tekst.",
"service_call_failed": "Service-aanroep validatie mislukt: {error_detail}"
"service_call_failed": "Service-aanroep validatie mislukt: {error_detail}",
"missing_entry_id": "Entry ID is vereist maar niet opgegeven.",
"invalid_entry_id": "Ongeldige entry ID of entry niet gevonden.",
"missing_home_id": "Huis-ID ontbreekt in de configuratie.",
"user_data_not_available": "Gebruikersgegevens niet beschikbaar. Vernieuw eerst de gebruikersgegevens.",
"price_fetch_failed": "Prijsgegevens ophalen mislukt. Controleer de logs voor details."
},
"abort": {
"already_configured": "Integratie is al geconfigureerd",
"already_configured": "Alle beschikbare Tibber-huizen zijn al geconfigureerd. Elk huis kan slechts één keer worden geconfigureerd.",
"entry_not_found": "Tibber-configuratie-item niet gevonden.",
"setup_complete": "Installatie voltooid! Je kunt aanvullende opties voor Tibber-prijzen wijzigen in de integratie-opties na het sluiten van dit dialoogvenster.",
"reauth_successful": "Herauthenticatie succesvol. De integratie is bijgewerkt met het nieuwe toegangstoken."
"setup_complete": "Installatie voltooid! Je kunt extra opties voor Tibber Prices wijzigen in de integratie-opties na het sluiten van deze dialoog.",
"reauth_successful": "Herauthenticatie geslaagd. De integratie is bijgewerkt met het nieuwe toegangstoken."
}
},
"common": {
@ -57,27 +82,59 @@
"config_subentries": {
"home": {
"initiate_flow": {
"user": "Tibber-huis toevoegen"
"user": "Tijdreisweergave maken"
},
"title": "Tibber-huis toevoegen",
"title": "Tijdreisweergave maken",
"step": {
"user": {
"title": "Tibber-huis toevoegen",
"description": "Selecteer een huis om toe te voegen aan je Tibber-integratie.\n\n**Opmerking:** Na het toevoegen van dit huis kun je extra huizen toevoegen via het contextmenu van de integratie door \"Tibber-huis toevoegen\" te selecteren.",
"title": "Configuratie-item selecteren",
"description": "Selecteer het configuratie-item waarvoor je een tijdreisweergave wilt maken.\n\n**Tijdreisweergaves** stellen je in staat om historische prijsgegevens te zien alsof het de huidige tijd is. Dit is handig voor het testen van automatiseringen of het analyseren van eerdere prijspatronen.",
"data": {
"home_id": "Huis"
"parent_entry_id": "Configuratie-item"
}
},
"time_offset": {
"title": "Tijdverschuiving configureren",
"description": "Configureer hoe ver terug in de tijd deze weergave moet reizen.\n\n**Aanbevolen:** Gebruik **≥2 dagen** verschuiving om conflicten met \"yesterday\"-entiteiten te vermijden die ook historische gegevens bieden.\n\n**Voorbeelden:**\n• **-7 dagen**: Toon prijzen van 7 dagen geleden\n• **-2 dagen, 3 uur**: Toon prijzen van 2 dagen en 3 uur geleden\n• **-14 dagen**: Toon prijzen van 2 weken geleden",
"data": {
"virtual_time_offset_days": "Dagen terug",
"time_offset": "Extra tijdverschuiving"
},
"data_description": {
"virtual_time_offset_days": "Hoeveel dagen terug in de tijd reizen. Schuifregelaar bereik: 0 tot 374 dagen (≈1 jaar). Aanbevolen: ≥2 dagen om conflicten met \"yesterday\"-entiteiten te vermijden.",
"time_offset": "Optionele fijnafstemming: Voeg uren en/of minuten toe aan de dagverschuiving. De tijd wordt automatisch afgetrokken (verder terug reizen). Let op: Seconden worden genegeerd - alleen precisie op minuutniveau wordt ondersteund."
}
},
"init": {
"title": "Tijdverschuiving opnieuw configureren",
"description": "Werk de tijdverschuiving voor deze tijdreisweergave bij.",
"data": {
"virtual_time_offset_days": "Dagen terug",
"time_offset": "Extra tijdverschuiving"
},
"data_description": {
"virtual_time_offset_days": "Hoeveel dagen terug in de tijd reizen. Schuifregelaar bereik: 0 tot 374 dagen (≈1 jaar). Aanbevolen: ≥2 dagen om conflicten met \"yesterday\"-entiteiten te vermijden.",
"time_offset": "Optionele fijnafstemming: Voeg uren en/of minuten toe aan de dagverschuiving. De tijd wordt automatisch afgetrokken (verder terug reizen). Let op: Seconden worden genegeerd - alleen precisie op minuutniveau wordt ondersteund."
}
}
},
"error": {
"api_error": "Ophalen van huizen van Tibber API mislukt"
"no_time_offset": "Ten minste één tijdverschuivingswaarde moet negatief zijn (alleen historische gegevens)."
},
"abort": {
"no_parent_entry": "Bovenliggend item niet gevonden",
"no_access_token": "Geen toegangstoken beschikbaar",
"home_not_found": "Geselecteerd huis niet gevonden",
"api_error": "Ophalen van huizen van Tibber API mislukt",
"no_available_homes": "Geen extra huizen beschikbaar om toe te voegen. Alle huizen van je Tibber-account zijn al toegevoegd."
"already_configured": "**Een tijdreis-weergave met deze exacte tijdverschuiving bestaat al.**\n\nKies een andere verschuiving.",
"no_main_entries": "Geen hoofdconfiguratie-items gevonden. Voeg eerst een Tibber-huis toe.",
"parent_entry_not_found": "Geselecteerd configuratie-item niet gevonden."
},
"time_units": {
"day": "{count} dag",
"days": "{count} dagen",
"hour": "{count} uur",
"hours": "{count} uur",
"minute": "{count} minuut",
"minutes": "{count} minuten",
"ago": "{parts} geleden",
"now": "nu"
}
}
},
@ -719,6 +776,24 @@
}
},
"services": {
"get_price": {
"name": "Prijsgegevens ophalen",
"description": "Haal prijsgegevens op voor een specifiek tijdsbereik met automatische routing. Ontwikkelings- en testservice voor de price_info_for_range API-functie. Gebruikt automatisch PRICE_INFO, PRICE_INFO_RANGE of beide op basis van de tijdsbereikgrens.",
"fields": {
"entry_id": {
"name": "Entry ID",
"description": "De configuratie entry ID voor de Tibber integratie."
},
"start_time": {
"name": "Starttijd",
"description": "Begin van het tijdsbereik (inclusief, tijdzonebewust)."
},
"end_time": {
"name": "Eindtijd",
"description": "Einde van het tijdsbereik (exclusief, tijdzonebewust)."
}
}
},
"get_apexcharts_yaml": {
"name": "ApexCharts-kaart YAML ophalen",
"description": "Retourneert een kant-en-klaar YAML-fragment voor een ApexCharts-kaart die Tibber-prijzen voor de geselecteerde dag visualiseert. Gebruik dit om eenvoudig een vooraf geconfigureerd diagram aan je dashboard toe te voegen. De YAML gebruikt de get_chartdata-service voor gegevens.",
@ -843,6 +918,11 @@
}
},
"selector": {
"account_choice": {
"options": {
"new_token": "Nieuw Tibber-account toevoegen met API-token"
}
},
"day": {
"options": {
"yesterday": "Gisteren",

View file

@ -1,6 +1,22 @@
{
"config": {
"step": {
"account_choice": {
"title": "Välj konto",
"description": "Du kan lägga till ett annat hem från ett befintligt Tibber-konto eller ange ett nytt API-token för ett annat konto.",
"data": {
"account_choice": "Konto"
},
"submit": "Fortsätt →"
},
"new_token": {
"title": "Ange API-token",
"description": "Konfigurera Tibber Prisinformation & Betyg.\n\nFör att generera en API-åtkomsttoken, besök https://developer.tibber.com.",
"data": {
"access_token": "API-åtkomsttoken"
},
"submit": "Validera token"
},
"user": {
"description": "Konfigurera Tibber Prisinformation & Betyg.\n\nFör att generera en API-åtkomsttoken, besök https://developer.tibber.com.",
"data": {
@ -39,15 +55,24 @@
"unknown": "Oväntat fel",
"cannot_connect": "Kunde inte ansluta",
"invalid_access_token": "Ogiltig åtkomsttoken",
"missing_homes": "Den nya åtkomsttoken har inte åtkomst till alla konfigurerade hem. Vänligen använd en åtkomsttoken som har åtkomst till samma Tibber-hem.",
"invalid_yaml_syntax": "Ogiltig YAML-syntax. Kontrollera indrag, kolon och specialtecken.",
"missing_homes": "Den nya åtkomsttoken har inte åtkomst till alla konfigurerade hem. Använd en åtkomsttoken som har åtkomst till samma Tibber-hem.",
"home_already_configured": "Detta hem är redan konfigurerat i en annan post. Varje hem kan endast konfigureras en gång.",
"no_active_subscription": "Detta hem har inget aktivt Tibber-avtal. Endast hem med aktiva elavtal kan läggas till i Home Assistant.",
"subscription_expired": "Tibber-avtalet för detta hem har gått ut. Endast hem med aktiva eller framtida elavtal kan läggas till i Home Assistant.",
"future_subscription_warning": "Obs: Tibber-avtalet för detta hem har inte startat än. Funktionaliteten kan vara begränsad tills avtalet blir aktivt.",
"invalid_yaml_syntax": "Ogiltig YAML-syntax. Kontrollera indragning, kolon och specialtecken.",
"invalid_yaml_structure": "YAML måste vara en ordbok/objekt (nyckel: värde-par), inte en lista eller ren text.",
"service_call_failed": "Service-anrop validering misslyckades: {error_detail}"
"service_call_failed": "Tjänsteanropsvalidering misslyckades: {error_detail}",
"missing_entry_id": "Post-ID krävs men tillhandahölls inte.",
"invalid_entry_id": "Ogiltig post-ID eller post hittades inte.",
"missing_home_id": "Hem-ID saknas från konfigurationsposten.",
"user_data_not_available": "Användardata är inte tillgänglig. Uppdatera användardata först.",
"price_fetch_failed": "Kunde inte hämta prisdata. Kontrollera loggarna för detaljer."
},
"abort": {
"already_configured": "Integrationen är redan konfigurerad",
"already_configured": "Alla tillgängliga Tibber-hem är redan konfigurerade. Varje hem kan endast konfigureras en gång.",
"entry_not_found": "Tibber-konfigurationspost hittades inte.",
"setup_complete": "Konfiguration klar! Du kan ändra ytterligare alternativ för Tibber-priser i integrationens alternativ efter att ha stängt denna dialog.",
"setup_complete": "Installation klar! Du kan ändra ytterligare alternativ för Tibber Prices i integrationens alternativ efter att ha stängt denna dialog.",
"reauth_successful": "Omautentisering lyckades. Integrationen har uppdaterats med den nya åtkomsttoken."
}
},
@ -57,27 +82,59 @@
"config_subentries": {
"home": {
"initiate_flow": {
"user": "Lägg till Tibber-hem"
"user": "Skapa tidsresevy"
},
"title": "Lägg till Tibber-hem",
"title": "Skapa tidsresevy",
"step": {
"user": {
"title": "Lägg till Tibber-hem",
"description": "Välj ett hem att lägga till i din Tibber-integration.\n\n**Obs:** Efter att ha lagt till detta hem kan du lägga till ytterligare hem från integrationens kontextmeny genom att välja \"Lägg till Tibber-hem\".",
"title": "Välj konfigurationspost",
"description": "Välj konfigurationsposten som du vill skapa en tidsresevy för.\n\n**Tidsresevyer** låter dig se historiska prisdata som om det vore nuvarande tid. Detta är användbart för att testa automationer eller analysera tidigare prismönster.",
"data": {
"home_id": "Hem"
"parent_entry_id": "Konfigurationspost"
}
},
"time_offset": {
"title": "Konfigurera tidsförskjutning",
"description": "Konfigurera hur långt tillbaka i tiden denna vy ska resa.\n\n**Rekommenderat:** Använd **≥2 dagar** förskjutning för att undvika konflikter med \"yesterday\"-entiteter som också tillhandahåller historisk data.\n\n**Exempel:**\n• **-7 dagar**: Visa priser från 7 dagar sedan\n• **-2 dagar, 3 timmar**: Visa priser från 2 dagar och 3 timmar sedan\n• **-14 dagar**: Visa priser från 2 veckor sedan",
"data": {
"virtual_time_offset_days": "Dagar tillbaka",
"time_offset": "Extra tidsförskjutning"
},
"data_description": {
"virtual_time_offset_days": "Hur många dagar att resa tillbaka i tiden. Skjutreglage område: 0 till 374 dagar (≈1 år). Rekommenderat: ≥2 dagar för att undvika konflikter med \"yesterday\"-entiteter.",
"time_offset": "Valfri finjustering: Lägg till timmar och/eller minuter till dagförskjutningen. Tiden subtraheras automatiskt (res längre tillbaka). Obs: Sekunder ignoreras - endast minutbaserad precision stöds."
}
},
"init": {
"title": "Konfigurera om tidsförskjutning",
"description": "Uppdatera tidsförskjutningen för denna tidsresevy.",
"data": {
"virtual_time_offset_days": "Dagar tillbaka",
"time_offset": "Extra tidsförskjutning"
},
"data_description": {
"virtual_time_offset_days": "Hur många dagar att resa tillbaka i tiden. Skjutreglage område: 0 till 374 dagar (≈1 år). Rekommenderat: ≥2 dagar för att undvika konflikter med \"yesterday\"-entiteter.",
"time_offset": "Valfri finjustering: Lägg till timmar och/eller minuter till dagförskjutningen. Tiden subtraheras automatiskt (res längre tillbaka). Obs: Sekunder ignoreras - endast minutbaserad precision stöds."
}
}
},
"error": {
"api_error": "Kunde inte hämta hem från Tibber API"
"no_time_offset": "Minst ett tidsförskjutningsvärde måste vara negativt (endast historiska data)."
},
"abort": {
"no_parent_entry": "Överordnad post hittades inte",
"no_access_token": "Ingen åtkomsttoken tillgänglig",
"home_not_found": "Valt hem hittades inte",
"api_error": "Kunde inte hämta hem från Tibber API",
"no_available_homes": "Inga ytterligare hem tillgängliga att lägga till. Alla hem från ditt Tibber-konto har redan lagts till."
"already_configured": "**En tidsresevy med denna exakta tidsförskjutning existerar redan.**\n\nVälj en annan förskjutning.",
"no_main_entries": "Inga huvudkonfigurationsposter hittades. Lägg först till ett Tibber-hem.",
"parent_entry_not_found": "Vald konfigurationspost hittades inte."
},
"time_units": {
"day": "{count} dag",
"days": "{count} dagar",
"hour": "{count} timme",
"hours": "{count} timmar",
"minute": "{count} minut",
"minutes": "{count} minuter",
"ago": "{parts} sedan",
"now": "nu"
}
}
},
@ -719,6 +776,24 @@
}
},
"services": {
"get_price": {
"name": "Hämta prisdata",
"description": "Hämta prisdata för ett specifikt tidsintervall med automatisk routing. Utvecklings- och testtjänst för price_info_for_range API-funktionen. Använder automatiskt PRICE_INFO, PRICE_INFO_RANGE eller båda baserat på tidsintervallgränsen.",
"fields": {
"entry_id": {
"name": "Post-ID",
"description": "Konfigurationspost-ID för Tibber-integrationen."
},
"start_time": {
"name": "Starttid",
"description": "Start av tidsintervallet (inklusiv, tidszonskänslig)."
},
"end_time": {
"name": "Sluttid",
"description": "Slut av tidsintervallet (exklusiv, tidszonskänslig)."
}
}
},
"get_apexcharts_yaml": {
"name": "Hämta ApexCharts-kort YAML",
"description": "Returnerar ett färdigt YAML-utklipp för ett ApexCharts-kort som visualiserar Tibber-priser för den valda dagen. Använd detta för att enkelt lägga till ett förkonfigurerat diagram till din instrumentpanel. YAML kommer att använda get_chartdata-tjänsten för data.",
@ -843,6 +918,11 @@
}
},
"selector": {
"account_choice": {
"options": {
"new_token": "Lägg till nytt Tibber-konto med API-token"
}
},
"day": {
"options": {
"yesterday": "Igår",