Add new device class for absolute humidity (#148567)

This commit is contained in:
Michael 2025-07-14 11:46:17 +02:00 committed by GitHub
parent 21b1122f83
commit 50047f0a4e
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
14 changed files with 62 additions and 4 deletions

View File

@ -8,6 +8,7 @@ from typing import Final
import voluptuous as vol
from homeassistant.const import (
CONCENTRATION_GRAMS_PER_CUBIC_METER,
CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
CONCENTRATION_MILLIGRAMS_PER_CUBIC_METER,
CONCENTRATION_PARTS_PER_BILLION,
@ -78,6 +79,11 @@ class NumberDeviceClass(StrEnum):
"""Device class for numbers."""
# NumberDeviceClass should be aligned with SensorDeviceClass
ABSOLUTE_HUMIDITY = "absolute_humidity"
"""Absolute humidity.
Unit of measurement: `g/`, `mg/`
"""
APPARENT_POWER = "apparent_power"
"""Apparent power.
@ -452,6 +458,10 @@ class NumberDeviceClass(StrEnum):
DEVICE_CLASSES_SCHEMA: Final = vol.All(vol.Lower, vol.Coerce(NumberDeviceClass))
DEVICE_CLASS_UNITS: dict[NumberDeviceClass, set[type[StrEnum] | str | None]] = {
NumberDeviceClass.ABSOLUTE_HUMIDITY: {
CONCENTRATION_GRAMS_PER_CUBIC_METER,
CONCENTRATION_MILLIGRAMS_PER_CUBIC_METER,
},
NumberDeviceClass.APPARENT_POWER: set(UnitOfApparentPower),
NumberDeviceClass.AQI: {None},
NumberDeviceClass.AREA: set(UnitOfArea),

View File

@ -3,6 +3,9 @@
"_": {
"default": "mdi:ray-vertex"
},
"absolute_humidity": {
"default": "mdi:water-opacity"
},
"apparent_power": {
"default": "mdi:flash"
},

View File

@ -31,6 +31,9 @@
}
}
},
"absolute_humidity": {
"name": "[%key:component::sensor::entity_component::absolute_humidity::name%]"
},
"apparent_power": {
"name": "[%key:component::sensor::entity_component::apparent_power::name%]"
},

View File

@ -8,6 +8,7 @@ from typing import Final
import voluptuous as vol
from homeassistant.const import (
CONCENTRATION_GRAMS_PER_CUBIC_METER,
CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
CONCENTRATION_MILLIGRAMS_PER_CUBIC_METER,
CONCENTRATION_PARTS_PER_BILLION,
@ -107,6 +108,12 @@ class SensorDeviceClass(StrEnum):
"""
# Numerical device classes, these should be aligned with NumberDeviceClass
ABSOLUTE_HUMIDITY = "absolute_humidity"
"""Absolute humidity.
Unit of measurement: `g/`, `mg/`
"""
APPARENT_POWER = "apparent_power"
"""Apparent power.
@ -521,6 +528,7 @@ STATE_CLASSES_SCHEMA: Final = vol.All(vol.Lower, vol.Coerce(SensorStateClass))
STATE_CLASSES: Final[list[str]] = [cls.value for cls in SensorStateClass]
UNIT_CONVERTERS: dict[SensorDeviceClass | str | None, type[BaseUnitConverter]] = {
SensorDeviceClass.ABSOLUTE_HUMIDITY: MassVolumeConcentrationConverter,
SensorDeviceClass.AREA: AreaConverter,
SensorDeviceClass.ATMOSPHERIC_PRESSURE: PressureConverter,
SensorDeviceClass.BLOOD_GLUCOSE_CONCENTRATION: BloodGlucoseConcentrationConverter,
@ -554,6 +562,10 @@ UNIT_CONVERTERS: dict[SensorDeviceClass | str | None, type[BaseUnitConverter]] =
}
DEVICE_CLASS_UNITS: dict[SensorDeviceClass, set[type[StrEnum] | str | None]] = {
SensorDeviceClass.ABSOLUTE_HUMIDITY: {
CONCENTRATION_GRAMS_PER_CUBIC_METER,
CONCENTRATION_MILLIGRAMS_PER_CUBIC_METER,
},
SensorDeviceClass.APPARENT_POWER: set(UnitOfApparentPower),
SensorDeviceClass.AQI: {None},
SensorDeviceClass.AREA: set(UnitOfArea),
@ -651,6 +663,7 @@ DEFAULT_PRECISION_LIMIT = 2
# have 0 decimals, that one should be used and not mW, even though mW also should have
# 0 decimals. Otherwise the smaller units will have more decimals than expected.
UNITS_PRECISION = {
SensorDeviceClass.ABSOLUTE_HUMIDITY: (CONCENTRATION_GRAMS_PER_CUBIC_METER, 1),
SensorDeviceClass.APPARENT_POWER: (UnitOfApparentPower.VOLT_AMPERE, 0),
SensorDeviceClass.AREA: (UnitOfArea.SQUARE_CENTIMETERS, 0),
SensorDeviceClass.ATMOSPHERIC_PRESSURE: (UnitOfPressure.PA, 0),
@ -691,6 +704,7 @@ UNITS_PRECISION = {
}
DEVICE_CLASS_STATE_CLASSES: dict[SensorDeviceClass, set[SensorStateClass]] = {
SensorDeviceClass.ABSOLUTE_HUMIDITY: {SensorStateClass.MEASUREMENT},
SensorDeviceClass.APPARENT_POWER: {SensorStateClass.MEASUREMENT},
SensorDeviceClass.AQI: {SensorStateClass.MEASUREMENT},
SensorDeviceClass.AREA: set(SensorStateClass),

View File

@ -33,6 +33,7 @@ from . import ATTR_STATE_CLASS, DOMAIN, SensorDeviceClass
DEVICE_CLASS_NONE = "none"
CONF_IS_ABSOLUTE_HUMIDITY = "is_absolute_humidity"
CONF_IS_APPARENT_POWER = "is_apparent_power"
CONF_IS_AQI = "is_aqi"
CONF_IS_AREA = "is_area"
@ -88,6 +89,7 @@ CONF_IS_WIND_DIRECTION = "is_wind_direction"
CONF_IS_WIND_SPEED = "is_wind_speed"
ENTITY_CONDITIONS = {
SensorDeviceClass.ABSOLUTE_HUMIDITY: [{CONF_TYPE: CONF_IS_ABSOLUTE_HUMIDITY}],
SensorDeviceClass.APPARENT_POWER: [{CONF_TYPE: CONF_IS_APPARENT_POWER}],
SensorDeviceClass.AQI: [{CONF_TYPE: CONF_IS_AQI}],
SensorDeviceClass.AREA: [{CONF_TYPE: CONF_IS_AREA}],
@ -159,6 +161,7 @@ CONDITION_SCHEMA = vol.All(
vol.Required(CONF_ENTITY_ID): cv.entity_id_or_uuid,
vol.Required(CONF_TYPE): vol.In(
[
CONF_IS_ABSOLUTE_HUMIDITY,
CONF_IS_APPARENT_POWER,
CONF_IS_AQI,
CONF_IS_AREA,

View File

@ -32,6 +32,7 @@ from . import ATTR_STATE_CLASS, DOMAIN, SensorDeviceClass
DEVICE_CLASS_NONE = "none"
CONF_ABSOLUTE_HUMIDITY = "absolute_humidity"
CONF_APPARENT_POWER = "apparent_power"
CONF_AQI = "aqi"
CONF_AREA = "area"
@ -87,6 +88,7 @@ CONF_WIND_DIRECTION = "wind_direction"
CONF_WIND_SPEED = "wind_speed"
ENTITY_TRIGGERS = {
SensorDeviceClass.ABSOLUTE_HUMIDITY: [{CONF_TYPE: CONF_ABSOLUTE_HUMIDITY}],
SensorDeviceClass.APPARENT_POWER: [{CONF_TYPE: CONF_APPARENT_POWER}],
SensorDeviceClass.AQI: [{CONF_TYPE: CONF_AQI}],
SensorDeviceClass.AREA: [{CONF_TYPE: CONF_AREA}],
@ -159,6 +161,7 @@ TRIGGER_SCHEMA = vol.All(
vol.Required(CONF_ENTITY_ID): cv.entity_id_or_uuid,
vol.Required(CONF_TYPE): vol.In(
[
CONF_ABSOLUTE_HUMIDITY,
CONF_APPARENT_POWER,
CONF_AQI,
CONF_AREA,

View File

@ -3,6 +3,9 @@
"_": {
"default": "mdi:eye"
},
"absolute_humidity": {
"default": "mdi:water-opacity"
},
"apparent_power": {
"default": "mdi:flash"
},

View File

@ -2,6 +2,7 @@
"title": "Sensor",
"device_automation": {
"condition_type": {
"is_absolute_humidity": "Current {entity_name} absolute humidity",
"is_apparent_power": "Current {entity_name} apparent power",
"is_aqi": "Current {entity_name} air quality index",
"is_area": "Current {entity_name} area",
@ -57,6 +58,7 @@
"is_wind_speed": "Current {entity_name} wind speed"
},
"trigger_type": {
"absolute_humidity": "{entity_name} absolute humidity changes",
"apparent_power": "{entity_name} apparent power changes",
"aqi": "{entity_name} air quality index changes",
"area": "{entity_name} area changes",
@ -148,6 +150,9 @@
"duration": {
"name": "Duration"
},
"absolute_humidity": {
"name": "Absolute humidity"
},
"apparent_power": {
"name": "Apparent power"
},

View File

@ -910,6 +910,7 @@ class UnitOfPrecipitationDepth(StrEnum):
# Concentration units
CONCENTRATION_GRAMS_PER_CUBIC_METER: Final = "g/m³"
CONCENTRATION_MICROGRAMS_PER_CUBIC_METER: Final = "µg/m³"
CONCENTRATION_MILLIGRAMS_PER_CUBIC_METER: Final = "mg/m³"
CONCENTRATION_MICROGRAMS_PER_CUBIC_FOOT: Final = "μg/ft³"

View File

@ -7,6 +7,7 @@ from functools import lru_cache
from math import floor, log10
from homeassistant.const import (
CONCENTRATION_GRAMS_PER_CUBIC_METER,
CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
CONCENTRATION_MILLIGRAMS_PER_CUBIC_METER,
CONCENTRATION_PARTS_PER_BILLION,
@ -693,12 +694,14 @@ class MassVolumeConcentrationConverter(BaseUnitConverter):
UNIT_CLASS = "concentration"
_UNIT_CONVERSION: dict[str | None, float] = {
CONCENTRATION_MICROGRAMS_PER_CUBIC_METER: 1000.0, # 1000 µg/m³ = 1 mg/m³
CONCENTRATION_MILLIGRAMS_PER_CUBIC_METER: 1.0,
CONCENTRATION_MICROGRAMS_PER_CUBIC_METER: 1000000.0, # 1000 µg/m³ = 1 mg/m³
CONCENTRATION_MILLIGRAMS_PER_CUBIC_METER: 1000.0, # 1000 mg/m³ = 1 g/m³
CONCENTRATION_GRAMS_PER_CUBIC_METER: 1.0,
}
VALID_UNITS = {
CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
CONCENTRATION_MILLIGRAMS_PER_CUBIC_METER,
CONCENTRATION_GRAMS_PER_CUBIC_METER,
}

View File

@ -7,6 +7,7 @@ from homeassistant.components.sensor import (
)
from homeassistant.components.sensor.const import DEVICE_CLASS_STATE_CLASSES
from homeassistant.const import (
CONCENTRATION_GRAMS_PER_CUBIC_METER,
CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
CONCENTRATION_PARTS_PER_MILLION,
DEGREE,
@ -44,6 +45,7 @@ from homeassistant.const import (
from tests.common import MockEntity
UNITS_OF_MEASUREMENT = {
SensorDeviceClass.ABSOLUTE_HUMIDITY: CONCENTRATION_GRAMS_PER_CUBIC_METER,
SensorDeviceClass.APPARENT_POWER: UnitOfApparentPower.VOLT_AMPERE,
SensorDeviceClass.AQI: None,
SensorDeviceClass.AREA: UnitOfArea.SQUARE_METERS,

View File

@ -125,7 +125,7 @@ async def test_get_conditions(
conditions = await async_get_device_automations(
hass, DeviceAutomationType.CONDITION, device_entry.id
)
assert len(conditions) == 54
assert len(conditions) == 55
assert conditions == unordered(expected_conditions)

View File

@ -126,7 +126,7 @@ async def test_get_triggers(
triggers = await async_get_device_automations(
hass, DeviceAutomationType.TRIGGER, device_entry.id
)
assert len(triggers) == 54
assert len(triggers) == 55
assert triggers == unordered(expected_triggers)

View File

@ -8,6 +8,7 @@ from itertools import chain
import pytest
from homeassistant.const import (
CONCENTRATION_GRAMS_PER_CUBIC_METER,
CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
CONCENTRATION_MILLIGRAMS_PER_CUBIC_METER,
CONCENTRATION_PARTS_PER_BILLION,
@ -762,6 +763,13 @@ _CONVERTED_VALUE: dict[
2000,
CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
),
# 3 g/m³ = 3000 mg/m³
(
3,
CONCENTRATION_GRAMS_PER_CUBIC_METER,
3000,
CONCENTRATION_MILLIGRAMS_PER_CUBIC_METER,
),
],
VolumeConverter: [
(5, UnitOfVolume.LITERS, 1.32086, UnitOfVolume.GALLONS),