Area units and conversion between metric and US (#123563)

* area conversions

* start work on tests

* add number device class

* update unit conversions to utilise distance constants

* add area unit

* update test unit system

* update device condition and trigger

* update statistic unit converters

* further tests work WIP

* update test unit system

* add missing string translations

* fix websocket tests

* add deprecated notice

* add more missing strings and missing initialisation of unit system

* adjust icon and remove strings from scrape and random

* Fix acre to meters conversion

Co-authored-by: epenet <6771947+epenet@users.noreply.github.com>

* Tidy up valid units

Co-authored-by: epenet <6771947+epenet@users.noreply.github.com>

* fix ordering of area

* update order alphabetically

* fix broken test

* update test_init

* Update homeassistant/const.py

Co-authored-by: epenet <6771947+epenet@users.noreply.github.com>

* remove deprecated unit and fix alphabetical order

* change deprecation and add tests, change to millimeter conversion for inches

* fix order

* re-order defs alphabetically

* add measurement as well

* update icons

* fix up Deprecation of area square meters

* Update core integrations to UnitOfArea

* update test recorder tests

* unit system tests in alphabetical

* update snapshot

* rebuild

* revert alphabetization of functions

* other revert of alphabetical order

---------

Co-authored-by: epenet <6771947+epenet@users.noreply.github.com>
This commit is contained in:
Michael Arthur 2024-11-22 04:10:44 +13:00 committed by GitHub
parent 9add3a6c9b
commit d8549409f7
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
29 changed files with 394 additions and 47 deletions

View File

@ -26,11 +26,11 @@ from homeassistant.components.sensor import (
SensorStateClass,
)
from homeassistant.const import (
AREA_SQUARE_METERS,
ATTR_BATTERY_LEVEL,
CONF_DESCRIPTION,
PERCENTAGE,
EntityCategory,
UnitOfArea,
UnitOfTime,
)
from homeassistant.core import HomeAssistant
@ -67,7 +67,7 @@ ENTITY_DESCRIPTIONS: tuple[EcovacsSensorEntityDescription, ...] = (
capability_fn=lambda caps: caps.stats.clean,
value_fn=lambda e: e.area,
translation_key="stats_area",
native_unit_of_measurement=AREA_SQUARE_METERS,
native_unit_of_measurement=UnitOfArea.SQUARE_METERS,
),
EcovacsSensorEntityDescription[StatsEvent](
key="stats_time",
@ -84,7 +84,7 @@ ENTITY_DESCRIPTIONS: tuple[EcovacsSensorEntityDescription, ...] = (
value_fn=lambda e: e.area,
key="total_stats_area",
translation_key="total_stats_area",
native_unit_of_measurement=AREA_SQUARE_METERS,
native_unit_of_measurement=UnitOfArea.SQUARE_METERS,
state_class=SensorStateClass.TOTAL_INCREASING,
),
EcovacsSensorEntityDescription[TotalStatsEvent](

View File

@ -17,6 +17,7 @@ from homeassistant.const import (
SIGNAL_STRENGTH_DECIBELS,
SIGNAL_STRENGTH_DECIBELS_MILLIWATT,
UnitOfApparentPower,
UnitOfArea,
UnitOfBloodGlucoseConcentration,
UnitOfConductivity,
UnitOfDataRate,
@ -98,6 +99,12 @@ class NumberDeviceClass(StrEnum):
Unit of measurement: `None`
"""
AREA = "area"
"""Area
Unit of measurement: `UnitOfArea` units
"""
ATMOSPHERIC_PRESSURE = "atmospheric_pressure"
"""Atmospheric pressure.
@ -434,6 +441,7 @@ DEVICE_CLASSES_SCHEMA: Final = vol.All(vol.Lower, vol.Coerce(NumberDeviceClass))
DEVICE_CLASS_UNITS: dict[NumberDeviceClass, set[type[StrEnum] | str | None]] = {
NumberDeviceClass.APPARENT_POWER: set(UnitOfApparentPower),
NumberDeviceClass.AQI: {None},
NumberDeviceClass.AREA: set(UnitOfArea),
NumberDeviceClass.ATMOSPHERIC_PRESSURE: set(UnitOfPressure),
NumberDeviceClass.BATTERY: {PERCENTAGE},
NumberDeviceClass.BLOOD_GLUCOSE_CONCENTRATION: set(UnitOfBloodGlucoseConcentration),

View File

@ -9,6 +9,9 @@
"aqi": {
"default": "mdi:air-filter"
},
"area": {
"default": "mdi:texture-box"
},
"atmospheric_pressure": {
"default": "mdi:thermometer-lines"
},

View File

@ -37,6 +37,9 @@
"aqi": {
"name": "[%key:component::sensor::entity_component::aqi::name%]"
},
"area": {
"name": "[%key:component::sensor::entity_component::area::name%]"
},
"atmospheric_pressure": {
"name": "[%key:component::sensor::entity_component::atmospheric_pressure::name%]"
},

View File

@ -27,6 +27,7 @@ from homeassistant.helpers.singleton import singleton
from homeassistant.helpers.typing import UNDEFINED, UndefinedType
from homeassistant.util import dt as dt_util
from homeassistant.util.unit_conversion import (
AreaConverter,
BaseUnitConverter,
BloodGlucoseConcentrationConverter,
ConductivityConverter,
@ -129,6 +130,7 @@ QUERY_STATISTICS_SUMMARY_SUM = (
STATISTIC_UNIT_TO_UNIT_CONVERTER: dict[str | None, type[BaseUnitConverter]] = {
**{unit: AreaConverter for unit in AreaConverter.VALID_UNITS},
**{
unit: BloodGlucoseConcentrationConverter
for unit in BloodGlucoseConcentrationConverter.VALID_UNITS

View File

@ -16,6 +16,7 @@ from homeassistant.helpers import config_validation as cv
from homeassistant.helpers.json import json_bytes
from homeassistant.util import dt as dt_util
from homeassistant.util.unit_conversion import (
AreaConverter,
BloodGlucoseConcentrationConverter,
ConductivityConverter,
DataRateConverter,
@ -55,6 +56,7 @@ UPDATE_STATISTICS_METADATA_TIME_OUT = 10
UNIT_SCHEMA = vol.Schema(
{
vol.Optional("area"): vol.In(AreaConverter.VALID_UNITS),
vol.Optional("blood_glucose_concentration"): vol.In(
BloodGlucoseConcentrationConverter.VALID_UNITS
),

View File

@ -9,7 +9,7 @@ from pyrituals import Diffuser
from homeassistant.components.select import SelectEntity, SelectEntityDescription
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import AREA_SQUARE_METERS, EntityCategory
from homeassistant.const import EntityCategory, UnitOfArea
from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddEntitiesCallback
@ -30,7 +30,7 @@ ENTITY_DESCRIPTIONS = (
RitualsSelectEntityDescription(
key="room_size_square_meter",
translation_key="room_size_square_meter",
unit_of_measurement=AREA_SQUARE_METERS,
unit_of_measurement=UnitOfArea.SQUARE_METERS,
entity_category=EntityCategory.CONFIG,
options=["15", "30", "60", "100"],
current_fn=lambda diffuser: str(diffuser.room_size_square_meter),

View File

@ -25,12 +25,7 @@ from homeassistant.components.sensor import (
SensorEntity,
SensorEntityDescription,
)
from homeassistant.const import (
AREA_SQUARE_METERS,
PERCENTAGE,
EntityCategory,
UnitOfTime,
)
from homeassistant.const import PERCENTAGE, EntityCategory, UnitOfArea, UnitOfTime
from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.helpers.typing import StateType
@ -131,14 +126,14 @@ SENSOR_DESCRIPTIONS = [
translation_key="cleaning_area",
value_fn=lambda data: data.status.square_meter_clean_area,
entity_category=EntityCategory.DIAGNOSTIC,
native_unit_of_measurement=AREA_SQUARE_METERS,
native_unit_of_measurement=UnitOfArea.SQUARE_METERS,
),
RoborockSensorDescription(
key="total_cleaning_area",
translation_key="total_cleaning_area",
value_fn=lambda data: data.clean_summary.square_meter_clean_area,
entity_category=EntityCategory.DIAGNOSTIC,
native_unit_of_measurement=AREA_SQUARE_METERS,
native_unit_of_measurement=UnitOfArea.SQUARE_METERS,
),
RoborockSensorDescription(
key="vacuum_error",

View File

@ -8,10 +8,10 @@ from homeassistant.components.sensor import (
)
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import (
AREA_SQUARE_METERS,
PERCENTAGE,
SIGNAL_STRENGTH_DECIBELS_MILLIWATT,
EntityCategory,
UnitOfArea,
UnitOfLength,
UnitOfTime,
)
@ -61,7 +61,7 @@ SENSORS: list[SensorEntityDescription] = [
key="total_area_cleaned",
translation_key="total_area_cleaned",
state_class=SensorStateClass.TOTAL,
native_unit_of_measurement=AREA_SQUARE_METERS,
native_unit_of_measurement=UnitOfArea.SQUARE_METERS,
entity_category=EntityCategory.DIAGNOSTIC,
),
SensorEntityDescription(

View File

@ -12,12 +12,7 @@ from homeassistant.components.sensor import (
SensorStateClass,
)
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import (
AREA_SQUARE_METERS,
PERCENTAGE,
EntityCategory,
UnitOfTime,
)
from homeassistant.const import PERCENTAGE, EntityCategory, UnitOfArea, UnitOfTime
from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.helpers.typing import StateType
@ -108,7 +103,7 @@ SENSORS: list[RoombaSensorEntityDescription] = [
RoombaSensorEntityDescription(
key="total_cleaned_area",
translation_key="total_cleaned_area",
native_unit_of_measurement=AREA_SQUARE_METERS,
native_unit_of_measurement=UnitOfArea.SQUARE_METERS,
entity_category=EntityCategory.DIAGNOSTIC,
value_fn=lambda self: (
None if (sqft := self.run_stats.get("sqft")) is None else sqft * 9.29

View File

@ -17,6 +17,7 @@ from homeassistant.const import (
SIGNAL_STRENGTH_DECIBELS,
SIGNAL_STRENGTH_DECIBELS_MILLIWATT,
UnitOfApparentPower,
UnitOfArea,
UnitOfBloodGlucoseConcentration,
UnitOfConductivity,
UnitOfDataRate,
@ -47,6 +48,7 @@ from homeassistant.helpers.deprecation import (
dir_with_deprecated_constants,
)
from homeassistant.util.unit_conversion import (
AreaConverter,
BaseUnitConverter,
BloodGlucoseConcentrationConverter,
ConductivityConverter,
@ -117,6 +119,12 @@ class SensorDeviceClass(StrEnum):
Unit of measurement: `None`
"""
AREA = "area"
"""Area
Unit of measurement: `UnitOfArea` units
"""
ATMOSPHERIC_PRESSURE = "atmospheric_pressure"
"""Atmospheric pressure.
@ -500,6 +508,7 @@ _DEPRECATED_STATE_CLASS_TOTAL_INCREASING: Final = DeprecatedConstantEnum(
STATE_CLASSES: Final[list[str]] = [cls.value for cls in SensorStateClass]
UNIT_CONVERTERS: dict[SensorDeviceClass | str | None, type[BaseUnitConverter]] = {
SensorDeviceClass.AREA: AreaConverter,
SensorDeviceClass.ATMOSPHERIC_PRESSURE: PressureConverter,
SensorDeviceClass.BLOOD_GLUCOSE_CONCENTRATION: BloodGlucoseConcentrationConverter,
SensorDeviceClass.CONDUCTIVITY: ConductivityConverter,
@ -531,6 +540,7 @@ UNIT_CONVERTERS: dict[SensorDeviceClass | str | None, type[BaseUnitConverter]] =
DEVICE_CLASS_UNITS: dict[SensorDeviceClass, set[type[StrEnum] | str | None]] = {
SensorDeviceClass.APPARENT_POWER: set(UnitOfApparentPower),
SensorDeviceClass.AQI: {None},
SensorDeviceClass.AREA: set(UnitOfArea),
SensorDeviceClass.ATMOSPHERIC_PRESSURE: set(UnitOfPressure),
SensorDeviceClass.BATTERY: {PERCENTAGE},
SensorDeviceClass.BLOOD_GLUCOSE_CONCENTRATION: set(UnitOfBloodGlucoseConcentration),
@ -607,6 +617,7 @@ DEVICE_CLASS_UNITS: dict[SensorDeviceClass, set[type[StrEnum] | str | None]] = {
DEVICE_CLASS_STATE_CLASSES: dict[SensorDeviceClass, set[SensorStateClass]] = {
SensorDeviceClass.APPARENT_POWER: {SensorStateClass.MEASUREMENT},
SensorDeviceClass.AQI: {SensorStateClass.MEASUREMENT},
SensorDeviceClass.AREA: set(SensorStateClass),
SensorDeviceClass.ATMOSPHERIC_PRESSURE: {SensorStateClass.MEASUREMENT},
SensorDeviceClass.BATTERY: {SensorStateClass.MEASUREMENT},
SensorDeviceClass.BLOOD_GLUCOSE_CONCENTRATION: {SensorStateClass.MEASUREMENT},

View File

@ -35,6 +35,7 @@ DEVICE_CLASS_NONE = "none"
CONF_IS_APPARENT_POWER = "is_apparent_power"
CONF_IS_AQI = "is_aqi"
CONF_IS_AREA = "is_area"
CONF_IS_ATMOSPHERIC_PRESSURE = "is_atmospheric_pressure"
CONF_IS_BATTERY_LEVEL = "is_battery_level"
CONF_IS_BLOOD_GLUCOSE_CONCENTRATION = "is_blood_glucose_concentration"
@ -86,6 +87,7 @@ CONF_IS_WIND_SPEED = "is_wind_speed"
ENTITY_CONDITIONS = {
SensorDeviceClass.APPARENT_POWER: [{CONF_TYPE: CONF_IS_APPARENT_POWER}],
SensorDeviceClass.AQI: [{CONF_TYPE: CONF_IS_AQI}],
SensorDeviceClass.AREA: [{CONF_TYPE: CONF_IS_AREA}],
SensorDeviceClass.ATMOSPHERIC_PRESSURE: [{CONF_TYPE: CONF_IS_ATMOSPHERIC_PRESSURE}],
SensorDeviceClass.BATTERY: [{CONF_TYPE: CONF_IS_BATTERY_LEVEL}],
SensorDeviceClass.BLOOD_GLUCOSE_CONCENTRATION: [
@ -153,6 +155,7 @@ CONDITION_SCHEMA = vol.All(
[
CONF_IS_APPARENT_POWER,
CONF_IS_AQI,
CONF_IS_AREA,
CONF_IS_ATMOSPHERIC_PRESSURE,
CONF_IS_BATTERY_LEVEL,
CONF_IS_BLOOD_GLUCOSE_CONCENTRATION,

View File

@ -34,6 +34,7 @@ DEVICE_CLASS_NONE = "none"
CONF_APPARENT_POWER = "apparent_power"
CONF_AQI = "aqi"
CONF_AREA = "area"
CONF_ATMOSPHERIC_PRESSURE = "atmospheric_pressure"
CONF_BATTERY_LEVEL = "battery_level"
CONF_BLOOD_GLUCOSE_CONCENTRATION = "blood_glucose_concentration"
@ -85,6 +86,7 @@ CONF_WIND_SPEED = "wind_speed"
ENTITY_TRIGGERS = {
SensorDeviceClass.APPARENT_POWER: [{CONF_TYPE: CONF_APPARENT_POWER}],
SensorDeviceClass.AQI: [{CONF_TYPE: CONF_AQI}],
SensorDeviceClass.AREA: [{CONF_TYPE: CONF_AREA}],
SensorDeviceClass.ATMOSPHERIC_PRESSURE: [{CONF_TYPE: CONF_ATMOSPHERIC_PRESSURE}],
SensorDeviceClass.BATTERY: [{CONF_TYPE: CONF_BATTERY_LEVEL}],
SensorDeviceClass.BLOOD_GLUCOSE_CONCENTRATION: [
@ -153,6 +155,7 @@ TRIGGER_SCHEMA = vol.All(
[
CONF_APPARENT_POWER,
CONF_AQI,
CONF_AREA,
CONF_ATMOSPHERIC_PRESSURE,
CONF_BATTERY_LEVEL,
CONF_BLOOD_GLUCOSE_CONCENTRATION,

View File

@ -9,6 +9,9 @@
"aqi": {
"default": "mdi:air-filter"
},
"area": {
"default": "mdi:texture-box"
},
"atmospheric_pressure": {
"default": "mdi:thermometer-lines"
},

View File

@ -4,6 +4,7 @@
"condition_type": {
"is_apparent_power": "Current {entity_name} apparent power",
"is_aqi": "Current {entity_name} air quality index",
"is_area": "Current {entity_name} area",
"is_atmospheric_pressure": "Current {entity_name} atmospheric pressure",
"is_battery_level": "Current {entity_name} battery level",
"is_blood_glucose_concentration": "Current {entity_name} blood glucose concentration",
@ -55,6 +56,7 @@
"trigger_type": {
"apparent_power": "{entity_name} apparent power changes",
"aqi": "{entity_name} air quality index changes",
"area": "{entity_name} area changes",
"atmospheric_pressure": "{entity_name} atmospheric pressure changes",
"battery_level": "{entity_name} battery level changes",
"blood_glucose_concentration": "{entity_name} blood glucose concentration changes",
@ -145,6 +147,9 @@
"aqi": {
"name": "Air quality index"
},
"area": {
"name": "Area"
},
"atmospheric_pressure": {
"name": "Atmospheric pressure"
},

View File

@ -15,11 +15,11 @@ from homeassistant.components.sensor import (
)
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import (
AREA_SQUARE_METERS,
CONCENTRATION_PARTS_PER_MILLION,
LIGHT_LUX,
PERCENTAGE,
EntityCategory,
UnitOfArea,
UnitOfElectricPotential,
UnitOfEnergy,
UnitOfMass,
@ -95,7 +95,7 @@ CAPABILITY_TO_SENSORS: dict[str, list[Map]] = {
Map(
Attribute.bmi_measurement,
"Body Mass Index",
f"{UnitOfMass.KILOGRAMS}/{AREA_SQUARE_METERS}",
f"{UnitOfMass.KILOGRAMS}/{UnitOfArea.SQUARE_METERS}",
None,
SensorStateClass.MEASUREMENT,
None,

View File

@ -24,7 +24,6 @@ from homeassistant.components.sensor import (
)
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import (
AREA_SQUARE_METERS,
ATTR_BATTERY_LEVEL,
ATTR_TEMPERATURE,
CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
@ -37,6 +36,7 @@ from homeassistant.const import (
PERCENTAGE,
REVOLUTIONS_PER_MINUTE,
EntityCategory,
UnitOfArea,
UnitOfPower,
UnitOfPressure,
UnitOfTemperature,
@ -622,7 +622,7 @@ VACUUM_SENSORS = {
entity_category=EntityCategory.DIAGNOSTIC,
),
f"last_clean_{ATTR_LAST_CLEAN_AREA}": XiaomiMiioSensorDescription(
native_unit_of_measurement=AREA_SQUARE_METERS,
native_unit_of_measurement=UnitOfArea.SQUARE_METERS,
icon="mdi:texture-box",
key=ATTR_LAST_CLEAN_AREA,
parent_key=VacuumCoordinatorDataAttributes.last_clean_details,
@ -639,7 +639,7 @@ VACUUM_SENSORS = {
entity_category=EntityCategory.DIAGNOSTIC,
),
f"current_{ATTR_LAST_CLEAN_AREA}": XiaomiMiioSensorDescription(
native_unit_of_measurement=AREA_SQUARE_METERS,
native_unit_of_measurement=UnitOfArea.SQUARE_METERS,
icon="mdi:texture-box",
key=ATTR_STATUS_CLEAN_AREA,
parent_key=VacuumCoordinatorDataAttributes.status,
@ -657,7 +657,7 @@ VACUUM_SENSORS = {
entity_category=EntityCategory.DIAGNOSTIC,
),
f"clean_history_{ATTR_CLEAN_HISTORY_TOTAL_AREA}": XiaomiMiioSensorDescription(
native_unit_of_measurement=AREA_SQUARE_METERS,
native_unit_of_measurement=UnitOfArea.SQUARE_METERS,
icon="mdi:texture-box",
key=ATTR_CLEAN_HISTORY_TOTAL_AREA,
parent_key=VacuumCoordinatorDataAttributes.clean_history_status,

View File

@ -1179,8 +1179,27 @@ _DEPRECATED_VOLUME_FLOW_RATE_CUBIC_FEET_PER_MINUTE: Final = DeprecatedConstantEn
)
"""Deprecated: please use UnitOfVolumeFlowRate.CUBIC_FEET_PER_MINUTE"""
# Area units
AREA_SQUARE_METERS: Final = ""
class UnitOfArea(StrEnum):
"""Area units."""
SQUARE_METERS = ""
SQUARE_CENTIMETERS = "cm²"
SQUARE_KILOMETERS = "km²"
SQUARE_MILLIMETERS = "mm²"
SQUARE_INCHES = "in²"
SQUARE_FEET = "ft²"
SQUARE_YARDS = "yd²"
SQUARE_MILES = "mi²"
ACRES = "ac"
HECTARES = "ha"
_DEPRECATED_AREA_SQUARE_METERS: Final = DeprecatedConstantEnum(
UnitOfArea.SQUARE_METERS,
"2025.12",
)
"""Deprecated: please use UnitOfArea.SQUARE_METERS"""
# Mass units
@ -1704,6 +1723,7 @@ RESTART_EXIT_CODE: Final = 100
UNIT_NOT_RECOGNIZED_TEMPLATE: Final = "{} is not a recognized {} unit."
LENGTH: Final = "length"
AREA: Final = "area"
MASS: Final = "mass"
PRESSURE: Final = "pressure"
VOLUME: Final = "volume"

View File

@ -10,6 +10,7 @@ from homeassistant.const import (
CONCENTRATION_PARTS_PER_MILLION,
PERCENTAGE,
UNIT_NOT_RECOGNIZED_TEMPLATE,
UnitOfArea,
UnitOfBloodGlucoseConcentration,
UnitOfConductivity,
UnitOfDataRate,
@ -42,6 +43,19 @@ _MILE_TO_M = _YARD_TO_M * 1760 # 1760 yard = 1 mile (1609.344 m)
_NAUTICAL_MILE_TO_M = 1852 # 1 nautical mile = 1852 m
# Area constants to square meters
_CM2_TO_M2 = _CM_TO_M**2 # 1 cm² = 0.0001 m²
_MM2_TO_M2 = _MM_TO_M**2 # 1 mm² = 0.000001 m²
_KM2_TO_M2 = _KM_TO_M**2 # 1 km² = 1,000,000 m²
_IN2_TO_M2 = _IN_TO_M**2 # 1 in² = 0.00064516 m²
_FT2_TO_M2 = _FOOT_TO_M**2 # 1 ft² = 0.092903 m²
_YD2_TO_M2 = _YARD_TO_M**2 # 1 yd² = 0.836127 m²
_MI2_TO_M2 = _MILE_TO_M**2 # 1 mi² = 2,590,000 m²
_ACRE_TO_M2 = 66 * 660 * _FT2_TO_M2 # 1 acre = 4,046.86 m²
_HECTARE_TO_M2 = 100 * 100 # 1 hectare = 10,000 m²
# Duration conversion constants
_MIN_TO_SEC = 60 # 1 min = 60 seconds
_HRS_TO_MINUTES = 60 # 1 hr = 60 minutes
@ -146,6 +160,25 @@ class DataRateConverter(BaseUnitConverter):
VALID_UNITS = set(UnitOfDataRate)
class AreaConverter(BaseUnitConverter):
"""Utility to convert area values."""
UNIT_CLASS = "area"
_UNIT_CONVERSION: dict[str | None, float] = {
UnitOfArea.SQUARE_METERS: 1,
UnitOfArea.SQUARE_CENTIMETERS: 1 / _CM2_TO_M2,
UnitOfArea.SQUARE_MILLIMETERS: 1 / _MM2_TO_M2,
UnitOfArea.SQUARE_KILOMETERS: 1 / _KM2_TO_M2,
UnitOfArea.SQUARE_INCHES: 1 / _IN2_TO_M2,
UnitOfArea.SQUARE_FEET: 1 / _FT2_TO_M2,
UnitOfArea.SQUARE_YARDS: 1 / _YD2_TO_M2,
UnitOfArea.SQUARE_MILES: 1 / _MI2_TO_M2,
UnitOfArea.ACRES: 1 / _ACRE_TO_M2,
UnitOfArea.HECTARES: 1 / _HECTARE_TO_M2,
}
VALID_UNITS = set(UnitOfArea)
class DistanceConverter(BaseUnitConverter):
"""Utility to convert distance values."""

View File

@ -9,6 +9,7 @@ import voluptuous as vol
from homeassistant.const import (
ACCUMULATED_PRECIPITATION,
AREA,
LENGTH,
MASS,
PRESSURE,
@ -16,6 +17,7 @@ from homeassistant.const import (
UNIT_NOT_RECOGNIZED_TEMPLATE,
VOLUME,
WIND_SPEED,
UnitOfArea,
UnitOfLength,
UnitOfMass,
UnitOfPrecipitationDepth,
@ -27,6 +29,7 @@ from homeassistant.const import (
)
from .unit_conversion import (
AreaConverter,
DistanceConverter,
PressureConverter,
SpeedConverter,
@ -41,6 +44,8 @@ _CONF_UNIT_SYSTEM_IMPERIAL: Final = "imperial"
_CONF_UNIT_SYSTEM_METRIC: Final = "metric"
_CONF_UNIT_SYSTEM_US_CUSTOMARY: Final = "us_customary"
AREA_UNITS = AreaConverter.VALID_UNITS
LENGTH_UNITS = DistanceConverter.VALID_UNITS
MASS_UNITS: set[str] = {
@ -66,6 +71,7 @@ _VALID_BY_TYPE: dict[str, set[str] | set[str | None]] = {
MASS: MASS_UNITS,
VOLUME: VOLUME_UNITS,
PRESSURE: PRESSURE_UNITS,
AREA: AREA_UNITS,
}
@ -84,6 +90,7 @@ class UnitSystem:
name: str,
*,
accumulated_precipitation: UnitOfPrecipitationDepth,
area: UnitOfArea,
conversions: dict[tuple[SensorDeviceClass | str | None, str | None], str],
length: UnitOfLength,
mass: UnitOfMass,
@ -97,6 +104,7 @@ class UnitSystem:
UNIT_NOT_RECOGNIZED_TEMPLATE.format(unit, unit_type)
for unit, unit_type in (
(accumulated_precipitation, ACCUMULATED_PRECIPITATION),
(area, AREA),
(temperature, TEMPERATURE),
(length, LENGTH),
(wind_speed, WIND_SPEED),
@ -112,10 +120,11 @@ class UnitSystem:
self._name = name
self.accumulated_precipitation_unit = accumulated_precipitation
self.temperature_unit = temperature
self.area_unit = area
self.length_unit = length
self.mass_unit = mass
self.pressure_unit = pressure
self.temperature_unit = temperature
self.volume_unit = volume
self.wind_speed_unit = wind_speed
self._conversions = conversions
@ -149,6 +158,16 @@ class UnitSystem:
precip, from_unit, self.accumulated_precipitation_unit
)
def area(self, area: float | None, from_unit: str) -> float:
"""Convert the given area to this unit system."""
if not isinstance(area, Number):
raise TypeError(f"{area!s} is not a numeric value.")
# type ignore: https://github.com/python/mypy/issues/7207
return AreaConverter.convert( # type: ignore[unreachable]
area, from_unit, self.area_unit
)
def pressure(self, pressure: float | None, from_unit: str) -> float:
"""Convert the given pressure to this unit system."""
if not isinstance(pressure, Number):
@ -184,6 +203,7 @@ class UnitSystem:
return {
LENGTH: self.length_unit,
ACCUMULATED_PRECIPITATION: self.accumulated_precipitation_unit,
AREA: self.area_unit,
MASS: self.mass_unit,
PRESSURE: self.pressure_unit,
TEMPERATURE: self.temperature_unit,
@ -234,6 +254,12 @@ METRIC_SYSTEM = UnitSystem(
for unit in UnitOfPressure
if unit != UnitOfPressure.HPA
},
# Convert non-metric area
("area", UnitOfArea.SQUARE_INCHES): UnitOfArea.SQUARE_CENTIMETERS,
("area", UnitOfArea.SQUARE_FEET): UnitOfArea.SQUARE_METERS,
("area", UnitOfArea.SQUARE_MILES): UnitOfArea.SQUARE_KILOMETERS,
("area", UnitOfArea.SQUARE_YARDS): UnitOfArea.SQUARE_METERS,
("area", UnitOfArea.ACRES): UnitOfArea.HECTARES,
# Convert non-metric distances
("distance", UnitOfLength.FEET): UnitOfLength.METERS,
("distance", UnitOfLength.INCHES): UnitOfLength.MILLIMETERS,
@ -285,6 +311,7 @@ METRIC_SYSTEM = UnitSystem(
if unit not in (UnitOfSpeed.KILOMETERS_PER_HOUR, UnitOfSpeed.KNOTS)
},
},
area=UnitOfArea.SQUARE_METERS,
length=UnitOfLength.KILOMETERS,
mass=UnitOfMass.GRAMS,
pressure=UnitOfPressure.PA,
@ -303,6 +330,12 @@ US_CUSTOMARY_SYSTEM = UnitSystem(
for unit in UnitOfPressure
if unit != UnitOfPressure.INHG
},
# Convert non-USCS areas
("area", UnitOfArea.SQUARE_METERS): UnitOfArea.SQUARE_FEET,
("area", UnitOfArea.SQUARE_CENTIMETERS): UnitOfArea.SQUARE_INCHES,
("area", UnitOfArea.SQUARE_MILLIMETERS): UnitOfArea.SQUARE_INCHES,
("area", UnitOfArea.SQUARE_KILOMETERS): UnitOfArea.SQUARE_MILES,
("area", UnitOfArea.HECTARES): UnitOfArea.ACRES,
# Convert non-USCS distances
("distance", UnitOfLength.CENTIMETERS): UnitOfLength.INCHES,
("distance", UnitOfLength.KILOMETERS): UnitOfLength.MILES,
@ -356,6 +389,7 @@ US_CUSTOMARY_SYSTEM = UnitSystem(
if unit not in (UnitOfSpeed.KNOTS, UnitOfSpeed.MILES_PER_HOUR)
},
},
area=UnitOfArea.SQUARE_FEET,
length=UnitOfLength.MILES,
mass=UnitOfMass.POUNDS,
pressure=UnitOfPressure.PSI,

View File

@ -177,14 +177,14 @@
'supported_features': 0,
'translation_key': 'stats_area',
'unique_id': '8516fbb1-17f1-4194-0000000_stats_area',
'unit_of_measurement': 'm²',
'unit_of_measurement': <UnitOfArea.SQUARE_METERS: 'm²'>,
})
# ---
# name: test_sensors[5xu9h3][sensor.goat_g1_area_cleaned:state]
StateSnapshot({
'attributes': ReadOnlyDict({
'friendly_name': 'Goat G1 Area cleaned',
'unit_of_measurement': 'm²',
'unit_of_measurement': <UnitOfArea.SQUARE_METERS: 'm²'>,
}),
'context': <ANY>,
'entity_id': 'sensor.goat_g1_area_cleaned',
@ -512,7 +512,7 @@
'supported_features': 0,
'translation_key': 'total_stats_area',
'unique_id': '8516fbb1-17f1-4194-0000000_total_stats_area',
'unit_of_measurement': 'm²',
'unit_of_measurement': <UnitOfArea.SQUARE_METERS: 'm²'>,
})
# ---
# name: test_sensors[5xu9h3][sensor.goat_g1_total_area_cleaned:state]
@ -520,7 +520,7 @@
'attributes': ReadOnlyDict({
'friendly_name': 'Goat G1 Total area cleaned',
'state_class': <SensorStateClass.TOTAL_INCREASING: 'total_increasing'>,
'unit_of_measurement': 'm²',
'unit_of_measurement': <UnitOfArea.SQUARE_METERS: 'm²'>,
}),
'context': <ANY>,
'entity_id': 'sensor.goat_g1_total_area_cleaned',
@ -755,14 +755,14 @@
'supported_features': 0,
'translation_key': 'stats_area',
'unique_id': 'E1234567890000000001_stats_area',
'unit_of_measurement': 'm²',
'unit_of_measurement': <UnitOfArea.SQUARE_METERS: 'm²'>,
})
# ---
# name: test_sensors[yna5x1][sensor.ozmo_950_area_cleaned:state]
StateSnapshot({
'attributes': ReadOnlyDict({
'friendly_name': 'Ozmo 950 Area cleaned',
'unit_of_measurement': 'm²',
'unit_of_measurement': <UnitOfArea.SQUARE_METERS: 'm²'>,
}),
'context': <ANY>,
'entity_id': 'sensor.ozmo_950_area_cleaned',
@ -1137,7 +1137,7 @@
'supported_features': 0,
'translation_key': 'total_stats_area',
'unique_id': 'E1234567890000000001_total_stats_area',
'unit_of_measurement': 'm²',
'unit_of_measurement': <UnitOfArea.SQUARE_METERS: 'm²'>,
})
# ---
# name: test_sensors[yna5x1][sensor.ozmo_950_total_area_cleaned:state]
@ -1145,7 +1145,7 @@
'attributes': ReadOnlyDict({
'friendly_name': 'Ozmo 950 Total area cleaned',
'state_class': <SensorStateClass.TOTAL_INCREASING: 'total_increasing'>,
'unit_of_measurement': 'm²',
'unit_of_measurement': <UnitOfArea.SQUARE_METERS: 'm²'>,
}),
'context': <ANY>,
'entity_id': 'sensor.ozmo_950_total_area_cleaned',

View File

@ -51,6 +51,16 @@ async def mock_recorder_before_hass(
"""Set up recorder."""
AREA_SENSOR_FT_ATTRIBUTES = {
"device_class": "area",
"state_class": "measurement",
"unit_of_measurement": "ft²",
}
AREA_SENSOR_M_ATTRIBUTES = {
"device_class": "area",
"state_class": "measurement",
"unit_of_measurement": "",
}
DISTANCE_SENSOR_FT_ATTRIBUTES = {
"device_class": "distance",
"state_class": "measurement",
@ -1247,6 +1257,9 @@ async def test_statistic_during_period_calendar(
@pytest.mark.parametrize(
("attributes", "state", "value", "custom_units", "converted_value"),
[
(AREA_SENSOR_M_ATTRIBUTES, 10, 10, {"area": "cm²"}, 100000),
(AREA_SENSOR_M_ATTRIBUTES, 10, 10, {"area": ""}, 10),
(AREA_SENSOR_M_ATTRIBUTES, 10, 10, {"area": "ft²"}, 107.639),
(DISTANCE_SENSOR_M_ATTRIBUTES, 10, 10, {"distance": "cm"}, 1000),
(DISTANCE_SENSOR_M_ATTRIBUTES, 10, 10, {"distance": "m"}, 10),
(DISTANCE_SENSOR_M_ATTRIBUTES, 10, 10, {"distance": "in"}, 10 / 0.0254),
@ -1434,6 +1447,7 @@ async def test_sum_statistics_during_period_unit_conversion(
"custom_units",
[
{"distance": "L"},
{"area": "L"},
{"energy": "W"},
{"power": "Pa"},
{"pressure": "K"},
@ -1678,6 +1692,8 @@ async def test_statistics_during_period_empty_statistic_ids(
@pytest.mark.parametrize(
("units", "attributes", "display_unit", "statistics_unit", "unit_class"),
[
(US_CUSTOMARY_SYSTEM, AREA_SENSOR_M_ATTRIBUTES, "", "", "area"),
(METRIC_SYSTEM, AREA_SENSOR_M_ATTRIBUTES, "", "", "area"),
(US_CUSTOMARY_SYSTEM, DISTANCE_SENSOR_M_ATTRIBUTES, "m", "m", "distance"),
(METRIC_SYSTEM, DISTANCE_SENSOR_M_ATTRIBUTES, "m", "m", "distance"),
(
@ -1852,6 +1868,13 @@ async def test_list_statistic_ids(
@pytest.mark.parametrize(
("attributes", "attributes2", "display_unit", "statistics_unit", "unit_class"),
[
(
AREA_SENSOR_M_ATTRIBUTES,
AREA_SENSOR_FT_ATTRIBUTES,
"ft²",
"",
"area",
),
(
DISTANCE_SENSOR_M_ATTRIBUTES,
DISTANCE_SENSOR_FT_ATTRIBUTES,

View File

@ -9,10 +9,10 @@ from homeassistant.components.select import (
DOMAIN as SELECT_DOMAIN,
)
from homeassistant.const import (
AREA_SQUARE_METERS,
ATTR_ENTITY_ID,
SERVICE_SELECT_OPTION,
EntityCategory,
UnitOfArea,
)
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import ServiceValidationError
@ -38,7 +38,7 @@ async def test_select_entity(
entry = entity_registry.async_get("select.genie_room_size")
assert entry
assert entry.unique_id == f"{diffuser.hublot}-room_size_square_meter"
assert entry.unit_of_measurement == AREA_SQUARE_METERS
assert entry.unit_of_measurement == UnitOfArea.SQUARE_METERS
assert entry.entity_category == EntityCategory.CONFIG

View File

@ -30,6 +30,7 @@ from homeassistant.const import (
PERCENTAGE,
STATE_UNKNOWN,
EntityCategory,
UnitOfArea,
UnitOfDataRate,
UnitOfEnergy,
UnitOfLength,
@ -651,6 +652,34 @@ async def test_custom_unit(
"device_class",
),
[
# Area
(
UnitOfArea.SQUARE_KILOMETERS,
UnitOfArea.SQUARE_MILES,
UnitOfArea.SQUARE_MILES,
1000,
"1000",
"386",
SensorDeviceClass.AREA,
),
(
UnitOfArea.SQUARE_CENTIMETERS,
UnitOfArea.SQUARE_INCHES,
UnitOfArea.SQUARE_INCHES,
7.24,
"7.24",
"1.12",
SensorDeviceClass.AREA,
),
(
UnitOfArea.SQUARE_KILOMETERS,
"peer_distance",
UnitOfArea.SQUARE_KILOMETERS,
1000,
"1000",
"1000",
SensorDeviceClass.AREA,
),
# Distance
(
UnitOfLength.KILOMETERS,
@ -1834,6 +1863,7 @@ async def test_non_numeric_device_class_with_unit_of_measurement(
[
SensorDeviceClass.APPARENT_POWER,
SensorDeviceClass.AQI,
SensorDeviceClass.AREA,
SensorDeviceClass.ATMOSPHERIC_PRESSURE,
SensorDeviceClass.BATTERY,
SensorDeviceClass.CO,

View File

@ -227,6 +227,8 @@ async def assert_validation_result(
),
[
(None, "%", "%", "%", "unitless", 13.050847, -10, 30),
("area", "", "", "", "area", 13.050847, -10, 30),
("area", "mi²", "mi²", "mi²", "area", 13.050847, -10, 30),
("battery", "%", "%", "%", "unitless", 13.050847, -10, 30),
("battery", None, None, None, "unitless", 13.050847, -10, 30),
("distance", "m", "m", "m", "distance", 13.050847, -10, 30),
@ -914,6 +916,8 @@ async def test_compile_hourly_statistics_wrong_unit(
"factor",
),
[
(US_CUSTOMARY_SYSTEM, "area", "", "", "", "area", 1),
(US_CUSTOMARY_SYSTEM, "area", "mi²", "mi²", "mi²", "area", 1),
(US_CUSTOMARY_SYSTEM, "distance", "m", "m", "m", "distance", 1),
(US_CUSTOMARY_SYSTEM, "distance", "mi", "mi", "mi", "distance", 1),
(US_CUSTOMARY_SYSTEM, "energy", "kWh", "kWh", "kWh", "energy", 1),
@ -926,6 +930,8 @@ async def test_compile_hourly_statistics_wrong_unit(
(US_CUSTOMARY_SYSTEM, "volume", "ft³", "ft³", "ft³", "volume", 1),
(US_CUSTOMARY_SYSTEM, "weight", "g", "g", "g", "mass", 1),
(US_CUSTOMARY_SYSTEM, "weight", "oz", "oz", "oz", "mass", 1),
(METRIC_SYSTEM, "area", "", "", "", "area", 1),
(METRIC_SYSTEM, "area", "mi²", "mi²", "mi²", "area", 1),
(METRIC_SYSTEM, "distance", "m", "m", "m", "distance", 1),
(METRIC_SYSTEM, "distance", "mi", "mi", "mi", "distance", 1),
(METRIC_SYSTEM, "energy", "kWh", "kWh", "kWh", "energy", 1),
@ -2228,6 +2234,8 @@ async def test_compile_hourly_energy_statistics_multiple(
[
("battery", "%", 30),
("battery", None, 30),
("area", "", 30),
("area", "mi²", 30),
("distance", "m", 30),
("distance", "mi", 30),
("humidity", "%", 30),
@ -2336,6 +2344,8 @@ async def test_compile_hourly_statistics_partially_unavailable(
[
("battery", "%", 30),
("battery", None, 30),
("area", "", 30),
("area", "mi²", 30),
("distance", "m", 30),
("distance", "mi", 30),
("humidity", "%", 30),
@ -2438,6 +2448,10 @@ async def test_compile_hourly_statistics_fails(
"statistic_type",
),
[
("measurement", "area", "", "", "", "area", "mean"),
("measurement", "area", "mi²", "mi²", "mi²", "area", "mean"),
("total", "area", "", "", "", "area", "sum"),
("total", "area", "mi²", "mi²", "mi²", "area", "sum"),
("measurement", "battery", "%", "%", "%", "unitless", "mean"),
("measurement", "battery", None, None, None, "unitless", "mean"),
("measurement", "distance", "m", "m", "m", "distance", "mean"),

View File

@ -24,6 +24,7 @@ from homeassistant.const import (
ATTR_UNIT_OF_MEASUREMENT,
STATE_ON,
STATE_UNAVAILABLE,
UnitOfArea,
UnitOfLength,
UnitOfMass,
UnitOfPrecipitationDepth,
@ -61,6 +62,7 @@ def _set_up_units(hass: HomeAssistant) -> None:
hass.config.units = UnitSystem(
"custom",
accumulated_precipitation=UnitOfPrecipitationDepth.MILLIMETERS,
area=UnitOfArea.SQUARE_METERS,
conversions={},
length=UnitOfLength.METERS,
mass=UnitOfMass.GRAMS,

View File

@ -177,18 +177,24 @@ def test_deprecated_constants(
@pytest.mark.parametrize(
("replacement", "constant_name"),
("replacement", "constant_name", "breaks_in_version"),
[
(const.UnitOfLength.YARDS, "LENGTH_YARD"),
(const.UnitOfSoundPressure.DECIBEL, "SOUND_PRESSURE_DB"),
(const.UnitOfSoundPressure.WEIGHTED_DECIBEL_A, "SOUND_PRESSURE_WEIGHTED_DBA"),
(const.UnitOfVolume.FLUID_OUNCES, "VOLUME_FLUID_OUNCE"),
(const.UnitOfLength.YARDS, "LENGTH_YARD", "2025.1"),
(const.UnitOfSoundPressure.DECIBEL, "SOUND_PRESSURE_DB", "2025.1"),
(
const.UnitOfSoundPressure.WEIGHTED_DECIBEL_A,
"SOUND_PRESSURE_WEIGHTED_DBA",
"2025.1",
),
(const.UnitOfVolume.FLUID_OUNCES, "VOLUME_FLUID_OUNCE", "2025.1"),
(const.UnitOfArea.SQUARE_METERS, "AREA_SQUARE_METERS", "2025.12"),
],
)
def test_deprecated_constant_name_changes(
caplog: pytest.LogCaptureFixture,
replacement: Enum,
constant_name: str,
breaks_in_version: str,
) -> None:
"""Test deprecated constants, where the name is not the same as the enum value."""
import_and_test_deprecated_constant(
@ -197,7 +203,7 @@ def test_deprecated_constant_name_changes(
constant_name,
f"{replacement.__class__.__name__}.{replacement.name}",
replacement,
"2025.1",
breaks_in_version,
)

View File

@ -11,6 +11,7 @@ from homeassistant.const import (
CONCENTRATION_PARTS_PER_BILLION,
CONCENTRATION_PARTS_PER_MILLION,
PERCENTAGE,
UnitOfArea,
UnitOfBloodGlucoseConcentration,
UnitOfConductivity,
UnitOfDataRate,
@ -32,6 +33,7 @@ from homeassistant.const import (
from homeassistant.exceptions import HomeAssistantError
from homeassistant.util import unit_conversion
from homeassistant.util.unit_conversion import (
AreaConverter,
BaseUnitConverter,
BloodGlucoseConcentrationConverter,
ConductivityConverter,
@ -61,6 +63,7 @@ INVALID_SYMBOL = "bob"
_ALL_CONVERTERS: dict[type[BaseUnitConverter], list[str | None]] = {
converter: sorted(converter.VALID_UNITS, key=lambda x: (x is None, x))
for converter in (
AreaConverter,
BloodGlucoseConcentrationConverter,
ConductivityConverter,
DataRateConverter,
@ -83,6 +86,7 @@ _ALL_CONVERTERS: dict[type[BaseUnitConverter], list[str | None]] = {
# Dict containing all converters with a corresponding unit ratio.
_GET_UNIT_RATIO: dict[type[BaseUnitConverter], tuple[str | None, str | None, float]] = {
AreaConverter: (UnitOfArea.SQUARE_KILOMETERS, UnitOfArea.SQUARE_METERS, 0.000001),
BloodGlucoseConcentrationConverter: (
UnitOfBloodGlucoseConcentration.MILLIGRAMS_PER_DECILITER,
UnitOfBloodGlucoseConcentration.MILLIMOLE_PER_LITER,
@ -138,6 +142,62 @@ _GET_UNIT_RATIO: dict[type[BaseUnitConverter], tuple[str | None, str | None, flo
_CONVERTED_VALUE: dict[
type[BaseUnitConverter], list[tuple[float, str | None, float, str | None]]
] = {
AreaConverter: [
# Square Meters to other units
(5, UnitOfArea.SQUARE_METERS, 50000, UnitOfArea.SQUARE_CENTIMETERS),
(5, UnitOfArea.SQUARE_METERS, 5000000, UnitOfArea.SQUARE_MILLIMETERS),
(5, UnitOfArea.SQUARE_METERS, 0.000005, UnitOfArea.SQUARE_KILOMETERS),
(5, UnitOfArea.SQUARE_METERS, 7750.015500031001, UnitOfArea.SQUARE_INCHES),
(5, UnitOfArea.SQUARE_METERS, 53.81955, UnitOfArea.SQUARE_FEET),
(5, UnitOfArea.SQUARE_METERS, 5.979950231505403, UnitOfArea.SQUARE_YARDS),
(5, UnitOfArea.SQUARE_METERS, 1.9305107927122295e-06, UnitOfArea.SQUARE_MILES),
(5, UnitOfArea.SQUARE_METERS, 0.0012355269073358272, UnitOfArea.ACRES),
(5, UnitOfArea.SQUARE_METERS, 0.0005, UnitOfArea.HECTARES),
# Square Kilometers to other units
(1, UnitOfArea.SQUARE_KILOMETERS, 1000000, UnitOfArea.SQUARE_METERS),
(1, UnitOfArea.SQUARE_KILOMETERS, 1e10, UnitOfArea.SQUARE_CENTIMETERS),
(1, UnitOfArea.SQUARE_KILOMETERS, 1e12, UnitOfArea.SQUARE_MILLIMETERS),
(5, UnitOfArea.SQUARE_KILOMETERS, 1.9305107927122296, UnitOfArea.SQUARE_MILES),
(5, UnitOfArea.SQUARE_KILOMETERS, 1235.5269073358272, UnitOfArea.ACRES),
(5, UnitOfArea.SQUARE_KILOMETERS, 500, UnitOfArea.HECTARES),
# Acres to other units
(5, UnitOfArea.ACRES, 20234.3, UnitOfArea.SQUARE_METERS),
(5, UnitOfArea.ACRES, 202342821.11999995, UnitOfArea.SQUARE_CENTIMETERS),
(5, UnitOfArea.ACRES, 20234282111.999992, UnitOfArea.SQUARE_MILLIMETERS),
(5, UnitOfArea.ACRES, 0.0202343, UnitOfArea.SQUARE_KILOMETERS),
(5, UnitOfArea.ACRES, 217800, UnitOfArea.SQUARE_FEET),
(5, UnitOfArea.ACRES, 24200.0, UnitOfArea.SQUARE_YARDS),
(5, UnitOfArea.ACRES, 0.0078125, UnitOfArea.SQUARE_MILES),
(5, UnitOfArea.ACRES, 2.02343, UnitOfArea.HECTARES),
# Hectares to other units
(5, UnitOfArea.HECTARES, 50000, UnitOfArea.SQUARE_METERS),
(5, UnitOfArea.HECTARES, 500000000, UnitOfArea.SQUARE_CENTIMETERS),
(5, UnitOfArea.HECTARES, 50000000000.0, UnitOfArea.SQUARE_MILLIMETERS),
(5, UnitOfArea.HECTARES, 0.019305107927122298, UnitOfArea.SQUARE_MILES),
(5, UnitOfArea.HECTARES, 538195.5, UnitOfArea.SQUARE_FEET),
(5, UnitOfArea.HECTARES, 59799.50231505403, UnitOfArea.SQUARE_YARDS),
(5, UnitOfArea.HECTARES, 12.355269073358272, UnitOfArea.ACRES),
# Square Miles to other units
(5, UnitOfArea.SQUARE_MILES, 12949940.551679997, UnitOfArea.SQUARE_METERS),
(5, UnitOfArea.SQUARE_MILES, 129499405516.79997, UnitOfArea.SQUARE_CENTIMETERS),
(5, UnitOfArea.SQUARE_MILES, 12949940551679.996, UnitOfArea.SQUARE_MILLIMETERS),
(5, UnitOfArea.SQUARE_MILES, 1294.9940551679997, UnitOfArea.HECTARES),
(5, UnitOfArea.SQUARE_MILES, 3200, UnitOfArea.ACRES),
# Square Yards to other units
(5, UnitOfArea.SQUARE_YARDS, 4.1806367999999985, UnitOfArea.SQUARE_METERS),
(5, UnitOfArea.SQUARE_YARDS, 41806.4, UnitOfArea.SQUARE_CENTIMETERS),
(5, UnitOfArea.SQUARE_YARDS, 4180636.7999999984, UnitOfArea.SQUARE_MILLIMETERS),
(
5,
UnitOfArea.SQUARE_YARDS,
4.180636799999998e-06,
UnitOfArea.SQUARE_KILOMETERS,
),
(5, UnitOfArea.SQUARE_YARDS, 45.0, UnitOfArea.SQUARE_FEET),
(5, UnitOfArea.SQUARE_YARDS, 6479.999999999998, UnitOfArea.SQUARE_INCHES),
(5, UnitOfArea.SQUARE_YARDS, 1.6141528925619832e-06, UnitOfArea.SQUARE_MILES),
(5, UnitOfArea.SQUARE_YARDS, 0.0010330578512396695, UnitOfArea.ACRES),
],
BloodGlucoseConcentrationConverter: [
(
90,

View File

@ -7,12 +7,14 @@ import pytest
from homeassistant.components.sensor import DEVICE_CLASS_UNITS, SensorDeviceClass
from homeassistant.const import (
ACCUMULATED_PRECIPITATION,
AREA,
LENGTH,
MASS,
PRESSURE,
TEMPERATURE,
VOLUME,
WIND_SPEED,
UnitOfArea,
UnitOfLength,
UnitOfMass,
UnitOfPrecipitationDepth,
@ -44,6 +46,7 @@ def test_invalid_units() -> None:
UnitSystem(
SYSTEM_NAME,
accumulated_precipitation=UnitOfPrecipitationDepth.MILLIMETERS,
area=UnitOfArea.SQUARE_METERS,
conversions={},
length=UnitOfLength.METERS,
mass=UnitOfMass.GRAMS,
@ -57,6 +60,7 @@ def test_invalid_units() -> None:
UnitSystem(
SYSTEM_NAME,
accumulated_precipitation=UnitOfPrecipitationDepth.MILLIMETERS,
area=UnitOfArea.SQUARE_METERS,
conversions={},
length=INVALID_UNIT,
mass=UnitOfMass.GRAMS,
@ -70,6 +74,7 @@ def test_invalid_units() -> None:
UnitSystem(
SYSTEM_NAME,
accumulated_precipitation=UnitOfPrecipitationDepth.MILLIMETERS,
area=UnitOfArea.SQUARE_METERS,
conversions={},
length=UnitOfLength.METERS,
mass=UnitOfMass.GRAMS,
@ -83,6 +88,7 @@ def test_invalid_units() -> None:
UnitSystem(
SYSTEM_NAME,
accumulated_precipitation=UnitOfPrecipitationDepth.MILLIMETERS,
area=UnitOfArea.SQUARE_METERS,
conversions={},
length=UnitOfLength.METERS,
mass=UnitOfMass.GRAMS,
@ -96,6 +102,7 @@ def test_invalid_units() -> None:
UnitSystem(
SYSTEM_NAME,
accumulated_precipitation=UnitOfPrecipitationDepth.MILLIMETERS,
area=UnitOfArea.SQUARE_METERS,
conversions={},
length=UnitOfLength.METERS,
mass=INVALID_UNIT,
@ -109,6 +116,7 @@ def test_invalid_units() -> None:
UnitSystem(
SYSTEM_NAME,
accumulated_precipitation=UnitOfPrecipitationDepth.MILLIMETERS,
area=UnitOfArea.SQUARE_METERS,
conversions={},
length=UnitOfLength.METERS,
mass=UnitOfMass.GRAMS,
@ -122,6 +130,21 @@ def test_invalid_units() -> None:
UnitSystem(
SYSTEM_NAME,
accumulated_precipitation=INVALID_UNIT,
area=UnitOfArea.SQUARE_METERS,
conversions={},
length=UnitOfLength.METERS,
mass=UnitOfMass.GRAMS,
pressure=UnitOfPressure.PA,
temperature=UnitOfTemperature.CELSIUS,
volume=UnitOfVolume.LITERS,
wind_speed=UnitOfSpeed.METERS_PER_SECOND,
)
with pytest.raises(ValueError):
UnitSystem(
SYSTEM_NAME,
accumulated_precipitation=UnitOfPrecipitationDepth.MILLIMETERS,
area=INVALID_UNIT,
conversions={},
length=UnitOfLength.METERS,
mass=UnitOfMass.GRAMS,
@ -146,6 +169,8 @@ def test_invalid_value() -> None:
METRIC_SYSTEM.pressure("50Pa", UnitOfPressure.PA)
with pytest.raises(TypeError):
METRIC_SYSTEM.accumulated_precipitation("50mm", UnitOfLength.MILLIMETERS)
with pytest.raises(TypeError):
METRIC_SYSTEM.area("2m²", UnitOfArea.SQUARE_METERS)
def test_as_dict() -> None:
@ -158,6 +183,7 @@ def test_as_dict() -> None:
MASS: UnitOfMass.GRAMS,
PRESSURE: UnitOfPressure.PA,
ACCUMULATED_PRECIPITATION: UnitOfLength.MILLIMETERS,
AREA: UnitOfArea.SQUARE_METERS,
}
assert expected == METRIC_SYSTEM.as_dict()
@ -303,6 +329,29 @@ def test_accumulated_precipitation_to_imperial() -> None:
) == pytest.approx(10, abs=1e-4)
def test_area_same_unit() -> None:
"""Test no conversion happens if to unit is same as from unit."""
assert METRIC_SYSTEM.area(5, METRIC_SYSTEM.area_unit) == 5
def test_area_unknown_unit() -> None:
"""Test no conversion happens if unknown unit."""
with pytest.raises(HomeAssistantError, match="is not a recognized .* unit"):
METRIC_SYSTEM.area(5, "abc")
def test_area_to_metric() -> None:
"""Test area conversion to metric system."""
assert METRIC_SYSTEM.area(25, METRIC_SYSTEM.area_unit) == 25
assert round(METRIC_SYSTEM.area(10, IMPERIAL_SYSTEM.area_unit), 1) == 0.9
def test_area_to_imperial() -> None:
"""Test area conversion to imperial system."""
assert IMPERIAL_SYSTEM.area(77, IMPERIAL_SYSTEM.area_unit) == 77
assert IMPERIAL_SYSTEM.area(25, METRIC_SYSTEM.area_unit) == 269.09776041774313
def test_properties() -> None:
"""Test the unit properties are returned as expected."""
assert METRIC_SYSTEM.length_unit == UnitOfLength.KILOMETERS
@ -312,6 +361,7 @@ def test_properties() -> None:
assert METRIC_SYSTEM.volume_unit == UnitOfVolume.LITERS
assert METRIC_SYSTEM.pressure_unit == UnitOfPressure.PA
assert METRIC_SYSTEM.accumulated_precipitation_unit == UnitOfLength.MILLIMETERS
assert METRIC_SYSTEM.area_unit == UnitOfArea.SQUARE_METERS
@pytest.mark.parametrize(
@ -338,6 +388,18 @@ def test_get_unit_system_invalid(key: str) -> None:
@pytest.mark.parametrize(
("device_class", "original_unit", "state_unit"),
[
# Test area conversion
(SensorDeviceClass.AREA, UnitOfArea.SQUARE_FEET, UnitOfArea.SQUARE_METERS),
(
SensorDeviceClass.AREA,
UnitOfArea.SQUARE_INCHES,
UnitOfArea.SQUARE_CENTIMETERS,
),
(SensorDeviceClass.AREA, UnitOfArea.SQUARE_MILES, UnitOfArea.SQUARE_KILOMETERS),
(SensorDeviceClass.AREA, UnitOfArea.SQUARE_YARDS, UnitOfArea.SQUARE_METERS),
(SensorDeviceClass.AREA, UnitOfArea.ACRES, UnitOfArea.HECTARES),
(SensorDeviceClass.AREA, UnitOfArea.SQUARE_KILOMETERS, None),
(SensorDeviceClass.AREA, "very_long", None),
# Test atmospheric pressure
(
SensorDeviceClass.ATMOSPHERIC_PRESSURE,
@ -495,6 +557,13 @@ def test_get_metric_converted_unit_(
UNCONVERTED_UNITS_METRIC_SYSTEM = {
SensorDeviceClass.AREA: (
UnitOfArea.SQUARE_MILLIMETERS,
UnitOfArea.SQUARE_CENTIMETERS,
UnitOfArea.SQUARE_METERS,
UnitOfArea.SQUARE_KILOMETERS,
UnitOfArea.HECTARES,
),
SensorDeviceClass.ATMOSPHERIC_PRESSURE: (UnitOfPressure.HPA,),
SensorDeviceClass.DISTANCE: (
UnitOfLength.CENTIMETERS,
@ -544,6 +613,7 @@ UNCONVERTED_UNITS_METRIC_SYSTEM = {
@pytest.mark.parametrize(
"device_class",
[
SensorDeviceClass.AREA,
SensorDeviceClass.ATMOSPHERIC_PRESSURE,
SensorDeviceClass.DISTANCE,
SensorDeviceClass.GAS,
@ -572,6 +642,21 @@ def test_metric_converted_units(device_class: SensorDeviceClass) -> None:
@pytest.mark.parametrize(
("device_class", "original_unit", "state_unit"),
[
# Test area conversion
(
SensorDeviceClass.AREA,
UnitOfArea.SQUARE_MILLIMETERS,
UnitOfArea.SQUARE_INCHES,
),
(
SensorDeviceClass.AREA,
UnitOfArea.SQUARE_CENTIMETERS,
UnitOfArea.SQUARE_INCHES,
),
(SensorDeviceClass.AREA, UnitOfArea.SQUARE_METERS, UnitOfArea.SQUARE_FEET),
(SensorDeviceClass.AREA, UnitOfArea.SQUARE_KILOMETERS, UnitOfArea.SQUARE_MILES),
(SensorDeviceClass.AREA, UnitOfArea.HECTARES, UnitOfArea.ACRES),
(SensorDeviceClass.AREA, "very_area", None),
# Test atmospheric pressure
(
SensorDeviceClass.ATMOSPHERIC_PRESSURE,
@ -721,6 +806,13 @@ def test_get_us_converted_unit(
UNCONVERTED_UNITS_US_SYSTEM = {
SensorDeviceClass.AREA: (
UnitOfArea.SQUARE_FEET,
UnitOfArea.SQUARE_INCHES,
UnitOfArea.SQUARE_MILES,
UnitOfArea.SQUARE_YARDS,
UnitOfArea.ACRES,
),
SensorDeviceClass.ATMOSPHERIC_PRESSURE: (UnitOfPressure.INHG,),
SensorDeviceClass.DISTANCE: (
UnitOfLength.FEET,