From 2449c28a886c2c86bca46f3ff90fbe410e9051df Mon Sep 17 00:00:00 2001 From: Julian Pawlowski Date: Tue, 25 Nov 2025 20:39:58 +0000 Subject: [PATCH] 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. --- .../config_flow_handlers/subentry_flow.py | 346 +++++++++++++----- .../config_flow_handlers/user_flow.py | 342 ++++++++++++++++- .../tibber_prices/translations/de.json | 116 +++++- .../tibber_prices/translations/en.json | 108 +++++- .../tibber_prices/translations/nb.json | 112 +++++- .../tibber_prices/translations/nl.json | 114 +++++- .../tibber_prices/translations/sv.json | 114 +++++- 7 files changed, 1072 insertions(+), 180 deletions(-) diff --git a/custom_components/tibber_prices/config_flow_handlers/subentry_flow.py b/custom_components/tibber_prices/config_flow_handlers/subentry_flow.py index 17de715..e5861f1 100644 --- a/custom_components/tibber_prices/config_flow_handlers/subentry_flow.py +++ b/custom_components/tibber_prices/config_flow_handlers/subentry_flow.py @@ -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, ) diff --git a/custom_components/tibber_prices/config_flow_handlers/user_flow.py b/custom_components/tibber_prices/config_flow_handlers/user_flow.py index a750833..da1a4b2 100644 --- a/custom_components/tibber_prices/config_flow_handlers/user_flow.py +++ b/custom_components/tibber_prices/config_flow_handlers/user_flow.py @@ -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) diff --git a/custom_components/tibber_prices/translations/de.json b/custom_components/tibber_prices/translations/de.json index 45cf011..4113384 100644 --- a/custom_components/tibber_prices/translations/de.json +++ b/custom_components/tibber_prices/translations/de.json @@ -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", @@ -921,4 +1001,4 @@ } }, "title": "Tibber Preisinformationen & Bewertungen" -} \ No newline at end of file +} diff --git a/custom_components/tibber_prices/translations/en.json b/custom_components/tibber_prices/translations/en.json index d032d24..70d11da 100644 --- a/custom_components/tibber_prices/translations/en.json +++ b/custom_components/tibber_prices/translations/en.json @@ -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", @@ -917,4 +997,4 @@ } }, "title": "Tibber Price Information & Ratings" -} \ No newline at end of file +} diff --git a/custom_components/tibber_prices/translations/nb.json b/custom_components/tibber_prices/translations/nb.json index 5af59a5..71037bd 100644 --- a/custom_components/tibber_prices/translations/nb.json +++ b/custom_components/tibber_prices/translations/nb.json @@ -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", @@ -917,4 +997,4 @@ } }, "title": "Tibber Prisinformasjon & Vurderinger" -} \ No newline at end of file +} diff --git a/custom_components/tibber_prices/translations/nl.json b/custom_components/tibber_prices/translations/nl.json index 51ef8e8..bd3b836 100644 --- a/custom_components/tibber_prices/translations/nl.json +++ b/custom_components/tibber_prices/translations/nl.json @@ -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", @@ -917,4 +997,4 @@ } }, "title": "Tibber Prijsinformatie & Beoordelingen" -} \ No newline at end of file +} diff --git a/custom_components/tibber_prices/translations/sv.json b/custom_components/tibber_prices/translations/sv.json index d9b553c..a9027ac 100644 --- a/custom_components/tibber_prices/translations/sv.json +++ b/custom_components/tibber_prices/translations/sv.json @@ -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", @@ -917,4 +997,4 @@ } }, "title": "Tibber Prisinformation & Betyg" -} \ No newline at end of file +}