diff --git a/homeassistant/components/sensor/device_condition.py b/homeassistant/components/sensor/device_condition.py index ded2eed6ddf..5746b8b8e8c 100644 --- a/homeassistant/components/sensor/device_condition.py +++ b/homeassistant/components/sensor/device_condition.py @@ -32,6 +32,7 @@ from . import ATTR_STATE_CLASS, DOMAIN, SensorDeviceClass DEVICE_CLASS_NONE = "none" CONF_IS_APPARENT_POWER = "is_apparent_power" +CONF_IS_AQI = "is_aqi" CONF_IS_ATMOSPHERIC_PRESSURE = "is_atmospheric_pressure" CONF_IS_BATTERY_LEVEL = "is_battery_level" CONF_IS_CO = "is_carbon_monoxide" @@ -40,6 +41,7 @@ CONF_IS_CURRENT = "is_current" CONF_IS_DATA_RATE = "is_data_rate" CONF_IS_DATA_SIZE = "is_data_size" CONF_IS_DISTANCE = "is_distance" +CONF_IS_DURATION = "is_duration" CONF_IS_ENERGY = "is_energy" CONF_IS_FREQUENCY = "is_frequency" CONF_IS_HUMIDITY = "is_humidity" @@ -47,6 +49,7 @@ CONF_IS_GAS = "is_gas" CONF_IS_ILLUMINANCE = "is_illuminance" CONF_IS_IRRADIANCE = "is_irradiance" CONF_IS_MOISTURE = "is_moisture" +CONF_IS_MONETARY = "is_monetary" CONF_IS_NITROGEN_DIOXIDE = "is_nitrogen_dioxide" CONF_IS_NITROGEN_MONOXIDE = "is_nitrogen_monoxide" CONF_IS_NITROUS_OXIDE = "is_nitrous_oxide" @@ -75,6 +78,7 @@ CONF_IS_WIND_SPEED = "is_wind_speed" ENTITY_CONDITIONS = { SensorDeviceClass.APPARENT_POWER: [{CONF_TYPE: CONF_IS_APPARENT_POWER}], + SensorDeviceClass.AQI: [{CONF_TYPE: CONF_IS_AQI}], SensorDeviceClass.ATMOSPHERIC_PRESSURE: [{CONF_TYPE: CONF_IS_ATMOSPHERIC_PRESSURE}], SensorDeviceClass.BATTERY: [{CONF_TYPE: CONF_IS_BATTERY_LEVEL}], SensorDeviceClass.CO: [{CONF_TYPE: CONF_IS_CO}], @@ -83,6 +87,7 @@ ENTITY_CONDITIONS = { SensorDeviceClass.DATA_RATE: [{CONF_TYPE: CONF_IS_DATA_RATE}], SensorDeviceClass.DATA_SIZE: [{CONF_TYPE: CONF_IS_DATA_SIZE}], SensorDeviceClass.DISTANCE: [{CONF_TYPE: CONF_IS_DISTANCE}], + SensorDeviceClass.DURATION: [{CONF_TYPE: CONF_IS_DURATION}], SensorDeviceClass.ENERGY: [{CONF_TYPE: CONF_IS_ENERGY}], SensorDeviceClass.FREQUENCY: [{CONF_TYPE: CONF_IS_FREQUENCY}], SensorDeviceClass.GAS: [{CONF_TYPE: CONF_IS_GAS}], @@ -90,6 +95,7 @@ ENTITY_CONDITIONS = { SensorDeviceClass.ILLUMINANCE: [{CONF_TYPE: CONF_IS_ILLUMINANCE}], SensorDeviceClass.IRRADIANCE: [{CONF_TYPE: CONF_IS_IRRADIANCE}], SensorDeviceClass.MOISTURE: [{CONF_TYPE: CONF_IS_MOISTURE}], + SensorDeviceClass.MONETARY: [{CONF_TYPE: CONF_IS_MONETARY}], SensorDeviceClass.NITROGEN_DIOXIDE: [{CONF_TYPE: CONF_IS_NITROGEN_DIOXIDE}], SensorDeviceClass.NITROGEN_MONOXIDE: [{CONF_TYPE: CONF_IS_NITROGEN_MONOXIDE}], SensorDeviceClass.NITROUS_OXIDE: [{CONF_TYPE: CONF_IS_NITROUS_OXIDE}], @@ -128,6 +134,7 @@ CONDITION_SCHEMA = vol.All( vol.Required(CONF_TYPE): vol.In( [ CONF_IS_APPARENT_POWER, + CONF_IS_AQI, CONF_IS_ATMOSPHERIC_PRESSURE, CONF_IS_BATTERY_LEVEL, CONF_IS_CO, @@ -136,6 +143,7 @@ CONDITION_SCHEMA = vol.All( CONF_IS_DATA_RATE, CONF_IS_DATA_SIZE, CONF_IS_DISTANCE, + CONF_IS_DURATION, CONF_IS_ENERGY, CONF_IS_FREQUENCY, CONF_IS_GAS, @@ -143,6 +151,7 @@ CONDITION_SCHEMA = vol.All( CONF_IS_ILLUMINANCE, CONF_IS_IRRADIANCE, CONF_IS_MOISTURE, + CONF_IS_MONETARY, CONF_IS_NITROGEN_DIOXIDE, CONF_IS_NITROGEN_MONOXIDE, CONF_IS_NITROUS_OXIDE, diff --git a/homeassistant/components/sensor/device_trigger.py b/homeassistant/components/sensor/device_trigger.py index 721df69cc50..dfd0a576d21 100644 --- a/homeassistant/components/sensor/device_trigger.py +++ b/homeassistant/components/sensor/device_trigger.py @@ -31,6 +31,7 @@ from . import ATTR_STATE_CLASS, DOMAIN, SensorDeviceClass DEVICE_CLASS_NONE = "none" CONF_APPARENT_POWER = "apparent_power" +CONF_AQI = "aqi" CONF_ATMOSPHERIC_PRESSURE = "atmospheric_pressure" CONF_BATTERY_LEVEL = "battery_level" CONF_CO = "carbon_monoxide" @@ -39,6 +40,7 @@ CONF_CURRENT = "current" CONF_DATA_RATE = "data_rate" CONF_DATA_SIZE = "data_size" CONF_DISTANCE = "distance" +CONF_DURATION = "duration" CONF_ENERGY = "energy" CONF_FREQUENCY = "frequency" CONF_GAS = "gas" @@ -46,6 +48,7 @@ CONF_HUMIDITY = "humidity" CONF_ILLUMINANCE = "illuminance" CONF_IRRADIANCE = "irradiance" CONF_MOISTURE = "moisture" +CONF_MONETARY = "monetary" CONF_NITROGEN_DIOXIDE = "nitrogen_dioxide" CONF_NITROGEN_MONOXIDE = "nitrogen_monoxide" CONF_NITROUS_OXIDE = "nitrous_oxide" @@ -74,6 +77,7 @@ CONF_WIND_SPEED = "wind_speed" ENTITY_TRIGGERS = { SensorDeviceClass.APPARENT_POWER: [{CONF_TYPE: CONF_APPARENT_POWER}], + SensorDeviceClass.AQI: [{CONF_TYPE: CONF_AQI}], SensorDeviceClass.ATMOSPHERIC_PRESSURE: [{CONF_TYPE: CONF_ATMOSPHERIC_PRESSURE}], SensorDeviceClass.BATTERY: [{CONF_TYPE: CONF_BATTERY_LEVEL}], SensorDeviceClass.CO: [{CONF_TYPE: CONF_CO}], @@ -82,6 +86,7 @@ ENTITY_TRIGGERS = { SensorDeviceClass.DATA_RATE: [{CONF_TYPE: CONF_DATA_RATE}], SensorDeviceClass.DATA_SIZE: [{CONF_TYPE: CONF_DATA_SIZE}], SensorDeviceClass.DISTANCE: [{CONF_TYPE: CONF_DISTANCE}], + SensorDeviceClass.DURATION: [{CONF_TYPE: CONF_DURATION}], SensorDeviceClass.ENERGY: [{CONF_TYPE: CONF_ENERGY}], SensorDeviceClass.FREQUENCY: [{CONF_TYPE: CONF_FREQUENCY}], SensorDeviceClass.GAS: [{CONF_TYPE: CONF_GAS}], @@ -89,6 +94,7 @@ ENTITY_TRIGGERS = { SensorDeviceClass.ILLUMINANCE: [{CONF_TYPE: CONF_ILLUMINANCE}], SensorDeviceClass.IRRADIANCE: [{CONF_TYPE: CONF_IRRADIANCE}], SensorDeviceClass.MOISTURE: [{CONF_TYPE: CONF_MOISTURE}], + SensorDeviceClass.MONETARY: [{CONF_TYPE: CONF_MONETARY}], SensorDeviceClass.NITROGEN_DIOXIDE: [{CONF_TYPE: CONF_NITROGEN_DIOXIDE}], SensorDeviceClass.NITROGEN_MONOXIDE: [{CONF_TYPE: CONF_NITROGEN_MONOXIDE}], SensorDeviceClass.NITROUS_OXIDE: [{CONF_TYPE: CONF_NITROUS_OXIDE}], @@ -128,6 +134,7 @@ TRIGGER_SCHEMA = vol.All( vol.Required(CONF_TYPE): vol.In( [ CONF_APPARENT_POWER, + CONF_AQI, CONF_ATMOSPHERIC_PRESSURE, CONF_BATTERY_LEVEL, CONF_CO, @@ -136,6 +143,7 @@ TRIGGER_SCHEMA = vol.All( CONF_DATA_RATE, CONF_DATA_SIZE, CONF_DISTANCE, + CONF_DURATION, CONF_ENERGY, CONF_FREQUENCY, CONF_GAS, @@ -143,6 +151,7 @@ TRIGGER_SCHEMA = vol.All( CONF_ILLUMINANCE, CONF_IRRADIANCE, CONF_MOISTURE, + CONF_MONETARY, CONF_NITROGEN_DIOXIDE, CONF_NITROGEN_MONOXIDE, CONF_NITROUS_OXIDE, diff --git a/homeassistant/components/sensor/strings.json b/homeassistant/components/sensor/strings.json index 2d536126980..2396bbf295b 100644 --- a/homeassistant/components/sensor/strings.json +++ b/homeassistant/components/sensor/strings.json @@ -3,6 +3,7 @@ "device_automation": { "condition_type": { "is_apparent_power": "Current {entity_name} apparent power", + "is_aqi": "Current {entity_name} air quality index", "is_atmospheric_pressure": "Current {entity_name} atmospheric pressure", "is_battery_level": "Current {entity_name} battery level", "is_carbon_monoxide": "Current {entity_name} carbon monoxide concentration level", @@ -11,6 +12,7 @@ "is_data_rate": "Current {entity_name} data rate", "is_data_size": "Current {entity_name} data size", "is_distance": "Current {entity_name} distance", + "is_duration": "Current {entity_name} duration", "is_energy": "Current {entity_name} energy", "is_frequency": "Current {entity_name} frequency", "is_gas": "Current {entity_name} gas", @@ -18,6 +20,7 @@ "is_illuminance": "Current {entity_name} illuminance", "is_irradiance": "Current {entity_name} irradiance", "is_moisture": "Current {entity_name} moisture", + "is_monetary": "Current {entity_name} money", "is_nitrogen_dioxide": "Current {entity_name} nitrogen dioxide concentration level", "is_nitrogen_monoxide": "Current {entity_name} nitrogen monoxide concentration level", "is_nitrous_oxide": "Current {entity_name} nitrous oxide concentration level", @@ -27,6 +30,8 @@ "is_pm25": "Current {entity_name} PM2.5 concentration level", "is_power": "Current {entity_name} power", "is_power_factor": "Current {entity_name} power factor", + "is_precipitation": "Current {entity_name} precipitation", + "is_precipitation_intensity": "Current {entity_name} precipitation intensity", "is_pressure": "Current {entity_name} pressure", "is_reactive_power": "Current {entity_name} reactive power", "is_signal_strength": "Current {entity_name} signal strength", @@ -39,17 +44,21 @@ "is_voltage": "Current {entity_name} voltage", "is_volume": "Current {entity_name} volume", "is_water": "Current {entity_name} water", - "is_weight": "Current {entity_name} weight" + "is_weight": "Current {entity_name} weight", + "is_wind_speed": "Current {entity_name} wind speed" }, "trigger_type": { "apparent_power": "{entity_name} apparent power changes", + "aqi": "{entity_name} air quality index changes", "atmospheric_pressure": "{entity_name} atmospheric pressure changes", "battery_level": "{entity_name} battery level changes", "carbon_monoxide": "{entity_name} carbon monoxide concentration changes", "carbon_dioxide": "{entity_name} carbon dioxide concentration changes", "current": "{entity_name} current changes", "data_rate": "{entity_name} data rate changes", + "data_size": "{entity_name} data size changes", "distance": "{entity_name} distance changes", + "duration": "{entity_name} duration changes", "energy": "{entity_name} energy changes", "frequency": "{entity_name} frequency changes", "gas": "{entity_name} gas changes", @@ -57,6 +66,7 @@ "illuminance": "{entity_name} illuminance changes", "irradiance": "{entity_name} irradiance changes", "moisture": "{entity_name} moisture changes", + "monetary": "{entity_name} money changes", "nitrogen_dioxide": "{entity_name} nitrogen dioxide concentration changes", "nitrogen_monoxide": "{entity_name} nitrogen monoxide concentration changes", "nitrous_oxide": "{entity_name} nitrous oxide concentration changes", @@ -66,6 +76,8 @@ "pm25": "{entity_name} PM2.5 concentration changes", "power": "{entity_name} power changes", "power_factor": "{entity_name} power factor changes", + "precipitation": "{entity_name} precipitation changes", + "precipitation_intensity": "{entity_name} precipitation intensity changes", "pressure": "{entity_name} pressure changes", "reactive_power": "{entity_name} reactive power changes", "signal_strength": "{entity_name} signal strength changes", @@ -78,7 +90,8 @@ "voltage": "{entity_name} voltage changes", "volume": "{entity_name} volume changes", "water": "{entity_name} water changes", - "weight": "{entity_name} weight changes" + "weight": "{entity_name} weight changes", + "wind_speed": "{entity_name} wind speed changes" } }, "state": { diff --git a/tests/components/sensor/test_device_condition.py b/tests/components/sensor/test_device_condition.py index c008acf7d00..02b369b6a67 100644 --- a/tests/components/sensor/test_device_condition.py +++ b/tests/components/sensor/test_device_condition.py @@ -8,13 +8,16 @@ from homeassistant.components.sensor import ( DOMAIN, SensorDeviceClass, SensorStateClass, + device_condition, ) +from homeassistant.components.sensor.const import NON_NUMERIC_DEVICE_CLASSES from homeassistant.components.sensor.device_condition import ENTITY_CONDITIONS from homeassistant.const import CONF_PLATFORM, PERCENTAGE, STATE_UNKNOWN, EntityCategory -from homeassistant.core import HomeAssistant +from homeassistant.core import HomeAssistant, ServiceCall from homeassistant.helpers import device_registry as dr, entity_registry as er from homeassistant.helpers.entity_registry import RegistryEntryHider from homeassistant.setup import async_setup_component +from homeassistant.util.json import load_json from tests.common import ( MockConfigEntry, @@ -28,11 +31,47 @@ from tests.testing_config.custom_components.test.sensor import UNITS_OF_MEASUREM @pytest.fixture -def calls(hass): +def calls(hass: HomeAssistant) -> list[ServiceCall]: """Track calls to a mock service.""" return async_mock_service(hass, "test", "automation") +@pytest.mark.parametrize( + "device_class", + [ + device_class + for device_class in SensorDeviceClass + if device_class not in NON_NUMERIC_DEVICE_CLASSES + ], +) +def test_matches_device_classes(device_class: SensorDeviceClass) -> None: + """Ensure device class constants are declared in device_condition module.""" + # Ensure it has corresponding CONF_IS_*** constant + constant_name = { + SensorDeviceClass.BATTERY: "CONF_IS_BATTERY_LEVEL", + SensorDeviceClass.CO: "CONF_IS_CO", + SensorDeviceClass.CO2: "CONF_IS_CO2", + }.get(device_class, f"CONF_IS_{device_class.value.upper()}") + assert hasattr(device_condition, constant_name), f"Missing constant {constant_name}" + + # Ensure it has correct value + constant_value = { + SensorDeviceClass.BATTERY: "is_battery_level", + }.get(device_class, f"is_{device_class.value}") + assert getattr(device_condition, constant_name) == constant_value + + # Ensure it is present in ENTITY_CONDITIONS + assert device_class in ENTITY_CONDITIONS + # Ensure it is present in CONDITION_SCHEMA + schema_types = ( + device_condition.CONDITION_SCHEMA.validators[0].schema["type"].container + ) + assert constant_value in schema_types + # Ensure it is present in string.json + strings = load_json("homeassistant/components/sensor/strings.json") + assert constant_value in strings["device_automation"]["condition_type"] + + async def test_get_conditions( hass: HomeAssistant, device_registry: dr.DeviceRegistry, diff --git a/tests/components/sensor/test_device_trigger.py b/tests/components/sensor/test_device_trigger.py index 56133353b44..298aab2953a 100644 --- a/tests/components/sensor/test_device_trigger.py +++ b/tests/components/sensor/test_device_trigger.py @@ -10,14 +10,17 @@ from homeassistant.components.sensor import ( DOMAIN, SensorDeviceClass, SensorStateClass, + device_trigger, ) +from homeassistant.components.sensor.const import NON_NUMERIC_DEVICE_CLASSES from homeassistant.components.sensor.device_trigger import ENTITY_TRIGGERS from homeassistant.const import CONF_PLATFORM, PERCENTAGE, STATE_UNKNOWN, EntityCategory -from homeassistant.core import HomeAssistant +from homeassistant.core import HomeAssistant, ServiceCall from homeassistant.helpers import device_registry as dr, entity_registry as er from homeassistant.helpers.entity_registry import RegistryEntryHider from homeassistant.setup import async_setup_component import homeassistant.util.dt as dt_util +from homeassistant.util.json import load_json from tests.common import ( MockConfigEntry, @@ -32,11 +35,45 @@ from tests.testing_config.custom_components.test.sensor import UNITS_OF_MEASUREM @pytest.fixture -def calls(hass): +def calls(hass: HomeAssistant) -> list[ServiceCall]: """Track calls to a mock service.""" return async_mock_service(hass, "test", "automation") +@pytest.mark.parametrize( + "device_class", + [ + device_class + for device_class in SensorDeviceClass + if device_class not in NON_NUMERIC_DEVICE_CLASSES + ], +) +def test_matches_device_classes(device_class: SensorDeviceClass) -> None: + """Ensure device class constants are declared in device_trigger module.""" + # Ensure it has corresponding CONF_*** constant + constant_name = { + SensorDeviceClass.BATTERY: "CONF_BATTERY_LEVEL", + SensorDeviceClass.CO: "CONF_CO", + SensorDeviceClass.CO2: "CONF_CO2", + }.get(device_class, f"CONF_{device_class.value.upper()}") + assert hasattr(device_trigger, constant_name), f"Missing constant {constant_name}" + + # Ensure it has correct value + constant_value = { + SensorDeviceClass.BATTERY: "battery_level", + }.get(device_class, device_class.value) + assert getattr(device_trigger, constant_name) == constant_value + + # Ensure it is present in ENTITY_TRIGGERS + assert device_class in ENTITY_TRIGGERS + # Ensure it is present in TRIGGER_SCHEMA + schema_types = device_trigger.TRIGGER_SCHEMA.validators[0].schema["type"].container + assert constant_value in schema_types + # Ensure it is present in string.json + strings = load_json("homeassistant/components/sensor/strings.json") + assert constant_value in strings["device_automation"]["trigger_type"] + + async def test_get_triggers( hass: HomeAssistant, device_registry: dr.DeviceRegistry,