mirror of
https://github.com/jpawlowski/hass.tibber_prices.git
synced 2026-03-30 13:23:41 +00:00
feat(icons): add dynamic icons and colors for all sensor types
Implemented comprehensive dynamic icon and color system across all sensor types: Price Sensors (5 sensors): - Current/hour prices: Dynamic cash-family icons based on price level (cash-multiple/plus/cash/minus/remove) - Next/previous: Static contextual icons (cash-fast, cash-refund, clock-fast) - All have icon_color attribute for card-mod styling Price Level Sensors (5 sensors): - Dynamic gauge-family icons: gauge-empty → gauge-low → gauge → gauge-full → alert - icon_color attribute with CSS variables (green/gray/orange/red) Price Rating Sensors (5 sensors): - Dynamic thumb-family icons: thumb-up → thumbs-up-down → thumb-down - icon_color attribute for LOW/NORMAL/HIGH ratings Volatility Sensors (4 sensors): - Dynamic chart-family icons: chart-line-variant → chart-timeline-variant → chart-bar → chart-scatter-plot - icon_color attribute for LOW/MODERATE/HIGH/VERY_HIGH levels Trend Sensors (8 sensors): - Dynamic trend icons: trending-up/down/neutral based on price movement - icon_color attribute (red=rising, green=falling, gray=stable) Binary Sensors (2 sensors): - Best Price Period: piggy-bank (ON) / timer-sand or timer-sand-complete (OFF) - Peak Price Period: alert-circle (ON) / shield-check or shield-check-outline (OFF) - 6-hour lookahead window for intelligent OFF state icons - icon_color attribute for all states Technical implementation: - PRICE_LEVEL_CASH_ICON_MAPPING in const.py for price sensor icons - PRICE_SENSOR_ICON_MAPPING removed (static icons now in entity descriptions) - Centralized icon logic in sensor.py icon property - All color mappings use CSS variables for theme compatibility - Binary sensors detect future periods within 6-hour window Impact: Users now have visual indicators for all price-related states without requiring card-mod. Optional card-mod styling available via icon_color attribute for advanced customization. Icons update dynamically as price levels, ratings, volatility, and trends change throughout the day.
This commit is contained in:
parent
fe5af68f8e
commit
c4f36d04de
3 changed files with 394 additions and 24 deletions
|
|
@ -2,6 +2,7 @@
|
|||
|
||||
from __future__ import annotations
|
||||
|
||||
from datetime import timedelta
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from homeassistant.components.binary_sensor import (
|
||||
|
|
@ -27,6 +28,8 @@ if TYPE_CHECKING:
|
|||
from .data import TibberPricesConfigEntry
|
||||
|
||||
from .const import (
|
||||
BINARY_SENSOR_COLOR_MAPPING,
|
||||
BINARY_SENSOR_ICON_MAPPING,
|
||||
CONF_EXTENDED_DESCRIPTIONS,
|
||||
DEFAULT_EXTENDED_DESCRIPTIONS,
|
||||
async_get_entity_description,
|
||||
|
|
@ -36,6 +39,10 @@ from .const import (
|
|||
MINUTES_PER_INTERVAL = 15
|
||||
MIN_TOMORROW_INTERVALS_15MIN = 96
|
||||
|
||||
# Look-ahead window for future period detection (hours)
|
||||
# Icons will show "waiting" state if a period starts within this window
|
||||
PERIOD_LOOKAHEAD_HOURS = 6
|
||||
|
||||
ENTITY_DESCRIPTIONS = (
|
||||
BinarySensorEntityDescription(
|
||||
key="peak_price_period",
|
||||
|
|
@ -434,6 +441,63 @@ class TibberPricesBinarySensor(TibberPricesEntity, BinarySensorEntity):
|
|||
)
|
||||
return None
|
||||
|
||||
@property
|
||||
def icon(self) -> str | None:
|
||||
"""Return the icon based on binary sensor state."""
|
||||
key = self.entity_description.key
|
||||
|
||||
# Dynamic icons for best/peak price period sensors
|
||||
if key in BINARY_SENSOR_ICON_MAPPING:
|
||||
if self.is_on:
|
||||
# Sensor is ON - use "on" icon
|
||||
icon = BINARY_SENSOR_ICON_MAPPING[key].get("on")
|
||||
else:
|
||||
# Sensor is OFF - check if future periods exist
|
||||
has_future_periods = self._has_future_periods()
|
||||
if has_future_periods:
|
||||
icon = BINARY_SENSOR_ICON_MAPPING[key].get("off")
|
||||
else:
|
||||
icon = BINARY_SENSOR_ICON_MAPPING[key].get("off_no_future")
|
||||
|
||||
if icon:
|
||||
return icon
|
||||
|
||||
# For all other sensors, use static icon from entity description
|
||||
return self.entity_description.icon
|
||||
|
||||
def _has_future_periods(self) -> bool:
|
||||
"""
|
||||
Check if there are periods starting within the next 6 hours.
|
||||
|
||||
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.
|
||||
"""
|
||||
if not self._attribute_getter:
|
||||
return False
|
||||
|
||||
attrs = self._attribute_getter()
|
||||
if not attrs or "periods" not in attrs:
|
||||
return False
|
||||
|
||||
now = dt_util.now()
|
||||
horizon = now + timedelta(hours=PERIOD_LOOKAHEAD_HOURS)
|
||||
periods = attrs.get("periods", [])
|
||||
|
||||
# Check if any period starts within the look-ahead window
|
||||
for period in periods:
|
||||
start_str = period.get("start")
|
||||
if start_str:
|
||||
# Parse datetime if it's a string, otherwise use as-is
|
||||
start_time = dt_util.parse_datetime(start_str) if isinstance(start_str, str) else start_str
|
||||
|
||||
if start_time:
|
||||
start_time_local = dt_util.as_local(start_time)
|
||||
# Period starts in the future but within our horizon
|
||||
if now < start_time_local <= horizon:
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
@property
|
||||
async def async_extra_state_attributes(self) -> dict | None:
|
||||
"""Return additional state attributes asynchronously."""
|
||||
|
|
@ -450,6 +514,14 @@ class TibberPricesBinarySensor(TibberPricesEntity, BinarySensorEntity):
|
|||
clean_attrs = {k: v for k, v in dynamic_attrs.items() if not k.startswith("_")}
|
||||
attributes.update(clean_attrs)
|
||||
|
||||
# Add icon_color for best/peak price period sensors
|
||||
key = self.entity_description.key
|
||||
if key in BINARY_SENSOR_COLOR_MAPPING:
|
||||
state = "on" if self.is_on else "off"
|
||||
color = BINARY_SENSOR_COLOR_MAPPING[key].get(state)
|
||||
if color:
|
||||
attributes["icon_color"] = color
|
||||
|
||||
# Add descriptions from the custom translations file
|
||||
if self.entity_description.translation_key and self.hass is not None:
|
||||
# Get user's language preference
|
||||
|
|
@ -524,6 +596,14 @@ class TibberPricesBinarySensor(TibberPricesEntity, BinarySensorEntity):
|
|||
clean_attrs = {k: v for k, v in dynamic_attrs.items() if not k.startswith("_")}
|
||||
attributes.update(clean_attrs)
|
||||
|
||||
# Add icon_color for best/peak price period sensors
|
||||
key = self.entity_description.key
|
||||
if key in BINARY_SENSOR_COLOR_MAPPING:
|
||||
state = "on" if self.is_on else "off"
|
||||
color = BINARY_SENSOR_COLOR_MAPPING[key].get(state)
|
||||
if color:
|
||||
attributes["icon_color"] = color
|
||||
|
||||
# Add descriptions from the cache (non-blocking)
|
||||
if self.entity_description.translation_key and self.hass is not None:
|
||||
# Get user's language preference
|
||||
|
|
|
|||
|
|
@ -248,6 +248,65 @@ PRICE_RATING_MAPPING = {
|
|||
PRICE_RATING_HIGH: 1,
|
||||
}
|
||||
|
||||
# Icon mapping for price levels (dynamic icons based on level)
|
||||
PRICE_LEVEL_ICON_MAPPING = {
|
||||
PRICE_LEVEL_VERY_CHEAP: "mdi:gauge-empty",
|
||||
PRICE_LEVEL_CHEAP: "mdi:gauge-low",
|
||||
PRICE_LEVEL_NORMAL: "mdi:gauge",
|
||||
PRICE_LEVEL_EXPENSIVE: "mdi:gauge-full",
|
||||
PRICE_LEVEL_VERY_EXPENSIVE: "mdi:alert",
|
||||
}
|
||||
|
||||
# Color mapping for price levels (CSS variables for theme compatibility)
|
||||
PRICE_LEVEL_COLOR_MAPPING = {
|
||||
PRICE_LEVEL_VERY_CHEAP: "var(--success-color)",
|
||||
PRICE_LEVEL_CHEAP: "var(--success-color)",
|
||||
PRICE_LEVEL_NORMAL: "var(--state-icon-color)",
|
||||
PRICE_LEVEL_EXPENSIVE: "var(--warning-color)",
|
||||
PRICE_LEVEL_VERY_EXPENSIVE: "var(--error-color)",
|
||||
}
|
||||
|
||||
# Icon mapping for current price sensors (dynamic icons based on price level)
|
||||
# Used by current_price and current_hour_average sensors
|
||||
# Icon shows price level (cheap/normal/expensive), icon_color reinforces with color
|
||||
PRICE_LEVEL_CASH_ICON_MAPPING = {
|
||||
PRICE_LEVEL_VERY_CHEAP: "mdi:cash-multiple", # Many coins (save a lot!)
|
||||
PRICE_LEVEL_CHEAP: "mdi:cash-plus", # Cash with plus (good price)
|
||||
PRICE_LEVEL_NORMAL: "mdi:cash", # Standard cash icon
|
||||
PRICE_LEVEL_EXPENSIVE: "mdi:cash-minus", # Cash with minus (expensive)
|
||||
PRICE_LEVEL_VERY_EXPENSIVE: "mdi:cash-remove", # Cash crossed out (very expensive)
|
||||
}
|
||||
|
||||
# Icon mapping for price ratings (dynamic icons based on rating)
|
||||
PRICE_RATING_ICON_MAPPING = {
|
||||
PRICE_RATING_LOW: "mdi:thumb-up",
|
||||
PRICE_RATING_NORMAL: "mdi:thumbs-up-down",
|
||||
PRICE_RATING_HIGH: "mdi:thumb-down",
|
||||
}
|
||||
|
||||
# Color mapping for price ratings (CSS variables for theme compatibility)
|
||||
PRICE_RATING_COLOR_MAPPING = {
|
||||
PRICE_RATING_LOW: "var(--success-color)",
|
||||
PRICE_RATING_NORMAL: "var(--state-icon-color)",
|
||||
PRICE_RATING_HIGH: "var(--error-color)",
|
||||
}
|
||||
|
||||
# Icon mapping for volatility levels (dynamic icons based on volatility)
|
||||
VOLATILITY_ICON_MAPPING = {
|
||||
VOLATILITY_LOW: "mdi:chart-line-variant",
|
||||
VOLATILITY_MODERATE: "mdi:chart-timeline-variant",
|
||||
VOLATILITY_HIGH: "mdi:chart-bar",
|
||||
VOLATILITY_VERY_HIGH: "mdi:chart-scatter-plot",
|
||||
}
|
||||
|
||||
# Color mapping for volatility levels (CSS variables for theme compatibility)
|
||||
VOLATILITY_COLOR_MAPPING = {
|
||||
VOLATILITY_LOW: "var(--success-color)",
|
||||
VOLATILITY_MODERATE: "var(--info-color)",
|
||||
VOLATILITY_HIGH: "var(--warning-color)",
|
||||
VOLATILITY_VERY_HIGH: "var(--error-color)",
|
||||
}
|
||||
|
||||
# Mapping for comparing volatility levels (used for sorting)
|
||||
VOLATILITY_MAPPING = {
|
||||
VOLATILITY_LOW: 0,
|
||||
|
|
@ -256,6 +315,33 @@ VOLATILITY_MAPPING = {
|
|||
VOLATILITY_VERY_HIGH: 3,
|
||||
}
|
||||
|
||||
# Icon mapping for binary sensors (dynamic icons based on state)
|
||||
# Note: OFF state icons can vary based on whether future periods exist
|
||||
BINARY_SENSOR_ICON_MAPPING = {
|
||||
"best_price_period": {
|
||||
"on": "mdi:piggy-bank",
|
||||
"off": "mdi:timer-sand", # Has future periods
|
||||
"off_no_future": "mdi:timer-sand-complete", # No future periods today
|
||||
},
|
||||
"peak_price_period": {
|
||||
"on": "mdi:alert-circle",
|
||||
"off": "mdi:shield-check", # Has future periods
|
||||
"off_no_future": "mdi:shield-check-outline", # No future periods today
|
||||
},
|
||||
}
|
||||
|
||||
# Color mapping for binary sensors (CSS variables for theme compatibility)
|
||||
BINARY_SENSOR_COLOR_MAPPING = {
|
||||
"best_price_period": {
|
||||
"on": "var(--success-color)",
|
||||
"off": "var(--state-icon-color)",
|
||||
},
|
||||
"peak_price_period": {
|
||||
"on": "var(--error-color)",
|
||||
"off": "var(--state-icon-color)",
|
||||
},
|
||||
}
|
||||
|
||||
LOGGER = logging.getLogger(__package__)
|
||||
|
||||
# Path to custom translations directory
|
||||
|
|
|
|||
|
|
@ -37,8 +37,15 @@ from .const import (
|
|||
DEFAULT_PRICE_TREND_THRESHOLD_FALLING,
|
||||
DEFAULT_PRICE_TREND_THRESHOLD_RISING,
|
||||
DOMAIN,
|
||||
PRICE_LEVEL_CASH_ICON_MAPPING,
|
||||
PRICE_LEVEL_COLOR_MAPPING,
|
||||
PRICE_LEVEL_ICON_MAPPING,
|
||||
PRICE_LEVEL_MAPPING,
|
||||
PRICE_RATING_COLOR_MAPPING,
|
||||
PRICE_RATING_ICON_MAPPING,
|
||||
PRICE_RATING_MAPPING,
|
||||
VOLATILITY_COLOR_MAPPING,
|
||||
VOLATILITY_ICON_MAPPING,
|
||||
async_get_entity_description,
|
||||
format_price_unit_minor,
|
||||
get_entity_description,
|
||||
|
|
@ -76,7 +83,7 @@ PRICE_SENSORS = (
|
|||
key="current_price",
|
||||
translation_key="current_price",
|
||||
name="Current Electricity Price",
|
||||
icon="mdi:cash",
|
||||
icon="mdi:cash", # Dynamic: will show cash-multiple/plus/cash/minus/remove based on level
|
||||
device_class=SensorDeviceClass.MONETARY,
|
||||
suggested_display_precision=2,
|
||||
),
|
||||
|
|
@ -84,7 +91,7 @@ PRICE_SENSORS = (
|
|||
key="next_interval_price",
|
||||
translation_key="next_interval_price",
|
||||
name="Next Price",
|
||||
icon="mdi:clock-fast",
|
||||
icon="mdi:cash-fast", # Static: motion lines indicate "coming soon"
|
||||
device_class=SensorDeviceClass.MONETARY,
|
||||
suggested_display_precision=2,
|
||||
),
|
||||
|
|
@ -92,7 +99,7 @@ PRICE_SENSORS = (
|
|||
key="previous_interval_price",
|
||||
translation_key="previous_interval_price",
|
||||
name="Previous Electricity Price",
|
||||
icon="mdi:history",
|
||||
icon="mdi:cash-refund", # Static: arrow back indicates "past"
|
||||
device_class=SensorDeviceClass.MONETARY,
|
||||
entity_registry_enabled_default=False,
|
||||
suggested_display_precision=2,
|
||||
|
|
@ -101,7 +108,7 @@ PRICE_SENSORS = (
|
|||
key="current_hour_average",
|
||||
translation_key="current_hour_average",
|
||||
name="Current Hour Average Price",
|
||||
icon="mdi:cash",
|
||||
icon="mdi:cash", # Dynamic: will show cash-multiple/plus/cash/minus/remove based on level
|
||||
device_class=SensorDeviceClass.MONETARY,
|
||||
suggested_display_precision=1,
|
||||
),
|
||||
|
|
@ -109,7 +116,7 @@ PRICE_SENSORS = (
|
|||
key="next_hour_average",
|
||||
translation_key="next_hour_average",
|
||||
name="Next Hour Average Price",
|
||||
icon="mdi:clock-fast",
|
||||
icon="mdi:clock-fast", # Static: clock indicates "next time period"
|
||||
device_class=SensorDeviceClass.MONETARY,
|
||||
suggested_display_precision=1,
|
||||
),
|
||||
|
|
@ -1536,6 +1543,10 @@ class TibberPricesSensor(TibberPricesEntity, SensorEntity):
|
|||
"interval_count": len(prices_to_analyze),
|
||||
}
|
||||
|
||||
# Add icon_color for dynamic styling
|
||||
if volatility in VOLATILITY_COLOR_MAPPING:
|
||||
self._last_volatility_attributes["icon_color"] = VOLATILITY_COLOR_MAPPING[volatility]
|
||||
|
||||
# Add type-specific attributes
|
||||
self._add_volatility_type_attributes(volatility_type, price_info, thresholds)
|
||||
|
||||
|
|
@ -1729,21 +1740,147 @@ class TibberPricesSensor(TibberPricesEntity, SensorEntity):
|
|||
@property
|
||||
def icon(self) -> str | None:
|
||||
"""Return the icon based on sensor type and state."""
|
||||
# Dynamic icons for trend sensors
|
||||
if self.entity_description.key.startswith("price_trend_"):
|
||||
match self.native_value:
|
||||
case "rising":
|
||||
return "mdi:trending-up"
|
||||
case "falling":
|
||||
return "mdi:trending-down"
|
||||
case "stable":
|
||||
return "mdi:trending-neutral"
|
||||
case _:
|
||||
# Fallback to static icon if value is None or unknown
|
||||
return self.entity_description.icon
|
||||
key = self.entity_description.key
|
||||
value = self.native_value
|
||||
|
||||
# For all other sensors, use static icon from entity description
|
||||
return self.entity_description.icon
|
||||
# Try to get icon from various sources
|
||||
icon = (
|
||||
self._get_trend_icon(key, value)
|
||||
or self._get_price_sensor_icon(key)
|
||||
or self._get_level_sensor_icon(key, value)
|
||||
or self._get_rating_sensor_icon(key, value)
|
||||
or self._get_volatility_sensor_icon(key, value)
|
||||
)
|
||||
|
||||
# Fall back to static icon from entity description
|
||||
return icon or self.entity_description.icon
|
||||
|
||||
def _get_trend_icon(self, key: str, value: Any) -> str | None:
|
||||
"""Get icon for trend sensors."""
|
||||
if not key.startswith("price_trend_") or not isinstance(value, str):
|
||||
return None
|
||||
|
||||
trend_icons = {
|
||||
"rising": "mdi:trending-up",
|
||||
"falling": "mdi:trending-down",
|
||||
"stable": "mdi:trending-neutral",
|
||||
}
|
||||
return trend_icons.get(value)
|
||||
|
||||
def _get_price_sensor_icon(self, key: str) -> str | None:
|
||||
"""
|
||||
Get icon for current price sensors (dynamic based on price level).
|
||||
|
||||
Only current_price and current_hour_average have dynamic icons.
|
||||
Other price sensors (next/previous) use static icons from entity description.
|
||||
"""
|
||||
# Only current price sensors get dynamic icons
|
||||
if key == "current_price":
|
||||
level = self._get_price_level_for_sensor(key)
|
||||
if level:
|
||||
return PRICE_LEVEL_CASH_ICON_MAPPING.get(level.upper())
|
||||
elif key == "current_hour_average":
|
||||
level = self._get_hour_level_for_sensor(key)
|
||||
if level:
|
||||
return PRICE_LEVEL_CASH_ICON_MAPPING.get(level.upper())
|
||||
|
||||
# For all other price sensors, let entity description handle the icon
|
||||
return None
|
||||
|
||||
def _get_level_sensor_icon(self, key: str, value: Any) -> str | None:
|
||||
"""Get icon for price level sensors."""
|
||||
if key not in [
|
||||
"price_level",
|
||||
"next_interval_price_level",
|
||||
"previous_interval_price_level",
|
||||
"current_hour_price_level",
|
||||
"next_hour_price_level",
|
||||
] or not isinstance(value, str):
|
||||
return None
|
||||
|
||||
return PRICE_LEVEL_ICON_MAPPING.get(value.upper())
|
||||
|
||||
def _get_rating_sensor_icon(self, key: str, value: Any) -> str | None:
|
||||
"""Get icon for price rating sensors."""
|
||||
if key not in [
|
||||
"price_rating",
|
||||
"next_interval_price_rating",
|
||||
"previous_interval_price_rating",
|
||||
"current_hour_price_rating",
|
||||
"next_hour_price_rating",
|
||||
] or not isinstance(value, str):
|
||||
return None
|
||||
|
||||
return PRICE_RATING_ICON_MAPPING.get(value.upper())
|
||||
|
||||
def _get_volatility_sensor_icon(self, key: str, value: Any) -> str | None:
|
||||
"""Get icon for volatility sensors."""
|
||||
if not key.endswith("_volatility") or not isinstance(value, str):
|
||||
return None
|
||||
|
||||
return VOLATILITY_ICON_MAPPING.get(value.upper())
|
||||
|
||||
def _get_price_level_for_sensor(self, key: str) -> str | None:
|
||||
"""Get the price level for a price sensor (current/next/previous interval)."""
|
||||
if not self.coordinator.data:
|
||||
return None
|
||||
|
||||
price_info = self.coordinator.data.get("priceInfo", {})
|
||||
now = dt_util.now()
|
||||
|
||||
# Map sensor key to interval offset
|
||||
offset_map = {
|
||||
"current_price": 0,
|
||||
"next_interval_price": 1,
|
||||
"previous_interval_price": -1,
|
||||
}
|
||||
|
||||
interval_offset = offset_map.get(key)
|
||||
if interval_offset is None:
|
||||
return None
|
||||
|
||||
target_time = now + timedelta(minutes=MINUTES_PER_INTERVAL * interval_offset)
|
||||
interval_data = find_price_data_for_interval(price_info, target_time)
|
||||
|
||||
if not interval_data or "level" not in interval_data:
|
||||
return None
|
||||
|
||||
return interval_data["level"]
|
||||
|
||||
def _get_hour_level_for_sensor(self, key: str) -> str | None:
|
||||
"""Get the price level for an hour average sensor (current/next hour)."""
|
||||
if not self.coordinator.data:
|
||||
return None
|
||||
|
||||
# Map sensor key to hour offset
|
||||
offset_map = {
|
||||
"current_hour_average": 0,
|
||||
"next_hour_average": 1,
|
||||
}
|
||||
|
||||
hour_offset = offset_map.get(key)
|
||||
if hour_offset is None:
|
||||
return None
|
||||
|
||||
# Use the same logic as _get_rolling_hour_level_value
|
||||
price_info = self.coordinator.data.get("priceInfo", {})
|
||||
yesterday_prices = price_info.get("yesterday", [])
|
||||
today_prices = price_info.get("today", [])
|
||||
tomorrow_prices = price_info.get("tomorrow", [])
|
||||
|
||||
all_prices = yesterday_prices + today_prices + tomorrow_prices
|
||||
if not all_prices:
|
||||
return None
|
||||
|
||||
center_idx = self._find_rolling_hour_center_index(all_prices, hour_offset)
|
||||
if center_idx is None:
|
||||
return None
|
||||
|
||||
levels = self._collect_rolling_window_levels(all_prices, center_idx)
|
||||
if not levels:
|
||||
return None
|
||||
|
||||
return aggregate_price_levels(levels)
|
||||
|
||||
@property
|
||||
async def async_extra_state_attributes(self) -> dict | None:
|
||||
|
|
@ -1926,6 +2063,8 @@ class TibberPricesSensor(TibberPricesEntity, SensorEntity):
|
|||
"current_hour_price_rating",
|
||||
]
|
||||
|
||||
# Set timestamp and interval data based on sensor type
|
||||
interval_data = None
|
||||
if key in next_interval_sensors:
|
||||
target_time = now + timedelta(minutes=MINUTES_PER_INTERVAL)
|
||||
interval_data = find_price_data_for_interval(price_info, target_time)
|
||||
|
|
@ -1935,21 +2074,48 @@ class TibberPricesSensor(TibberPricesEntity, SensorEntity):
|
|||
interval_data = find_price_data_for_interval(price_info, target_time)
|
||||
attributes["timestamp"] = interval_data["startsAt"] if interval_data else None
|
||||
elif key in next_hour_sensors:
|
||||
# For next hour sensors, show timestamp 1 hour ahead
|
||||
target_time = now + timedelta(hours=1)
|
||||
interval_data = find_price_data_for_interval(price_info, target_time)
|
||||
attributes["timestamp"] = interval_data["startsAt"] if interval_data else None
|
||||
elif key in current_hour_sensors:
|
||||
# For current hour sensors, use current interval timestamp
|
||||
current_interval_data = self._get_current_interval_data()
|
||||
attributes["timestamp"] = current_interval_data["startsAt"] if current_interval_data else None
|
||||
else:
|
||||
# Default: use current interval timestamp
|
||||
current_interval_data = self._get_current_interval_data()
|
||||
attributes["timestamp"] = current_interval_data["startsAt"] if current_interval_data else None
|
||||
|
||||
# Add price level info for price level sensors
|
||||
if key == "price_level":
|
||||
# Add icon_color for price sensors (based on their price level)
|
||||
if key in ["current_price", "next_interval_price", "previous_interval_price"]:
|
||||
# For interval-based price sensors, get level from interval_data
|
||||
if interval_data and "level" in interval_data:
|
||||
level = interval_data["level"]
|
||||
if level in PRICE_LEVEL_COLOR_MAPPING:
|
||||
attributes["icon_color"] = PRICE_LEVEL_COLOR_MAPPING[level]
|
||||
elif key in ["current_hour_average", "next_hour_average"]:
|
||||
# For hour-based price sensors, get level from the corresponding level sensor
|
||||
level = self._get_hour_level_for_sensor(key)
|
||||
if level and level in PRICE_LEVEL_COLOR_MAPPING:
|
||||
attributes["icon_color"] = PRICE_LEVEL_COLOR_MAPPING[level]
|
||||
|
||||
# Add price level attributes for all level sensors
|
||||
self._add_level_attributes_for_sensor(attributes, key, interval_data)
|
||||
|
||||
# Add price rating attributes for all rating sensors
|
||||
self._add_rating_attributes_for_sensor(attributes, key, interval_data)
|
||||
|
||||
def _add_level_attributes_for_sensor(self, attributes: dict, key: str, interval_data: dict | None) -> None:
|
||||
"""Add price level attributes based on sensor type."""
|
||||
# For interval-based level sensors (next/previous), use interval data
|
||||
if key in ["next_interval_price_level", "previous_interval_price_level"]:
|
||||
if interval_data and "level" in interval_data:
|
||||
self._add_price_level_attributes(attributes, interval_data["level"])
|
||||
# For hour-aggregated level sensors, use native_value
|
||||
elif key in ["current_hour_price_level", "next_hour_price_level"]:
|
||||
level_value = self.native_value
|
||||
if level_value and isinstance(level_value, str):
|
||||
self._add_price_level_attributes(attributes, level_value.upper())
|
||||
# For current price level sensor
|
||||
elif key == "price_level":
|
||||
current_interval_data = self._get_current_interval_data()
|
||||
if current_interval_data and "level" in current_interval_data:
|
||||
self._add_price_level_attributes(attributes, current_interval_data["level"])
|
||||
|
|
@ -1967,6 +2133,44 @@ class TibberPricesSensor(TibberPricesEntity, SensorEntity):
|
|||
attributes["level_value"] = PRICE_LEVEL_MAPPING[level]
|
||||
attributes["level_id"] = level
|
||||
|
||||
# Add icon_color for dynamic styling
|
||||
if level in PRICE_LEVEL_COLOR_MAPPING:
|
||||
attributes["icon_color"] = PRICE_LEVEL_COLOR_MAPPING[level]
|
||||
|
||||
def _add_rating_attributes_for_sensor(self, attributes: dict, key: str, interval_data: dict | None) -> None:
|
||||
"""Add price rating attributes based on sensor type."""
|
||||
# For interval-based rating sensors (next/previous), use interval data
|
||||
if key in ["next_interval_price_rating", "previous_interval_price_rating"]:
|
||||
if interval_data and "rating_level" in interval_data:
|
||||
self._add_price_rating_attributes(attributes, interval_data["rating_level"])
|
||||
# For hour-aggregated rating sensors, use native_value
|
||||
elif key in ["current_hour_price_rating", "next_hour_price_rating"]:
|
||||
rating_value = self.native_value
|
||||
if rating_value and isinstance(rating_value, str):
|
||||
self._add_price_rating_attributes(attributes, rating_value.upper())
|
||||
# For current price rating sensor
|
||||
elif key == "price_rating":
|
||||
current_interval_data = self._get_current_interval_data()
|
||||
if current_interval_data and "rating_level" in current_interval_data:
|
||||
self._add_price_rating_attributes(attributes, current_interval_data["rating_level"])
|
||||
|
||||
def _add_price_rating_attributes(self, attributes: dict, rating: str) -> None:
|
||||
"""
|
||||
Add price rating specific attributes.
|
||||
|
||||
Args:
|
||||
attributes: Dictionary to add attributes to
|
||||
rating: The price rating value (e.g., LOW, NORMAL, HIGH)
|
||||
|
||||
"""
|
||||
if rating in PRICE_RATING_MAPPING:
|
||||
attributes["rating_value"] = PRICE_RATING_MAPPING[rating]
|
||||
attributes["rating_id"] = rating
|
||||
|
||||
# Add icon_color for dynamic styling
|
||||
if rating in PRICE_RATING_COLOR_MAPPING:
|
||||
attributes["icon_color"] = PRICE_RATING_COLOR_MAPPING[rating]
|
||||
|
||||
def _find_price_timestamp(
|
||||
self,
|
||||
attributes: dict,
|
||||
|
|
|
|||
Loading…
Reference in a new issue