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 __future__ import annotations
|
||||||
|
|
||||||
|
from datetime import timedelta
|
||||||
from typing import TYPE_CHECKING
|
from typing import TYPE_CHECKING
|
||||||
|
|
||||||
from homeassistant.components.binary_sensor import (
|
from homeassistant.components.binary_sensor import (
|
||||||
|
|
@ -27,6 +28,8 @@ if TYPE_CHECKING:
|
||||||
from .data import TibberPricesConfigEntry
|
from .data import TibberPricesConfigEntry
|
||||||
|
|
||||||
from .const import (
|
from .const import (
|
||||||
|
BINARY_SENSOR_COLOR_MAPPING,
|
||||||
|
BINARY_SENSOR_ICON_MAPPING,
|
||||||
CONF_EXTENDED_DESCRIPTIONS,
|
CONF_EXTENDED_DESCRIPTIONS,
|
||||||
DEFAULT_EXTENDED_DESCRIPTIONS,
|
DEFAULT_EXTENDED_DESCRIPTIONS,
|
||||||
async_get_entity_description,
|
async_get_entity_description,
|
||||||
|
|
@ -36,6 +39,10 @@ from .const import (
|
||||||
MINUTES_PER_INTERVAL = 15
|
MINUTES_PER_INTERVAL = 15
|
||||||
MIN_TOMORROW_INTERVALS_15MIN = 96
|
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 = (
|
ENTITY_DESCRIPTIONS = (
|
||||||
BinarySensorEntityDescription(
|
BinarySensorEntityDescription(
|
||||||
key="peak_price_period",
|
key="peak_price_period",
|
||||||
|
|
@ -434,6 +441,63 @@ class TibberPricesBinarySensor(TibberPricesEntity, BinarySensorEntity):
|
||||||
)
|
)
|
||||||
return None
|
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
|
@property
|
||||||
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."""
|
||||||
|
|
@ -450,6 +514,14 @@ class TibberPricesBinarySensor(TibberPricesEntity, BinarySensorEntity):
|
||||||
clean_attrs = {k: v for k, v in dynamic_attrs.items() if not k.startswith("_")}
|
clean_attrs = {k: v for k, v in dynamic_attrs.items() if not k.startswith("_")}
|
||||||
attributes.update(clean_attrs)
|
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
|
# Add descriptions from the custom translations file
|
||||||
if self.entity_description.translation_key and self.hass is not None:
|
if self.entity_description.translation_key and self.hass is not None:
|
||||||
# Get user's language preference
|
# 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("_")}
|
clean_attrs = {k: v for k, v in dynamic_attrs.items() if not k.startswith("_")}
|
||||||
attributes.update(clean_attrs)
|
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)
|
# Add descriptions from the cache (non-blocking)
|
||||||
if self.entity_description.translation_key and self.hass is not None:
|
if self.entity_description.translation_key and self.hass is not None:
|
||||||
# Get user's language preference
|
# Get user's language preference
|
||||||
|
|
|
||||||
|
|
@ -248,6 +248,65 @@ PRICE_RATING_MAPPING = {
|
||||||
PRICE_RATING_HIGH: 1,
|
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)
|
# Mapping for comparing volatility levels (used for sorting)
|
||||||
VOLATILITY_MAPPING = {
|
VOLATILITY_MAPPING = {
|
||||||
VOLATILITY_LOW: 0,
|
VOLATILITY_LOW: 0,
|
||||||
|
|
@ -256,6 +315,33 @@ VOLATILITY_MAPPING = {
|
||||||
VOLATILITY_VERY_HIGH: 3,
|
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__)
|
LOGGER = logging.getLogger(__package__)
|
||||||
|
|
||||||
# Path to custom translations directory
|
# Path to custom translations directory
|
||||||
|
|
|
||||||
|
|
@ -37,8 +37,15 @@ from .const import (
|
||||||
DEFAULT_PRICE_TREND_THRESHOLD_FALLING,
|
DEFAULT_PRICE_TREND_THRESHOLD_FALLING,
|
||||||
DEFAULT_PRICE_TREND_THRESHOLD_RISING,
|
DEFAULT_PRICE_TREND_THRESHOLD_RISING,
|
||||||
DOMAIN,
|
DOMAIN,
|
||||||
|
PRICE_LEVEL_CASH_ICON_MAPPING,
|
||||||
|
PRICE_LEVEL_COLOR_MAPPING,
|
||||||
|
PRICE_LEVEL_ICON_MAPPING,
|
||||||
PRICE_LEVEL_MAPPING,
|
PRICE_LEVEL_MAPPING,
|
||||||
|
PRICE_RATING_COLOR_MAPPING,
|
||||||
|
PRICE_RATING_ICON_MAPPING,
|
||||||
PRICE_RATING_MAPPING,
|
PRICE_RATING_MAPPING,
|
||||||
|
VOLATILITY_COLOR_MAPPING,
|
||||||
|
VOLATILITY_ICON_MAPPING,
|
||||||
async_get_entity_description,
|
async_get_entity_description,
|
||||||
format_price_unit_minor,
|
format_price_unit_minor,
|
||||||
get_entity_description,
|
get_entity_description,
|
||||||
|
|
@ -76,7 +83,7 @@ PRICE_SENSORS = (
|
||||||
key="current_price",
|
key="current_price",
|
||||||
translation_key="current_price",
|
translation_key="current_price",
|
||||||
name="Current Electricity 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,
|
device_class=SensorDeviceClass.MONETARY,
|
||||||
suggested_display_precision=2,
|
suggested_display_precision=2,
|
||||||
),
|
),
|
||||||
|
|
@ -84,7 +91,7 @@ PRICE_SENSORS = (
|
||||||
key="next_interval_price",
|
key="next_interval_price",
|
||||||
translation_key="next_interval_price",
|
translation_key="next_interval_price",
|
||||||
name="Next Price",
|
name="Next Price",
|
||||||
icon="mdi:clock-fast",
|
icon="mdi:cash-fast", # Static: motion lines indicate "coming soon"
|
||||||
device_class=SensorDeviceClass.MONETARY,
|
device_class=SensorDeviceClass.MONETARY,
|
||||||
suggested_display_precision=2,
|
suggested_display_precision=2,
|
||||||
),
|
),
|
||||||
|
|
@ -92,7 +99,7 @@ PRICE_SENSORS = (
|
||||||
key="previous_interval_price",
|
key="previous_interval_price",
|
||||||
translation_key="previous_interval_price",
|
translation_key="previous_interval_price",
|
||||||
name="Previous Electricity Price",
|
name="Previous Electricity Price",
|
||||||
icon="mdi:history",
|
icon="mdi:cash-refund", # Static: arrow back indicates "past"
|
||||||
device_class=SensorDeviceClass.MONETARY,
|
device_class=SensorDeviceClass.MONETARY,
|
||||||
entity_registry_enabled_default=False,
|
entity_registry_enabled_default=False,
|
||||||
suggested_display_precision=2,
|
suggested_display_precision=2,
|
||||||
|
|
@ -101,7 +108,7 @@ PRICE_SENSORS = (
|
||||||
key="current_hour_average",
|
key="current_hour_average",
|
||||||
translation_key="current_hour_average",
|
translation_key="current_hour_average",
|
||||||
name="Current Hour Average Price",
|
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,
|
device_class=SensorDeviceClass.MONETARY,
|
||||||
suggested_display_precision=1,
|
suggested_display_precision=1,
|
||||||
),
|
),
|
||||||
|
|
@ -109,7 +116,7 @@ PRICE_SENSORS = (
|
||||||
key="next_hour_average",
|
key="next_hour_average",
|
||||||
translation_key="next_hour_average",
|
translation_key="next_hour_average",
|
||||||
name="Next Hour Average Price",
|
name="Next Hour Average Price",
|
||||||
icon="mdi:clock-fast",
|
icon="mdi:clock-fast", # Static: clock indicates "next time period"
|
||||||
device_class=SensorDeviceClass.MONETARY,
|
device_class=SensorDeviceClass.MONETARY,
|
||||||
suggested_display_precision=1,
|
suggested_display_precision=1,
|
||||||
),
|
),
|
||||||
|
|
@ -1536,6 +1543,10 @@ class TibberPricesSensor(TibberPricesEntity, SensorEntity):
|
||||||
"interval_count": len(prices_to_analyze),
|
"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
|
# Add type-specific attributes
|
||||||
self._add_volatility_type_attributes(volatility_type, price_info, thresholds)
|
self._add_volatility_type_attributes(volatility_type, price_info, thresholds)
|
||||||
|
|
||||||
|
|
@ -1729,21 +1740,147 @@ class TibberPricesSensor(TibberPricesEntity, SensorEntity):
|
||||||
@property
|
@property
|
||||||
def icon(self) -> str | None:
|
def icon(self) -> str | None:
|
||||||
"""Return the icon based on sensor type and state."""
|
"""Return the icon based on sensor type and state."""
|
||||||
# Dynamic icons for trend sensors
|
key = self.entity_description.key
|
||||||
if self.entity_description.key.startswith("price_trend_"):
|
value = self.native_value
|
||||||
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
|
|
||||||
|
|
||||||
# For all other sensors, use static icon from entity description
|
# Try to get icon from various sources
|
||||||
return self.entity_description.icon
|
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
|
@property
|
||||||
async def async_extra_state_attributes(self) -> dict | None:
|
async def async_extra_state_attributes(self) -> dict | None:
|
||||||
|
|
@ -1926,6 +2063,8 @@ class TibberPricesSensor(TibberPricesEntity, SensorEntity):
|
||||||
"current_hour_price_rating",
|
"current_hour_price_rating",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
# Set timestamp and interval data based on sensor type
|
||||||
|
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)
|
||||||
|
|
@ -1935,21 +2074,48 @@ class TibberPricesSensor(TibberPricesEntity, SensorEntity):
|
||||||
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
|
attributes["timestamp"] = interval_data["startsAt"] if interval_data else None
|
||||||
elif key in next_hour_sensors:
|
elif key in next_hour_sensors:
|
||||||
# For next hour sensors, show timestamp 1 hour ahead
|
|
||||||
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
|
attributes["timestamp"] = interval_data["startsAt"] if interval_data else None
|
||||||
elif key in current_hour_sensors:
|
elif key in current_hour_sensors:
|
||||||
# For current hour sensors, use current interval timestamp
|
|
||||||
current_interval_data = self._get_current_interval_data()
|
current_interval_data = self._get_current_interval_data()
|
||||||
attributes["timestamp"] = current_interval_data["startsAt"] if current_interval_data else None
|
attributes["timestamp"] = current_interval_data["startsAt"] if current_interval_data else None
|
||||||
else:
|
else:
|
||||||
# Default: use current interval timestamp
|
|
||||||
current_interval_data = self._get_current_interval_data()
|
current_interval_data = self._get_current_interval_data()
|
||||||
attributes["timestamp"] = current_interval_data["startsAt"] if current_interval_data else None
|
attributes["timestamp"] = current_interval_data["startsAt"] if current_interval_data else None
|
||||||
|
|
||||||
# Add price level info for price level sensors
|
# Add icon_color for price sensors (based on their price level)
|
||||||
if key == "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()
|
current_interval_data = self._get_current_interval_data()
|
||||||
if current_interval_data and "level" in current_interval_data:
|
if current_interval_data and "level" in current_interval_data:
|
||||||
self._add_price_level_attributes(attributes, current_interval_data["level"])
|
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_value"] = PRICE_LEVEL_MAPPING[level]
|
||||||
attributes["level_id"] = 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(
|
def _find_price_timestamp(
|
||||||
self,
|
self,
|
||||||
attributes: dict,
|
attributes: dict,
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue