mirror of
https://github.com/jpawlowski/hass.tibber_prices.git
synced 2026-03-29 21:03:40 +00:00
fix(imports): update imports after utils package reorganization
Updated all imports to reflect new module structure:
1. Utils package imports:
- average_utils → utils.average
- price_utils → utils.price
- Added MINUTES_PER_INTERVAL imports from const.py
2. Entity utils imports:
- Added entity_utils.helpers imports where needed
- Fixed find_rolling_hour_center_index import paths
- Added get_price_value import in binary_sensor
3. Type imports:
- Added coordinator/period_handlers/types.py MINUTES_PER_INTERVAL
re-export (with noqa:F401) for period handler modules
4. Platform imports:
- Updated sensor platform imports (utils.average, utils.price)
- Updated binary_sensor imports (entity_utils helpers)
- Updated coordinator imports (utils packages)
All import paths validated:
✓ Integration loads successfully
✓ All service handlers importable
✓ No circular dependencies
✓ Lint checks passing
Impact: Clean import structure, no breaking changes to functionality.
All sensors and services work identically to before.
This commit is contained in:
parent
4876a2cc29
commit
5ab7703d90
13 changed files with 315 additions and 185 deletions
|
|
@ -1,4 +1,18 @@
|
||||||
"""API client package for Tibber Prices integration."""
|
"""
|
||||||
|
Tibber GraphQL API client package.
|
||||||
|
|
||||||
|
This package handles all communication with Tibber's GraphQL API:
|
||||||
|
- GraphQL query construction and execution
|
||||||
|
- Authentication and session management
|
||||||
|
- Error handling and retry logic
|
||||||
|
- Response parsing and validation
|
||||||
|
|
||||||
|
Main components:
|
||||||
|
- client.py: TibberPricesApiClient (aiohttp-based GraphQL client)
|
||||||
|
- queries.py: GraphQL query definitions
|
||||||
|
- exceptions.py: API-specific error classes
|
||||||
|
- helpers.py: Response parsing utilities
|
||||||
|
"""
|
||||||
|
|
||||||
from .client import TibberPricesApiClient
|
from .client import TibberPricesApiClient
|
||||||
from .exceptions import (
|
from .exceptions import (
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,17 @@
|
||||||
"""Binary sensor platform for tibber_prices."""
|
"""
|
||||||
|
Binary sensor platform for Tibber Prices integration.
|
||||||
|
|
||||||
|
Provides binary (on/off) sensors for price-based automation:
|
||||||
|
- Best price period detection (cheapest intervals)
|
||||||
|
- Peak price period detection (most expensive intervals)
|
||||||
|
- Price threshold indicators (below/above configured limits)
|
||||||
|
- Tomorrow data availability status
|
||||||
|
|
||||||
|
These sensors enable simple automations like "run dishwasher during
|
||||||
|
cheap periods" without complex template logic.
|
||||||
|
|
||||||
|
See definitions.py for complete binary sensor catalog.
|
||||||
|
"""
|
||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -4,13 +4,8 @@ from __future__ import annotations
|
||||||
|
|
||||||
from typing import TYPE_CHECKING
|
from typing import TYPE_CHECKING
|
||||||
|
|
||||||
from custom_components.tibber_prices.const import (
|
|
||||||
CONF_EXTENDED_DESCRIPTIONS,
|
|
||||||
DEFAULT_EXTENDED_DESCRIPTIONS,
|
|
||||||
async_get_entity_description,
|
|
||||||
get_entity_description,
|
|
||||||
)
|
|
||||||
from custom_components.tibber_prices.entity_utils import add_icon_color_attribute
|
from custom_components.tibber_prices.entity_utils import add_icon_color_attribute
|
||||||
|
from custom_components.tibber_prices.utils.average import round_to_nearest_quarter_hour
|
||||||
from homeassistant.util import dt as dt_util
|
from homeassistant.util import dt as dt_util
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
|
|
@ -280,7 +275,7 @@ async def build_async_extra_state_attributes( # noqa: PLR0913
|
||||||
hass: HomeAssistant,
|
hass: HomeAssistant,
|
||||||
*,
|
*,
|
||||||
config_entry: TibberPricesConfigEntry,
|
config_entry: TibberPricesConfigEntry,
|
||||||
dynamic_attrs: dict | None = None,
|
sensor_attrs: dict | None = None,
|
||||||
is_on: bool | None = None,
|
is_on: bool | None = None,
|
||||||
) -> dict | None:
|
) -> dict | None:
|
||||||
"""
|
"""
|
||||||
|
|
@ -293,69 +288,44 @@ async def build_async_extra_state_attributes( # noqa: PLR0913
|
||||||
translation_key: Translation key for entity
|
translation_key: Translation key for entity
|
||||||
hass: Home Assistant instance
|
hass: Home Assistant instance
|
||||||
config_entry: Config entry with options (keyword-only)
|
config_entry: Config entry with options (keyword-only)
|
||||||
dynamic_attrs: Dynamic attributes from attribute getter (keyword-only)
|
sensor_attrs: Sensor-specific attributes (keyword-only)
|
||||||
is_on: Binary sensor state (keyword-only)
|
is_on: Binary sensor state (keyword-only)
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
Complete attributes dict with descriptions
|
Complete attributes dict with descriptions
|
||||||
|
|
||||||
"""
|
"""
|
||||||
attributes = {}
|
# Calculate default timestamp: current time rounded to nearest quarter hour
|
||||||
|
# This ensures all binary sensors have a consistent reference time for when calculations were made
|
||||||
|
# Individual sensors can override this via sensor_attrs if needed
|
||||||
|
now = dt_util.now()
|
||||||
|
default_timestamp = round_to_nearest_quarter_hour(now)
|
||||||
|
|
||||||
# Add dynamic attributes first
|
attributes = {
|
||||||
if dynamic_attrs:
|
"timestamp": default_timestamp.isoformat(),
|
||||||
|
}
|
||||||
|
|
||||||
|
# Add sensor-specific attributes (may override timestamp)
|
||||||
|
if sensor_attrs:
|
||||||
# Copy and remove internal fields before exposing to user
|
# Copy and remove internal fields before exposing to user
|
||||||
clean_attrs = {k: v for k, v in dynamic_attrs.items() if not k.startswith("_")}
|
clean_attrs = {k: v for k, v in sensor_attrs.items() if not k.startswith("_")}
|
||||||
|
# Merge sensor attributes (can override default timestamp)
|
||||||
attributes.update(clean_attrs)
|
attributes.update(clean_attrs)
|
||||||
|
|
||||||
# Add icon_color for best/peak price period sensors using shared utility
|
# Add icon_color for best/peak price period sensors using shared utility
|
||||||
add_icon_color_attribute(attributes, entity_key, is_on=is_on)
|
add_icon_color_attribute(attributes, entity_key, is_on=is_on)
|
||||||
|
|
||||||
# Add description from the custom translations file
|
# Add description attributes (always last, via central utility)
|
||||||
if translation_key and hass is not None:
|
from ..entity_utils import async_add_description_attributes # noqa: PLC0415, TID252
|
||||||
# Get user's language preference
|
|
||||||
language = hass.config.language if hass.config.language else "en"
|
|
||||||
|
|
||||||
# Add basic description
|
await async_add_description_attributes(
|
||||||
description = await async_get_entity_description(
|
attributes,
|
||||||
hass,
|
|
||||||
"binary_sensor",
|
"binary_sensor",
|
||||||
translation_key,
|
translation_key,
|
||||||
language,
|
|
||||||
"description",
|
|
||||||
)
|
|
||||||
if description:
|
|
||||||
attributes["description"] = description
|
|
||||||
|
|
||||||
# Check if extended descriptions are enabled in the config
|
|
||||||
extended_descriptions = config_entry.options.get(
|
|
||||||
CONF_EXTENDED_DESCRIPTIONS,
|
|
||||||
config_entry.data.get(CONF_EXTENDED_DESCRIPTIONS, DEFAULT_EXTENDED_DESCRIPTIONS),
|
|
||||||
)
|
|
||||||
|
|
||||||
# Add extended descriptions if enabled
|
|
||||||
if extended_descriptions:
|
|
||||||
# Add long description if available
|
|
||||||
long_desc = await async_get_entity_description(
|
|
||||||
hass,
|
hass,
|
||||||
"binary_sensor",
|
config_entry,
|
||||||
translation_key,
|
position="end",
|
||||||
language,
|
|
||||||
"long_description",
|
|
||||||
)
|
)
|
||||||
if long_desc:
|
|
||||||
attributes["long_description"] = long_desc
|
|
||||||
|
|
||||||
# Add usage tips if available
|
|
||||||
usage_tips = await async_get_entity_description(
|
|
||||||
hass,
|
|
||||||
"binary_sensor",
|
|
||||||
translation_key,
|
|
||||||
language,
|
|
||||||
"usage_tips",
|
|
||||||
)
|
|
||||||
if usage_tips:
|
|
||||||
attributes["usage_tips"] = usage_tips
|
|
||||||
|
|
||||||
return attributes if attributes else None
|
return attributes if attributes else None
|
||||||
|
|
||||||
|
|
@ -366,7 +336,7 @@ def build_sync_extra_state_attributes( # noqa: PLR0913
|
||||||
hass: HomeAssistant,
|
hass: HomeAssistant,
|
||||||
*,
|
*,
|
||||||
config_entry: TibberPricesConfigEntry,
|
config_entry: TibberPricesConfigEntry,
|
||||||
dynamic_attrs: dict | None = None,
|
sensor_attrs: dict | None = None,
|
||||||
is_on: bool | None = None,
|
is_on: bool | None = None,
|
||||||
) -> dict | None:
|
) -> dict | None:
|
||||||
"""
|
"""
|
||||||
|
|
@ -379,65 +349,43 @@ def build_sync_extra_state_attributes( # noqa: PLR0913
|
||||||
translation_key: Translation key for entity
|
translation_key: Translation key for entity
|
||||||
hass: Home Assistant instance
|
hass: Home Assistant instance
|
||||||
config_entry: Config entry with options (keyword-only)
|
config_entry: Config entry with options (keyword-only)
|
||||||
dynamic_attrs: Dynamic attributes from attribute getter (keyword-only)
|
sensor_attrs: Sensor-specific attributes (keyword-only)
|
||||||
is_on: Binary sensor state (keyword-only)
|
is_on: Binary sensor state (keyword-only)
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
Complete attributes dict with cached descriptions
|
Complete attributes dict with cached descriptions
|
||||||
|
|
||||||
"""
|
"""
|
||||||
attributes = {}
|
# Calculate default timestamp: current time rounded to nearest quarter hour
|
||||||
|
# This ensures all binary sensors have a consistent reference time for when calculations were made
|
||||||
|
# Individual sensors can override this via sensor_attrs if needed
|
||||||
|
now = dt_util.now()
|
||||||
|
default_timestamp = round_to_nearest_quarter_hour(now)
|
||||||
|
|
||||||
# Add dynamic attributes first
|
attributes = {
|
||||||
if dynamic_attrs:
|
"timestamp": default_timestamp.isoformat(),
|
||||||
|
}
|
||||||
|
|
||||||
|
# Add sensor-specific attributes (may override timestamp)
|
||||||
|
if sensor_attrs:
|
||||||
# Copy and remove internal fields before exposing to user
|
# Copy and remove internal fields before exposing to user
|
||||||
clean_attrs = {k: v for k, v in dynamic_attrs.items() if not k.startswith("_")}
|
clean_attrs = {k: v for k, v in sensor_attrs.items() if not k.startswith("_")}
|
||||||
|
# Merge sensor attributes (can override default timestamp)
|
||||||
attributes.update(clean_attrs)
|
attributes.update(clean_attrs)
|
||||||
|
|
||||||
# Add icon_color for best/peak price period sensors using shared utility
|
# Add icon_color for best/peak price period sensors using shared utility
|
||||||
add_icon_color_attribute(attributes, entity_key, is_on=is_on)
|
add_icon_color_attribute(attributes, entity_key, is_on=is_on)
|
||||||
|
|
||||||
# Add descriptions from the cache (non-blocking)
|
# Add description attributes (always last, via central utility)
|
||||||
if translation_key and hass is not None:
|
from ..entity_utils import add_description_attributes # noqa: PLC0415, TID252
|
||||||
# Get user's language preference
|
|
||||||
language = hass.config.language if hass.config.language else "en"
|
|
||||||
|
|
||||||
# Add basic description from cache
|
add_description_attributes(
|
||||||
description = get_entity_description(
|
attributes,
|
||||||
"binary_sensor",
|
"binary_sensor",
|
||||||
translation_key,
|
translation_key,
|
||||||
language,
|
hass,
|
||||||
"description",
|
config_entry,
|
||||||
|
position="end",
|
||||||
)
|
)
|
||||||
if description:
|
|
||||||
attributes["description"] = description
|
|
||||||
|
|
||||||
# Check if extended descriptions are enabled in the config
|
|
||||||
extended_descriptions = config_entry.options.get(
|
|
||||||
CONF_EXTENDED_DESCRIPTIONS,
|
|
||||||
config_entry.data.get(CONF_EXTENDED_DESCRIPTIONS, DEFAULT_EXTENDED_DESCRIPTIONS),
|
|
||||||
)
|
|
||||||
|
|
||||||
# Add extended descriptions if enabled
|
|
||||||
if extended_descriptions:
|
|
||||||
# Add long description from cache
|
|
||||||
long_desc = get_entity_description(
|
|
||||||
"binary_sensor",
|
|
||||||
translation_key,
|
|
||||||
language,
|
|
||||||
"long_description",
|
|
||||||
)
|
|
||||||
if long_desc:
|
|
||||||
attributes["long_description"] = long_desc
|
|
||||||
|
|
||||||
# Add usage tips from cache
|
|
||||||
usage_tips = get_entity_description(
|
|
||||||
"binary_sensor",
|
|
||||||
translation_key,
|
|
||||||
language,
|
|
||||||
"usage_tips",
|
|
||||||
)
|
|
||||||
if usage_tips:
|
|
||||||
attributes["usage_tips"] = usage_tips
|
|
||||||
|
|
||||||
return attributes if attributes else None
|
return attributes if attributes else None
|
||||||
|
|
|
||||||
|
|
@ -46,8 +46,7 @@ class TibberPricesBinarySensor(TibberPricesEntity, BinarySensorEntity):
|
||||||
super().__init__(coordinator)
|
super().__init__(coordinator)
|
||||||
self.entity_description = entity_description
|
self.entity_description = entity_description
|
||||||
self._attr_unique_id = f"{coordinator.config_entry.entry_id}_{entity_description.key}"
|
self._attr_unique_id = f"{coordinator.config_entry.entry_id}_{entity_description.key}"
|
||||||
self._state_getter: Callable | None = self._get_state_getter()
|
self._state_getter: Callable | None = self._get_value_getter()
|
||||||
self._attribute_getter: Callable | None = self._get_attribute_getter()
|
|
||||||
self._time_sensitive_remove_listener: Callable | None = None
|
self._time_sensitive_remove_listener: Callable | None = None
|
||||||
|
|
||||||
async def async_added_to_hass(self) -> None:
|
async def async_added_to_hass(self) -> None:
|
||||||
|
|
@ -74,8 +73,8 @@ class TibberPricesBinarySensor(TibberPricesEntity, BinarySensorEntity):
|
||||||
"""Handle time-sensitive update from coordinator."""
|
"""Handle time-sensitive update from coordinator."""
|
||||||
self.async_write_ha_state()
|
self.async_write_ha_state()
|
||||||
|
|
||||||
def _get_state_getter(self) -> Callable | None:
|
def _get_value_getter(self) -> Callable | None:
|
||||||
"""Return the appropriate state getter method based on the sensor type."""
|
"""Return the appropriate value getter method based on the sensor type."""
|
||||||
key = self.entity_description.key
|
key = self.entity_description.key
|
||||||
|
|
||||||
state_getters = {
|
state_getters = {
|
||||||
|
|
@ -178,16 +177,21 @@ class TibberPricesBinarySensor(TibberPricesEntity, BinarySensorEntity):
|
||||||
"""Return attributes for tomorrow_data_available binary sensor."""
|
"""Return attributes for tomorrow_data_available binary sensor."""
|
||||||
return get_tomorrow_data_available_attributes(self.coordinator.data)
|
return get_tomorrow_data_available_attributes(self.coordinator.data)
|
||||||
|
|
||||||
def _get_attribute_getter(self) -> Callable | None:
|
def _get_sensor_attributes(self) -> dict | None:
|
||||||
"""Return the appropriate attribute getter method based on the sensor type."""
|
"""
|
||||||
|
Get sensor-specific attributes.
|
||||||
|
|
||||||
|
Returns a dictionary of sensor-specific attributes, or None if no
|
||||||
|
attributes are needed.
|
||||||
|
"""
|
||||||
key = self.entity_description.key
|
key = self.entity_description.key
|
||||||
|
|
||||||
if key == "peak_price_period":
|
if key == "peak_price_period":
|
||||||
return lambda: get_price_intervals_attributes(self.coordinator.data, reverse_sort=True)
|
return get_price_intervals_attributes(self.coordinator.data, reverse_sort=True)
|
||||||
if key == "best_price_period":
|
if key == "best_price_period":
|
||||||
return lambda: get_price_intervals_attributes(self.coordinator.data, reverse_sort=False)
|
return get_price_intervals_attributes(self.coordinator.data, reverse_sort=False)
|
||||||
if key == "tomorrow_data_available":
|
if key == "tomorrow_data_available":
|
||||||
return self._get_tomorrow_data_available_attributes
|
return self._get_tomorrow_data_available_attributes()
|
||||||
|
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
@ -241,10 +245,7 @@ class TibberPricesBinarySensor(TibberPricesEntity, BinarySensorEntity):
|
||||||
Returns True if any period starts between now and PERIOD_LOOKAHEAD_HOURS from now.
|
Returns True if any period starts between now and PERIOD_LOOKAHEAD_HOURS from now.
|
||||||
This provides a practical planning horizon instead of hard midnight cutoff.
|
This provides a practical planning horizon instead of hard midnight cutoff.
|
||||||
"""
|
"""
|
||||||
if not self._attribute_getter:
|
attrs = self._get_sensor_attributes()
|
||||||
return False
|
|
||||||
|
|
||||||
attrs = self._attribute_getter()
|
|
||||||
if not attrs or "periods" not in attrs:
|
if not attrs or "periods" not in attrs:
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
|
@ -271,13 +272,11 @@ class TibberPricesBinarySensor(TibberPricesEntity, BinarySensorEntity):
|
||||||
async def async_extra_state_attributes(self) -> dict | None:
|
async def async_extra_state_attributes(self) -> dict | None:
|
||||||
"""Return additional state attributes asynchronously."""
|
"""Return additional state attributes asynchronously."""
|
||||||
try:
|
try:
|
||||||
# Get the dynamic attributes if the getter is available
|
# Get the sensor-specific attributes
|
||||||
if not self.coordinator.data:
|
if not self.coordinator.data:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
dynamic_attrs = None
|
sensor_attrs = self._get_sensor_attributes()
|
||||||
if self._attribute_getter:
|
|
||||||
dynamic_attrs = self._attribute_getter()
|
|
||||||
|
|
||||||
# Use extracted function to build all attributes
|
# Use extracted function to build all attributes
|
||||||
return await build_async_extra_state_attributes(
|
return await build_async_extra_state_attributes(
|
||||||
|
|
@ -285,7 +284,7 @@ class TibberPricesBinarySensor(TibberPricesEntity, BinarySensorEntity):
|
||||||
self.entity_description.translation_key,
|
self.entity_description.translation_key,
|
||||||
self.hass,
|
self.hass,
|
||||||
config_entry=self.coordinator.config_entry,
|
config_entry=self.coordinator.config_entry,
|
||||||
dynamic_attrs=dynamic_attrs,
|
sensor_attrs=sensor_attrs,
|
||||||
is_on=self.is_on,
|
is_on=self.is_on,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
@ -303,13 +302,11 @@ class TibberPricesBinarySensor(TibberPricesEntity, BinarySensorEntity):
|
||||||
def extra_state_attributes(self) -> dict | None:
|
def extra_state_attributes(self) -> dict | None:
|
||||||
"""Return additional state attributes synchronously."""
|
"""Return additional state attributes synchronously."""
|
||||||
try:
|
try:
|
||||||
# Get the dynamic attributes if the getter is available
|
# Get the sensor-specific attributes
|
||||||
if not self.coordinator.data:
|
if not self.coordinator.data:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
dynamic_attrs = None
|
sensor_attrs = self._get_sensor_attributes()
|
||||||
if self._attribute_getter:
|
|
||||||
dynamic_attrs = self._attribute_getter()
|
|
||||||
|
|
||||||
# Use extracted function to build all attributes
|
# Use extracted function to build all attributes
|
||||||
return build_sync_extra_state_attributes(
|
return build_sync_extra_state_attributes(
|
||||||
|
|
@ -317,7 +314,7 @@ class TibberPricesBinarySensor(TibberPricesEntity, BinarySensorEntity):
|
||||||
self.entity_description.translation_key,
|
self.entity_description.translation_key,
|
||||||
self.hass,
|
self.hass,
|
||||||
config_entry=self.coordinator.config_entry,
|
config_entry=self.coordinator.config_entry,
|
||||||
dynamic_attrs=dynamic_attrs,
|
sensor_attrs=sensor_attrs,
|
||||||
is_on=self.is_on,
|
is_on=self.is_on,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -9,7 +9,6 @@ from homeassistant.components.binary_sensor import (
|
||||||
from homeassistant.const import EntityCategory
|
from homeassistant.const import EntityCategory
|
||||||
|
|
||||||
# Constants
|
# Constants
|
||||||
MINUTES_PER_INTERVAL = 15
|
|
||||||
MIN_TOMORROW_INTERVALS_15MIN = 96
|
MIN_TOMORROW_INTERVALS_15MIN = 96
|
||||||
|
|
||||||
# Look-ahead window for future period detection (hours)
|
# Look-ahead window for future period detection (hours)
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,21 @@
|
||||||
"""Config flow for Tibber Prices integration."""
|
"""
|
||||||
|
Configuration flow package for UI-based setup.
|
||||||
|
|
||||||
|
This package handles all user interaction for integration configuration:
|
||||||
|
- Initial setup: API token validation, home selection
|
||||||
|
- Subentry flow: Add additional Tibber homes
|
||||||
|
- Options flow: Multi-step configuration wizard
|
||||||
|
- Reauthentication: Token refresh when expired
|
||||||
|
|
||||||
|
Flow handlers:
|
||||||
|
- user_flow.py: Initial setup and reauth
|
||||||
|
- subentry_flow.py: Add additional homes
|
||||||
|
- options_flow.py: 6-step configuration wizard
|
||||||
|
|
||||||
|
Supporting modules:
|
||||||
|
- schemas.py: Form schema definitions (vol.Schema)
|
||||||
|
- validators.py: Input validation and API testing
|
||||||
|
"""
|
||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -17,6 +17,11 @@ from homeassistant.const import (
|
||||||
from homeassistant.core import HomeAssistant
|
from homeassistant.core import HomeAssistant
|
||||||
|
|
||||||
DOMAIN = "tibber_prices"
|
DOMAIN = "tibber_prices"
|
||||||
|
|
||||||
|
# Time constants
|
||||||
|
MINUTES_PER_INTERVAL = 15 # Tibber uses 15-minute intervals for price data
|
||||||
|
|
||||||
|
# Configuration keys
|
||||||
CONF_EXTENDED_DESCRIPTIONS = "extended_descriptions"
|
CONF_EXTENDED_DESCRIPTIONS = "extended_descriptions"
|
||||||
CONF_BEST_PRICE_FLEX = "best_price_flex"
|
CONF_BEST_PRICE_FLEX = "best_price_flex"
|
||||||
CONF_PEAK_PRICE_FLEX = "peak_price_flex"
|
CONF_PEAK_PRICE_FLEX = "peak_price_flex"
|
||||||
|
|
|
||||||
|
|
@ -26,7 +26,7 @@ from custom_components.tibber_prices.api import (
|
||||||
TibberPricesApiClientError,
|
TibberPricesApiClientError,
|
||||||
)
|
)
|
||||||
from custom_components.tibber_prices.const import DOMAIN
|
from custom_components.tibber_prices.const import DOMAIN
|
||||||
from custom_components.tibber_prices.price_utils import (
|
from custom_components.tibber_prices.utils.price import (
|
||||||
find_price_data_for_interval,
|
find_price_data_for_interval,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,7 @@ import logging
|
||||||
from typing import TYPE_CHECKING, Any
|
from typing import TYPE_CHECKING, Any
|
||||||
|
|
||||||
from custom_components.tibber_prices import const as _const
|
from custom_components.tibber_prices import const as _const
|
||||||
from custom_components.tibber_prices.price_utils import enrich_price_info_with_differences
|
from custom_components.tibber_prices.utils.price import enrich_price_info_with_differences
|
||||||
from homeassistant.util import dt as dt_util
|
from homeassistant.util import dt as dt_util
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
|
|
|
||||||
|
|
@ -13,7 +13,7 @@ if TYPE_CHECKING:
|
||||||
ThresholdConfig,
|
ThresholdConfig,
|
||||||
)
|
)
|
||||||
|
|
||||||
from custom_components.tibber_prices.price_utils import (
|
from custom_components.tibber_prices.utils.price import (
|
||||||
aggregate_period_levels,
|
aggregate_period_levels,
|
||||||
aggregate_period_ratings,
|
aggregate_period_ratings,
|
||||||
calculate_volatility_level,
|
calculate_volatility_level,
|
||||||
|
|
|
||||||
|
|
@ -13,11 +13,9 @@ from custom_components.tibber_prices.const import (
|
||||||
DEFAULT_VOLATILITY_THRESHOLD_HIGH,
|
DEFAULT_VOLATILITY_THRESHOLD_HIGH,
|
||||||
DEFAULT_VOLATILITY_THRESHOLD_MODERATE,
|
DEFAULT_VOLATILITY_THRESHOLD_MODERATE,
|
||||||
DEFAULT_VOLATILITY_THRESHOLD_VERY_HIGH,
|
DEFAULT_VOLATILITY_THRESHOLD_VERY_HIGH,
|
||||||
|
MINUTES_PER_INTERVAL, # noqa: F401 - Re-exported for period handler modules
|
||||||
)
|
)
|
||||||
|
|
||||||
# Constants
|
|
||||||
MINUTES_PER_INTERVAL = 15
|
|
||||||
|
|
||||||
# Log indentation levels for visual hierarchy
|
# Log indentation levels for visual hierarchy
|
||||||
INDENT_L0 = "" # Top level (calculate_periods_with_relaxation)
|
INDENT_L0 = "" # Top level (calculate_periods_with_relaxation)
|
||||||
INDENT_L1 = " " # Per-day loop
|
INDENT_L1 = " " # Per-day loop
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,17 @@
|
||||||
"""Sensor platform for Tibber Prices integration."""
|
"""
|
||||||
|
Sensor platform for Tibber Prices integration.
|
||||||
|
|
||||||
|
Provides electricity price sensors organized by calculation method:
|
||||||
|
- Interval-based: Current/next/previous price intervals
|
||||||
|
- Rolling hour: 5-interval sliding windows (2h 30m periods)
|
||||||
|
- Daily statistics: Min/max/avg within calendar day boundaries
|
||||||
|
- 24h windows: Trailing/leading statistics from current interval
|
||||||
|
- Future forecast: N-hour price predictions
|
||||||
|
- Volatility: Price variation analysis
|
||||||
|
- Diagnostic: System information and metadata
|
||||||
|
|
||||||
|
See definitions.py for complete sensor catalog.
|
||||||
|
"""
|
||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -11,12 +11,13 @@ from datetime import datetime, timedelta
|
||||||
from typing import TYPE_CHECKING, Any
|
from typing import TYPE_CHECKING, Any
|
||||||
|
|
||||||
from custom_components.tibber_prices.const import (
|
from custom_components.tibber_prices.const import (
|
||||||
|
MINUTES_PER_INTERVAL,
|
||||||
PRICE_LEVEL_MAPPING,
|
PRICE_LEVEL_MAPPING,
|
||||||
PRICE_RATING_MAPPING,
|
PRICE_RATING_MAPPING,
|
||||||
)
|
)
|
||||||
from custom_components.tibber_prices.entity_utils import add_icon_color_attribute
|
from custom_components.tibber_prices.entity_utils import add_icon_color_attribute
|
||||||
from custom_components.tibber_prices.price_utils import (
|
from custom_components.tibber_prices.utils.average import round_to_nearest_quarter_hour
|
||||||
MINUTES_PER_INTERVAL,
|
from custom_components.tibber_prices.utils.price import (
|
||||||
calculate_volatility_level,
|
calculate_volatility_level,
|
||||||
find_price_data_for_interval,
|
find_price_data_for_interval,
|
||||||
)
|
)
|
||||||
|
|
@ -27,6 +28,8 @@ if TYPE_CHECKING:
|
||||||
from custom_components.tibber_prices.coordinator import (
|
from custom_components.tibber_prices.coordinator import (
|
||||||
TibberPricesDataUpdateCoordinator,
|
TibberPricesDataUpdateCoordinator,
|
||||||
)
|
)
|
||||||
|
from custom_components.tibber_prices.data import TibberPricesConfigEntry
|
||||||
|
from homeassistant.core import HomeAssistant
|
||||||
|
|
||||||
# Constants
|
# Constants
|
||||||
MAX_FORECAST_INTERVALS = 8 # Show up to 8 future intervals (2 hours with 15-min intervals)
|
MAX_FORECAST_INTERVALS = 8 # Show up to 8 future intervals (2 hours with 15-min intervals)
|
||||||
|
|
@ -67,21 +70,11 @@ def _add_cached_trend_attributes(attributes: dict, key: str, cached_data: dict)
|
||||||
if key.startswith("price_trend_") and cached_data.get("trend_attributes"):
|
if key.startswith("price_trend_") and cached_data.get("trend_attributes"):
|
||||||
attributes.update(cached_data["trend_attributes"])
|
attributes.update(cached_data["trend_attributes"])
|
||||||
elif key == "current_price_trend" and cached_data.get("current_trend_attributes"):
|
elif key == "current_price_trend" and cached_data.get("current_trend_attributes"):
|
||||||
# Add timestamp of current interval FIRST (when calculation was made)
|
# Add cached attributes (timestamp already set by platform)
|
||||||
now = dt_util.now()
|
|
||||||
minute = (now.minute // 15) * 15
|
|
||||||
current_interval_timestamp = now.replace(minute=minute, second=0, microsecond=0)
|
|
||||||
attributes["timestamp"] = current_interval_timestamp.isoformat()
|
|
||||||
# Then add other cached attributes
|
|
||||||
attributes.update(cached_data["current_trend_attributes"])
|
attributes.update(cached_data["current_trend_attributes"])
|
||||||
elif key == "next_price_trend_change" and cached_data.get("trend_change_attributes"):
|
elif key == "next_price_trend_change" and cached_data.get("trend_change_attributes"):
|
||||||
# Add timestamp of current interval FIRST (when calculation was made)
|
# Add cached attributes (timestamp already set by platform)
|
||||||
# State contains the timestamp of the trend change itself
|
# State contains the timestamp of the trend change itself
|
||||||
now = dt_util.now()
|
|
||||||
minute = (now.minute // 15) * 15
|
|
||||||
current_interval_timestamp = now.replace(minute=minute, second=0, microsecond=0)
|
|
||||||
attributes["timestamp"] = current_interval_timestamp.isoformat()
|
|
||||||
# Then add other cached attributes
|
|
||||||
attributes.update(cached_data["trend_change_attributes"])
|
attributes.update(cached_data["trend_change_attributes"])
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -168,7 +161,6 @@ def build_sensor_attributes(
|
||||||
add_statistics_attributes(
|
add_statistics_attributes(
|
||||||
attributes=attributes,
|
attributes=attributes,
|
||||||
key=key,
|
key=key,
|
||||||
coordinator=coordinator,
|
|
||||||
cached_data=cached_data,
|
cached_data=cached_data,
|
||||||
)
|
)
|
||||||
elif key == "price_forecast":
|
elif key == "price_forecast":
|
||||||
|
|
@ -247,27 +239,35 @@ def add_current_interval_price_attributes(
|
||||||
"current_hour_price_rating",
|
"current_hour_price_rating",
|
||||||
]
|
]
|
||||||
|
|
||||||
# Set timestamp and interval data based on sensor type
|
# Set interval data based on sensor type
|
||||||
|
# For sensors showing data from OTHER intervals (next/previous), override timestamp with that interval's startsAt
|
||||||
|
# For current interval sensors, keep the default platform timestamp (calculation time)
|
||||||
interval_data = None
|
interval_data = None
|
||||||
if key in next_interval_sensors:
|
if key in next_interval_sensors:
|
||||||
target_time = now + timedelta(minutes=MINUTES_PER_INTERVAL)
|
target_time = now + timedelta(minutes=MINUTES_PER_INTERVAL)
|
||||||
interval_data = find_price_data_for_interval(price_info, target_time)
|
interval_data = find_price_data_for_interval(price_info, target_time)
|
||||||
attributes["timestamp"] = interval_data["startsAt"] if interval_data else None
|
# Override timestamp with the NEXT interval's startsAt (when that interval starts)
|
||||||
|
if interval_data:
|
||||||
|
attributes["timestamp"] = interval_data["startsAt"]
|
||||||
elif key in previous_interval_sensors:
|
elif key in previous_interval_sensors:
|
||||||
target_time = now - timedelta(minutes=MINUTES_PER_INTERVAL)
|
target_time = now - timedelta(minutes=MINUTES_PER_INTERVAL)
|
||||||
interval_data = find_price_data_for_interval(price_info, target_time)
|
interval_data = find_price_data_for_interval(price_info, target_time)
|
||||||
attributes["timestamp"] = interval_data["startsAt"] if interval_data else None
|
# Override timestamp with the PREVIOUS interval's startsAt
|
||||||
|
if interval_data:
|
||||||
|
attributes["timestamp"] = interval_data["startsAt"]
|
||||||
elif key in next_hour_sensors:
|
elif key in next_hour_sensors:
|
||||||
target_time = now + timedelta(hours=1)
|
target_time = now + timedelta(hours=1)
|
||||||
interval_data = find_price_data_for_interval(price_info, target_time)
|
interval_data = find_price_data_for_interval(price_info, target_time)
|
||||||
attributes["timestamp"] = interval_data["startsAt"] if interval_data else None
|
# Override timestamp with the center of the next rolling hour window
|
||||||
|
if interval_data:
|
||||||
|
attributes["timestamp"] = interval_data["startsAt"]
|
||||||
elif key in current_hour_sensors:
|
elif key in current_hour_sensors:
|
||||||
current_interval_data = get_current_interval_data(coordinator)
|
current_interval_data = get_current_interval_data(coordinator)
|
||||||
attributes["timestamp"] = current_interval_data["startsAt"] if current_interval_data else None
|
# Keep default timestamp (when calculation was made) for current hour sensors
|
||||||
else:
|
else:
|
||||||
current_interval_data = get_current_interval_data(coordinator)
|
current_interval_data = get_current_interval_data(coordinator)
|
||||||
interval_data = current_interval_data # Use current_interval_data as interval_data for current_interval_price
|
interval_data = current_interval_data # Use current_interval_data as interval_data for current_interval_price
|
||||||
attributes["timestamp"] = current_interval_data["startsAt"] if current_interval_data else None
|
# Keep default timestamp (current calculation time) for current interval sensors
|
||||||
|
|
||||||
# Add icon_color for price sensors (based on their price level)
|
# Add icon_color for price sensors (based on their price level)
|
||||||
if key in ["current_interval_price", "next_interval_price", "previous_interval_price"]:
|
if key in ["current_interval_price", "next_interval_price", "previous_interval_price"]:
|
||||||
|
|
@ -402,10 +402,22 @@ def add_price_rating_attributes(attributes: dict, rating: str) -> None:
|
||||||
add_icon_color_attribute(attributes, key="price_rating", state_value=rating)
|
add_icon_color_attribute(attributes, key="price_rating", state_value=rating)
|
||||||
|
|
||||||
|
|
||||||
|
def _get_day_midnight_timestamp(key: str) -> str:
|
||||||
|
"""Get midnight timestamp for a given day sensor key."""
|
||||||
|
now = dt_util.now()
|
||||||
|
local_midnight = dt_util.start_of_local_day(now)
|
||||||
|
|
||||||
|
if key.startswith("yesterday") or key == "average_price_yesterday":
|
||||||
|
local_midnight = local_midnight - timedelta(days=1)
|
||||||
|
elif key.startswith("tomorrow") or key == "average_price_tomorrow":
|
||||||
|
local_midnight = local_midnight + timedelta(days=1)
|
||||||
|
|
||||||
|
return local_midnight.isoformat()
|
||||||
|
|
||||||
|
|
||||||
def add_statistics_attributes(
|
def add_statistics_attributes(
|
||||||
attributes: dict,
|
attributes: dict,
|
||||||
key: str,
|
key: str,
|
||||||
coordinator: TibberPricesDataUpdateCoordinator,
|
|
||||||
cached_data: dict,
|
cached_data: dict,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""
|
"""
|
||||||
|
|
@ -414,21 +426,18 @@ def add_statistics_attributes(
|
||||||
Args:
|
Args:
|
||||||
attributes: Dictionary to add attributes to
|
attributes: Dictionary to add attributes to
|
||||||
key: The sensor entity key
|
key: The sensor entity key
|
||||||
coordinator: The data update coordinator
|
|
||||||
cached_data: Dictionary containing cached sensor data
|
cached_data: Dictionary containing cached sensor data
|
||||||
|
|
||||||
"""
|
"""
|
||||||
price_info = coordinator.data.get("priceInfo", {})
|
# Data timestamp sensor - shows API fetch time
|
||||||
now = dt_util.now()
|
|
||||||
|
|
||||||
if key == "data_timestamp":
|
if key == "data_timestamp":
|
||||||
# For data_timestamp sensor, use the latest timestamp from cached_data
|
|
||||||
latest_timestamp = cached_data.get("data_timestamp")
|
latest_timestamp = cached_data.get("data_timestamp")
|
||||||
if latest_timestamp:
|
if latest_timestamp:
|
||||||
attributes["timestamp"] = latest_timestamp.isoformat()
|
attributes["timestamp"] = latest_timestamp.isoformat()
|
||||||
elif key == "current_interval_price_rating":
|
return
|
||||||
interval_data = find_price_data_for_interval(price_info, now)
|
|
||||||
attributes["timestamp"] = interval_data["startsAt"] if interval_data else None
|
# Current interval price rating - add rating attributes
|
||||||
|
if key == "current_interval_price_rating":
|
||||||
if cached_data.get("last_rating_difference") is not None:
|
if cached_data.get("last_rating_difference") is not None:
|
||||||
attributes["diff_" + PERCENTAGE] = cached_data["last_rating_difference"]
|
attributes["diff_" + PERCENTAGE] = cached_data["last_rating_difference"]
|
||||||
if cached_data.get("last_rating_level") is not None:
|
if cached_data.get("last_rating_level") is not None:
|
||||||
|
|
@ -436,35 +445,42 @@ def add_statistics_attributes(
|
||||||
attributes["level_value"] = PRICE_RATING_MAPPING.get(
|
attributes["level_value"] = PRICE_RATING_MAPPING.get(
|
||||||
cached_data["last_rating_level"], cached_data["last_rating_level"]
|
cached_data["last_rating_level"], cached_data["last_rating_level"]
|
||||||
)
|
)
|
||||||
elif key in [
|
return
|
||||||
|
|
||||||
|
# Extreme value sensors - show when the extreme occurs
|
||||||
|
extreme_sensors = {
|
||||||
"lowest_price_today",
|
"lowest_price_today",
|
||||||
"highest_price_today",
|
"highest_price_today",
|
||||||
"lowest_price_tomorrow",
|
"lowest_price_tomorrow",
|
||||||
"highest_price_tomorrow",
|
"highest_price_tomorrow",
|
||||||
]:
|
}
|
||||||
# Use the timestamp from the interval that has the extreme price
|
if key in extreme_sensors:
|
||||||
if cached_data.get("last_extreme_interval"):
|
if cached_data.get("last_extreme_interval"):
|
||||||
attributes["timestamp"] = cached_data["last_extreme_interval"].get("startsAt")
|
extreme_starts_at = cached_data["last_extreme_interval"].get("startsAt")
|
||||||
else:
|
if extreme_starts_at:
|
||||||
# Fallback: use the first timestamp of the appropriate day
|
attributes["timestamp"] = extreme_starts_at
|
||||||
_add_fallback_timestamp(attributes, key, price_info)
|
return
|
||||||
elif key in [
|
|
||||||
|
# Daily average sensors - show midnight to indicate whole day
|
||||||
|
daily_avg_sensors = {"average_price_today", "average_price_tomorrow"}
|
||||||
|
if key in daily_avg_sensors:
|
||||||
|
attributes["timestamp"] = _get_day_midnight_timestamp(key)
|
||||||
|
return
|
||||||
|
|
||||||
|
# Daily aggregated level/rating sensors - show midnight to indicate whole day
|
||||||
|
daily_aggregated_sensors = {
|
||||||
"yesterday_price_level",
|
"yesterday_price_level",
|
||||||
"today_price_level",
|
"today_price_level",
|
||||||
"tomorrow_price_level",
|
"tomorrow_price_level",
|
||||||
"yesterday_price_rating",
|
"yesterday_price_rating",
|
||||||
"today_price_rating",
|
"today_price_rating",
|
||||||
"tomorrow_price_rating",
|
"tomorrow_price_rating",
|
||||||
]:
|
}
|
||||||
# Daily aggregated level/rating sensors - add timestamp
|
if key in daily_aggregated_sensors:
|
||||||
day_key = _get_day_key_from_sensor_key(key)
|
attributes["timestamp"] = _get_day_midnight_timestamp(key)
|
||||||
day_data = price_info.get(day_key, [])
|
return
|
||||||
if day_data:
|
|
||||||
# Use first timestamp of the day (00:00)
|
# All other statistics sensors - keep default timestamp (when calculation was made)
|
||||||
attributes["timestamp"] = day_data[0].get("startsAt")
|
|
||||||
else:
|
|
||||||
# Fallback: use the first timestamp of the appropriate day
|
|
||||||
_add_fallback_timestamp(attributes, key, price_info)
|
|
||||||
|
|
||||||
|
|
||||||
def _get_day_key_from_sensor_key(key: str) -> str:
|
def _get_day_key_from_sensor_key(key: str) -> str:
|
||||||
|
|
@ -978,3 +994,113 @@ def add_period_timing_attributes(
|
||||||
|
|
||||||
# Add icon_color for dynamic styling
|
# Add icon_color for dynamic styling
|
||||||
add_icon_color_attribute(attributes, key=key, state_value=state_value)
|
add_icon_color_attribute(attributes, key=key, state_value=state_value)
|
||||||
|
|
||||||
|
|
||||||
|
def build_extra_state_attributes( # noqa: PLR0913
|
||||||
|
entity_key: str,
|
||||||
|
translation_key: str | None,
|
||||||
|
hass: HomeAssistant,
|
||||||
|
*,
|
||||||
|
config_entry: TibberPricesConfigEntry,
|
||||||
|
coordinator_data: dict,
|
||||||
|
sensor_attrs: dict | None = None,
|
||||||
|
) -> dict[str, Any] | None:
|
||||||
|
"""
|
||||||
|
Build extra state attributes for sensors.
|
||||||
|
|
||||||
|
This function implements the unified attribute building pattern:
|
||||||
|
1. Generate default timestamp (current time rounded to nearest quarter hour)
|
||||||
|
2. Merge sensor-specific attributes (may override timestamp)
|
||||||
|
3. Preserve timestamp ordering (always FIRST in dict)
|
||||||
|
4. Add description attributes (always LAST)
|
||||||
|
|
||||||
|
Args:
|
||||||
|
entity_key: Entity key (e.g., "current_interval_price")
|
||||||
|
translation_key: Translation key for entity
|
||||||
|
hass: Home Assistant instance
|
||||||
|
config_entry: Config entry with options (keyword-only)
|
||||||
|
coordinator_data: Coordinator data dict (keyword-only)
|
||||||
|
sensor_attrs: Sensor-specific attributes (keyword-only)
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Complete attributes dict or None if no data available
|
||||||
|
|
||||||
|
"""
|
||||||
|
if not coordinator_data:
|
||||||
|
return None
|
||||||
|
|
||||||
|
# Calculate default timestamp: current time rounded to nearest quarter hour
|
||||||
|
# This ensures all sensors have a consistent reference time for when calculations were made
|
||||||
|
# Individual sensors can override this if they need a different timestamp
|
||||||
|
now = dt_util.now()
|
||||||
|
default_timestamp = round_to_nearest_quarter_hour(now)
|
||||||
|
|
||||||
|
# Special handling for chart_data_export: metadata → descriptions → service data
|
||||||
|
if entity_key == "chart_data_export":
|
||||||
|
attributes: dict[str, Any] = {
|
||||||
|
"timestamp": default_timestamp.isoformat(),
|
||||||
|
}
|
||||||
|
|
||||||
|
# Step 1: Add metadata (timestamp + error if present)
|
||||||
|
if sensor_attrs:
|
||||||
|
if "timestamp" in sensor_attrs and sensor_attrs["timestamp"] is not None:
|
||||||
|
# Chart data has its own timestamp (when service was last called)
|
||||||
|
attributes["timestamp"] = sensor_attrs["timestamp"]
|
||||||
|
|
||||||
|
if "error" in sensor_attrs:
|
||||||
|
attributes["error"] = sensor_attrs["error"]
|
||||||
|
|
||||||
|
# Step 2: Add descriptions before service data (via central utility)
|
||||||
|
from ..entity_utils import add_description_attributes # noqa: PLC0415, TID252
|
||||||
|
|
||||||
|
add_description_attributes(
|
||||||
|
attributes,
|
||||||
|
"sensor",
|
||||||
|
translation_key,
|
||||||
|
hass,
|
||||||
|
config_entry,
|
||||||
|
position="before_service_data",
|
||||||
|
)
|
||||||
|
|
||||||
|
# Step 3: Add service data (everything except metadata)
|
||||||
|
if sensor_attrs:
|
||||||
|
attributes.update({k: v for k, v in sensor_attrs.items() if k not in ("timestamp", "error")})
|
||||||
|
|
||||||
|
return attributes if attributes else None
|
||||||
|
|
||||||
|
# For all other sensors: standard behavior
|
||||||
|
# Start with default timestamp
|
||||||
|
attributes: dict[str, Any] = {
|
||||||
|
"timestamp": default_timestamp.isoformat(),
|
||||||
|
}
|
||||||
|
|
||||||
|
# Add sensor-specific attributes (may override timestamp)
|
||||||
|
if sensor_attrs:
|
||||||
|
# Extract timestamp override if present
|
||||||
|
timestamp_override = sensor_attrs.pop("timestamp", None)
|
||||||
|
|
||||||
|
# Add all other sensor attributes
|
||||||
|
attributes.update(sensor_attrs)
|
||||||
|
|
||||||
|
# If sensor wants to override timestamp, rebuild dict with timestamp FIRST
|
||||||
|
if timestamp_override is not None:
|
||||||
|
temp_attrs = dict(attributes)
|
||||||
|
attributes.clear()
|
||||||
|
attributes["timestamp"] = timestamp_override
|
||||||
|
for key, value in temp_attrs.items():
|
||||||
|
if key != "timestamp":
|
||||||
|
attributes[key] = value
|
||||||
|
|
||||||
|
# Add description attributes (always last, via central utility)
|
||||||
|
from ..entity_utils import add_description_attributes # noqa: PLC0415, TID252
|
||||||
|
|
||||||
|
add_description_attributes(
|
||||||
|
attributes,
|
||||||
|
"sensor",
|
||||||
|
translation_key,
|
||||||
|
hass,
|
||||||
|
config_entry,
|
||||||
|
position="end",
|
||||||
|
)
|
||||||
|
|
||||||
|
return attributes if attributes else None
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue