Add Beaufort to wind_speed (#105795)

* Add Beaufort to wind_speed

* Add Bft to UnitOfSpeed

* Update tests with Bft

* Remove check for unit

* Fix test_deprecated_constants

* Test depricated constant Beaufort

* Fix test_unit_system.py for Beaufort

* Remove _DEPRECATED_SPEED_FEET_BEAUFORT

* Remove maxsize from lru_cache

* Update test_deprecated_constants

* Update comment

* Add missing docstring

* Apply suggestions from code review

---------

Co-authored-by: Erik Montnemery <erik@montnemery.com>
This commit is contained in:
fwestenberg 2024-03-05 14:55:59 +01:00 committed by GitHub
parent f3eb292c2d
commit 385b29bdf5
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
7 changed files with 95 additions and 2 deletions

View File

@ -344,6 +344,7 @@ class SensorDeviceClass(StrEnum):
- SI /metric: `mm/d`, `mm/h`, `m/s`, `km/h`
- USCS / imperial: `in/d`, `in/h`, `ft/s`, `mph`
- Nautical: `kn`
- Beaufort: `Beaufort`
"""
SULPHUR_DIOXIDE = "sulphur_dioxide"
@ -431,6 +432,7 @@ class SensorDeviceClass(StrEnum):
- SI /metric: `m/s`, `km/h`
- USCS / imperial: `ft/s`, `mph`
- Nautical: `kn`
- Beaufort: `Beaufort`
"""

View File

@ -1214,6 +1214,7 @@ CONCENTRATION_PARTS_PER_BILLION: Final = "ppb"
class UnitOfSpeed(StrEnum):
"""Speed units."""
BEAUFORT = "Beaufort"
FEET_PER_SECOND = "ft/s"
METERS_PER_SECOND = "m/s"
KILOMETERS_PER_HOUR = "km/h"

View File

@ -334,6 +334,7 @@ class SpeedConverter(BaseUnitConverter):
UnitOfSpeed.KNOTS: _HRS_TO_SECS / _NAUTICAL_MILE_TO_M,
UnitOfSpeed.METERS_PER_SECOND: 1,
UnitOfSpeed.MILES_PER_HOUR: _HRS_TO_SECS / _MILE_TO_M,
UnitOfSpeed.BEAUFORT: 1,
}
VALID_UNITS = {
UnitOfVolumetricFlux.INCHES_PER_DAY,
@ -345,8 +346,73 @@ class SpeedConverter(BaseUnitConverter):
UnitOfSpeed.KNOTS,
UnitOfSpeed.METERS_PER_SECOND,
UnitOfSpeed.MILES_PER_HOUR,
UnitOfSpeed.BEAUFORT,
}
@classmethod
@lru_cache
def converter_factory(
cls, from_unit: str | None, to_unit: str | None
) -> Callable[[float], float]:
"""Return a function to convert a speed from one unit to another."""
if from_unit == to_unit:
# Return a function that does nothing. This is not
# in _converter_factory because we do not want to wrap
# it with the None check in converter_factory_allow_none.
return lambda value: value
return cls._converter_factory(from_unit, to_unit)
@classmethod
@lru_cache
def converter_factory_allow_none(
cls, from_unit: str | None, to_unit: str | None
) -> Callable[[float | None], float | None]:
"""Return a function to convert a speed from one unit to another which allows None."""
if from_unit == to_unit:
# Return a function that does nothing. This is not
# in _converter_factory because we do not want to wrap
# it with the None check in this case.
return lambda value: value
convert = cls._converter_factory(from_unit, to_unit)
return lambda value: None if value is None else convert(value)
@classmethod
def _converter_factory(
cls, from_unit: str | None, to_unit: str | None
) -> Callable[[float], float]:
"""Convert a speed from one unit to another, eg. 14m/s will return 7Bft."""
# We cannot use the implementation from BaseUnitConverter here because the
# Beaufort scale is not a constant value to divide or multiply with.
if (
from_unit not in SpeedConverter.VALID_UNITS
or to_unit not in SpeedConverter.VALID_UNITS
):
raise HomeAssistantError(
UNIT_NOT_RECOGNIZED_TEMPLATE.format(to_unit, cls.UNIT_CLASS)
)
if from_unit == UnitOfSpeed.BEAUFORT:
to_ratio = cls._UNIT_CONVERSION[to_unit]
return lambda val: cls._beaufort_to_ms(val) * to_ratio
if to_unit == UnitOfSpeed.BEAUFORT:
from_ratio = cls._UNIT_CONVERSION[from_unit]
return lambda val: cls._ms_to_beaufort(val / from_ratio)
from_ratio, to_ratio = cls._get_from_to_ratio(from_unit, to_unit)
return lambda val: (val / from_ratio) * to_ratio
@classmethod
def _ms_to_beaufort(cls, ms: float) -> float:
"""Convert a speed in m/s to Beaufort."""
return float(round(((ms / 0.836) ** 2) ** (1 / 3)))
@classmethod
def _beaufort_to_ms(cls, beaufort: float) -> float:
"""Convert a speed in Beaufort to m/s."""
return float(0.836 * beaufort ** (3 / 2))
class TemperatureConverter(BaseUnitConverter):
"""Utility to convert temperature values."""

View File

@ -30,7 +30,18 @@ async def test_device_class_units(
msg = await client.receive_json()
assert msg["success"]
assert msg["result"] == {
"units": ["ft/s", "in/d", "in/h", "km/h", "kn", "m/s", "mm/d", "mm/h", "mph"]
"units": [
"Beaufort",
"ft/s",
"in/d",
"in/h",
"km/h",
"kn",
"m/s",
"mm/d",
"mm/h",
"mph",
]
}
# Device class with units which include `None`

View File

@ -131,7 +131,16 @@ def test_all() -> None:
],
"PRECIPITATION_",
)
+ _create_tuples(const.UnitOfSpeed, "SPEED_")
+ _create_tuples(
[
const.UnitOfSpeed.FEET_PER_SECOND,
const.UnitOfSpeed.METERS_PER_SECOND,
const.UnitOfSpeed.KILOMETERS_PER_HOUR,
const.UnitOfSpeed.KNOTS,
const.UnitOfSpeed.MILES_PER_HOUR,
],
"SPEED_",
)
+ _create_tuples(
[
const.UnitOfVolumetricFlux.MILLIMETERS_PER_DAY,

View File

@ -413,6 +413,8 @@ _CONVERTED_VALUE: dict[
(5, UnitOfSpeed.KNOTS, 2.57222, UnitOfSpeed.METERS_PER_SECOND),
# 5 ft/s * 0.3048 m/ft = 1.524 m/s
(5, UnitOfSpeed.FEET_PER_SECOND, 1.524, UnitOfSpeed.METERS_PER_SECOND),
# float(round(((20.7 m/s / 0.836) ** 2) ** (1 / 3))) = 8.0Bft
(20.7, UnitOfSpeed.METERS_PER_SECOND, 8.0, UnitOfSpeed.BEAUFORT),
],
TemperatureConverter: [
(100, UnitOfTemperature.CELSIUS, 212, UnitOfTemperature.FAHRENHEIT),

View File

@ -515,6 +515,7 @@ UNCONVERTED_UNITS_METRIC_SYSTEM = {
UnitOfPressure.PA,
),
SensorDeviceClass.SPEED: (
UnitOfSpeed.BEAUFORT,
UnitOfSpeed.KILOMETERS_PER_HOUR,
UnitOfSpeed.KNOTS,
UnitOfSpeed.METERS_PER_SECOND,
@ -723,6 +724,7 @@ UNCONVERTED_UNITS_US_SYSTEM = {
),
SensorDeviceClass.PRESSURE: (UnitOfPressure.INHG, UnitOfPressure.PSI),
SensorDeviceClass.SPEED: (
UnitOfSpeed.BEAUFORT,
UnitOfSpeed.FEET_PER_SECOND,
UnitOfSpeed.KNOTS,
UnitOfSpeed.MILES_PER_HOUR,