feat(sensors)!: use native minutes for all duration sensors

Changed native_unit_of_measurement from HOURS to MINUTES for all 7
duration sensors. HA auto-converts to hours for display via
suggested_unit_of_measurement=HOURS.

Sensors affected:
- next_price_trend_change_in
- best_price_period_duration, best_price_remaining_minutes,
  best_price_next_in_minutes
- peak_price_period_duration, peak_price_remaining_minutes,
  peak_price_next_in_minutes

Removed _minutes_to_hours() conversion function — calculator values
(minutes) are now passed through directly.

BREAKING CHANGE: State values for all duration sensors change from
hours to minutes (e.g. 1.5 → 90). The display unit remains hours
(suggested_unit_of_measurement). Automations using numeric state
comparisons must be updated (multiply old thresholds by 60).

Impact: Users with automations comparing duration sensor states
numerically need to update thresholds. Dashboard display is unchanged
for new installations. Existing installations retain their configured
display unit but the underlying numeric value changes.
This commit is contained in:
Julian Pawlowski 2026-04-10 09:08:38 +00:00
parent b1b41be9aa
commit faa3b2b71a
2 changed files with 28 additions and 48 deletions

View file

@ -519,11 +519,11 @@ FUTURE_TREND_SENSORS = (
),
# Trend change countdown sensor (how long until trend changes?)
SensorEntityDescription(
key="trend_change_in_minutes",
translation_key="trend_change_in_minutes",
key="next_price_trend_change_in",
translation_key="next_price_trend_change_in",
icon="mdi:timer-outline",
device_class=SensorDeviceClass.DURATION,
native_unit_of_measurement=UnitOfTime.HOURS,
native_unit_of_measurement=UnitOfTime.MINUTES,
suggested_unit_of_measurement=UnitOfTime.HOURS,
state_class=None, # Countdown timer: no statistics
suggested_display_precision=2,
@ -766,7 +766,7 @@ BEST_PRICE_TIMING_SENSORS = (
translation_key="best_price_period_duration",
icon="mdi:timer",
device_class=SensorDeviceClass.DURATION,
native_unit_of_measurement=UnitOfTime.HOURS,
native_unit_of_measurement=UnitOfTime.MINUTES,
suggested_unit_of_measurement=UnitOfTime.HOURS,
state_class=None, # Duration not needed in long-term statistics
suggested_display_precision=2,
@ -777,7 +777,7 @@ BEST_PRICE_TIMING_SENSORS = (
translation_key="best_price_remaining_minutes",
icon="mdi:timer-sand",
device_class=SensorDeviceClass.DURATION,
native_unit_of_measurement=UnitOfTime.HOURS,
native_unit_of_measurement=UnitOfTime.MINUTES,
suggested_unit_of_measurement=UnitOfTime.HOURS,
state_class=None, # Countdown timers excluded from statistics
suggested_display_precision=2,
@ -802,7 +802,7 @@ BEST_PRICE_TIMING_SENSORS = (
translation_key="best_price_next_in_minutes",
icon="mdi:timer-outline",
device_class=SensorDeviceClass.DURATION,
native_unit_of_measurement=UnitOfTime.HOURS,
native_unit_of_measurement=UnitOfTime.MINUTES,
suggested_unit_of_measurement=UnitOfTime.HOURS,
state_class=None, # Next-start timers excluded from statistics
suggested_display_precision=2,
@ -822,7 +822,7 @@ PEAK_PRICE_TIMING_SENSORS = (
translation_key="peak_price_period_duration",
icon="mdi:timer",
device_class=SensorDeviceClass.DURATION,
native_unit_of_measurement=UnitOfTime.HOURS,
native_unit_of_measurement=UnitOfTime.MINUTES,
suggested_unit_of_measurement=UnitOfTime.HOURS,
state_class=None, # Duration not needed in long-term statistics
suggested_display_precision=2,
@ -833,7 +833,7 @@ PEAK_PRICE_TIMING_SENSORS = (
translation_key="peak_price_remaining_minutes",
icon="mdi:timer-sand",
device_class=SensorDeviceClass.DURATION,
native_unit_of_measurement=UnitOfTime.HOURS,
native_unit_of_measurement=UnitOfTime.MINUTES,
suggested_unit_of_measurement=UnitOfTime.HOURS,
state_class=None, # Countdown timers excluded from statistics
suggested_display_precision=2,
@ -858,7 +858,7 @@ PEAK_PRICE_TIMING_SENSORS = (
translation_key="peak_price_next_in_minutes",
icon="mdi:timer-outline",
device_class=SensorDeviceClass.DURATION,
native_unit_of_measurement=UnitOfTime.HOURS,
native_unit_of_measurement=UnitOfTime.MINUTES,
suggested_unit_of_measurement=UnitOfTime.HOURS,
state_class=None, # Next-start timers excluded from statistics
suggested_display_precision=2,

View file

@ -70,14 +70,6 @@ def get_value_getter_mapping( # noqa: PLR0913 - needs all calculators as parame
Dictionary mapping entity keys to their value getter callables.
"""
def _minutes_to_hours(value: float | None) -> float | None:
"""Convert minutes to hours for duration-oriented sensors."""
if value is None:
return None
return value / 60
return {
# ================================================================
# INTERVAL-BASED SENSORS - via IntervalCalculator
@ -205,7 +197,7 @@ def get_value_getter_mapping( # noqa: PLR0913 - needs all calculators as parame
# Current and next trend change sensors
"current_price_trend": trend_calculator.get_current_trend_value,
"next_price_trend_change": trend_calculator.get_next_trend_change_value,
"trend_change_in_minutes": lambda: _minutes_to_hours(trend_calculator.get_trend_change_in_minutes_value()),
"next_price_trend_change_in": trend_calculator.get_trend_change_in_minutes_value,
# Price outlook sensors (current price vs average of next Xh)
"price_outlook_1h": lambda: trend_calculator.get_price_outlook_value(hours=1),
"price_outlook_2h": lambda: trend_calculator.get_price_outlook_value(hours=2),
@ -260,17 +252,13 @@ def get_value_getter_mapping( # noqa: PLR0913 - needs all calculators as parame
"best_price_end_time": lambda: timing_calculator.get_period_timing_value(
period_type="best_price", value_type="end_time"
),
"best_price_period_duration": lambda: _minutes_to_hours(
cast(
"best_price_period_duration": lambda: cast(
"float | None",
timing_calculator.get_period_timing_value(period_type="best_price", value_type="period_duration"),
)
),
"best_price_remaining_minutes": lambda: _minutes_to_hours(
cast(
"best_price_remaining_minutes": lambda: cast(
"float | None",
timing_calculator.get_period_timing_value(period_type="best_price", value_type="remaining_minutes"),
)
),
"best_price_progress": lambda: timing_calculator.get_period_timing_value(
period_type="best_price", value_type="progress"
@ -278,27 +266,21 @@ def get_value_getter_mapping( # noqa: PLR0913 - needs all calculators as parame
"best_price_next_start_time": lambda: timing_calculator.get_period_timing_value(
period_type="best_price", value_type="next_start_time"
),
"best_price_next_in_minutes": lambda: _minutes_to_hours(
cast(
"best_price_next_in_minutes": lambda: cast(
"float | None",
timing_calculator.get_period_timing_value(period_type="best_price", value_type="next_in_minutes"),
)
),
# Peak Price timing sensors
"peak_price_end_time": lambda: timing_calculator.get_period_timing_value(
period_type="peak_price", value_type="end_time"
),
"peak_price_period_duration": lambda: _minutes_to_hours(
cast(
"peak_price_period_duration": lambda: cast(
"float | None",
timing_calculator.get_period_timing_value(period_type="peak_price", value_type="period_duration"),
)
),
"peak_price_remaining_minutes": lambda: _minutes_to_hours(
cast(
"peak_price_remaining_minutes": lambda: cast(
"float | None",
timing_calculator.get_period_timing_value(period_type="peak_price", value_type="remaining_minutes"),
)
),
"peak_price_progress": lambda: timing_calculator.get_period_timing_value(
period_type="peak_price", value_type="progress"
@ -306,11 +288,9 @@ def get_value_getter_mapping( # noqa: PLR0913 - needs all calculators as parame
"peak_price_next_start_time": lambda: timing_calculator.get_period_timing_value(
period_type="peak_price", value_type="next_start_time"
),
"peak_price_next_in_minutes": lambda: _minutes_to_hours(
cast(
"peak_price_next_in_minutes": lambda: cast(
"float | None",
timing_calculator.get_period_timing_value(period_type="peak_price", value_type="next_in_minutes"),
)
),
# Chart data export sensor
"chart_data_export": get_chart_data_export_value,