diff --git a/homeassistant/components/number/const.py b/homeassistant/components/number/const.py index aa9988f8987..071f480f766 100644 --- a/homeassistant/components/number/const.py +++ b/homeassistant/components/number/const.py @@ -34,6 +34,7 @@ from homeassistant.const import ( UnitOfTemperature, UnitOfTime, UnitOfVolume, + UnitOfVolumeFlowRate, UnitOfVolumetricFlux, ) from homeassistant.helpers.deprecation import ( @@ -42,7 +43,11 @@ from homeassistant.helpers.deprecation import ( check_if_deprecated_constant, dir_with_deprecated_constants, ) -from homeassistant.util.unit_conversion import BaseUnitConverter, TemperatureConverter +from homeassistant.util.unit_conversion import ( + BaseUnitConverter, + TemperatureConverter, + VolumeFlowRateConverter, +) ATTR_VALUE = "value" ATTR_MIN = "min" @@ -372,6 +377,14 @@ class NumberDeviceClass(StrEnum): USCS/imperial units are currently assumed to be US volumes) """ + VOLUME_FLOW_RATE = "volume_flow_rate" + """Generic flow rate + + Unit of measurement: UnitOfVolumeFlowRate + - SI / metric: `m³/h`, `L/min` + - USCS / imperial: `ft³/min`, `gal/min` + """ + WATER = "water" """Water. @@ -464,6 +477,7 @@ DEVICE_CLASS_UNITS: dict[NumberDeviceClass, set[type[StrEnum] | str | None]] = { NumberDeviceClass.VOLTAGE: set(UnitOfElectricPotential), NumberDeviceClass.VOLUME: set(UnitOfVolume), NumberDeviceClass.VOLUME_STORAGE: set(UnitOfVolume), + NumberDeviceClass.VOLUME_FLOW_RATE: set(UnitOfVolumeFlowRate), NumberDeviceClass.WATER: { UnitOfVolume.CENTUM_CUBIC_FEET, UnitOfVolume.CUBIC_FEET, @@ -477,6 +491,7 @@ DEVICE_CLASS_UNITS: dict[NumberDeviceClass, set[type[StrEnum] | str | None]] = { UNIT_CONVERTERS: dict[NumberDeviceClass, type[BaseUnitConverter]] = { NumberDeviceClass.TEMPERATURE: TemperatureConverter, + NumberDeviceClass.VOLUME_FLOW_RATE: VolumeFlowRateConverter, } # These can be removed if no deprecated constant are in this module anymore diff --git a/homeassistant/components/number/strings.json b/homeassistant/components/number/strings.json index 2d72cdbf203..ffddc0c2b3c 100644 --- a/homeassistant/components/number/strings.json +++ b/homeassistant/components/number/strings.json @@ -148,6 +148,9 @@ "volume_storage": { "name": "[%key:component::sensor::entity_component::volume_storage::name%]" }, + "volume_flow_rate": { + "name": "[%key:component::sensor::entity_component::volume_flow_rate::name%]" + }, "water": { "name": "[%key:component::sensor::entity_component::water::name%]" }, diff --git a/homeassistant/components/recorder/statistics.py b/homeassistant/components/recorder/statistics.py index 023f94ec88e..5786c9ee542 100644 --- a/homeassistant/components/recorder/statistics.py +++ b/homeassistant/components/recorder/statistics.py @@ -41,6 +41,7 @@ from homeassistant.util.unit_conversion import ( TemperatureConverter, UnitlessRatioConverter, VolumeConverter, + VolumeFlowRateConverter, ) from .const import ( @@ -139,6 +140,7 @@ STATISTIC_UNIT_TO_UNIT_CONVERTER: dict[str | None, type[BaseUnitConverter]] = { **{unit: TemperatureConverter for unit in TemperatureConverter.VALID_UNITS}, **{unit: UnitlessRatioConverter for unit in UnitlessRatioConverter.VALID_UNITS}, **{unit: VolumeConverter for unit in VolumeConverter.VALID_UNITS}, + **{unit: VolumeFlowRateConverter for unit in VolumeFlowRateConverter.VALID_UNITS}, } DATA_SHORT_TERM_STATISTICS_RUN_CACHE = "recorder_short_term_statistics_run_cache" diff --git a/homeassistant/components/recorder/websocket_api.py b/homeassistant/components/recorder/websocket_api.py index 30c3bb31a47..11271d1e0cd 100644 --- a/homeassistant/components/recorder/websocket_api.py +++ b/homeassistant/components/recorder/websocket_api.py @@ -28,6 +28,7 @@ from homeassistant.util.unit_conversion import ( TemperatureConverter, UnitlessRatioConverter, VolumeConverter, + VolumeFlowRateConverter, ) from .models import StatisticPeriod @@ -67,6 +68,7 @@ UNIT_SCHEMA = vol.Schema( vol.Optional("temperature"): vol.In(TemperatureConverter.VALID_UNITS), vol.Optional("unitless"): vol.In(UnitlessRatioConverter.VALID_UNITS), vol.Optional("volume"): vol.In(VolumeConverter.VALID_UNITS), + vol.Optional("volume_flow_rate"): vol.In(VolumeFlowRateConverter.VALID_UNITS), } ) diff --git a/homeassistant/components/sensor/const.py b/homeassistant/components/sensor/const.py index b1cb120e3fe..861338f257a 100644 --- a/homeassistant/components/sensor/const.py +++ b/homeassistant/components/sensor/const.py @@ -34,6 +34,7 @@ from homeassistant.const import ( UnitOfTemperature, UnitOfTime, UnitOfVolume, + UnitOfVolumeFlowRate, UnitOfVolumetricFlux, ) from homeassistant.helpers.deprecation import ( @@ -57,6 +58,7 @@ from homeassistant.util.unit_conversion import ( TemperatureConverter, UnitlessRatioConverter, VolumeConverter, + VolumeFlowRateConverter, ) DOMAIN: Final = "sensor" @@ -394,6 +396,14 @@ class SensorDeviceClass(StrEnum): USCS/imperial units are currently assumed to be US volumes) """ + VOLUME_FLOW_RATE = "volume_flow_rate" + """Generic flow rate + + Unit of measurement: UnitOfVolumeFlowRate + - SI / metric: `m³/h`, `L/min` + - USCS / imperial: `ft³/min`, `gal/min` + """ + WATER = "water" """Water. @@ -489,6 +499,7 @@ UNIT_CONVERTERS: dict[SensorDeviceClass | str | None, type[BaseUnitConverter]] = SensorDeviceClass.VOLTAGE: ElectricPotentialConverter, SensorDeviceClass.VOLUME: VolumeConverter, SensorDeviceClass.VOLUME_STORAGE: VolumeConverter, + SensorDeviceClass.VOLUME_FLOW_RATE: VolumeFlowRateConverter, SensorDeviceClass.WATER: VolumeConverter, SensorDeviceClass.WEIGHT: MassConverter, SensorDeviceClass.WIND_SPEED: SpeedConverter, @@ -555,6 +566,7 @@ DEVICE_CLASS_UNITS: dict[SensorDeviceClass, set[type[StrEnum] | str | None]] = { }, SensorDeviceClass.VOLTAGE: set(UnitOfElectricPotential), SensorDeviceClass.VOLUME: set(UnitOfVolume), + SensorDeviceClass.VOLUME_FLOW_RATE: set(UnitOfVolumeFlowRate), SensorDeviceClass.VOLUME_STORAGE: set(UnitOfVolume), SensorDeviceClass.WATER: { UnitOfVolume.CENTUM_CUBIC_FEET, @@ -621,6 +633,7 @@ DEVICE_CLASS_STATE_CLASSES: dict[SensorDeviceClass, set[SensorStateClass]] = { SensorStateClass.TOTAL_INCREASING, }, SensorDeviceClass.VOLUME_STORAGE: {SensorStateClass.MEASUREMENT}, + SensorDeviceClass.VOLUME_FLOW_RATE: {SensorStateClass.MEASUREMENT}, SensorDeviceClass.WATER: { SensorStateClass.TOTAL, SensorStateClass.TOTAL_INCREASING, diff --git a/homeassistant/components/sensor/device_condition.py b/homeassistant/components/sensor/device_condition.py index b12cdb570eb..b7cf533d3da 100644 --- a/homeassistant/components/sensor/device_condition.py +++ b/homeassistant/components/sensor/device_condition.py @@ -77,6 +77,7 @@ CONF_IS_VOLATILE_ORGANIC_COMPOUNDS = "is_volatile_organic_compounds" CONF_IS_VOLATILE_ORGANIC_COMPOUNDS_PARTS = "is_volatile_organic_compounds_parts" CONF_IS_VOLTAGE = "is_voltage" CONF_IS_VOLUME = "is_volume" +CONF_IS_VOLUME_FLOW_RATE = "is_volume_flow_rate" CONF_IS_WATER = "is_water" CONF_IS_WEIGHT = "is_weight" CONF_IS_WIND_SPEED = "is_wind_speed" @@ -132,6 +133,7 @@ ENTITY_CONDITIONS = { SensorDeviceClass.VOLTAGE: [{CONF_TYPE: CONF_IS_VOLTAGE}], SensorDeviceClass.VOLUME: [{CONF_TYPE: CONF_IS_VOLUME}], SensorDeviceClass.VOLUME_STORAGE: [{CONF_TYPE: CONF_IS_VOLUME}], + SensorDeviceClass.VOLUME_FLOW_RATE: [{CONF_TYPE: CONF_IS_VOLUME_FLOW_RATE}], SensorDeviceClass.WATER: [{CONF_TYPE: CONF_IS_WATER}], SensorDeviceClass.WEIGHT: [{CONF_TYPE: CONF_IS_WEIGHT}], SensorDeviceClass.WIND_SPEED: [{CONF_TYPE: CONF_IS_WIND_SPEED}], @@ -186,6 +188,7 @@ CONDITION_SCHEMA = vol.All( CONF_IS_VOLATILE_ORGANIC_COMPOUNDS_PARTS, CONF_IS_VOLTAGE, CONF_IS_VOLUME, + CONF_IS_VOLUME_FLOW_RATE, CONF_IS_WATER, CONF_IS_WEIGHT, CONF_IS_WIND_SPEED, diff --git a/homeassistant/components/sensor/device_trigger.py b/homeassistant/components/sensor/device_trigger.py index 1c0da89692b..c5c19a19d0b 100644 --- a/homeassistant/components/sensor/device_trigger.py +++ b/homeassistant/components/sensor/device_trigger.py @@ -76,6 +76,7 @@ CONF_VOLATILE_ORGANIC_COMPOUNDS = "volatile_organic_compounds" CONF_VOLATILE_ORGANIC_COMPOUNDS_PARTS = "volatile_organic_compounds_parts" CONF_VOLTAGE = "voltage" CONF_VOLUME = "volume" +CONF_VOLUME_FLOW_RATE = "volume_flow_rate" CONF_WATER = "water" CONF_WEIGHT = "weight" CONF_WIND_SPEED = "wind_speed" @@ -131,6 +132,7 @@ ENTITY_TRIGGERS = { SensorDeviceClass.VOLTAGE: [{CONF_TYPE: CONF_VOLTAGE}], SensorDeviceClass.VOLUME: [{CONF_TYPE: CONF_VOLUME}], SensorDeviceClass.VOLUME_STORAGE: [{CONF_TYPE: CONF_VOLUME}], + SensorDeviceClass.VOLUME_FLOW_RATE: [{CONF_TYPE: CONF_VOLUME_FLOW_RATE}], SensorDeviceClass.WATER: [{CONF_TYPE: CONF_WATER}], SensorDeviceClass.WEIGHT: [{CONF_TYPE: CONF_WEIGHT}], SensorDeviceClass.WIND_SPEED: [{CONF_TYPE: CONF_WIND_SPEED}], @@ -186,6 +188,7 @@ TRIGGER_SCHEMA = vol.All( CONF_VOLATILE_ORGANIC_COMPOUNDS_PARTS, CONF_VOLTAGE, CONF_VOLUME, + CONF_VOLUME_FLOW_RATE, CONF_WATER, CONF_WEIGHT, CONF_WIND_SPEED, diff --git a/homeassistant/components/sensor/strings.json b/homeassistant/components/sensor/strings.json index 1db5e4c8cfd..fad1086c034 100644 --- a/homeassistant/components/sensor/strings.json +++ b/homeassistant/components/sensor/strings.json @@ -45,6 +45,7 @@ "is_volatile_organic_compounds_parts": "[%key:component::sensor::device_automation::condition_type::is_volatile_organic_compounds%]", "is_voltage": "Current {entity_name} voltage", "is_volume": "Current {entity_name} volume", + "is_volume_flow_rate": "Current {entity_name} volume flow rate", "is_water": "Current {entity_name} water", "is_weight": "Current {entity_name} weight", "is_wind_speed": "Current {entity_name} wind speed" @@ -93,6 +94,7 @@ "volatile_organic_compounds_parts": "[%key:component::sensor::device_automation::trigger_type::volatile_organic_compounds%]", "voltage": "{entity_name} voltage changes", "volume": "{entity_name} volume changes", + "volume_flow_rate": "{entity_name} volume flow rate changes", "water": "{entity_name} water changes", "weight": "{entity_name} weight changes", "wind_speed": "{entity_name} wind speed changes" @@ -260,6 +262,9 @@ "volume": { "name": "Volume" }, + "volume_flow_rate": { + "name": "Volume flow rate" + }, "volume_storage": { "name": "Stored volume" }, diff --git a/homeassistant/const.py b/homeassistant/const.py index 35cd8a5e23a..8db9be36902 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -1042,7 +1042,9 @@ class UnitOfVolumeFlowRate(StrEnum): """Volume flow rate units.""" CUBIC_METERS_PER_HOUR = "m³/h" - CUBIC_FEET_PER_MINUTE = "ft³/m" + CUBIC_FEET_PER_MINUTE = "ft³/min" + LITERS_PER_MINUTE = "L/min" + GALLONS_PER_MINUTE = "gal/min" _DEPRECATED_VOLUME_FLOW_RATE_CUBIC_METERS_PER_HOUR: Final = DeprecatedConstantEnum( diff --git a/homeassistant/util/unit_conversion.py b/homeassistant/util/unit_conversion.py index 5ce31b072cf..15912fa2f6e 100644 --- a/homeassistant/util/unit_conversion.py +++ b/homeassistant/util/unit_conversion.py @@ -21,6 +21,7 @@ from homeassistant.const import ( UnitOfSpeed, UnitOfTemperature, UnitOfVolume, + UnitOfVolumeFlowRate, UnitOfVolumetricFlux, ) from homeassistant.exceptions import HomeAssistantError @@ -39,6 +40,7 @@ _NAUTICAL_MILE_TO_M = 1852 # 1 nautical mile = 1852 m # Duration conversion constants _HRS_TO_SECS = 60 * 60 # 1 hr = 3600 seconds +_HRS_TO_MINUTES = 60 # 1 hr = 60 minutes _DAYS_TO_SECS = 24 * _HRS_TO_SECS # 1 day = 24 hours = 86400 seconds # Mass conversion constants @@ -516,3 +518,26 @@ class VolumeConverter(BaseUnitConverter): UnitOfVolume.CUBIC_FEET, UnitOfVolume.CENTUM_CUBIC_FEET, } + + +class VolumeFlowRateConverter(BaseUnitConverter): + """Utility to convert volume values.""" + + UNIT_CLASS = "volume_flow_rate" + NORMALIZED_UNIT = UnitOfVolumeFlowRate.CUBIC_METERS_PER_HOUR + # Units in terms of m³/h + _UNIT_CONVERSION: dict[str | None, float] = { + UnitOfVolumeFlowRate.CUBIC_METERS_PER_HOUR: 1, + UnitOfVolumeFlowRate.CUBIC_FEET_PER_MINUTE: 1 + / (_HRS_TO_MINUTES * _CUBIC_FOOT_TO_CUBIC_METER), + UnitOfVolumeFlowRate.LITERS_PER_MINUTE: 1 + / (_HRS_TO_MINUTES * _L_TO_CUBIC_METER), + UnitOfVolumeFlowRate.GALLONS_PER_MINUTE: 1 + / (_HRS_TO_MINUTES * _GALLON_TO_CUBIC_METER), + } + VALID_UNITS = { + UnitOfVolumeFlowRate.CUBIC_FEET_PER_MINUTE, + UnitOfVolumeFlowRate.CUBIC_METERS_PER_HOUR, + UnitOfVolumeFlowRate.LITERS_PER_MINUTE, + UnitOfVolumeFlowRate.GALLONS_PER_MINUTE, + } diff --git a/tests/components/number/test_init.py b/tests/components/number/test_init.py index 4de47b9b844..9c66b45df25 100644 --- a/tests/components/number/test_init.py +++ b/tests/components/number/test_init.py @@ -32,6 +32,7 @@ from homeassistant.const import ( ATTR_UNIT_OF_MEASUREMENT, CONF_PLATFORM, UnitOfTemperature, + UnitOfVolumeFlowRate, ) from homeassistant.core import HomeAssistant, State from homeassistant.helpers import entity_registry as er @@ -686,6 +687,22 @@ async def test_restore_number_restore_state( 100, 38.0, ), + ( + SensorDeviceClass.VOLUME_FLOW_RATE, + UnitOfVolumeFlowRate.LITERS_PER_MINUTE, + UnitOfVolumeFlowRate.GALLONS_PER_MINUTE, + UnitOfVolumeFlowRate.GALLONS_PER_MINUTE, + 50.0, + "13.2", + ), + ( + SensorDeviceClass.VOLUME_FLOW_RATE, + UnitOfVolumeFlowRate.GALLONS_PER_MINUTE, + UnitOfVolumeFlowRate.LITERS_PER_MINUTE, + UnitOfVolumeFlowRate.LITERS_PER_MINUTE, + 13.0, + "49.2", + ), ], ) async def test_custom_unit( diff --git a/tests/components/sensor/test_init.py b/tests/components/sensor/test_init.py index 8e28a4fe382..3172759520d 100644 --- a/tests/components/sensor/test_init.py +++ b/tests/components/sensor/test_init.py @@ -36,6 +36,7 @@ from homeassistant.const import ( UnitOfSpeed, UnitOfTemperature, UnitOfVolume, + UnitOfVolumeFlowRate, UnitOfVolumetricFlux, ) from homeassistant.core import HomeAssistant, State @@ -580,6 +581,22 @@ async def test_restore_sensor_restore_state( -0.00001, "0", ), + ( + SensorDeviceClass.VOLUME_FLOW_RATE, + UnitOfVolumeFlowRate.LITERS_PER_MINUTE, + UnitOfVolumeFlowRate.GALLONS_PER_MINUTE, + UnitOfVolumeFlowRate.GALLONS_PER_MINUTE, + 50.0, + "13.2", + ), + ( + SensorDeviceClass.VOLUME_FLOW_RATE, + UnitOfVolumeFlowRate.GALLONS_PER_MINUTE, + UnitOfVolumeFlowRate.LITERS_PER_MINUTE, + UnitOfVolumeFlowRate.LITERS_PER_MINUTE, + 13.0, + "49.2", + ), ], ) async def test_custom_unit( diff --git a/tests/test_const.py b/tests/test_const.py index 4b9be4f27f1..7ca4812ca8e 100644 --- a/tests/test_const.py +++ b/tests/test_const.py @@ -103,7 +103,13 @@ def test_all() -> None: ], "VOLUME_", ) - + _create_tuples(const.UnitOfVolumeFlowRate, "VOLUME_FLOW_RATE_") + + _create_tuples( + [ + const.UnitOfVolumeFlowRate.CUBIC_METERS_PER_HOUR, + const.UnitOfVolumeFlowRate.CUBIC_FEET_PER_MINUTE, + ], + "VOLUME_FLOW_RATE_", + ) + _create_tuples( [ const.UnitOfMass.GRAMS, diff --git a/tests/util/test_unit_conversion.py b/tests/util/test_unit_conversion.py index e7affecfaf4..08d362072d4 100644 --- a/tests/util/test_unit_conversion.py +++ b/tests/util/test_unit_conversion.py @@ -22,6 +22,7 @@ from homeassistant.const import ( UnitOfSpeed, UnitOfTemperature, UnitOfVolume, + UnitOfVolumeFlowRate, UnitOfVolumetricFlux, ) from homeassistant.exceptions import HomeAssistantError @@ -41,6 +42,7 @@ from homeassistant.util.unit_conversion import ( TemperatureConverter, UnitlessRatioConverter, VolumeConverter, + VolumeFlowRateConverter, ) INVALID_SYMBOL = "bob" @@ -65,6 +67,7 @@ _ALL_CONVERTERS: dict[type[BaseUnitConverter], list[str | None]] = { TemperatureConverter, UnitlessRatioConverter, VolumeConverter, + VolumeFlowRateConverter, ) } @@ -103,6 +106,11 @@ _GET_UNIT_RATIO: dict[type[BaseUnitConverter], tuple[str | None, str | None, flo ), UnitlessRatioConverter: (PERCENTAGE, None, 100), VolumeConverter: (UnitOfVolume.GALLONS, UnitOfVolume.LITERS, 0.264172), + VolumeFlowRateConverter: ( + UnitOfVolumeFlowRate.CUBIC_METERS_PER_HOUR, + UnitOfVolumeFlowRate.LITERS_PER_MINUTE, + 0.06, + ), } # Dict containing a conversion test for every known unit. @@ -413,6 +421,62 @@ _CONVERTED_VALUE: dict[ (5, UnitOfVolume.CENTUM_CUBIC_FEET, 3740.26, UnitOfVolume.GALLONS), (5, UnitOfVolume.CENTUM_CUBIC_FEET, 14158.42, UnitOfVolume.LITERS), ], + VolumeFlowRateConverter: [ + ( + 1, + UnitOfVolumeFlowRate.CUBIC_METERS_PER_HOUR, + 16.6666667, + UnitOfVolumeFlowRate.LITERS_PER_MINUTE, + ), + ( + 1, + UnitOfVolumeFlowRate.CUBIC_METERS_PER_HOUR, + 0.58857777, + UnitOfVolumeFlowRate.CUBIC_FEET_PER_MINUTE, + ), + ( + 1, + UnitOfVolumeFlowRate.CUBIC_METERS_PER_HOUR, + 4.40286754, + UnitOfVolumeFlowRate.GALLONS_PER_MINUTE, + ), + ( + 1, + UnitOfVolumeFlowRate.LITERS_PER_MINUTE, + 0.06, + UnitOfVolumeFlowRate.CUBIC_METERS_PER_HOUR, + ), + ( + 1, + UnitOfVolumeFlowRate.LITERS_PER_MINUTE, + 0.03531466, + UnitOfVolumeFlowRate.CUBIC_FEET_PER_MINUTE, + ), + ( + 1, + UnitOfVolumeFlowRate.LITERS_PER_MINUTE, + 0.264172052, + UnitOfVolumeFlowRate.GALLONS_PER_MINUTE, + ), + ( + 1, + UnitOfVolumeFlowRate.CUBIC_FEET_PER_MINUTE, + 1.69901079, + UnitOfVolumeFlowRate.CUBIC_METERS_PER_HOUR, + ), + ( + 1, + UnitOfVolumeFlowRate.CUBIC_FEET_PER_MINUTE, + 28.3168465, + UnitOfVolumeFlowRate.LITERS_PER_MINUTE, + ), + ( + 1, + UnitOfVolumeFlowRate.CUBIC_FEET_PER_MINUTE, + 7.48051948, + UnitOfVolumeFlowRate.GALLONS_PER_MINUTE, + ), + ], }