Add DurationConverter (#108865)

* Add DurationConverter

* Update withings snapshots

* Add sensor test

* Fix tests

* Update snapshots after #108902 was merged
This commit is contained in:
Robert Resch 2024-01-30 23:08:12 +01:00 committed by GitHub
parent e7d5ae7ef6
commit b409933d19
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
7 changed files with 141 additions and 22 deletions

View File

@ -30,6 +30,7 @@ from homeassistant.util.unit_conversion import (
BaseUnitConverter,
DataRateConverter,
DistanceConverter,
DurationConverter,
ElectricCurrentConverter,
ElectricPotentialConverter,
EnergyConverter,
@ -126,6 +127,7 @@ QUERY_STATISTICS_SUMMARY_SUM = (
STATISTIC_UNIT_TO_UNIT_CONVERTER: dict[str | None, type[BaseUnitConverter]] = {
**{unit: DataRateConverter for unit in DataRateConverter.VALID_UNITS},
**{unit: DistanceConverter for unit in DistanceConverter.VALID_UNITS},
**{unit: DurationConverter for unit in DurationConverter.VALID_UNITS},
**{unit: ElectricCurrentConverter for unit in ElectricCurrentConverter.VALID_UNITS},
**{
unit: ElectricPotentialConverter

View File

@ -17,6 +17,7 @@ from homeassistant.util import dt as dt_util
from homeassistant.util.unit_conversion import (
DataRateConverter,
DistanceConverter,
DurationConverter,
ElectricCurrentConverter,
ElectricPotentialConverter,
EnergyConverter,
@ -57,6 +58,7 @@ UNIT_SCHEMA = vol.Schema(
{
vol.Optional("data_rate"): vol.In(DataRateConverter.VALID_UNITS),
vol.Optional("distance"): vol.In(DistanceConverter.VALID_UNITS),
vol.Optional("duration"): vol.In(DurationConverter.VALID_UNITS),
vol.Optional("electric_current"): vol.In(ElectricCurrentConverter.VALID_UNITS),
vol.Optional("voltage"): vol.In(ElectricPotentialConverter.VALID_UNITS),
vol.Optional("energy"): vol.In(EnergyConverter.VALID_UNITS),

View File

@ -47,6 +47,7 @@ from homeassistant.util.unit_conversion import (
BaseUnitConverter,
DataRateConverter,
DistanceConverter,
DurationConverter,
ElectricCurrentConverter,
ElectricPotentialConverter,
EnergyConverter,
@ -485,6 +486,7 @@ UNIT_CONVERTERS: dict[SensorDeviceClass | str | None, type[BaseUnitConverter]] =
SensorDeviceClass.DATA_RATE: DataRateConverter,
SensorDeviceClass.DATA_SIZE: InformationConverter,
SensorDeviceClass.DISTANCE: DistanceConverter,
SensorDeviceClass.DURATION: DurationConverter,
SensorDeviceClass.ENERGY: EnergyConverter,
SensorDeviceClass.ENERGY_STORAGE: EnergyConverter,
SensorDeviceClass.GAS: VolumeConverter,

View File

@ -20,6 +20,7 @@ from homeassistant.const import (
UnitOfPressure,
UnitOfSpeed,
UnitOfTemperature,
UnitOfTime,
UnitOfVolume,
UnitOfVolumeFlowRate,
UnitOfVolumetricFlux,
@ -39,8 +40,9 @@ _MILE_TO_M = _YARD_TO_M * 1760 # 1760 yard = 1 mile (1609.344 m)
_NAUTICAL_MILE_TO_M = 1852 # 1 nautical mile = 1852 m
# Duration conversion constants
_HRS_TO_SECS = 60 * 60 # 1 hr = 3600 seconds
_MIN_TO_SEC = 60 # 1 min = 60 seconds
_HRS_TO_MINUTES = 60 # 1 hr = 60 minutes
_HRS_TO_SECS = _HRS_TO_MINUTES * _MIN_TO_SEC # 1 hr = 60 minutes = 3600 seconds
_DAYS_TO_SECS = 24 * _HRS_TO_SECS # 1 day = 24 hours = 86400 seconds
# Mass conversion constants
@ -541,3 +543,28 @@ class VolumeFlowRateConverter(BaseUnitConverter):
UnitOfVolumeFlowRate.LITERS_PER_MINUTE,
UnitOfVolumeFlowRate.GALLONS_PER_MINUTE,
}
class DurationConverter(BaseUnitConverter):
"""Utility to convert duration values."""
UNIT_CLASS = "duration"
NORMALIZED_UNIT = UnitOfTime.SECONDS
_UNIT_CONVERSION: dict[str | None, float] = {
UnitOfTime.MICROSECONDS: 1000000,
UnitOfTime.MILLISECONDS: 1000,
UnitOfTime.SECONDS: 1,
UnitOfTime.MINUTES: 1 / _MIN_TO_SEC,
UnitOfTime.HOURS: 1 / _HRS_TO_SECS,
UnitOfTime.DAYS: 1 / _DAYS_TO_SECS,
UnitOfTime.WEEKS: 1 / (7 * _DAYS_TO_SECS),
}
VALID_UNITS = {
UnitOfTime.MICROSECONDS,
UnitOfTime.MILLISECONDS,
UnitOfTime.SECONDS,
UnitOfTime.MINUTES,
UnitOfTime.HOURS,
UnitOfTime.DAYS,
UnitOfTime.WEEKS,
}

View File

@ -37,6 +37,7 @@ from homeassistant.const import (
UnitOfPressure,
UnitOfSpeed,
UnitOfTemperature,
UnitOfTime,
UnitOfVolume,
UnitOfVolumeFlowRate,
UnitOfVolumetricFlux,
@ -599,6 +600,22 @@ async def test_restore_sensor_restore_state(
13.0,
"49.2",
),
(
SensorDeviceClass.DURATION,
UnitOfTime.SECONDS,
UnitOfTime.HOURS,
UnitOfTime.HOURS,
5400.0,
"1.5000",
),
(
SensorDeviceClass.DURATION,
UnitOfTime.DAYS,
UnitOfTime.MINUTES,
UnitOfTime.MINUTES,
0.5,
"720.0",
),
],
)
async def test_custom_unit(

View File

@ -71,6 +71,9 @@
'id': <ANY>,
'name': None,
'options': dict({
'sensor.private': dict({
'suggested_unit_of_measurement': <UnitOfTime.HOURS: 'h'>,
}),
}),
'original_device_class': <SensorDeviceClass.DURATION: 'duration'>,
'original_icon': None,
@ -80,7 +83,7 @@
'supported_features': 0,
'translation_key': 'activity_active_duration_today',
'unique_id': 'withings_12345_activity_active_duration_today',
'unit_of_measurement': <UnitOfTime.SECONDS: 's'>,
'unit_of_measurement': <UnitOfTime.HOURS: 'h'>,
})
# ---
# name: test_all_entities[sensor.henk_active_time_today-state]
@ -90,13 +93,13 @@
'friendly_name': 'henk Active time today',
'last_reset': '2023-10-20T00:00:00-07:00',
'state_class': <SensorStateClass.TOTAL: 'total'>,
'unit_of_measurement': <UnitOfTime.SECONDS: 's'>,
'unit_of_measurement': <UnitOfTime.HOURS: 'h'>,
}),
'context': <ANY>,
'entity_id': 'sensor.henk_active_time_today',
'last_changed': <ANY>,
'last_updated': <ANY>,
'state': '1907',
'state': '0.530',
})
# ---
# name: test_all_entities[sensor.henk_average_heart_rate-entry]
@ -1173,6 +1176,9 @@
'id': <ANY>,
'name': None,
'options': dict({
'sensor.private': dict({
'suggested_unit_of_measurement': <UnitOfTime.MINUTES: 'min'>,
}),
}),
'original_device_class': <SensorDeviceClass.DURATION: 'duration'>,
'original_icon': None,
@ -1182,7 +1188,7 @@
'supported_features': 0,
'translation_key': 'activity_intense_duration_today',
'unique_id': 'withings_12345_activity_intense_duration_today',
'unit_of_measurement': <UnitOfTime.SECONDS: 's'>,
'unit_of_measurement': <UnitOfTime.MINUTES: 'min'>,
})
# ---
# name: test_all_entities[sensor.henk_intense_activity_today-state]
@ -1192,13 +1198,13 @@
'friendly_name': 'henk Intense activity today',
'last_reset': '2023-10-20T00:00:00-07:00',
'state_class': <SensorStateClass.TOTAL: 'total'>,
'unit_of_measurement': <UnitOfTime.SECONDS: 's'>,
'unit_of_measurement': <UnitOfTime.MINUTES: 'min'>,
}),
'context': <ANY>,
'entity_id': 'sensor.henk_intense_activity_today',
'last_changed': <ANY>,
'last_updated': <ANY>,
'state': '420',
'state': '7.0',
})
# ---
# name: test_all_entities[sensor.henk_intracellular_water-entry]
@ -1268,6 +1274,9 @@
'id': <ANY>,
'name': None,
'options': dict({
'sensor.private': dict({
'suggested_unit_of_measurement': <UnitOfTime.MINUTES: 'min'>,
}),
}),
'original_device_class': <SensorDeviceClass.DURATION: 'duration'>,
'original_icon': None,
@ -1277,7 +1286,7 @@
'supported_features': 0,
'translation_key': 'workout_duration',
'unique_id': 'withings_12345_workout_duration',
'unit_of_measurement': <UnitOfTime.SECONDS: 's'>,
'unit_of_measurement': <UnitOfTime.MINUTES: 'min'>,
})
# ---
# name: test_all_entities[sensor.henk_last_workout_duration-state]
@ -1285,13 +1294,13 @@
'attributes': ReadOnlyDict({
'device_class': 'duration',
'friendly_name': 'henk Last workout duration',
'unit_of_measurement': <UnitOfTime.SECONDS: 's'>,
'unit_of_measurement': <UnitOfTime.MINUTES: 'min'>,
}),
'context': <ANY>,
'entity_id': 'sensor.henk_last_workout_duration',
'last_changed': <ANY>,
'last_updated': <ANY>,
'state': '255.0',
'state': '4.25',
})
# ---
# name: test_all_entities[sensor.henk_last_workout_intensity-entry]
@ -1741,6 +1750,9 @@
'id': <ANY>,
'name': None,
'options': dict({
'sensor.private': dict({
'suggested_unit_of_measurement': <UnitOfTime.MINUTES: 'min'>,
}),
}),
'original_device_class': <SensorDeviceClass.DURATION: 'duration'>,
'original_icon': None,
@ -1750,7 +1762,7 @@
'supported_features': 0,
'translation_key': 'activity_moderate_duration_today',
'unique_id': 'withings_12345_activity_moderate_duration_today',
'unit_of_measurement': <UnitOfTime.SECONDS: 's'>,
'unit_of_measurement': <UnitOfTime.MINUTES: 'min'>,
})
# ---
# name: test_all_entities[sensor.henk_moderate_activity_today-state]
@ -1760,13 +1772,13 @@
'friendly_name': 'henk Moderate activity today',
'last_reset': '2023-10-20T00:00:00-07:00',
'state_class': <SensorStateClass.TOTAL: 'total'>,
'unit_of_measurement': <UnitOfTime.SECONDS: 's'>,
'unit_of_measurement': <UnitOfTime.MINUTES: 'min'>,
}),
'context': <ANY>,
'entity_id': 'sensor.henk_moderate_activity_today',
'last_changed': <ANY>,
'last_updated': <ANY>,
'state': '1487',
'state': '24.8',
})
# ---
# name: test_all_entities[sensor.henk_muscle_mass-entry]
@ -1839,6 +1851,9 @@
'id': <ANY>,
'name': None,
'options': dict({
'sensor.private': dict({
'suggested_unit_of_measurement': <UnitOfTime.MINUTES: 'min'>,
}),
}),
'original_device_class': <SensorDeviceClass.DURATION: 'duration'>,
'original_icon': None,
@ -1848,7 +1863,7 @@
'supported_features': 0,
'translation_key': 'workout_pause_duration',
'unique_id': 'withings_12345_workout_pause_duration',
'unit_of_measurement': <UnitOfTime.SECONDS: 's'>,
'unit_of_measurement': <UnitOfTime.MINUTES: 'min'>,
})
# ---
# name: test_all_entities[sensor.henk_pause_during_last_workout-state]
@ -1856,13 +1871,13 @@
'attributes': ReadOnlyDict({
'device_class': 'duration',
'friendly_name': 'henk Pause during last workout',
'unit_of_measurement': <UnitOfTime.SECONDS: 's'>,
'unit_of_measurement': <UnitOfTime.MINUTES: 'min'>,
}),
'context': <ANY>,
'entity_id': 'sensor.henk_pause_during_last_workout',
'last_changed': <ANY>,
'last_updated': <ANY>,
'state': '0',
'state': '0.0',
})
# ---
# name: test_all_entities[sensor.henk_pulse_wave_velocity-entry]
@ -2030,6 +2045,9 @@
'id': <ANY>,
'name': None,
'options': dict({
'sensor.private': dict({
'suggested_unit_of_measurement': <UnitOfTime.HOURS: 'h'>,
}),
}),
'original_device_class': <SensorDeviceClass.DURATION: 'duration'>,
'original_icon': None,
@ -2039,7 +2057,7 @@
'supported_features': 0,
'translation_key': 'sleep_goal',
'unique_id': 'withings_12345_sleep_goal',
'unit_of_measurement': <UnitOfTime.SECONDS: 's'>,
'unit_of_measurement': <UnitOfTime.HOURS: 'h'>,
})
# ---
# name: test_all_entities[sensor.henk_sleep_goal-state]
@ -2048,13 +2066,13 @@
'device_class': 'duration',
'friendly_name': 'henk Sleep goal',
'state_class': <SensorStateClass.MEASUREMENT: 'measurement'>,
'unit_of_measurement': <UnitOfTime.SECONDS: 's'>,
'unit_of_measurement': <UnitOfTime.HOURS: 'h'>,
}),
'context': <ANY>,
'entity_id': 'sensor.henk_sleep_goal',
'last_changed': <ANY>,
'last_updated': <ANY>,
'state': '28800',
'state': '8.000',
})
# ---
# name: test_all_entities[sensor.henk_sleep_score-entry]
@ -2217,6 +2235,9 @@
'id': <ANY>,
'name': None,
'options': dict({
'sensor.private': dict({
'suggested_unit_of_measurement': <UnitOfTime.MINUTES: 'min'>,
}),
}),
'original_device_class': <SensorDeviceClass.DURATION: 'duration'>,
'original_icon': None,
@ -2226,7 +2247,7 @@
'supported_features': 0,
'translation_key': 'activity_soft_duration_today',
'unique_id': 'withings_12345_activity_soft_duration_today',
'unit_of_measurement': <UnitOfTime.SECONDS: 's'>,
'unit_of_measurement': <UnitOfTime.MINUTES: 'min'>,
})
# ---
# name: test_all_entities[sensor.henk_soft_activity_today-state]
@ -2236,13 +2257,13 @@
'friendly_name': 'henk Soft activity today',
'last_reset': '2023-10-20T00:00:00-07:00',
'state_class': <SensorStateClass.TOTAL: 'total'>,
'unit_of_measurement': <UnitOfTime.SECONDS: 's'>,
'unit_of_measurement': <UnitOfTime.MINUTES: 'min'>,
}),
'context': <ANY>,
'entity_id': 'sensor.henk_soft_activity_today',
'last_changed': <ANY>,
'last_updated': <ANY>,
'state': '1516',
'state': '25.3',
})
# ---
# name: test_all_entities[sensor.henk_spo2-entry]

View File

@ -21,6 +21,7 @@ from homeassistant.const import (
UnitOfPressure,
UnitOfSpeed,
UnitOfTemperature,
UnitOfTime,
UnitOfVolume,
UnitOfVolumeFlowRate,
UnitOfVolumetricFlux,
@ -31,6 +32,7 @@ from homeassistant.util.unit_conversion import (
BaseUnitConverter,
DataRateConverter,
DistanceConverter,
DurationConverter,
ElectricCurrentConverter,
ElectricPotentialConverter,
EnergyConverter,
@ -56,6 +58,7 @@ _ALL_CONVERTERS: dict[type[BaseUnitConverter], list[str | None]] = {
for converter in (
DataRateConverter,
DistanceConverter,
DurationConverter,
ElectricCurrentConverter,
ElectricPotentialConverter,
EnergyConverter,
@ -79,6 +82,7 @@ _GET_UNIT_RATIO: dict[type[BaseUnitConverter], tuple[str | None, str | None, flo
8,
),
DistanceConverter: (UnitOfLength.KILOMETERS, UnitOfLength.METERS, 0.001),
DurationConverter: (UnitOfTime.MINUTES, UnitOfTime.SECONDS, 1 / 60),
ElectricCurrentConverter: (
UnitOfElectricCurrent.AMPERE,
UnitOfElectricCurrent.MILLIAMPERE,
@ -202,6 +206,50 @@ _CONVERTED_VALUE: dict[
(5000000, UnitOfLength.MILLIMETERS, 16404.2, UnitOfLength.FEET),
(5000000, UnitOfLength.MILLIMETERS, 196850.5, UnitOfLength.INCHES),
],
DurationConverter: [
(5, UnitOfTime.MICROSECONDS, 0.005, UnitOfTime.MILLISECONDS),
(5, UnitOfTime.MICROSECONDS, 5e-6, UnitOfTime.SECONDS),
(5, UnitOfTime.MICROSECONDS, 8.333333333333333e-8, UnitOfTime.MINUTES),
(5, UnitOfTime.MICROSECONDS, 1.388888888888889e-9, UnitOfTime.HOURS),
(5, UnitOfTime.MICROSECONDS, 5.787e-11, UnitOfTime.DAYS),
(5, UnitOfTime.MICROSECONDS, 8.267195767195767e-12, UnitOfTime.WEEKS),
(5, UnitOfTime.MILLISECONDS, 5000, UnitOfTime.MICROSECONDS),
(5, UnitOfTime.MILLISECONDS, 0.005, UnitOfTime.SECONDS),
(5, UnitOfTime.MILLISECONDS, 8.333333333333333e-5, UnitOfTime.MINUTES),
(5, UnitOfTime.MILLISECONDS, 1.388888888888889e-6, UnitOfTime.HOURS),
(5, UnitOfTime.MILLISECONDS, 5.787e-8, UnitOfTime.DAYS),
(5, UnitOfTime.MILLISECONDS, 8.267195767195767e-9, UnitOfTime.WEEKS),
(5, UnitOfTime.SECONDS, 5e6, UnitOfTime.MICROSECONDS),
(5, UnitOfTime.SECONDS, 5000, UnitOfTime.MILLISECONDS),
(5, UnitOfTime.SECONDS, 0.0833333, UnitOfTime.MINUTES),
(5, UnitOfTime.SECONDS, 0.00138889, UnitOfTime.HOURS),
(5, UnitOfTime.SECONDS, 5.787037037037037e-5, UnitOfTime.DAYS),
(5, UnitOfTime.SECONDS, 8.267195767195768e-06, UnitOfTime.WEEKS),
(5, UnitOfTime.MINUTES, 3e8, UnitOfTime.MICROSECONDS),
(5, UnitOfTime.MINUTES, 300000, UnitOfTime.MILLISECONDS),
(5, UnitOfTime.MINUTES, 300, UnitOfTime.SECONDS),
(5, UnitOfTime.MINUTES, 0.0833333, UnitOfTime.HOURS),
(5, UnitOfTime.MINUTES, 0.00347222, UnitOfTime.DAYS),
(5, UnitOfTime.MINUTES, 0.000496031746031746, UnitOfTime.WEEKS),
(5, UnitOfTime.HOURS, 18000000000, UnitOfTime.MICROSECONDS),
(5, UnitOfTime.HOURS, 18000000, UnitOfTime.MILLISECONDS),
(5, UnitOfTime.HOURS, 18000, UnitOfTime.SECONDS),
(5, UnitOfTime.HOURS, 300, UnitOfTime.MINUTES),
(5, UnitOfTime.HOURS, 0.208333333, UnitOfTime.DAYS),
(5, UnitOfTime.HOURS, 0.02976190476190476, UnitOfTime.WEEKS),
(5, UnitOfTime.DAYS, 4.32e11, UnitOfTime.MICROSECONDS),
(5, UnitOfTime.DAYS, 4.32e8, UnitOfTime.MILLISECONDS),
(5, UnitOfTime.DAYS, 432000, UnitOfTime.SECONDS),
(5, UnitOfTime.DAYS, 7200, UnitOfTime.MINUTES),
(5, UnitOfTime.DAYS, 120, UnitOfTime.HOURS),
(5, UnitOfTime.DAYS, 0.7142857142857143, UnitOfTime.WEEKS),
(5, UnitOfTime.WEEKS, 3.024e12, UnitOfTime.MICROSECONDS),
(5, UnitOfTime.WEEKS, 3.024e9, UnitOfTime.MILLISECONDS),
(5, UnitOfTime.WEEKS, 3024000, UnitOfTime.SECONDS),
(5, UnitOfTime.WEEKS, 50400, UnitOfTime.MINUTES),
(5, UnitOfTime.WEEKS, 840, UnitOfTime.HOURS),
(5, UnitOfTime.WEEKS, 35, UnitOfTime.DAYS),
],
ElectricCurrentConverter: [
(5, UnitOfElectricCurrent.AMPERE, 5000, UnitOfElectricCurrent.MILLIAMPERE),
(5, UnitOfElectricCurrent.MILLIAMPERE, 0.005, UnitOfElectricCurrent.AMPERE),