Move distance and speed util to unit_conversion (#78967)

This commit is contained in:
epenet 2022-09-23 14:23:59 +02:00 committed by GitHub
parent 95e3572277
commit 4200778eaf
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 252 additions and 101 deletions

View File

@ -1,9 +1,7 @@
"""Distance util functions."""
from __future__ import annotations
from numbers import Number
from homeassistant.const import (
from homeassistant.const import ( # pylint: disable=unused-import # noqa: F401
LENGTH,
LENGTH_CENTIMETERS,
LENGTH_FEET,
@ -16,53 +14,11 @@ from homeassistant.const import (
UNIT_NOT_RECOGNIZED_TEMPLATE,
)
VALID_UNITS: tuple[str, ...] = (
LENGTH_KILOMETERS,
LENGTH_MILES,
LENGTH_FEET,
LENGTH_METERS,
LENGTH_CENTIMETERS,
LENGTH_MILLIMETERS,
LENGTH_INCHES,
LENGTH_YARD,
)
from .unit_conversion import DistanceConverter
MM_TO_M = 0.001 # 1 mm = 0.001 m
CM_TO_M = 0.01 # 1 cm = 0.01 m
KM_TO_M = 1000 # 1 km = 1000 m
IN_TO_M = 0.0254 # 1 inch = 0.0254 m
FOOT_TO_M = IN_TO_M * 12 # 12 inches = 1 foot (0.3048 m)
YARD_TO_M = FOOT_TO_M * 3 # 3 feet = 1 yard (0.9144 m)
MILE_TO_M = YARD_TO_M * 1760 # 1760 yard = 1 mile (1609.344 m)
NAUTICAL_MILE_TO_M = 1852 # 1 nautical mile = 1852 m
UNIT_CONVERSION: dict[str, float] = {
LENGTH_METERS: 1,
LENGTH_MILLIMETERS: 1 / MM_TO_M,
LENGTH_CENTIMETERS: 1 / CM_TO_M,
LENGTH_KILOMETERS: 1 / KM_TO_M,
LENGTH_INCHES: 1 / IN_TO_M,
LENGTH_FEET: 1 / FOOT_TO_M,
LENGTH_YARD: 1 / YARD_TO_M,
LENGTH_MILES: 1 / MILE_TO_M,
}
VALID_UNITS = DistanceConverter.VALID_UNITS
def convert(value: float, unit_1: str, unit_2: str) -> float:
def convert(value: float, from_unit: str, to_unit: str) -> float:
"""Convert one unit of measurement to another."""
if unit_1 not in VALID_UNITS:
raise ValueError(UNIT_NOT_RECOGNIZED_TEMPLATE.format(unit_1, LENGTH))
if unit_2 not in VALID_UNITS:
raise ValueError(UNIT_NOT_RECOGNIZED_TEMPLATE.format(unit_2, LENGTH))
if not isinstance(value, Number):
raise TypeError(f"{value} is not of numeric type")
if unit_1 == unit_2 or unit_1 not in VALID_UNITS:
return value
meters: float = value / UNIT_CONVERSION[unit_1]
return meters * UNIT_CONVERSION[unit_2]
return DistanceConverter.convert(value, from_unit, to_unit)

View File

@ -1,9 +1,7 @@
"""Distance util functions."""
from __future__ import annotations
from numbers import Number
from homeassistant.const import (
from homeassistant.const import ( # pylint: disable=unused-import # noqa: F401
SPEED,
SPEED_FEET_PER_SECOND,
SPEED_INCHES_PER_DAY,
@ -16,54 +14,20 @@ from homeassistant.const import (
UNIT_NOT_RECOGNIZED_TEMPLATE,
)
from .distance import (
FOOT_TO_M,
IN_TO_M,
KM_TO_M,
MILE_TO_M,
MM_TO_M,
NAUTICAL_MILE_TO_M,
from .unit_conversion import ( # pylint: disable=unused-import # noqa: F401
_FOOT_TO_M as FOOT_TO_M,
_HRS_TO_SECS as HRS_TO_SECS,
_IN_TO_M as IN_TO_M,
_KM_TO_M as KM_TO_M,
_MILE_TO_M as MILE_TO_M,
_NAUTICAL_MILE_TO_M as NAUTICAL_MILE_TO_M,
SpeedConverter,
)
VALID_UNITS: tuple[str, ...] = (
SPEED_FEET_PER_SECOND,
SPEED_INCHES_PER_DAY,
SPEED_INCHES_PER_HOUR,
SPEED_KILOMETERS_PER_HOUR,
SPEED_KNOTS,
SPEED_METERS_PER_SECOND,
SPEED_MILES_PER_HOUR,
SPEED_MILLIMETERS_PER_DAY,
)
HRS_TO_SECS = 60 * 60 # 1 hr = 3600 seconds
DAYS_TO_SECS = 24 * HRS_TO_SECS # 1 day = 24 hours = 86400 seconds
# Units in terms of m/s
UNIT_CONVERSION: dict[str, float] = {
SPEED_FEET_PER_SECOND: 1 / FOOT_TO_M,
SPEED_INCHES_PER_DAY: DAYS_TO_SECS / IN_TO_M,
SPEED_INCHES_PER_HOUR: HRS_TO_SECS / IN_TO_M,
SPEED_KILOMETERS_PER_HOUR: HRS_TO_SECS / KM_TO_M,
SPEED_KNOTS: HRS_TO_SECS / NAUTICAL_MILE_TO_M,
SPEED_METERS_PER_SECOND: 1,
SPEED_MILES_PER_HOUR: HRS_TO_SECS / MILE_TO_M,
SPEED_MILLIMETERS_PER_DAY: DAYS_TO_SECS / MM_TO_M,
}
UNIT_CONVERSION = SpeedConverter.UNIT_CONVERSION
VALID_UNITS = SpeedConverter.VALID_UNITS
def convert(value: float, unit_1: str, unit_2: str) -> float:
def convert(value: float, from_unit: str, to_unit: str) -> float:
"""Convert one unit of measurement to another."""
if unit_1 not in VALID_UNITS:
raise ValueError(UNIT_NOT_RECOGNIZED_TEMPLATE.format(unit_1, SPEED))
if unit_2 not in VALID_UNITS:
raise ValueError(UNIT_NOT_RECOGNIZED_TEMPLATE.format(unit_2, SPEED))
if not isinstance(value, Number):
raise TypeError(f"{value} is not of numeric type")
if unit_1 == unit_2:
return value
meters_per_second = value / UNIT_CONVERSION[unit_1]
return meters_per_second * UNIT_CONVERSION[unit_2]
return SpeedConverter.convert(value, from_unit, to_unit)

View File

@ -8,6 +8,14 @@ from homeassistant.const import (
ENERGY_KILO_WATT_HOUR,
ENERGY_MEGA_WATT_HOUR,
ENERGY_WATT_HOUR,
LENGTH_CENTIMETERS,
LENGTH_FEET,
LENGTH_INCHES,
LENGTH_KILOMETERS,
LENGTH_METERS,
LENGTH_MILES,
LENGTH_MILLIMETERS,
LENGTH_YARD,
POWER_KILO_WATT,
POWER_WATT,
PRESSURE_BAR,
@ -19,6 +27,14 @@ from homeassistant.const import (
PRESSURE_MMHG,
PRESSURE_PA,
PRESSURE_PSI,
SPEED_FEET_PER_SECOND,
SPEED_INCHES_PER_DAY,
SPEED_INCHES_PER_HOUR,
SPEED_KILOMETERS_PER_HOUR,
SPEED_KNOTS,
SPEED_METERS_PER_SECOND,
SPEED_MILES_PER_HOUR,
SPEED_MILLIMETERS_PER_DAY,
TEMP_CELSIUS,
TEMP_FAHRENHEIT,
TEMP_KELVIN,
@ -31,14 +47,28 @@ from homeassistant.const import (
VOLUME_MILLILITERS,
)
from .distance import FOOT_TO_M, IN_TO_M
# Distance conversion constants
_MM_TO_M = 0.001 # 1 mm = 0.001 m
_CM_TO_M = 0.01 # 1 cm = 0.01 m
_KM_TO_M = 1000 # 1 km = 1000 m
_IN_TO_M = 0.0254 # 1 inch = 0.0254 m
_FOOT_TO_M = _IN_TO_M * 12 # 12 inches = 1 foot (0.3048 m)
_YARD_TO_M = _FOOT_TO_M * 3 # 3 feet = 1 yard (0.9144 m)
_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
_DAYS_TO_SECS = 24 * _HRS_TO_SECS # 1 day = 24 hours = 86400 seconds
# Volume conversion constants
_L_TO_CUBIC_METER = 0.001 # 1 L = 0.001 m³
_ML_TO_CUBIC_METER = 0.001 * _L_TO_CUBIC_METER # 1 mL = 0.001 L
_GALLON_TO_CUBIC_METER = 231 * pow(IN_TO_M, 3) # US gallon is 231 cubic inches
_GALLON_TO_CUBIC_METER = 231 * pow(_IN_TO_M, 3) # US gallon is 231 cubic inches
_FLUID_OUNCE_TO_CUBIC_METER = _GALLON_TO_CUBIC_METER / 128 # 128 fl. oz. in a US gallon
_CUBIC_FOOT_TO_CUBIC_METER = pow(FOOT_TO_M, 3)
_CUBIC_FOOT_TO_CUBIC_METER = pow(_FOOT_TO_M, 3)
class BaseUnitConverter:
@ -86,6 +116,33 @@ class BaseUnitConverterWithUnitConversion(BaseUnitConverter):
return new_value * cls.UNIT_CONVERSION[to_unit]
class DistanceConverter(BaseUnitConverterWithUnitConversion):
"""Utility to convert distance values."""
UNIT_CLASS = "distance"
NORMALIZED_UNIT = LENGTH_METERS
UNIT_CONVERSION: dict[str, float] = {
LENGTH_METERS: 1,
LENGTH_MILLIMETERS: 1 / _MM_TO_M,
LENGTH_CENTIMETERS: 1 / _CM_TO_M,
LENGTH_KILOMETERS: 1 / _KM_TO_M,
LENGTH_INCHES: 1 / _IN_TO_M,
LENGTH_FEET: 1 / _FOOT_TO_M,
LENGTH_YARD: 1 / _YARD_TO_M,
LENGTH_MILES: 1 / _MILE_TO_M,
}
VALID_UNITS: tuple[str, ...] = (
LENGTH_KILOMETERS,
LENGTH_MILES,
LENGTH_FEET,
LENGTH_METERS,
LENGTH_CENTIMETERS,
LENGTH_MILLIMETERS,
LENGTH_INCHES,
LENGTH_YARD,
)
class EnergyConverter(BaseUnitConverterWithUnitConversion):
"""Utility to convert energy values."""
@ -147,6 +204,33 @@ class PressureConverter(BaseUnitConverterWithUnitConversion):
)
class SpeedConverter(BaseUnitConverterWithUnitConversion):
"""Utility to convert speed values."""
UNIT_CLASS = "speed"
NORMALIZED_UNIT = SPEED_METERS_PER_SECOND
UNIT_CONVERSION: dict[str, float] = {
SPEED_FEET_PER_SECOND: 1 / _FOOT_TO_M,
SPEED_INCHES_PER_DAY: _DAYS_TO_SECS / _IN_TO_M,
SPEED_INCHES_PER_HOUR: _HRS_TO_SECS / _IN_TO_M,
SPEED_KILOMETERS_PER_HOUR: _HRS_TO_SECS / _KM_TO_M,
SPEED_KNOTS: _HRS_TO_SECS / _NAUTICAL_MILE_TO_M,
SPEED_METERS_PER_SECOND: 1,
SPEED_MILES_PER_HOUR: _HRS_TO_SECS / _MILE_TO_M,
SPEED_MILLIMETERS_PER_DAY: _DAYS_TO_SECS / _MM_TO_M,
}
VALID_UNITS: tuple[str, ...] = (
SPEED_FEET_PER_SECOND,
SPEED_INCHES_PER_DAY,
SPEED_INCHES_PER_HOUR,
SPEED_KILOMETERS_PER_HOUR,
SPEED_KNOTS,
SPEED_METERS_PER_SECOND,
SPEED_MILES_PER_HOUR,
SPEED_MILLIMETERS_PER_DAY,
)
class TemperatureConverter(BaseUnitConverter):
"""Utility to convert temperature values."""

View File

@ -5,6 +5,14 @@ from homeassistant.const import (
ENERGY_KILO_WATT_HOUR,
ENERGY_MEGA_WATT_HOUR,
ENERGY_WATT_HOUR,
LENGTH_CENTIMETERS,
LENGTH_FEET,
LENGTH_INCHES,
LENGTH_KILOMETERS,
LENGTH_METERS,
LENGTH_MILES,
LENGTH_MILLIMETERS,
LENGTH_YARD,
POWER_KILO_WATT,
POWER_WATT,
PRESSURE_CBAR,
@ -15,6 +23,14 @@ from homeassistant.const import (
PRESSURE_MMHG,
PRESSURE_PA,
PRESSURE_PSI,
SPEED_FEET_PER_SECOND,
SPEED_INCHES_PER_DAY,
SPEED_INCHES_PER_HOUR,
SPEED_KILOMETERS_PER_HOUR,
SPEED_KNOTS,
SPEED_METERS_PER_SECOND,
SPEED_MILES_PER_HOUR,
SPEED_MILLIMETERS_PER_DAY,
TEMP_CELSIUS,
TEMP_FAHRENHEIT,
TEMP_KELVIN,
@ -27,9 +43,11 @@ from homeassistant.const import (
)
from homeassistant.util.unit_conversion import (
BaseUnitConverter,
DistanceConverter,
EnergyConverter,
PowerConverter,
PressureConverter,
SpeedConverter,
TemperatureConverter,
VolumeConverter,
)
@ -40,6 +58,14 @@ INVALID_SYMBOL = "bob"
@pytest.mark.parametrize(
"converter,valid_unit",
[
(DistanceConverter, LENGTH_KILOMETERS),
(DistanceConverter, LENGTH_METERS),
(DistanceConverter, LENGTH_CENTIMETERS),
(DistanceConverter, LENGTH_MILLIMETERS),
(DistanceConverter, LENGTH_MILES),
(DistanceConverter, LENGTH_YARD),
(DistanceConverter, LENGTH_FEET),
(DistanceConverter, LENGTH_INCHES),
(EnergyConverter, ENERGY_WATT_HOUR),
(EnergyConverter, ENERGY_KILO_WATT_HOUR),
(EnergyConverter, ENERGY_MEGA_WATT_HOUR),
@ -53,6 +79,14 @@ INVALID_SYMBOL = "bob"
(PressureConverter, PRESSURE_CBAR),
(PressureConverter, PRESSURE_MMHG),
(PressureConverter, PRESSURE_PSI),
(SpeedConverter, SPEED_FEET_PER_SECOND),
(SpeedConverter, SPEED_INCHES_PER_DAY),
(SpeedConverter, SPEED_INCHES_PER_HOUR),
(SpeedConverter, SPEED_KILOMETERS_PER_HOUR),
(SpeedConverter, SPEED_KNOTS),
(SpeedConverter, SPEED_METERS_PER_SECOND),
(SpeedConverter, SPEED_MILES_PER_HOUR),
(SpeedConverter, SPEED_MILLIMETERS_PER_DAY),
(TemperatureConverter, TEMP_CELSIUS),
(TemperatureConverter, TEMP_FAHRENHEIT),
(TemperatureConverter, TEMP_KELVIN),
@ -70,9 +104,11 @@ def test_convert_same_unit(converter: type[BaseUnitConverter], valid_unit: str)
@pytest.mark.parametrize(
"converter,valid_unit",
[
(DistanceConverter, LENGTH_KILOMETERS),
(EnergyConverter, ENERGY_KILO_WATT_HOUR),
(PowerConverter, POWER_WATT),
(PressureConverter, PRESSURE_PA),
(SpeedConverter, SPEED_KILOMETERS_PER_HOUR),
(TemperatureConverter, TEMP_CELSIUS),
(VolumeConverter, VOLUME_LITERS),
],
@ -91,9 +127,11 @@ def test_convert_invalid_unit(
@pytest.mark.parametrize(
"converter,from_unit,to_unit",
[
(DistanceConverter, LENGTH_KILOMETERS, LENGTH_METERS),
(EnergyConverter, ENERGY_WATT_HOUR, ENERGY_KILO_WATT_HOUR),
(PowerConverter, POWER_WATT, POWER_KILO_WATT),
(PressureConverter, PRESSURE_HPA, PRESSURE_INHG),
(SpeedConverter, SPEED_KILOMETERS_PER_HOUR, SPEED_MILES_PER_HOUR),
(TemperatureConverter, TEMP_CELSIUS, TEMP_FAHRENHEIT),
(VolumeConverter, VOLUME_GALLONS, VOLUME_LITERS),
],
@ -106,6 +144,77 @@ def test_convert_nonnumeric_value(
converter.convert("a", from_unit, to_unit)
@pytest.mark.parametrize(
"value,from_unit,expected,to_unit",
[
(5, LENGTH_MILES, pytest.approx(8.04672), LENGTH_KILOMETERS),
(5, LENGTH_MILES, pytest.approx(8046.72), LENGTH_METERS),
(5, LENGTH_MILES, pytest.approx(804672.0), LENGTH_CENTIMETERS),
(5, LENGTH_MILES, pytest.approx(8046720.0), LENGTH_MILLIMETERS),
(5, LENGTH_MILES, pytest.approx(8800.0), LENGTH_YARD),
(5, LENGTH_MILES, pytest.approx(26400.0008448), LENGTH_FEET),
(5, LENGTH_MILES, pytest.approx(316800.171072), LENGTH_INCHES),
(5, LENGTH_YARD, pytest.approx(0.0045720000000000005), LENGTH_KILOMETERS),
(5, LENGTH_YARD, pytest.approx(4.572), LENGTH_METERS),
(5, LENGTH_YARD, pytest.approx(457.2), LENGTH_CENTIMETERS),
(5, LENGTH_YARD, pytest.approx(4572), LENGTH_MILLIMETERS),
(5, LENGTH_YARD, pytest.approx(0.002840908212), LENGTH_MILES),
(5, LENGTH_YARD, pytest.approx(15.00000048), LENGTH_FEET),
(5, LENGTH_YARD, pytest.approx(180.0000972), LENGTH_INCHES),
(5000, LENGTH_FEET, pytest.approx(1.524), LENGTH_KILOMETERS),
(5000, LENGTH_FEET, pytest.approx(1524), LENGTH_METERS),
(5000, LENGTH_FEET, pytest.approx(152400.0), LENGTH_CENTIMETERS),
(5000, LENGTH_FEET, pytest.approx(1524000.0), LENGTH_MILLIMETERS),
(5000, LENGTH_FEET, pytest.approx(0.9469694040000001), LENGTH_MILES),
(5000, LENGTH_FEET, pytest.approx(1666.66667), LENGTH_YARD),
(5000, LENGTH_FEET, pytest.approx(60000.032400000004), LENGTH_INCHES),
(5000, LENGTH_INCHES, pytest.approx(0.127), LENGTH_KILOMETERS),
(5000, LENGTH_INCHES, pytest.approx(127.0), LENGTH_METERS),
(5000, LENGTH_INCHES, pytest.approx(12700.0), LENGTH_CENTIMETERS),
(5000, LENGTH_INCHES, pytest.approx(127000.0), LENGTH_MILLIMETERS),
(5000, LENGTH_INCHES, pytest.approx(0.078914117), LENGTH_MILES),
(5000, LENGTH_INCHES, pytest.approx(138.88889), LENGTH_YARD),
(5000, LENGTH_INCHES, pytest.approx(416.66668), LENGTH_FEET),
(5, LENGTH_KILOMETERS, pytest.approx(5000), LENGTH_METERS),
(5, LENGTH_KILOMETERS, pytest.approx(500000), LENGTH_CENTIMETERS),
(5, LENGTH_KILOMETERS, pytest.approx(5000000), LENGTH_MILLIMETERS),
(5, LENGTH_KILOMETERS, pytest.approx(3.106855), LENGTH_MILES),
(5, LENGTH_KILOMETERS, pytest.approx(5468.066), LENGTH_YARD),
(5, LENGTH_KILOMETERS, pytest.approx(16404.2), LENGTH_FEET),
(5, LENGTH_KILOMETERS, pytest.approx(196850.5), LENGTH_INCHES),
(5000, LENGTH_METERS, pytest.approx(5), LENGTH_KILOMETERS),
(5000, LENGTH_METERS, pytest.approx(500000), LENGTH_CENTIMETERS),
(5000, LENGTH_METERS, pytest.approx(5000000), LENGTH_MILLIMETERS),
(5000, LENGTH_METERS, pytest.approx(3.106855), LENGTH_MILES),
(5000, LENGTH_METERS, pytest.approx(5468.066), LENGTH_YARD),
(5000, LENGTH_METERS, pytest.approx(16404.2), LENGTH_FEET),
(5000, LENGTH_METERS, pytest.approx(196850.5), LENGTH_INCHES),
(500000, LENGTH_CENTIMETERS, pytest.approx(5), LENGTH_KILOMETERS),
(500000, LENGTH_CENTIMETERS, pytest.approx(5000), LENGTH_METERS),
(500000, LENGTH_CENTIMETERS, pytest.approx(5000000), LENGTH_MILLIMETERS),
(500000, LENGTH_CENTIMETERS, pytest.approx(3.106855), LENGTH_MILES),
(500000, LENGTH_CENTIMETERS, pytest.approx(5468.066), LENGTH_YARD),
(500000, LENGTH_CENTIMETERS, pytest.approx(16404.2), LENGTH_FEET),
(500000, LENGTH_CENTIMETERS, pytest.approx(196850.5), LENGTH_INCHES),
(5000000, LENGTH_MILLIMETERS, pytest.approx(5), LENGTH_KILOMETERS),
(5000000, LENGTH_MILLIMETERS, pytest.approx(5000), LENGTH_METERS),
(5000000, LENGTH_MILLIMETERS, pytest.approx(500000), LENGTH_CENTIMETERS),
(5000000, LENGTH_MILLIMETERS, pytest.approx(3.106855), LENGTH_MILES),
(5000000, LENGTH_MILLIMETERS, pytest.approx(5468.066), LENGTH_YARD),
(5000000, LENGTH_MILLIMETERS, pytest.approx(16404.2), LENGTH_FEET),
(5000000, LENGTH_MILLIMETERS, pytest.approx(196850.5), LENGTH_INCHES),
],
)
def test_distance_convert(
value: float,
from_unit: str,
expected: float,
to_unit: str,
) -> None:
"""Test conversion to other units."""
assert DistanceConverter.convert(value, from_unit, to_unit) == expected
@pytest.mark.parametrize(
"value,from_unit,expected,to_unit",
[
@ -185,6 +294,44 @@ def test_pressure_convert(
assert PressureConverter.convert(value, from_unit, to_unit) == expected
@pytest.mark.parametrize(
"value,from_unit,expected,to_unit",
[
# 5 km/h / 1.609 km/mi = 3.10686 mi/h
(5, SPEED_KILOMETERS_PER_HOUR, pytest.approx(3.106856), SPEED_MILES_PER_HOUR),
# 5 mi/h * 1.609 km/mi = 8.04672 km/h
(5, SPEED_MILES_PER_HOUR, 8.04672, SPEED_KILOMETERS_PER_HOUR),
# 5 in/day * 25.4 mm/in = 127 mm/day
(5, SPEED_INCHES_PER_DAY, 127, SPEED_MILLIMETERS_PER_DAY),
# 5 mm/day / 25.4 mm/in = 0.19685 in/day
(5, SPEED_MILLIMETERS_PER_DAY, pytest.approx(0.1968504), SPEED_INCHES_PER_DAY),
# 5 in/hr * 24 hr/day = 3048 mm/day
(5, SPEED_INCHES_PER_HOUR, 3048, SPEED_MILLIMETERS_PER_DAY),
# 5 m/s * 39.3701 in/m * 3600 s/hr = 708661
(5, SPEED_METERS_PER_SECOND, pytest.approx(708661.42), SPEED_INCHES_PER_HOUR),
# 5000 in/h / 39.3701 in/m / 3600 s/h = 0.03528 m/s
(
5000,
SPEED_INCHES_PER_HOUR,
pytest.approx(0.0352778),
SPEED_METERS_PER_SECOND,
),
# 5 kt * 1852 m/nmi / 3600 s/h = 2.5722 m/s
(5, SPEED_KNOTS, pytest.approx(2.57222), SPEED_METERS_PER_SECOND),
# 5 ft/s * 0.3048 m/ft = 1.524 m/s
(5, SPEED_FEET_PER_SECOND, pytest.approx(1.524), SPEED_METERS_PER_SECOND),
],
)
def test_speed_convert(
value: float,
from_unit: str,
expected: float,
to_unit: str,
) -> None:
"""Test conversion to other units."""
assert SpeedConverter.convert(value, from_unit, to_unit) == expected
@pytest.mark.parametrize(
"value,from_unit,expected,to_unit",
[