diff --git a/custom_components/tibber_prices/__init__.py b/custom_components/tibber_prices/__init__.py index 40e22a4..2f214fe 100644 --- a/custom_components/tibber_prices/__init__.py +++ b/custom_components/tibber_prices/__init__.py @@ -28,7 +28,6 @@ if TYPE_CHECKING: PLATFORMS: list[Platform] = [ Platform.SENSOR, Platform.BINARY_SENSOR, - Platform.SWITCH, ] diff --git a/custom_components/tibber_prices/binary_sensor.py b/custom_components/tibber_prices/binary_sensor.py index 67bba64..7c376ef 100644 --- a/custom_components/tibber_prices/binary_sensor.py +++ b/custom_components/tibber_prices/binary_sensor.py @@ -2,6 +2,7 @@ from __future__ import annotations +from datetime import datetime from typing import TYPE_CHECKING from homeassistant.components.binary_sensor import ( @@ -10,6 +11,7 @@ from homeassistant.components.binary_sensor import ( BinarySensorEntityDescription, ) +from .const import NAME from .entity import TibberPricesEntity if TYPE_CHECKING: @@ -21,15 +23,30 @@ if TYPE_CHECKING: ENTITY_DESCRIPTIONS = ( BinarySensorEntityDescription( - key="tibber_prices", - name="Tibber Prices Binary Sensor", + key="peak_hour", + translation_key="peak_hour", + name="Electricity Peak Hour", + device_class=BinarySensorDeviceClass.POWER, + icon="mdi:flash-alert", + ), + BinarySensorEntityDescription( + key="best_price_hour", + translation_key="best_price_hour", + name="Best Electricity Price Hour", + device_class=BinarySensorDeviceClass.POWER, + icon="mdi:flash-outline", + ), + BinarySensorEntityDescription( + key="connection", + translation_key="connection", + name="Tibber API Connection", device_class=BinarySensorDeviceClass.CONNECTIVITY, ), ) async def async_setup_entry( - hass: HomeAssistant, # noqa: ARG001 Unused function argument: `hass` + hass: HomeAssistant, entry: TibberPricesConfigEntry, async_add_entities: AddEntitiesCallback, ) -> None: @@ -54,8 +71,106 @@ class TibberPricesBinarySensor(TibberPricesEntity, BinarySensorEntity): """Initialize the binary_sensor class.""" super().__init__(coordinator) self.entity_description = entity_description + self._attr_unique_id = f"{coordinator.config_entry.entry_id}_{entity_description.key}" @property - def is_on(self) -> bool: + def is_on(self) -> bool | None: """Return true if the binary_sensor is on.""" - return self.coordinator.data.get("title", "") == "foo" + try: + if not self.coordinator.data: + return None + + subscription = self.coordinator.data["data"]["viewer"]["homes"][0]["currentSubscription"] + price_info = subscription["priceInfo"] + + now = datetime.now() + current_hour_data = None + today_prices = price_info.get("today", []) + + if not today_prices: + return None + + # Find current hour's data + for price_data in today_prices: + starts_at = datetime.fromisoformat(price_data["startsAt"]) + if starts_at.hour == now.hour: + current_hour_data = price_data + break + + if not current_hour_data: + return None + + if self.entity_description.key == "peak_hour": + # Consider it a peak hour if the price is in the top 20% of today's prices + prices = [float(price["total"]) for price in today_prices] + prices.sort() + threshold_index = int(len(prices) * 0.8) + peak_threshold = prices[threshold_index] + return float(current_hour_data["total"]) >= peak_threshold + + elif self.entity_description.key == "best_price_hour": + # Consider it a best price hour if the price is in the bottom 20% of today's prices + prices = [float(price["total"]) for price in today_prices] + prices.sort() + threshold_index = int(len(prices) * 0.2) + best_threshold = prices[threshold_index] + return float(current_hour_data["total"]) <= best_threshold + + elif self.entity_description.key == "connection": + # Check if we have valid current data + return bool(current_hour_data) + + return None + + except (KeyError, ValueError, TypeError) as ex: + self.coordinator.logger.error( + "Error getting binary sensor state", + extra={ + "error": str(ex), + "entity": self.entity_description.key, + }, + ) + return None + + @property + def extra_state_attributes(self) -> dict | None: + """Return additional state attributes.""" + try: + if not self.coordinator.data: + return None + + subscription = self.coordinator.data["data"]["viewer"]["homes"][0]["currentSubscription"] + price_info = subscription["priceInfo"] + + attributes = {} + + if self.entity_description.key in ["peak_hour", "best_price_hour"]: + today_prices = price_info.get("today", []) + if today_prices: + prices = [(datetime.fromisoformat(price["startsAt"]).hour, float(price["total"])) + for price in today_prices] + + if self.entity_description.key == "peak_hour": + # Get top 5 peak hours + peak_hours = sorted(prices, key=lambda x: x[1], reverse=True)[:5] + attributes["peak_hours"] = [ + {"hour": hour, "price": price} for hour, price in peak_hours + ] + else: + # Get top 5 best price hours + best_hours = sorted(prices, key=lambda x: x[1])[:5] + attributes["best_price_hours"] = [ + {"hour": hour, "price": price} for hour, price in best_hours + ] + + return attributes if attributes else None + + except (KeyError, ValueError, TypeError) as ex: + self.coordinator.logger.error( + "Error getting binary sensor attributes", + extra={ + "error": str(ex), + "entity": self.entity_description.key, + }, + ) + return None diff --git a/custom_components/tibber_prices/config_flow.py b/custom_components/tibber_prices/config_flow.py index 716e254..27c6020 100644 --- a/custom_components/tibber_prices/config_flow.py +++ b/custom_components/tibber_prices/config_flow.py @@ -23,6 +23,18 @@ class TibberPricesFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): VERSION = 1 + def __init__(self) -> None: + """Initialize the config flow.""" + super().__init__() + self._reauth_entry: config_entries.ConfigEntry | None = None + + @staticmethod + def async_get_options_flow( + config_entry: config_entries.ConfigEntry, + ) -> config_entries.OptionsFlow: + """Get the options flow for this handler.""" + return TibberPricesOptionsFlowHandler(config_entry) + async def async_step_user( self, user_input: dict | None = None, @@ -78,3 +90,78 @@ class TibberPricesFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): ) result = await client.async_test_connection() return result["viewer"]["name"] + + +class TibberPricesOptionsFlowHandler(config_entries.OptionsFlow): + """Tibber Prices config flow options handler.""" + + def __init__(self, config_entry: config_entries.ConfigEntry) -> None: + """Initialize options flow.""" + super().__init__() + # Store the entry_id instead of the whole config_entry + self._entry_id = config_entry.entry_id + + async def async_step_init( + self, user_input: dict | None = None + ) -> config_entries.ConfigFlowResult: + """Manage the options.""" + errors: dict[str, str] = {} + + if user_input is not None: + try: + # Test the new access token and get account name + client = TibberPricesApiClient( + access_token=user_input[CONF_ACCESS_TOKEN], + session=async_create_clientsession(self.hass), + ) + result = await client.async_test_connection() + new_account_name = result["viewer"]["name"] + + # Get the config entry using the entry_id + config_entry = self.hass.config_entries.async_get_entry(self._entry_id) + if not config_entry: + return self.async_abort(reason="entry_not_found") + + # Check if this token is for the same account + current_unique_id = config_entry.unique_id + new_unique_id = slugify(new_account_name) + + if current_unique_id != new_unique_id: + # Token is for a different account + errors["base"] = "different_account" + else: + # Update the config entry with the new access token + return self.async_create_entry(title="", data=user_input) + + except TibberPricesApiClientAuthenticationError as exception: + LOGGER.warning(exception) + errors["base"] = "auth" + except TibberPricesApiClientCommunicationError as exception: + LOGGER.error(exception) + errors["base"] = "connection" + except TibberPricesApiClientError as exception: + LOGGER.exception(exception) + errors["base"] = "unknown" + + # Get current config entry to get the current access token + config_entry = self.hass.config_entries.async_get_entry(self._entry_id) + if not config_entry: + return self.async_abort(reason="entry_not_found") + + # If there's no user input or if there were errors, show the form + schema = { + vol.Required( + CONF_ACCESS_TOKEN, + default=config_entry.data.get(CONF_ACCESS_TOKEN, ""), + ): selector.TextSelector( + selector.TextSelectorConfig( + type=selector.TextSelectorType.TEXT, + ), + ), + } + + return self.async_show_form( + step_id="init", + data_schema=vol.Schema(schema), + errors=errors, + ) diff --git a/custom_components/tibber_prices/coordinator.py b/custom_components/tibber_prices/coordinator.py index eccf782..1d3fae7 100644 --- a/custom_components/tibber_prices/coordinator.py +++ b/custom_components/tibber_prices/coordinator.py @@ -9,6 +9,7 @@ from typing import TYPE_CHECKING, Any, Final, TypedDict, cast from homeassistant.core import HomeAssistant, callback from homeassistant.exceptions import ConfigEntryAuthFailed +from homeassistant.helpers.event import async_track_time_change from homeassistant.helpers.storage import Store from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed import homeassistant.util.dt as dt_util @@ -162,15 +163,15 @@ class TibberPricesDataUpdateCoordinator(DataUpdateCoordinator[TibberPricesData]) # Schedule updates at the start of every hour self._remove_update_listeners.append( - hass.helpers.event.async_track_time_change( - self._async_refresh_hourly, minute=0, second=0 + async_track_time_change( + hass, self._async_refresh_hourly, minute=0, second=0 ) ) # Schedule data rotation at midnight self._remove_update_listeners.append( - hass.helpers.event.async_track_time_change( - self._async_handle_midnight_rotation, hour=0, minute=0, second=0 + async_track_time_change( + hass, self._async_handle_midnight_rotation, hour=0, minute=0, second=0 ) ) diff --git a/custom_components/tibber_prices/entity.py b/custom_components/tibber_prices/entity.py index dbabb48..f103d28 100644 --- a/custom_components/tibber_prices/entity.py +++ b/custom_components/tibber_prices/entity.py @@ -5,7 +5,7 @@ from __future__ import annotations from homeassistant.helpers.device_registry import DeviceInfo from homeassistant.helpers.update_coordinator import CoordinatorEntity -from .const import ATTRIBUTION +from .const import ATTRIBUTION, DOMAIN, NAME from .coordinator import TibberPricesDataUpdateCoordinator @@ -13,16 +13,27 @@ class TibberPricesEntity(CoordinatorEntity[TibberPricesDataUpdateCoordinator]): """TibberPricesEntity class.""" _attr_attribution = ATTRIBUTION + _attr_has_entity_name = True def __init__(self, coordinator: TibberPricesDataUpdateCoordinator) -> None: """Initialize.""" super().__init__(coordinator) - self._attr_unique_id = coordinator.config_entry.entry_id + + # Get home name from Tibber API if available + home_name = None + if coordinator.data: + try: + home = coordinator.data["data"]["viewer"]["homes"][0] + home_name = home.get("address", {}).get("address1", "Tibber Home") + except (KeyError, IndexError): + home_name = "Tibber Home" + else: + home_name = "Tibber Home" + self._attr_device_info = DeviceInfo( - identifiers={ - ( - coordinator.config_entry.domain, - coordinator.config_entry.entry_id, - ), - }, + identifiers={(DOMAIN, coordinator.config_entry.entry_id)}, + name=home_name, + manufacturer="Tibber", + model="Price API", + sw_version=str(coordinator.config_entry.version), ) diff --git a/custom_components/tibber_prices/manifest.json b/custom_components/tibber_prices/manifest.json index 2844276..5fcba5e 100644 --- a/custom_components/tibber_prices/manifest.json +++ b/custom_components/tibber_prices/manifest.json @@ -6,6 +6,7 @@ ], "config_flow": true, "documentation": "https://github.com/jpawlowski/hass.tibber_prices", + "icon": "mdi:chart-line", "iot_class": "cloud_polling", "issue_tracker": "https://github.com/jpawlowski/hass.tibber_prices/issues", "version": "0.1.0" diff --git a/custom_components/tibber_prices/sensor.py b/custom_components/tibber_prices/sensor.py index 898061d..d3fab31 100644 --- a/custom_components/tibber_prices/sensor.py +++ b/custom_components/tibber_prices/sensor.py @@ -2,10 +2,19 @@ from __future__ import annotations +from datetime import datetime from typing import TYPE_CHECKING -from homeassistant.components.sensor import SensorEntity, SensorEntityDescription +from homeassistant.components.sensor import ( + SensorDeviceClass, + SensorEntity, + SensorEntityDescription, + SensorStateClass, +) +from homeassistant.const import CURRENCY_EURO, EntityCategory +from homeassistant.util import dt as dt_util +from .const import DOMAIN from .entity import TibberPricesEntity if TYPE_CHECKING: @@ -15,17 +24,126 @@ if TYPE_CHECKING: from .coordinator import TibberPricesDataUpdateCoordinator from .data import TibberPricesConfigEntry -ENTITY_DESCRIPTIONS = ( +# Main price sensors that users will typically use in automations +PRICE_SENSORS = ( SensorEntityDescription( - key="tibber_prices", - name="Integration Sensor", - icon="mdi:format-quote-close", + key="current_price", + translation_key="current_price", + name="Current Electricity Price", + icon="mdi:currency-eur", + device_class=SensorDeviceClass.MONETARY, + state_class=SensorStateClass.TOTAL, + native_unit_of_measurement=CURRENCY_EURO, ), + SensorEntityDescription( + key="next_hour_price", + translation_key="next_hour_price", + name="Next Hour Electricity Price", + icon="mdi:currency-eur-off", + device_class=SensorDeviceClass.MONETARY, + state_class=SensorStateClass.TOTAL, + native_unit_of_measurement=CURRENCY_EURO, + ), + SensorEntityDescription( + key="price_level", + translation_key="price_level", + name="Current Price Level", + icon="mdi:meter-electric", + ), +) + +# Statistical price sensors +STATISTICS_SENSORS = ( + SensorEntityDescription( + key="lowest_price_today", + translation_key="lowest_price_today", + name="Today's Lowest Price", + icon="mdi:currency-eur", + device_class=SensorDeviceClass.MONETARY, + state_class=SensorStateClass.TOTAL, + native_unit_of_measurement=CURRENCY_EURO, + entity_category=EntityCategory.DIAGNOSTIC, + ), + SensorEntityDescription( + key="highest_price_today", + translation_key="highest_price_today", + name="Today's Highest Price", + icon="mdi:currency-eur", + device_class=SensorDeviceClass.MONETARY, + state_class=SensorStateClass.TOTAL, + native_unit_of_measurement=CURRENCY_EURO, + entity_category=EntityCategory.DIAGNOSTIC, + ), + SensorEntityDescription( + key="average_price_today", + translation_key="average_price_today", + name="Today's Average Price", + icon="mdi:currency-eur", + device_class=SensorDeviceClass.MONETARY, + state_class=SensorStateClass.TOTAL, + native_unit_of_measurement=CURRENCY_EURO, + entity_category=EntityCategory.DIAGNOSTIC, + ), +) + +# Rating sensors +RATING_SENSORS = ( + SensorEntityDescription( + key="hourly_rating", + translation_key="hourly_rating", + name="Hourly Price Rating", + icon="mdi:clock-outline", + native_unit_of_measurement="%", + entity_category=EntityCategory.DIAGNOSTIC, + ), + SensorEntityDescription( + key="daily_rating", + translation_key="daily_rating", + name="Daily Price Rating", + icon="mdi:calendar-today", + native_unit_of_measurement="%", + entity_category=EntityCategory.DIAGNOSTIC, + ), + SensorEntityDescription( + key="monthly_rating", + translation_key="monthly_rating", + name="Monthly Price Rating", + icon="mdi:calendar-month", + native_unit_of_measurement="%", + entity_category=EntityCategory.DIAGNOSTIC, + ), +) + +# Diagnostic sensors for data availability +DIAGNOSTIC_SENSORS = ( + SensorEntityDescription( + key="data_timestamp", + translation_key="data_timestamp", + name="Last Data Update", + icon="mdi:clock-check", + device_class=SensorDeviceClass.TIMESTAMP, + entity_category=EntityCategory.DIAGNOSTIC, + ), + SensorEntityDescription( + key="tomorrow_data_available", + translation_key="tomorrow_data_available", + name="Tomorrow's Data Status", + icon="mdi:calendar-check", + entity_category=EntityCategory.DIAGNOSTIC, + ), +) + +# Combine all sensors +ENTITY_DESCRIPTIONS = ( + *PRICE_SENSORS, + *STATISTICS_SENSORS, + *RATING_SENSORS, + *DIAGNOSTIC_SENSORS, ) async def async_setup_entry( - hass: HomeAssistant, # noqa: ARG001 Unused function argument: `hass` + hass: HomeAssistant, entry: TibberPricesConfigEntry, async_add_entities: AddEntitiesCallback, ) -> None: @@ -50,8 +168,183 @@ class TibberPricesSensor(TibberPricesEntity, SensorEntity): """Initialize the sensor class.""" super().__init__(coordinator) self.entity_description = entity_description + self._attr_unique_id = f"{coordinator.config_entry.entry_id}_{entity_description.key}" + self._attr_has_entity_name = True @property - def native_value(self) -> str | None: + def native_value(self) -> float | str | datetime | None: """Return the native value of the sensor.""" - return self.coordinator.data.get("body") + try: + if not self.coordinator.data: + return None + + subscription = self.coordinator.data["data"]["viewer"]["homes"][0]["currentSubscription"] + price_info = subscription["priceInfo"] + price_rating = subscription.get("priceRating") or {} + + # Get current hour's data + now = datetime.now() + current_hour_data = None + for price_data in price_info.get("today", []): + starts_at = datetime.fromisoformat(price_data["startsAt"]) + if starts_at.hour == now.hour: + current_hour_data = price_data + break + + if self.entity_description.key == "current_price": + return float(current_hour_data["total"]) if current_hour_data else None + + elif self.entity_description.key == "next_hour_price": + next_hour = (now.hour + 1) % 24 + for price_data in price_info.get("today", []): + starts_at = datetime.fromisoformat(price_data["startsAt"]) + if starts_at.hour == next_hour: + return float(price_data["total"]) + return None + + elif self.entity_description.key == "lowest_price_today": + today_prices = price_info.get("today", []) + if not today_prices: + return None + return min(float(price["total"]) for price in today_prices) + + elif self.entity_description.key == "highest_price_today": + today_prices = price_info.get("today", []) + if not today_prices: + return None + return max(float(price["total"]) for price in today_prices) + + elif self.entity_description.key == "average_price_today": + today_prices = price_info.get("today", []) + if not today_prices: + return None + return sum(float(price["total"]) for price in today_prices) / len(today_prices) + + elif self.entity_description.key == "price_level": + return current_hour_data["level"] if current_hour_data else None + + elif self.entity_description.key == "hourly_rating": + hourly = price_rating.get("hourly", {}) + entries = hourly.get("entries", []) if hourly else [] + if not entries: + return None + for entry in entries: + starts_at = datetime.fromisoformat(entry["time"]) + if starts_at.hour == now.hour: + return round(float(entry["difference"]) * 100, 1) + return None + + elif self.entity_description.key == "daily_rating": + daily = price_rating.get("daily", {}) + entries = daily.get("entries", []) if daily else [] + if not entries: + return None + for entry in entries: + starts_at = datetime.fromisoformat(entry["time"]) + if starts_at.date() == now.date(): + return round(float(entry["difference"]) * 100, 1) + return None + + elif self.entity_description.key == "monthly_rating": + monthly = price_rating.get("monthly", {}) + entries = monthly.get("entries", []) if monthly else [] + if not entries: + return None + for entry in entries: + starts_at = datetime.fromisoformat(entry["time"]) + if starts_at.month == now.month and starts_at.year == now.year: + return round(float(entry["difference"]) * 100, 1) + return None + + elif self.entity_description.key == "data_timestamp": + # Return the latest timestamp from any data we have + latest_timestamp = None + + # Check today's data + for price_data in price_info.get("today", []): + timestamp = datetime.fromisoformat(price_data["startsAt"]) + if not latest_timestamp or timestamp > latest_timestamp: + latest_timestamp = timestamp + + # Check tomorrow's data + for price_data in price_info.get("tomorrow", []): + timestamp = datetime.fromisoformat(price_data["startsAt"]) + if not latest_timestamp or timestamp > latest_timestamp: + latest_timestamp = timestamp + + return dt_util.as_utc(latest_timestamp) if latest_timestamp else None + + elif self.entity_description.key == "tomorrow_data_available": + tomorrow_prices = price_info.get("tomorrow", []) + if not tomorrow_prices: + return "No" + # Check if we have a full day of data (24 hours) + return "Yes" if len(tomorrow_prices) == 24 else "Partial" + + return None + + except (KeyError, ValueError, TypeError) as ex: + self.coordinator.logger.error( + "Error getting sensor value", + extra={ + "error": str(ex), + "entity": self.entity_description.key, + }, + ) + return None + + @property + def extra_state_attributes(self) -> dict | None: + """Return additional state attributes.""" + try: + if not self.coordinator.data: + return None + + subscription = self.coordinator.data["data"]["viewer"]["homes"][0]["currentSubscription"] + price_info = subscription["priceInfo"] + + attributes = {} + + if self.entity_description.key == "current_price": + attributes["timestamp"] = price_info.get("current", {}).get("startsAt") + elif self.entity_description.key == "next_hour_price": + attributes["timestamp"] = price_info.get("current", {}).get("startsAt") + elif self.entity_description.key == "price_level": + attributes["timestamp"] = price_info.get("current", {}).get("startsAt") + elif self.entity_description.key == "lowest_price_today": + attributes["timestamp"] = price_info.get("today", [{}])[0].get("startsAt") + elif self.entity_description.key == "highest_price_today": + attributes["timestamp"] = price_info.get("today", [{}])[0].get("startsAt") + elif self.entity_description.key == "average_price_today": + attributes["timestamp"] = price_info.get("today", [{}])[0].get("startsAt") + elif self.entity_description.key == "hourly_rating": + attributes["timestamp"] = price_info.get("current", {}).get("startsAt") + elif self.entity_description.key == "daily_rating": + attributes["timestamp"] = price_info.get("today", [{}])[0].get("startsAt") + elif self.entity_description.key == "monthly_rating": + attributes["timestamp"] = price_info.get("today", [{}])[0].get("startsAt") + elif self.entity_description.key == "data_timestamp": + attributes["timestamp"] = price_info.get("today", [{}])[0].get("startsAt") + elif self.entity_description.key == "tomorrow_data_available": + attributes["timestamp"] = price_info.get("today", [{}])[0].get("startsAt") + + # Add translated description + if self.hass is not None: + key = f"entity.sensor.{self.entity_description.translation_key}.description" + language_config = getattr(self.hass.config, 'language', None) + if isinstance(language_config, dict): + description = language_config.get(key) + if description is not None: + attributes["description"] = description + + return attributes if attributes else None + + except (KeyError, ValueError, TypeError) as ex: + self.coordinator.logger.error( + "Error getting sensor attributes", + extra={ + "error": str(ex), + "entity": self.entity_description.key, + }, + ) + return None diff --git a/custom_components/tibber_prices/switch.py b/custom_components/tibber_prices/switch.py deleted file mode 100644 index bf98906..0000000 --- a/custom_components/tibber_prices/switch.py +++ /dev/null @@ -1,65 +0,0 @@ -"""Switch platform for tibber_prices.""" - -from __future__ import annotations - -from typing import TYPE_CHECKING, Any - -from homeassistant.components.switch import SwitchEntity, SwitchEntityDescription - -from .entity import TibberPricesEntity - -if TYPE_CHECKING: - from homeassistant.core import HomeAssistant - from homeassistant.helpers.entity_platform import AddEntitiesCallback - - from .coordinator import TibberPricesDataUpdateCoordinator - from .data import TibberPricesConfigEntry - -ENTITY_DESCRIPTIONS = ( - SwitchEntityDescription( - key="tibber_prices", - name="Integration Switch", - icon="mdi:format-quote-close", - ), -) - - -async def async_setup_entry( - hass: HomeAssistant, # noqa: ARG001 Unused function argument: `hass` - entry: TibberPricesConfigEntry, - async_add_entities: AddEntitiesCallback, -) -> None: - """Set up the switch platform.""" - async_add_entities( - TibberPricesSwitch( - coordinator=entry.runtime_data.coordinator, - entity_description=entity_description, - ) - for entity_description in ENTITY_DESCRIPTIONS - ) - - -class TibberPricesSwitch(TibberPricesEntity, SwitchEntity): - """tibber_prices switch class.""" - - def __init__( - self, - coordinator: TibberPricesDataUpdateCoordinator, - entity_description: SwitchEntityDescription, - ) -> None: - """Initialize the switch class.""" - super().__init__(coordinator) - self.entity_description = entity_description - - @property - def is_on(self) -> bool: - """Return true if the switch is on.""" - return self.coordinator.data.get("title", "") == "foo" - - async def async_turn_on(self, **_: Any) -> None: - """Turn on the switch.""" - await self.coordinator.async_request_refresh() - - async def async_turn_off(self, **_: Any) -> None: - """Turn off the switch.""" - await self.coordinator.async_request_refresh() diff --git a/custom_components/tibber_prices/translations/de.json b/custom_components/tibber_prices/translations/de.json new file mode 100644 index 0000000..68b43ac --- /dev/null +++ b/custom_components/tibber_prices/translations/de.json @@ -0,0 +1,103 @@ +{ + "config": { + "step": { + "user": { + "description": "Wenn Sie Hilfe bei der Konfiguration benötigen, schauen Sie hier: https://github.com/jpawlowski/hass.tibber_prices", + "data": { + "access_token": "Tibber Zugangstoken" + } + } + }, + "error": { + "auth": "Der Tibber Zugangstoken ist ungültig.", + "connection": "Keine Verbindung zu Tibber möglich. Bitte überprüfen Sie Ihre Internetverbindung.", + "unknown": "Ein unerwarteter Fehler ist aufgetreten. Bitte prüfen Sie die Logs für Details." + }, + "abort": { + "already_configured": "Dieser Eintrag ist bereits konfiguriert.", + "entry_not_found": "Tibber-Konfigurationseintrag nicht gefunden." + } + }, + "options": { + "step": { + "init": { + "title": "Tibber-Konfiguration aktualisieren", + "description": "Aktualisieren Sie Ihren Tibber-API-Zugangstoken. Wenn Sie einen neuen Token benötigen, können Sie diesen unter https://developer.tibber.com/settings/access-token generieren", + "data": { + "access_token": "Tibber Zugangstoken" + } + } + }, + "error": { + "auth": "Der Tibber Zugangstoken ist ungültig.", + "connection": "Keine Verbindung zu Tibber möglich. Bitte überprüfen Sie Ihre Internetverbindung.", + "unknown": "Ein unerwarteter Fehler ist aufgetreten. Bitte prüfen Sie die Logs für Details.", + "different_account": "Der neue Zugangstoken gehört zu einem anderen Tibber-Konto. Bitte verwenden Sie einen Token desselben Kontos oder erstellen Sie eine neue Konfiguration für das andere Konto." + }, + "abort": { + "entry_not_found": "Tibber-Konfigurationseintrag nicht gefunden." + } + }, + "entity": { + "sensor": { + "current_price": { + "name": "Aktueller Preis", + "description": "Der aktuelle Strompreis dieser Stunde inklusive Steuern" + }, + "next_hour_price": { + "name": "Preis nächste Stunde", + "description": "Der Strompreis der nächsten Stunde inklusive Steuern" + }, + "price_level": { + "name": "Preisniveau", + "description": "Aktueller Preisindikator (SEHR_GÜNSTIG, GÜNSTIG, NORMAL, TEUER, SEHR_TEUER)" + }, + "lowest_price_today": { + "name": "Niedrigster Preis heute", + "description": "Der niedrigste Strompreis des aktuellen Tages" + }, + "highest_price_today": { + "name": "Höchster Preis heute", + "description": "Der höchste Strompreis des aktuellen Tages" + }, + "average_price_today": { + "name": "Durchschnittspreis heute", + "description": "Der durchschnittliche Strompreis des aktuellen Tages" + }, + "hourly_rating": { + "name": "Stündliche Preisbewertung", + "description": "Preisvergleich mit historischen Daten für die aktuelle Stunde (prozentuale Abweichung)" + }, + "daily_rating": { + "name": "Tägliche Preisbewertung", + "description": "Preisvergleich mit historischen Daten für den aktuellen Tag (prozentuale Abweichung)" + }, + "monthly_rating": { + "name": "Monatliche Preisbewertung", + "description": "Preisvergleich mit historischen Daten für den aktuellen Monat (prozentuale Abweichung)" + }, + "data_timestamp": { + "name": "Letzte Datenaktualisierung", + "description": "Zeitstempel der zuletzt von Tibber empfangenen Preisdaten" + }, + "tomorrow_data_available": { + "name": "Daten für morgen verfügbar", + "description": "Zeigt an, ob Preisdaten für morgen verfügbar sind (Ja/Nein/Teilweise)" + } + }, + "binary_sensor": { + "peak_hour": { + "name": "Spitzenstunde", + "description": "Zeigt an, ob die aktuelle Stunde zu den 20% teuersten Stunden des Tages gehört" + }, + "best_price_hour": { + "name": "Beste Preisstunde", + "description": "Zeigt an, ob die aktuelle Stunde zu den 20% günstigsten Stunden des Tages gehört" + }, + "connection": { + "name": "Verbindungsstatus", + "description": "Zeigt an, ob aktuelle gültige Preisdaten von Tibber vorliegen" + } + } + } +} \ 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 237e7ff..d7a8723 100644 --- a/custom_components/tibber_prices/translations/en.json +++ b/custom_components/tibber_prices/translations/en.json @@ -9,12 +9,95 @@ } }, "error": { - "auth": "Tibber Access Token is wrong.", - "connection": "Unable to connect to the server.", - "unknown": "Unknown error occurred." + "auth": "The Tibber Access Token is invalid.", + "connection": "Unable to connect to Tibber. Please check your internet connection.", + "unknown": "An unexpected error occurred. Please check the logs for details." }, "abort": { - "already_configured": "This entry is already configured." + "already_configured": "This entry is already configured.", + "entry_not_found": "Tibber configuration entry not found." + } + }, + "options": { + "step": { + "init": { + "title": "Update Tibber Configuration", + "description": "Update your Tibber API access token. If you need a new token, you can generate one at https://developer.tibber.com/settings/access-token", + "data": { + "access_token": "Tibber Access Token" + } + } + }, + "error": { + "auth": "The Tibber Access Token is invalid.", + "connection": "Unable to connect to Tibber. Please check your internet connection.", + "unknown": "An unexpected error occurred. Please check the logs for details.", + "different_account": "The new access token belongs to a different Tibber account. Please use a token from the same account or create a new configuration for the other account." + }, + "abort": { + "entry_not_found": "Tibber configuration entry not found." + } + }, + "entity": { + "sensor": { + "current_price": { + "name": "Current Price", + "description": "The current hour's electricity price including taxes" + }, + "next_hour_price": { + "name": "Next Hour Price", + "description": "The next hour's electricity price including taxes" + }, + "price_level": { + "name": "Price Level", + "description": "Current price level indicator (VERY_CHEAP, CHEAP, NORMAL, EXPENSIVE, VERY_EXPENSIVE)" + }, + "lowest_price_today": { + "name": "Lowest Price Today", + "description": "The lowest electricity price for the current day" + }, + "highest_price_today": { + "name": "Highest Price Today", + "description": "The highest electricity price for the current day" + }, + "average_price_today": { + "name": "Average Price Today", + "description": "The average electricity price for the current day" + }, + "hourly_rating": { + "name": "Hourly Price Rating", + "description": "Price comparison with historical data for the current hour (percentage difference)" + }, + "daily_rating": { + "name": "Daily Price Rating", + "description": "Price comparison with historical data for the current day (percentage difference)" + }, + "monthly_rating": { + "name": "Monthly Price Rating", + "description": "Price comparison with historical data for the current month (percentage difference)" + }, + "data_timestamp": { + "name": "Last Data Update", + "description": "Timestamp of the most recent price data received from Tibber" + }, + "tomorrow_data_available": { + "name": "Tomorrow's Data Available", + "description": "Indicates if price data for tomorrow is available (Yes/No/Partial)" + } + }, + "binary_sensor": { + "peak_hour": { + "name": "Peak Hour", + "description": "Indicates if the current hour is in the top 20% most expensive hours of the day" + }, + "best_price_hour": { + "name": "Best Price Hour", + "description": "Indicates if the current hour is in the bottom 20% cheapest hours of the day" + }, + "connection": { + "name": "Connection Status", + "description": "Indicates if we have valid current price data from Tibber" + } } } } \ No newline at end of file