mirror of
https://github.com/jpawlowski/hass.tibber_prices.git
synced 2026-03-30 05:13:40 +00:00
Added timestamp attributes to all sensors and enhanced the dynamic icon
system for comprehensive price sensor coverage with rolling hour support.
TIMESTAMP ATTRIBUTES:
Core Changes:
- sensor/attributes.py:
* Enhanced add_average_price_attributes() to track extreme intervals
for min/max sensors and add appropriate timestamps
* Added _update_extreme_interval() helper to reduce complexity
* Extended add_volatility_type_attributes() with timestamp logic for
all 4 volatility types (today/tomorrow/today_tomorrow/next_24h)
* Fixed current_interval_price timestamp assignment (use interval_data)
Timestamp Logic:
- Interval-based sensors: Use startsAt of specific 15-minute interval
- Min/Max sensors: Use startsAt of interval with extreme price
- Average sensors: Use startsAt of first interval in window
- Volatility sensors: Use midnight (00:00) for calendar day sensors,
current time for rolling 24h window
- Daily sensors: Already used fallback to midnight (verified)
ICON SYSTEM ENHANCEMENTS:
Major Extensions:
- entity_utils/icons.py:
* Created get_rolling_hour_price_level_for_icon() implementing
5-interval window aggregation matching sensor calculation logic
* Extended get_price_sensor_icon() coverage from 1 to 4 sensors:
- current_interval_price (existing)
- next_interval_price (NEW - dynamic instead of static)
- current_hour_average_price (NEW - uses rolling hour aggregation)
- next_hour_average_price (NEW - uses rolling hour aggregation)
* Added imports for aggregate_level_data and find_rolling_hour_center_index
Documentation:
- sensor/definitions.py:
* Updated 30+ sensor descriptions with detailed icon behavior comments
* Changed next_interval_price from static to dynamic icon
* Documented dynamic vs static icons for all sensor types
* Added clear icon mapping source documentation
SENSOR KEY RENAMING:
Renamed for clarity (current_hour_average → current_hour_average_price):
- sensor/core.py: Updated value getters and cached data lookup
- sensor/definitions.py: Updated entity descriptions
- sensor/attributes.py: Updated key references in attribute builders
- coordinator.py: Updated TIME_SENSITIVE_ENTITY_KEYS set
- const.py: Updated comment documentation
Translation Updates:
- custom_translations/*.json (5 files): Updated sensor keys
- translations/*.json (5 files): Updated sensor keys
Impact:
- All sensors now have timestamp attribute showing applicable time/interval
- Icon system provides richer visual feedback for more sensor types
- Consistent sensor naming improves code readability
- Users get temporal context for all sensor values
- Dynamic icons adapt to price conditions across more sensors
279 lines
8.6 KiB
Python
279 lines
8.6 KiB
Python
"""Icon utilities for Tibber Prices entities."""
|
|
|
|
from __future__ import annotations
|
|
|
|
from datetime import timedelta
|
|
from typing import TYPE_CHECKING, Any
|
|
|
|
from custom_components.tibber_prices.const import (
|
|
BINARY_SENSOR_ICON_MAPPING,
|
|
PRICE_LEVEL_CASH_ICON_MAPPING,
|
|
PRICE_LEVEL_ICON_MAPPING,
|
|
PRICE_RATING_ICON_MAPPING,
|
|
VOLATILITY_ICON_MAPPING,
|
|
)
|
|
from custom_components.tibber_prices.price_utils import find_price_data_for_interval
|
|
from custom_components.tibber_prices.sensor.helpers import (
|
|
aggregate_level_data,
|
|
find_rolling_hour_center_index,
|
|
)
|
|
from homeassistant.util import dt as dt_util
|
|
|
|
if TYPE_CHECKING:
|
|
from collections.abc import Callable
|
|
|
|
# Constants imported from price_utils
|
|
MINUTES_PER_INTERVAL = 15
|
|
|
|
|
|
def get_dynamic_icon(
|
|
key: str,
|
|
value: Any,
|
|
*,
|
|
is_on: bool | None = None,
|
|
coordinator_data: dict | None = None,
|
|
has_future_periods_callback: Callable[[], bool] | None = None,
|
|
) -> str | None:
|
|
"""
|
|
Get dynamic icon based on sensor state.
|
|
|
|
Unified function for both sensor and binary_sensor platforms.
|
|
|
|
Args:
|
|
key: Entity description key
|
|
value: Native value of the sensor
|
|
is_on: Binary sensor state (None for regular sensors)
|
|
coordinator_data: Coordinator data for price level lookups
|
|
has_future_periods_callback: Callback to check if future periods exist (binary sensors)
|
|
|
|
Returns:
|
|
Icon string or None if no dynamic icon applies
|
|
|
|
"""
|
|
# Try various icon sources in order
|
|
return (
|
|
get_trend_icon(key, value)
|
|
or get_price_sensor_icon(key, coordinator_data)
|
|
or get_level_sensor_icon(key, value)
|
|
or get_rating_sensor_icon(key, value)
|
|
or get_volatility_sensor_icon(key, value)
|
|
or get_binary_sensor_icon(key, is_on=is_on, has_future_periods_callback=has_future_periods_callback)
|
|
)
|
|
|
|
|
|
def get_trend_icon(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(key: str, coordinator_data: dict | None) -> str | None:
|
|
"""
|
|
Get icon for current price sensors (dynamic based on price level).
|
|
|
|
Dynamic icons for: current_interval_price, next_interval_price,
|
|
current_hour_average_price, next_hour_average_price
|
|
Other price sensors (previous interval) use static icons from entity description.
|
|
|
|
Args:
|
|
key: Entity description key
|
|
coordinator_data: Coordinator data for price level lookups
|
|
|
|
Returns:
|
|
Icon string or None if not a current price sensor
|
|
|
|
"""
|
|
if not coordinator_data:
|
|
return None
|
|
|
|
# Only current price sensors get dynamic icons
|
|
if key == "current_interval_price":
|
|
level = get_price_level_for_icon(coordinator_data, interval_offset=0)
|
|
if level:
|
|
return PRICE_LEVEL_CASH_ICON_MAPPING.get(level.upper())
|
|
elif key == "next_interval_price":
|
|
# For next interval, use the next interval price level to determine icon
|
|
level = get_price_level_for_icon(coordinator_data, interval_offset=1)
|
|
if level:
|
|
return PRICE_LEVEL_CASH_ICON_MAPPING.get(level.upper())
|
|
elif key == "current_hour_average_price":
|
|
# For current hour average, use the current hour price level to determine icon
|
|
level = get_rolling_hour_price_level_for_icon(coordinator_data, hour_offset=0)
|
|
if level:
|
|
return PRICE_LEVEL_CASH_ICON_MAPPING.get(level.upper())
|
|
elif key == "next_hour_average_price":
|
|
# For next hour average, use the next hour price level to determine icon
|
|
level = get_rolling_hour_price_level_for_icon(coordinator_data, hour_offset=1)
|
|
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(key: str, value: Any) -> str | None:
|
|
"""Get icon for price level sensors."""
|
|
if key not in [
|
|
"current_interval_price_level",
|
|
"next_interval_price_level",
|
|
"previous_interval_price_level",
|
|
"current_hour_price_level",
|
|
"next_hour_price_level",
|
|
"yesterday_price_level",
|
|
"today_price_level",
|
|
"tomorrow_price_level",
|
|
] or not isinstance(value, str):
|
|
return None
|
|
|
|
return PRICE_LEVEL_ICON_MAPPING.get(value.upper())
|
|
|
|
|
|
def get_rating_sensor_icon(key: str, value: Any) -> str | None:
|
|
"""Get icon for price rating sensors."""
|
|
if key not in [
|
|
"current_interval_price_rating",
|
|
"next_interval_price_rating",
|
|
"previous_interval_price_rating",
|
|
"current_hour_price_rating",
|
|
"next_hour_price_rating",
|
|
"yesterday_price_rating",
|
|
"today_price_rating",
|
|
"tomorrow_price_rating",
|
|
] or not isinstance(value, str):
|
|
return None
|
|
|
|
return PRICE_RATING_ICON_MAPPING.get(value.upper())
|
|
|
|
|
|
def get_volatility_sensor_icon(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_binary_sensor_icon(
|
|
key: str,
|
|
*,
|
|
is_on: bool | None,
|
|
has_future_periods_callback: Callable[[], bool] | None = None,
|
|
) -> str | None:
|
|
"""
|
|
Get icon for binary sensors with dynamic state-based icons.
|
|
|
|
Args:
|
|
key: Entity description key
|
|
is_on: Binary sensor state
|
|
has_future_periods_callback: Callback to check if future periods exist
|
|
|
|
Returns:
|
|
Icon string or None if not a binary sensor with dynamic icons
|
|
|
|
"""
|
|
if key not in BINARY_SENSOR_ICON_MAPPING or is_on is None:
|
|
return None
|
|
|
|
if is_on:
|
|
# Sensor is ON - use "on" icon
|
|
return BINARY_SENSOR_ICON_MAPPING[key].get("on")
|
|
|
|
# Sensor is OFF - check if future periods exist
|
|
has_future_periods = has_future_periods_callback() if has_future_periods_callback else False
|
|
|
|
if has_future_periods:
|
|
return BINARY_SENSOR_ICON_MAPPING[key].get("off")
|
|
|
|
return BINARY_SENSOR_ICON_MAPPING[key].get("off_no_future")
|
|
|
|
|
|
def get_price_level_for_icon(
|
|
coordinator_data: dict,
|
|
*,
|
|
interval_offset: int | None = None,
|
|
) -> str | None:
|
|
"""
|
|
Get the price level for icon determination.
|
|
|
|
Supports interval-based lookups (current/next/previous interval).
|
|
|
|
Args:
|
|
coordinator_data: Coordinator data
|
|
interval_offset: Interval offset (0=current, 1=next, -1=previous)
|
|
|
|
Returns:
|
|
Price level string or None if not found
|
|
|
|
"""
|
|
if not coordinator_data or interval_offset is None:
|
|
return None
|
|
|
|
price_info = coordinator_data.get("priceInfo", {})
|
|
now = dt_util.now()
|
|
|
|
# Interval-based lookup
|
|
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_rolling_hour_price_level_for_icon(
|
|
coordinator_data: dict,
|
|
*,
|
|
hour_offset: int = 0,
|
|
) -> str | None:
|
|
"""
|
|
Get the aggregated price level for rolling hour icon determination.
|
|
|
|
Uses the same logic as the sensor platform: 5-interval rolling window
|
|
(2 before + center + 2 after) to determine the price level.
|
|
|
|
This ensures icon calculation matches the actual sensor value calculation.
|
|
|
|
Args:
|
|
coordinator_data: Coordinator data
|
|
hour_offset: Hour offset (0=current hour, 1=next hour)
|
|
|
|
Returns:
|
|
Aggregated price level string or None if not found
|
|
|
|
"""
|
|
if not coordinator_data:
|
|
return None
|
|
|
|
price_info = coordinator_data.get("priceInfo", {})
|
|
all_prices = price_info.get("yesterday", []) + price_info.get("today", []) + price_info.get("tomorrow", [])
|
|
|
|
if not all_prices:
|
|
return None
|
|
|
|
# Find center index using the same helper function as the sensor platform
|
|
now = dt_util.now()
|
|
center_idx = find_rolling_hour_center_index(all_prices, now, hour_offset)
|
|
|
|
if center_idx is None:
|
|
return None
|
|
|
|
# Collect data from 5-interval window (-2, -1, 0, +1, +2) - same as sensor platform
|
|
window_data = []
|
|
for offset in range(-2, 3):
|
|
idx = center_idx + offset
|
|
if 0 <= idx < len(all_prices):
|
|
window_data.append(all_prices[idx])
|
|
|
|
if not window_data:
|
|
return None
|
|
|
|
# Use the same aggregation function as the sensor platform
|
|
return aggregate_level_data(window_data)
|