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

View File

@ -17,6 +17,7 @@ from homeassistant.const import (
SIGNAL_STRENGTH_DECIBELS, SIGNAL_STRENGTH_DECIBELS,
SIGNAL_STRENGTH_DECIBELS_MILLIWATT, SIGNAL_STRENGTH_DECIBELS_MILLIWATT,
UnitOfApparentPower, UnitOfApparentPower,
UnitOfArea,
UnitOfBloodGlucoseConcentration, UnitOfBloodGlucoseConcentration,
UnitOfConductivity, UnitOfConductivity,
UnitOfDataRate, UnitOfDataRate,
@ -98,6 +99,12 @@ class NumberDeviceClass(StrEnum):
Unit of measurement: `None` Unit of measurement: `None`
""" """
AREA = "area"
"""Area
Unit of measurement: `UnitOfArea` units
"""
ATMOSPHERIC_PRESSURE = "atmospheric_pressure" ATMOSPHERIC_PRESSURE = "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]] = { DEVICE_CLASS_UNITS: dict[NumberDeviceClass, set[type[StrEnum] | str | None]] = {
NumberDeviceClass.APPARENT_POWER: set(UnitOfApparentPower), NumberDeviceClass.APPARENT_POWER: set(UnitOfApparentPower),
NumberDeviceClass.AQI: {None}, NumberDeviceClass.AQI: {None},
NumberDeviceClass.AREA: set(UnitOfArea),
NumberDeviceClass.ATMOSPHERIC_PRESSURE: set(UnitOfPressure), NumberDeviceClass.ATMOSPHERIC_PRESSURE: set(UnitOfPressure),
NumberDeviceClass.BATTERY: {PERCENTAGE}, NumberDeviceClass.BATTERY: {PERCENTAGE},
NumberDeviceClass.BLOOD_GLUCOSE_CONCENTRATION: set(UnitOfBloodGlucoseConcentration), NumberDeviceClass.BLOOD_GLUCOSE_CONCENTRATION: set(UnitOfBloodGlucoseConcentration),

View File

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

View File

@ -37,6 +37,9 @@
"aqi": { "aqi": {
"name": "[%key:component::sensor::entity_component::aqi::name%]" "name": "[%key:component::sensor::entity_component::aqi::name%]"
}, },
"area": {
"name": "[%key:component::sensor::entity_component::area::name%]"
},
"atmospheric_pressure": { "atmospheric_pressure": {
"name": "[%key:component::sensor::entity_component::atmospheric_pressure::name%]" "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.helpers.typing import UNDEFINED, UndefinedType
from homeassistant.util import dt as dt_util from homeassistant.util import dt as dt_util
from homeassistant.util.unit_conversion import ( from homeassistant.util.unit_conversion import (
AreaConverter,
BaseUnitConverter, BaseUnitConverter,
BloodGlucoseConcentrationConverter, BloodGlucoseConcentrationConverter,
ConductivityConverter, ConductivityConverter,
@ -129,6 +130,7 @@ QUERY_STATISTICS_SUMMARY_SUM = (
STATISTIC_UNIT_TO_UNIT_CONVERTER: dict[str | None, type[BaseUnitConverter]] = { STATISTIC_UNIT_TO_UNIT_CONVERTER: dict[str | None, type[BaseUnitConverter]] = {
**{unit: AreaConverter for unit in AreaConverter.VALID_UNITS},
**{ **{
unit: BloodGlucoseConcentrationConverter unit: BloodGlucoseConcentrationConverter
for unit in BloodGlucoseConcentrationConverter.VALID_UNITS 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.helpers.json import json_bytes
from homeassistant.util import dt as dt_util from homeassistant.util import dt as dt_util
from homeassistant.util.unit_conversion import ( from homeassistant.util.unit_conversion import (
AreaConverter,
BloodGlucoseConcentrationConverter, BloodGlucoseConcentrationConverter,
ConductivityConverter, ConductivityConverter,
DataRateConverter, DataRateConverter,
@ -55,6 +56,7 @@ UPDATE_STATISTICS_METADATA_TIME_OUT = 10
UNIT_SCHEMA = vol.Schema( UNIT_SCHEMA = vol.Schema(
{ {
vol.Optional("area"): vol.In(AreaConverter.VALID_UNITS),
vol.Optional("blood_glucose_concentration"): vol.In( vol.Optional("blood_glucose_concentration"): vol.In(
BloodGlucoseConcentrationConverter.VALID_UNITS BloodGlucoseConcentrationConverter.VALID_UNITS
), ),

View File

@ -9,7 +9,7 @@ from pyrituals import Diffuser
from homeassistant.components.select import SelectEntity, SelectEntityDescription from homeassistant.components.select import SelectEntity, SelectEntityDescription
from homeassistant.config_entries import ConfigEntry 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.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.entity_platform import AddEntitiesCallback
@ -30,7 +30,7 @@ ENTITY_DESCRIPTIONS = (
RitualsSelectEntityDescription( RitualsSelectEntityDescription(
key="room_size_square_meter", key="room_size_square_meter",
translation_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, entity_category=EntityCategory.CONFIG,
options=["15", "30", "60", "100"], options=["15", "30", "60", "100"],
current_fn=lambda diffuser: str(diffuser.room_size_square_meter), current_fn=lambda diffuser: str(diffuser.room_size_square_meter),

View File

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

View File

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

View File

@ -12,12 +12,7 @@ from homeassistant.components.sensor import (
SensorStateClass, SensorStateClass,
) )
from homeassistant.config_entries import ConfigEntry from homeassistant.config_entries import ConfigEntry
from homeassistant.const import ( from homeassistant.const import PERCENTAGE, EntityCategory, UnitOfArea, UnitOfTime
AREA_SQUARE_METERS,
PERCENTAGE,
EntityCategory,
UnitOfTime,
)
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.helpers.typing import StateType from homeassistant.helpers.typing import StateType
@ -108,7 +103,7 @@ SENSORS: list[RoombaSensorEntityDescription] = [
RoombaSensorEntityDescription( RoombaSensorEntityDescription(
key="total_cleaned_area", key="total_cleaned_area",
translation_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, entity_category=EntityCategory.DIAGNOSTIC,
value_fn=lambda self: ( value_fn=lambda self: (
None if (sqft := self.run_stats.get("sqft")) is None else sqft * 9.29 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,
SIGNAL_STRENGTH_DECIBELS_MILLIWATT, SIGNAL_STRENGTH_DECIBELS_MILLIWATT,
UnitOfApparentPower, UnitOfApparentPower,
UnitOfArea,
UnitOfBloodGlucoseConcentration, UnitOfBloodGlucoseConcentration,
UnitOfConductivity, UnitOfConductivity,
UnitOfDataRate, UnitOfDataRate,
@ -47,6 +48,7 @@ from homeassistant.helpers.deprecation import (
dir_with_deprecated_constants, dir_with_deprecated_constants,
) )
from homeassistant.util.unit_conversion import ( from homeassistant.util.unit_conversion import (
AreaConverter,
BaseUnitConverter, BaseUnitConverter,
BloodGlucoseConcentrationConverter, BloodGlucoseConcentrationConverter,
ConductivityConverter, ConductivityConverter,
@ -117,6 +119,12 @@ class SensorDeviceClass(StrEnum):
Unit of measurement: `None` Unit of measurement: `None`
""" """
AREA = "area"
"""Area
Unit of measurement: `UnitOfArea` units
"""
ATMOSPHERIC_PRESSURE = "atmospheric_pressure" ATMOSPHERIC_PRESSURE = "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] STATE_CLASSES: Final[list[str]] = [cls.value for cls in SensorStateClass]
UNIT_CONVERTERS: dict[SensorDeviceClass | str | None, type[BaseUnitConverter]] = { UNIT_CONVERTERS: dict[SensorDeviceClass | str | None, type[BaseUnitConverter]] = {
SensorDeviceClass.AREA: AreaConverter,
SensorDeviceClass.ATMOSPHERIC_PRESSURE: PressureConverter, SensorDeviceClass.ATMOSPHERIC_PRESSURE: PressureConverter,
SensorDeviceClass.BLOOD_GLUCOSE_CONCENTRATION: BloodGlucoseConcentrationConverter, SensorDeviceClass.BLOOD_GLUCOSE_CONCENTRATION: BloodGlucoseConcentrationConverter,
SensorDeviceClass.CONDUCTIVITY: ConductivityConverter, 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]] = { DEVICE_CLASS_UNITS: dict[SensorDeviceClass, set[type[StrEnum] | str | None]] = {
SensorDeviceClass.APPARENT_POWER: set(UnitOfApparentPower), SensorDeviceClass.APPARENT_POWER: set(UnitOfApparentPower),
SensorDeviceClass.AQI: {None}, SensorDeviceClass.AQI: {None},
SensorDeviceClass.AREA: set(UnitOfArea),
SensorDeviceClass.ATMOSPHERIC_PRESSURE: set(UnitOfPressure), SensorDeviceClass.ATMOSPHERIC_PRESSURE: set(UnitOfPressure),
SensorDeviceClass.BATTERY: {PERCENTAGE}, SensorDeviceClass.BATTERY: {PERCENTAGE},
SensorDeviceClass.BLOOD_GLUCOSE_CONCENTRATION: set(UnitOfBloodGlucoseConcentration), 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]] = { DEVICE_CLASS_STATE_CLASSES: dict[SensorDeviceClass, set[SensorStateClass]] = {
SensorDeviceClass.APPARENT_POWER: {SensorStateClass.MEASUREMENT}, SensorDeviceClass.APPARENT_POWER: {SensorStateClass.MEASUREMENT},
SensorDeviceClass.AQI: {SensorStateClass.MEASUREMENT}, SensorDeviceClass.AQI: {SensorStateClass.MEASUREMENT},
SensorDeviceClass.AREA: set(SensorStateClass),
SensorDeviceClass.ATMOSPHERIC_PRESSURE: {SensorStateClass.MEASUREMENT}, SensorDeviceClass.ATMOSPHERIC_PRESSURE: {SensorStateClass.MEASUREMENT},
SensorDeviceClass.BATTERY: {SensorStateClass.MEASUREMENT}, SensorDeviceClass.BATTERY: {SensorStateClass.MEASUREMENT},
SensorDeviceClass.BLOOD_GLUCOSE_CONCENTRATION: {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_APPARENT_POWER = "is_apparent_power"
CONF_IS_AQI = "is_aqi" CONF_IS_AQI = "is_aqi"
CONF_IS_AREA = "is_area"
CONF_IS_ATMOSPHERIC_PRESSURE = "is_atmospheric_pressure" CONF_IS_ATMOSPHERIC_PRESSURE = "is_atmospheric_pressure"
CONF_IS_BATTERY_LEVEL = "is_battery_level" CONF_IS_BATTERY_LEVEL = "is_battery_level"
CONF_IS_BLOOD_GLUCOSE_CONCENTRATION = "is_blood_glucose_concentration" CONF_IS_BLOOD_GLUCOSE_CONCENTRATION = "is_blood_glucose_concentration"
@ -86,6 +87,7 @@ CONF_IS_WIND_SPEED = "is_wind_speed"
ENTITY_CONDITIONS = { ENTITY_CONDITIONS = {
SensorDeviceClass.APPARENT_POWER: [{CONF_TYPE: CONF_IS_APPARENT_POWER}], SensorDeviceClass.APPARENT_POWER: [{CONF_TYPE: CONF_IS_APPARENT_POWER}],
SensorDeviceClass.AQI: [{CONF_TYPE: CONF_IS_AQI}], SensorDeviceClass.AQI: [{CONF_TYPE: CONF_IS_AQI}],
SensorDeviceClass.AREA: [{CONF_TYPE: CONF_IS_AREA}],
SensorDeviceClass.ATMOSPHERIC_PRESSURE: [{CONF_TYPE: CONF_IS_ATMOSPHERIC_PRESSURE}], SensorDeviceClass.ATMOSPHERIC_PRESSURE: [{CONF_TYPE: CONF_IS_ATMOSPHERIC_PRESSURE}],
SensorDeviceClass.BATTERY: [{CONF_TYPE: CONF_IS_BATTERY_LEVEL}], SensorDeviceClass.BATTERY: [{CONF_TYPE: CONF_IS_BATTERY_LEVEL}],
SensorDeviceClass.BLOOD_GLUCOSE_CONCENTRATION: [ SensorDeviceClass.BLOOD_GLUCOSE_CONCENTRATION: [
@ -153,6 +155,7 @@ CONDITION_SCHEMA = vol.All(
[ [
CONF_IS_APPARENT_POWER, CONF_IS_APPARENT_POWER,
CONF_IS_AQI, CONF_IS_AQI,
CONF_IS_AREA,
CONF_IS_ATMOSPHERIC_PRESSURE, CONF_IS_ATMOSPHERIC_PRESSURE,
CONF_IS_BATTERY_LEVEL, CONF_IS_BATTERY_LEVEL,
CONF_IS_BLOOD_GLUCOSE_CONCENTRATION, CONF_IS_BLOOD_GLUCOSE_CONCENTRATION,

View File

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

View File

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

View File

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

View File

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

View File

@ -24,7 +24,6 @@ from homeassistant.components.sensor import (
) )
from homeassistant.config_entries import ConfigEntry from homeassistant.config_entries import ConfigEntry
from homeassistant.const import ( from homeassistant.const import (
AREA_SQUARE_METERS,
ATTR_BATTERY_LEVEL, ATTR_BATTERY_LEVEL,
ATTR_TEMPERATURE, ATTR_TEMPERATURE,
CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
@ -37,6 +36,7 @@ from homeassistant.const import (
PERCENTAGE, PERCENTAGE,
REVOLUTIONS_PER_MINUTE, REVOLUTIONS_PER_MINUTE,
EntityCategory, EntityCategory,
UnitOfArea,
UnitOfPower, UnitOfPower,
UnitOfPressure, UnitOfPressure,
UnitOfTemperature, UnitOfTemperature,
@ -622,7 +622,7 @@ VACUUM_SENSORS = {
entity_category=EntityCategory.DIAGNOSTIC, entity_category=EntityCategory.DIAGNOSTIC,
), ),
f"last_clean_{ATTR_LAST_CLEAN_AREA}": XiaomiMiioSensorDescription( 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", icon="mdi:texture-box",
key=ATTR_LAST_CLEAN_AREA, key=ATTR_LAST_CLEAN_AREA,
parent_key=VacuumCoordinatorDataAttributes.last_clean_details, parent_key=VacuumCoordinatorDataAttributes.last_clean_details,
@ -639,7 +639,7 @@ VACUUM_SENSORS = {
entity_category=EntityCategory.DIAGNOSTIC, entity_category=EntityCategory.DIAGNOSTIC,
), ),
f"current_{ATTR_LAST_CLEAN_AREA}": XiaomiMiioSensorDescription( 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", icon="mdi:texture-box",
key=ATTR_STATUS_CLEAN_AREA, key=ATTR_STATUS_CLEAN_AREA,
parent_key=VacuumCoordinatorDataAttributes.status, parent_key=VacuumCoordinatorDataAttributes.status,
@ -657,7 +657,7 @@ VACUUM_SENSORS = {
entity_category=EntityCategory.DIAGNOSTIC, entity_category=EntityCategory.DIAGNOSTIC,
), ),
f"clean_history_{ATTR_CLEAN_HISTORY_TOTAL_AREA}": XiaomiMiioSensorDescription( 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", icon="mdi:texture-box",
key=ATTR_CLEAN_HISTORY_TOTAL_AREA, key=ATTR_CLEAN_HISTORY_TOTAL_AREA,
parent_key=VacuumCoordinatorDataAttributes.clean_history_status, 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""" """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 # Mass units
@ -1704,6 +1723,7 @@ RESTART_EXIT_CODE: Final = 100
UNIT_NOT_RECOGNIZED_TEMPLATE: Final = "{} is not a recognized {} unit." UNIT_NOT_RECOGNIZED_TEMPLATE: Final = "{} is not a recognized {} unit."
LENGTH: Final = "length" LENGTH: Final = "length"
AREA: Final = "area"
MASS: Final = "mass" MASS: Final = "mass"
PRESSURE: Final = "pressure" PRESSURE: Final = "pressure"
VOLUME: Final = "volume" VOLUME: Final = "volume"

View File

@ -10,6 +10,7 @@ from homeassistant.const import (
CONCENTRATION_PARTS_PER_MILLION, CONCENTRATION_PARTS_PER_MILLION,
PERCENTAGE, PERCENTAGE,
UNIT_NOT_RECOGNIZED_TEMPLATE, UNIT_NOT_RECOGNIZED_TEMPLATE,
UnitOfArea,
UnitOfBloodGlucoseConcentration, UnitOfBloodGlucoseConcentration,
UnitOfConductivity, UnitOfConductivity,
UnitOfDataRate, 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 _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 # Duration conversion constants
_MIN_TO_SEC = 60 # 1 min = 60 seconds _MIN_TO_SEC = 60 # 1 min = 60 seconds
_HRS_TO_MINUTES = 60 # 1 hr = 60 minutes _HRS_TO_MINUTES = 60 # 1 hr = 60 minutes
@ -146,6 +160,25 @@ class DataRateConverter(BaseUnitConverter):
VALID_UNITS = set(UnitOfDataRate) 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): class DistanceConverter(BaseUnitConverter):
"""Utility to convert distance values.""" """Utility to convert distance values."""

View File

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

View File

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

View File

@ -51,6 +51,16 @@ async def mock_recorder_before_hass(
"""Set up recorder.""" """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 = { DISTANCE_SENSOR_FT_ATTRIBUTES = {
"device_class": "distance", "device_class": "distance",
"state_class": "measurement", "state_class": "measurement",
@ -1247,6 +1257,9 @@ async def test_statistic_during_period_calendar(
@pytest.mark.parametrize( @pytest.mark.parametrize(
("attributes", "state", "value", "custom_units", "converted_value"), ("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": "cm"}, 1000),
(DISTANCE_SENSOR_M_ATTRIBUTES, 10, 10, {"distance": "m"}, 10), (DISTANCE_SENSOR_M_ATTRIBUTES, 10, 10, {"distance": "m"}, 10),
(DISTANCE_SENSOR_M_ATTRIBUTES, 10, 10, {"distance": "in"}, 10 / 0.0254), (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", "custom_units",
[ [
{"distance": "L"}, {"distance": "L"},
{"area": "L"},
{"energy": "W"}, {"energy": "W"},
{"power": "Pa"}, {"power": "Pa"},
{"pressure": "K"}, {"pressure": "K"},
@ -1678,6 +1692,8 @@ async def test_statistics_during_period_empty_statistic_ids(
@pytest.mark.parametrize( @pytest.mark.parametrize(
("units", "attributes", "display_unit", "statistics_unit", "unit_class"), ("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"), (US_CUSTOMARY_SYSTEM, DISTANCE_SENSOR_M_ATTRIBUTES, "m", "m", "distance"),
(METRIC_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( @pytest.mark.parametrize(
("attributes", "attributes2", "display_unit", "statistics_unit", "unit_class"), ("attributes", "attributes2", "display_unit", "statistics_unit", "unit_class"),
[ [
(
AREA_SENSOR_M_ATTRIBUTES,
AREA_SENSOR_FT_ATTRIBUTES,
"ft²",
"",
"area",
),
( (
DISTANCE_SENSOR_M_ATTRIBUTES, DISTANCE_SENSOR_M_ATTRIBUTES,
DISTANCE_SENSOR_FT_ATTRIBUTES, DISTANCE_SENSOR_FT_ATTRIBUTES,

View File

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

View File

@ -30,6 +30,7 @@ from homeassistant.const import (
PERCENTAGE, PERCENTAGE,
STATE_UNKNOWN, STATE_UNKNOWN,
EntityCategory, EntityCategory,
UnitOfArea,
UnitOfDataRate, UnitOfDataRate,
UnitOfEnergy, UnitOfEnergy,
UnitOfLength, UnitOfLength,
@ -651,6 +652,34 @@ async def test_custom_unit(
"device_class", "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 # Distance
( (
UnitOfLength.KILOMETERS, UnitOfLength.KILOMETERS,
@ -1834,6 +1863,7 @@ async def test_non_numeric_device_class_with_unit_of_measurement(
[ [
SensorDeviceClass.APPARENT_POWER, SensorDeviceClass.APPARENT_POWER,
SensorDeviceClass.AQI, SensorDeviceClass.AQI,
SensorDeviceClass.AREA,
SensorDeviceClass.ATMOSPHERIC_PRESSURE, SensorDeviceClass.ATMOSPHERIC_PRESSURE,
SensorDeviceClass.BATTERY, SensorDeviceClass.BATTERY,
SensorDeviceClass.CO, SensorDeviceClass.CO,

View File

@ -227,6 +227,8 @@ async def assert_validation_result(
), ),
[ [
(None, "%", "%", "%", "unitless", 13.050847, -10, 30), (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", "%", "%", "%", "unitless", 13.050847, -10, 30),
("battery", None, None, None, "unitless", 13.050847, -10, 30), ("battery", None, None, None, "unitless", 13.050847, -10, 30),
("distance", "m", "m", "m", "distance", 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", "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", "m", "m", "m", "distance", 1),
(US_CUSTOMARY_SYSTEM, "distance", "mi", "mi", "mi", "distance", 1), (US_CUSTOMARY_SYSTEM, "distance", "mi", "mi", "mi", "distance", 1),
(US_CUSTOMARY_SYSTEM, "energy", "kWh", "kWh", "kWh", "energy", 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, "volume", "ft³", "ft³", "ft³", "volume", 1),
(US_CUSTOMARY_SYSTEM, "weight", "g", "g", "g", "mass", 1), (US_CUSTOMARY_SYSTEM, "weight", "g", "g", "g", "mass", 1),
(US_CUSTOMARY_SYSTEM, "weight", "oz", "oz", "oz", "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", "m", "m", "m", "distance", 1),
(METRIC_SYSTEM, "distance", "mi", "mi", "mi", "distance", 1), (METRIC_SYSTEM, "distance", "mi", "mi", "mi", "distance", 1),
(METRIC_SYSTEM, "energy", "kWh", "kWh", "kWh", "energy", 1), (METRIC_SYSTEM, "energy", "kWh", "kWh", "kWh", "energy", 1),
@ -2228,6 +2234,8 @@ async def test_compile_hourly_energy_statistics_multiple(
[ [
("battery", "%", 30), ("battery", "%", 30),
("battery", None, 30), ("battery", None, 30),
("area", "", 30),
("area", "mi²", 30),
("distance", "m", 30), ("distance", "m", 30),
("distance", "mi", 30), ("distance", "mi", 30),
("humidity", "%", 30), ("humidity", "%", 30),
@ -2336,6 +2344,8 @@ async def test_compile_hourly_statistics_partially_unavailable(
[ [
("battery", "%", 30), ("battery", "%", 30),
("battery", None, 30), ("battery", None, 30),
("area", "", 30),
("area", "mi²", 30),
("distance", "m", 30), ("distance", "m", 30),
("distance", "mi", 30), ("distance", "mi", 30),
("humidity", "%", 30), ("humidity", "%", 30),
@ -2438,6 +2448,10 @@ async def test_compile_hourly_statistics_fails(
"statistic_type", "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", "%", "%", "%", "unitless", "mean"),
("measurement", "battery", None, None, None, "unitless", "mean"), ("measurement", "battery", None, None, None, "unitless", "mean"),
("measurement", "distance", "m", "m", "m", "distance", "mean"), ("measurement", "distance", "m", "m", "m", "distance", "mean"),

View File

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

View File

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

View File

@ -11,6 +11,7 @@ from homeassistant.const import (
CONCENTRATION_PARTS_PER_BILLION, CONCENTRATION_PARTS_PER_BILLION,
CONCENTRATION_PARTS_PER_MILLION, CONCENTRATION_PARTS_PER_MILLION,
PERCENTAGE, PERCENTAGE,
UnitOfArea,
UnitOfBloodGlucoseConcentration, UnitOfBloodGlucoseConcentration,
UnitOfConductivity, UnitOfConductivity,
UnitOfDataRate, UnitOfDataRate,
@ -32,6 +33,7 @@ from homeassistant.const import (
from homeassistant.exceptions import HomeAssistantError from homeassistant.exceptions import HomeAssistantError
from homeassistant.util import unit_conversion from homeassistant.util import unit_conversion
from homeassistant.util.unit_conversion import ( from homeassistant.util.unit_conversion import (
AreaConverter,
BaseUnitConverter, BaseUnitConverter,
BloodGlucoseConcentrationConverter, BloodGlucoseConcentrationConverter,
ConductivityConverter, ConductivityConverter,
@ -61,6 +63,7 @@ INVALID_SYMBOL = "bob"
_ALL_CONVERTERS: dict[type[BaseUnitConverter], list[str | None]] = { _ALL_CONVERTERS: dict[type[BaseUnitConverter], list[str | None]] = {
converter: sorted(converter.VALID_UNITS, key=lambda x: (x is None, x)) converter: sorted(converter.VALID_UNITS, key=lambda x: (x is None, x))
for converter in ( for converter in (
AreaConverter,
BloodGlucoseConcentrationConverter, BloodGlucoseConcentrationConverter,
ConductivityConverter, ConductivityConverter,
DataRateConverter, DataRateConverter,
@ -83,6 +86,7 @@ _ALL_CONVERTERS: dict[type[BaseUnitConverter], list[str | None]] = {
# Dict containing all converters with a corresponding unit ratio. # Dict containing all converters with a corresponding unit ratio.
_GET_UNIT_RATIO: dict[type[BaseUnitConverter], tuple[str | None, str | None, float]] = { _GET_UNIT_RATIO: dict[type[BaseUnitConverter], tuple[str | None, str | None, float]] = {
AreaConverter: (UnitOfArea.SQUARE_KILOMETERS, UnitOfArea.SQUARE_METERS, 0.000001),
BloodGlucoseConcentrationConverter: ( BloodGlucoseConcentrationConverter: (
UnitOfBloodGlucoseConcentration.MILLIGRAMS_PER_DECILITER, UnitOfBloodGlucoseConcentration.MILLIGRAMS_PER_DECILITER,
UnitOfBloodGlucoseConcentration.MILLIMOLE_PER_LITER, UnitOfBloodGlucoseConcentration.MILLIMOLE_PER_LITER,
@ -138,6 +142,62 @@ _GET_UNIT_RATIO: dict[type[BaseUnitConverter], tuple[str | None, str | None, flo
_CONVERTED_VALUE: dict[ _CONVERTED_VALUE: dict[
type[BaseUnitConverter], list[tuple[float, str | None, float, str | None]] 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: [ BloodGlucoseConcentrationConverter: [
( (
90, 90,

View File

@ -7,12 +7,14 @@ import pytest
from homeassistant.components.sensor import DEVICE_CLASS_UNITS, SensorDeviceClass from homeassistant.components.sensor import DEVICE_CLASS_UNITS, SensorDeviceClass
from homeassistant.const import ( from homeassistant.const import (
ACCUMULATED_PRECIPITATION, ACCUMULATED_PRECIPITATION,
AREA,
LENGTH, LENGTH,
MASS, MASS,
PRESSURE, PRESSURE,
TEMPERATURE, TEMPERATURE,
VOLUME, VOLUME,
WIND_SPEED, WIND_SPEED,
UnitOfArea,
UnitOfLength, UnitOfLength,
UnitOfMass, UnitOfMass,
UnitOfPrecipitationDepth, UnitOfPrecipitationDepth,
@ -44,6 +46,7 @@ def test_invalid_units() -> None:
UnitSystem( UnitSystem(
SYSTEM_NAME, SYSTEM_NAME,
accumulated_precipitation=UnitOfPrecipitationDepth.MILLIMETERS, accumulated_precipitation=UnitOfPrecipitationDepth.MILLIMETERS,
area=UnitOfArea.SQUARE_METERS,
conversions={}, conversions={},
length=UnitOfLength.METERS, length=UnitOfLength.METERS,
mass=UnitOfMass.GRAMS, mass=UnitOfMass.GRAMS,
@ -57,6 +60,7 @@ def test_invalid_units() -> None:
UnitSystem( UnitSystem(
SYSTEM_NAME, SYSTEM_NAME,
accumulated_precipitation=UnitOfPrecipitationDepth.MILLIMETERS, accumulated_precipitation=UnitOfPrecipitationDepth.MILLIMETERS,
area=UnitOfArea.SQUARE_METERS,
conversions={}, conversions={},
length=INVALID_UNIT, length=INVALID_UNIT,
mass=UnitOfMass.GRAMS, mass=UnitOfMass.GRAMS,
@ -70,6 +74,7 @@ def test_invalid_units() -> None:
UnitSystem( UnitSystem(
SYSTEM_NAME, SYSTEM_NAME,
accumulated_precipitation=UnitOfPrecipitationDepth.MILLIMETERS, accumulated_precipitation=UnitOfPrecipitationDepth.MILLIMETERS,
area=UnitOfArea.SQUARE_METERS,
conversions={}, conversions={},
length=UnitOfLength.METERS, length=UnitOfLength.METERS,
mass=UnitOfMass.GRAMS, mass=UnitOfMass.GRAMS,
@ -83,6 +88,7 @@ def test_invalid_units() -> None:
UnitSystem( UnitSystem(
SYSTEM_NAME, SYSTEM_NAME,
accumulated_precipitation=UnitOfPrecipitationDepth.MILLIMETERS, accumulated_precipitation=UnitOfPrecipitationDepth.MILLIMETERS,
area=UnitOfArea.SQUARE_METERS,
conversions={}, conversions={},
length=UnitOfLength.METERS, length=UnitOfLength.METERS,
mass=UnitOfMass.GRAMS, mass=UnitOfMass.GRAMS,
@ -96,6 +102,7 @@ def test_invalid_units() -> None:
UnitSystem( UnitSystem(
SYSTEM_NAME, SYSTEM_NAME,
accumulated_precipitation=UnitOfPrecipitationDepth.MILLIMETERS, accumulated_precipitation=UnitOfPrecipitationDepth.MILLIMETERS,
area=UnitOfArea.SQUARE_METERS,
conversions={}, conversions={},
length=UnitOfLength.METERS, length=UnitOfLength.METERS,
mass=INVALID_UNIT, mass=INVALID_UNIT,
@ -109,6 +116,7 @@ def test_invalid_units() -> None:
UnitSystem( UnitSystem(
SYSTEM_NAME, SYSTEM_NAME,
accumulated_precipitation=UnitOfPrecipitationDepth.MILLIMETERS, accumulated_precipitation=UnitOfPrecipitationDepth.MILLIMETERS,
area=UnitOfArea.SQUARE_METERS,
conversions={}, conversions={},
length=UnitOfLength.METERS, length=UnitOfLength.METERS,
mass=UnitOfMass.GRAMS, mass=UnitOfMass.GRAMS,
@ -122,6 +130,21 @@ def test_invalid_units() -> None:
UnitSystem( UnitSystem(
SYSTEM_NAME, SYSTEM_NAME,
accumulated_precipitation=INVALID_UNIT, 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={}, conversions={},
length=UnitOfLength.METERS, length=UnitOfLength.METERS,
mass=UnitOfMass.GRAMS, mass=UnitOfMass.GRAMS,
@ -146,6 +169,8 @@ def test_invalid_value() -> None:
METRIC_SYSTEM.pressure("50Pa", UnitOfPressure.PA) METRIC_SYSTEM.pressure("50Pa", UnitOfPressure.PA)
with pytest.raises(TypeError): with pytest.raises(TypeError):
METRIC_SYSTEM.accumulated_precipitation("50mm", UnitOfLength.MILLIMETERS) METRIC_SYSTEM.accumulated_precipitation("50mm", UnitOfLength.MILLIMETERS)
with pytest.raises(TypeError):
METRIC_SYSTEM.area("2m²", UnitOfArea.SQUARE_METERS)
def test_as_dict() -> None: def test_as_dict() -> None:
@ -158,6 +183,7 @@ def test_as_dict() -> None:
MASS: UnitOfMass.GRAMS, MASS: UnitOfMass.GRAMS,
PRESSURE: UnitOfPressure.PA, PRESSURE: UnitOfPressure.PA,
ACCUMULATED_PRECIPITATION: UnitOfLength.MILLIMETERS, ACCUMULATED_PRECIPITATION: UnitOfLength.MILLIMETERS,
AREA: UnitOfArea.SQUARE_METERS,
} }
assert expected == METRIC_SYSTEM.as_dict() assert expected == METRIC_SYSTEM.as_dict()
@ -303,6 +329,29 @@ def test_accumulated_precipitation_to_imperial() -> None:
) == pytest.approx(10, abs=1e-4) ) == 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: def test_properties() -> None:
"""Test the unit properties are returned as expected.""" """Test the unit properties are returned as expected."""
assert METRIC_SYSTEM.length_unit == UnitOfLength.KILOMETERS 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.volume_unit == UnitOfVolume.LITERS
assert METRIC_SYSTEM.pressure_unit == UnitOfPressure.PA assert METRIC_SYSTEM.pressure_unit == UnitOfPressure.PA
assert METRIC_SYSTEM.accumulated_precipitation_unit == UnitOfLength.MILLIMETERS assert METRIC_SYSTEM.accumulated_precipitation_unit == UnitOfLength.MILLIMETERS
assert METRIC_SYSTEM.area_unit == UnitOfArea.SQUARE_METERS
@pytest.mark.parametrize( @pytest.mark.parametrize(
@ -338,6 +388,18 @@ def test_get_unit_system_invalid(key: str) -> None:
@pytest.mark.parametrize( @pytest.mark.parametrize(
("device_class", "original_unit", "state_unit"), ("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 # Test atmospheric pressure
( (
SensorDeviceClass.ATMOSPHERIC_PRESSURE, SensorDeviceClass.ATMOSPHERIC_PRESSURE,
@ -495,6 +557,13 @@ def test_get_metric_converted_unit_(
UNCONVERTED_UNITS_METRIC_SYSTEM = { 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.ATMOSPHERIC_PRESSURE: (UnitOfPressure.HPA,),
SensorDeviceClass.DISTANCE: ( SensorDeviceClass.DISTANCE: (
UnitOfLength.CENTIMETERS, UnitOfLength.CENTIMETERS,
@ -544,6 +613,7 @@ UNCONVERTED_UNITS_METRIC_SYSTEM = {
@pytest.mark.parametrize( @pytest.mark.parametrize(
"device_class", "device_class",
[ [
SensorDeviceClass.AREA,
SensorDeviceClass.ATMOSPHERIC_PRESSURE, SensorDeviceClass.ATMOSPHERIC_PRESSURE,
SensorDeviceClass.DISTANCE, SensorDeviceClass.DISTANCE,
SensorDeviceClass.GAS, SensorDeviceClass.GAS,
@ -572,6 +642,21 @@ def test_metric_converted_units(device_class: SensorDeviceClass) -> None:
@pytest.mark.parametrize( @pytest.mark.parametrize(
("device_class", "original_unit", "state_unit"), ("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 # Test atmospheric pressure
( (
SensorDeviceClass.ATMOSPHERIC_PRESSURE, SensorDeviceClass.ATMOSPHERIC_PRESSURE,
@ -721,6 +806,13 @@ def test_get_us_converted_unit(
UNCONVERTED_UNITS_US_SYSTEM = { 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.ATMOSPHERIC_PRESSURE: (UnitOfPressure.INHG,),
SensorDeviceClass.DISTANCE: ( SensorDeviceClass.DISTANCE: (
UnitOfLength.FEET, UnitOfLength.FEET,