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 import voluptuous as vol
from homeassistant.const import ( from homeassistant.const import (
CONCENTRATION_GRAMS_PER_CUBIC_METER,
CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
CONCENTRATION_MILLIGRAMS_PER_CUBIC_METER, CONCENTRATION_MILLIGRAMS_PER_CUBIC_METER,
CONCENTRATION_PARTS_PER_BILLION, CONCENTRATION_PARTS_PER_BILLION,
@ -78,6 +79,11 @@ class NumberDeviceClass(StrEnum):
"""Device class for numbers.""" """Device class for numbers."""
# NumberDeviceClass should be aligned with SensorDeviceClass # NumberDeviceClass should be aligned with SensorDeviceClass
ABSOLUTE_HUMIDITY = "absolute_humidity"
"""Absolute humidity.
Unit of measurement: `g/`, `mg/`
"""
APPARENT_POWER = "apparent_power" APPARENT_POWER = "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_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.ABSOLUTE_HUMIDITY: {
CONCENTRATION_GRAMS_PER_CUBIC_METER,
CONCENTRATION_MILLIGRAMS_PER_CUBIC_METER,
},
NumberDeviceClass.APPARENT_POWER: set(UnitOfApparentPower), NumberDeviceClass.APPARENT_POWER: set(UnitOfApparentPower),
NumberDeviceClass.AQI: {None}, NumberDeviceClass.AQI: {None},
NumberDeviceClass.AREA: set(UnitOfArea), NumberDeviceClass.AREA: set(UnitOfArea),

View File

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

View File

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

View File

@ -8,6 +8,7 @@ from typing import Final
import voluptuous as vol import voluptuous as vol
from homeassistant.const import ( from homeassistant.const import (
CONCENTRATION_GRAMS_PER_CUBIC_METER,
CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
CONCENTRATION_MILLIGRAMS_PER_CUBIC_METER, CONCENTRATION_MILLIGRAMS_PER_CUBIC_METER,
CONCENTRATION_PARTS_PER_BILLION, CONCENTRATION_PARTS_PER_BILLION,
@ -107,6 +108,12 @@ class SensorDeviceClass(StrEnum):
""" """
# Numerical device classes, these should be aligned with NumberDeviceClass # 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 = "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] 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.ABSOLUTE_HUMIDITY: MassVolumeConcentrationConverter,
SensorDeviceClass.AREA: AreaConverter, SensorDeviceClass.AREA: AreaConverter,
SensorDeviceClass.ATMOSPHERIC_PRESSURE: PressureConverter, SensorDeviceClass.ATMOSPHERIC_PRESSURE: PressureConverter,
SensorDeviceClass.BLOOD_GLUCOSE_CONCENTRATION: BloodGlucoseConcentrationConverter, 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]] = { 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.APPARENT_POWER: set(UnitOfApparentPower),
SensorDeviceClass.AQI: {None}, SensorDeviceClass.AQI: {None},
SensorDeviceClass.AREA: set(UnitOfArea), 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 # 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. # 0 decimals. Otherwise the smaller units will have more decimals than expected.
UNITS_PRECISION = { UNITS_PRECISION = {
SensorDeviceClass.ABSOLUTE_HUMIDITY: (CONCENTRATION_GRAMS_PER_CUBIC_METER, 1),
SensorDeviceClass.APPARENT_POWER: (UnitOfApparentPower.VOLT_AMPERE, 0), SensorDeviceClass.APPARENT_POWER: (UnitOfApparentPower.VOLT_AMPERE, 0),
SensorDeviceClass.AREA: (UnitOfArea.SQUARE_CENTIMETERS, 0), SensorDeviceClass.AREA: (UnitOfArea.SQUARE_CENTIMETERS, 0),
SensorDeviceClass.ATMOSPHERIC_PRESSURE: (UnitOfPressure.PA, 0), SensorDeviceClass.ATMOSPHERIC_PRESSURE: (UnitOfPressure.PA, 0),
@ -691,6 +704,7 @@ UNITS_PRECISION = {
} }
DEVICE_CLASS_STATE_CLASSES: dict[SensorDeviceClass, set[SensorStateClass]] = { DEVICE_CLASS_STATE_CLASSES: dict[SensorDeviceClass, set[SensorStateClass]] = {
SensorDeviceClass.ABSOLUTE_HUMIDITY: {SensorStateClass.MEASUREMENT},
SensorDeviceClass.APPARENT_POWER: {SensorStateClass.MEASUREMENT}, SensorDeviceClass.APPARENT_POWER: {SensorStateClass.MEASUREMENT},
SensorDeviceClass.AQI: {SensorStateClass.MEASUREMENT}, SensorDeviceClass.AQI: {SensorStateClass.MEASUREMENT},
SensorDeviceClass.AREA: set(SensorStateClass), SensorDeviceClass.AREA: set(SensorStateClass),

View File

@ -33,6 +33,7 @@ from . import ATTR_STATE_CLASS, DOMAIN, SensorDeviceClass
DEVICE_CLASS_NONE = "none" DEVICE_CLASS_NONE = "none"
CONF_IS_ABSOLUTE_HUMIDITY = "is_absolute_humidity"
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_AREA = "is_area"
@ -88,6 +89,7 @@ CONF_IS_WIND_DIRECTION = "is_wind_direction"
CONF_IS_WIND_SPEED = "is_wind_speed" CONF_IS_WIND_SPEED = "is_wind_speed"
ENTITY_CONDITIONS = { ENTITY_CONDITIONS = {
SensorDeviceClass.ABSOLUTE_HUMIDITY: [{CONF_TYPE: CONF_IS_ABSOLUTE_HUMIDITY}],
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.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_ENTITY_ID): cv.entity_id_or_uuid,
vol.Required(CONF_TYPE): vol.In( vol.Required(CONF_TYPE): vol.In(
[ [
CONF_IS_ABSOLUTE_HUMIDITY,
CONF_IS_APPARENT_POWER, CONF_IS_APPARENT_POWER,
CONF_IS_AQI, CONF_IS_AQI,
CONF_IS_AREA, CONF_IS_AREA,

View File

@ -32,6 +32,7 @@ from . import ATTR_STATE_CLASS, DOMAIN, SensorDeviceClass
DEVICE_CLASS_NONE = "none" DEVICE_CLASS_NONE = "none"
CONF_ABSOLUTE_HUMIDITY = "absolute_humidity"
CONF_APPARENT_POWER = "apparent_power" CONF_APPARENT_POWER = "apparent_power"
CONF_AQI = "aqi" CONF_AQI = "aqi"
CONF_AREA = "area" CONF_AREA = "area"
@ -87,6 +88,7 @@ CONF_WIND_DIRECTION = "wind_direction"
CONF_WIND_SPEED = "wind_speed" CONF_WIND_SPEED = "wind_speed"
ENTITY_TRIGGERS = { ENTITY_TRIGGERS = {
SensorDeviceClass.ABSOLUTE_HUMIDITY: [{CONF_TYPE: CONF_ABSOLUTE_HUMIDITY}],
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.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_ENTITY_ID): cv.entity_id_or_uuid,
vol.Required(CONF_TYPE): vol.In( vol.Required(CONF_TYPE): vol.In(
[ [
CONF_ABSOLUTE_HUMIDITY,
CONF_APPARENT_POWER, CONF_APPARENT_POWER,
CONF_AQI, CONF_AQI,
CONF_AREA, CONF_AREA,

View File

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

View File

@ -2,6 +2,7 @@
"title": "Sensor", "title": "Sensor",
"device_automation": { "device_automation": {
"condition_type": { "condition_type": {
"is_absolute_humidity": "Current {entity_name} absolute humidity",
"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_area": "Current {entity_name} area",
@ -57,6 +58,7 @@
"is_wind_speed": "Current {entity_name} wind speed" "is_wind_speed": "Current {entity_name} wind speed"
}, },
"trigger_type": { "trigger_type": {
"absolute_humidity": "{entity_name} absolute humidity changes",
"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", "area": "{entity_name} area changes",
@ -148,6 +150,9 @@
"duration": { "duration": {
"name": "Duration" "name": "Duration"
}, },
"absolute_humidity": {
"name": "Absolute humidity"
},
"apparent_power": { "apparent_power": {
"name": "Apparent power" "name": "Apparent power"
}, },

View File

@ -910,6 +910,7 @@ class UnitOfPrecipitationDepth(StrEnum):
# Concentration units # Concentration units
CONCENTRATION_GRAMS_PER_CUBIC_METER: Final = "g/m³"
CONCENTRATION_MICROGRAMS_PER_CUBIC_METER: Final = "µg/m³" CONCENTRATION_MICROGRAMS_PER_CUBIC_METER: Final = "µg/m³"
CONCENTRATION_MILLIGRAMS_PER_CUBIC_METER: Final = "mg/m³" CONCENTRATION_MILLIGRAMS_PER_CUBIC_METER: Final = "mg/m³"
CONCENTRATION_MICROGRAMS_PER_CUBIC_FOOT: Final = "μg/ft³" 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 math import floor, log10
from homeassistant.const import ( from homeassistant.const import (
CONCENTRATION_GRAMS_PER_CUBIC_METER,
CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
CONCENTRATION_MILLIGRAMS_PER_CUBIC_METER, CONCENTRATION_MILLIGRAMS_PER_CUBIC_METER,
CONCENTRATION_PARTS_PER_BILLION, CONCENTRATION_PARTS_PER_BILLION,
@ -693,12 +694,14 @@ class MassVolumeConcentrationConverter(BaseUnitConverter):
UNIT_CLASS = "concentration" UNIT_CLASS = "concentration"
_UNIT_CONVERSION: dict[str | None, float] = { _UNIT_CONVERSION: dict[str | None, float] = {
CONCENTRATION_MICROGRAMS_PER_CUBIC_METER: 1000.0, # 1000 µg/m³ = 1 mg/m³ CONCENTRATION_MICROGRAMS_PER_CUBIC_METER: 1000000.0, # 1000 µg/m³ = 1 mg/m³
CONCENTRATION_MILLIGRAMS_PER_CUBIC_METER: 1.0, CONCENTRATION_MILLIGRAMS_PER_CUBIC_METER: 1000.0, # 1000 mg/m³ = 1 g/m³
CONCENTRATION_GRAMS_PER_CUBIC_METER: 1.0,
} }
VALID_UNITS = { VALID_UNITS = {
CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
CONCENTRATION_MILLIGRAMS_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.components.sensor.const import DEVICE_CLASS_STATE_CLASSES
from homeassistant.const import ( from homeassistant.const import (
CONCENTRATION_GRAMS_PER_CUBIC_METER,
CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
CONCENTRATION_PARTS_PER_MILLION, CONCENTRATION_PARTS_PER_MILLION,
DEGREE, DEGREE,
@ -44,6 +45,7 @@ from homeassistant.const import (
from tests.common import MockEntity from tests.common import MockEntity
UNITS_OF_MEASUREMENT = { UNITS_OF_MEASUREMENT = {
SensorDeviceClass.ABSOLUTE_HUMIDITY: CONCENTRATION_GRAMS_PER_CUBIC_METER,
SensorDeviceClass.APPARENT_POWER: UnitOfApparentPower.VOLT_AMPERE, SensorDeviceClass.APPARENT_POWER: UnitOfApparentPower.VOLT_AMPERE,
SensorDeviceClass.AQI: None, SensorDeviceClass.AQI: None,
SensorDeviceClass.AREA: UnitOfArea.SQUARE_METERS, SensorDeviceClass.AREA: UnitOfArea.SQUARE_METERS,

View File

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

View File

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

View File

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