From 56abd5f2cffe34bea9c8577b6db1f3712db4b3b3 Mon Sep 17 00:00:00 2001 From: Lester Lo <21245380+lesterlo@users.noreply.github.com> Date: Tue, 8 Mar 2022 15:37:20 +0800 Subject: [PATCH] Add homekit pm type sensor (#46060) Co-authored-by: J. Nick Koston <nick@koston.org> --- .../components/homekit/accessories.py | 10 ++ homeassistant/components/homekit/const.py | 3 +- .../components/homekit/type_sensors.py | 76 ++++++++++++++- homeassistant/components/homekit/util.py | 26 +++++ .../homekit/test_get_accessories.py | 16 +++- tests/components/homekit/test_type_sensors.py | 96 +++++++++++++++++++ 6 files changed, 220 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/homekit/accessories.py b/homeassistant/components/homekit/accessories.py index 4129c3225b7..c77fa96a532 100644 --- a/homeassistant/components/homekit/accessories.py +++ b/homeassistant/components/homekit/accessories.py @@ -173,9 +173,19 @@ def get_accessory(hass, driver, state, aid, config): # noqa: C901 a_type = "TemperatureSensor" elif device_class == SensorDeviceClass.HUMIDITY and unit == PERCENTAGE: a_type = "HumiditySensor" + elif ( + device_class == SensorDeviceClass.PM10 + or SensorDeviceClass.PM10 in state.entity_id + ): + a_type = "PM10Sensor" elif ( device_class == SensorDeviceClass.PM25 or SensorDeviceClass.PM25 in state.entity_id + ): + a_type = "PM25Sensor" + elif ( + device_class == SensorDeviceClass.GAS + or SensorDeviceClass.GAS in state.entity_id ): a_type = "AirQualitySensor" elif device_class == SensorDeviceClass.CO: diff --git a/homeassistant/components/homekit/const.py b/homeassistant/components/homekit/const.py index ed7b3d6b293..264801c521f 100644 --- a/homeassistant/components/homekit/const.py +++ b/homeassistant/components/homekit/const.py @@ -150,6 +150,8 @@ SERV_WINDOW_COVERING = "WindowCovering" CHAR_ACTIVE = "Active" CHAR_ACTIVE_IDENTIFIER = "ActiveIdentifier" CHAR_AIR_PARTICULATE_DENSITY = "AirParticulateDensity" +CHAR_PM25_DENSITY = "PM2.5Density" +CHAR_PM10_DENSITY = "PM10Density" CHAR_AIR_QUALITY = "AirQuality" CHAR_BATTERY_LEVEL = "BatteryLevel" CHAR_BRIGHTNESS = "Brightness" @@ -235,7 +237,6 @@ PROP_MIN_VALUE = "minValue" PROP_MIN_STEP = "minStep" PROP_CELSIUS = {"minValue": -273, "maxValue": 999} PROP_VALID_VALUES = "ValidValues" - # #### Thresholds #### THRESHOLD_CO = 25 THRESHOLD_CO2 = 1000 diff --git a/homeassistant/components/homekit/type_sensors.py b/homeassistant/components/homekit/type_sensors.py index 46e241efab0..594d95494f1 100644 --- a/homeassistant/components/homekit/type_sensors.py +++ b/homeassistant/components/homekit/type_sensors.py @@ -35,6 +35,8 @@ from .const import ( CHAR_LEAK_DETECTED, CHAR_MOTION_DETECTED, CHAR_OCCUPANCY_DETECTED, + CHAR_PM10_DENSITY, + CHAR_PM25_DENSITY, CHAR_SMOKE_DETECTED, PROP_CELSIUS, SERV_AIR_QUALITY_SENSOR, @@ -51,7 +53,13 @@ from .const import ( THRESHOLD_CO, THRESHOLD_CO2, ) -from .util import convert_to_float, density_to_air_quality, temperature_to_homekit +from .util import ( + convert_to_float, + density_to_air_quality, + density_to_air_quality_pm10, + density_to_air_quality_pm25, + temperature_to_homekit, +) _LOGGER = logging.getLogger(__name__) @@ -156,6 +164,15 @@ class AirQualitySensor(HomeAccessory): """Initialize a AirQualitySensor accessory object.""" super().__init__(*args, category=CATEGORY_SENSOR) state = self.hass.states.get(self.entity_id) + + self.create_services() + + # Set the state so it is in sync on initial + # GET to avoid an event storm after homekit startup + self.async_update_state(state) + + def create_services(self): + """Initialize a AirQualitySensor accessory object.""" serv_air_quality = self.add_preload_service( SERV_AIR_QUALITY_SENSOR, [CHAR_AIR_PARTICULATE_DENSITY] ) @@ -163,9 +180,6 @@ class AirQualitySensor(HomeAccessory): self.char_density = serv_air_quality.configure_char( CHAR_AIR_PARTICULATE_DENSITY, value=0 ) - # Set the state so it is in sync on initial - # GET to avoid an event storm after homekit startup - self.async_update_state(state) @callback def async_update_state(self, new_state): @@ -179,6 +193,60 @@ class AirQualitySensor(HomeAccessory): _LOGGER.debug("%s: Set air_quality to %d", self.entity_id, air_quality) +@TYPES.register("PM10Sensor") +class PM10Sensor(AirQualitySensor): + """Generate a PM10Sensor accessory as PM 10 sensor.""" + + def create_services(self): + """Override the init function for PM 10 Sensor.""" + serv_air_quality = self.add_preload_service( + SERV_AIR_QUALITY_SENSOR, [CHAR_PM10_DENSITY] + ) + self.char_quality = serv_air_quality.configure_char(CHAR_AIR_QUALITY, value=0) + self.char_density = serv_air_quality.configure_char(CHAR_PM10_DENSITY, value=0) + + @callback + def async_update_state(self, new_state): + """Update accessory after state change.""" + density = convert_to_float(new_state.state) + if not density: + return + if self.char_density.value != density: + self.char_density.set_value(density) + _LOGGER.debug("%s: Set density to %d", self.entity_id, density) + air_quality = density_to_air_quality_pm10(density) + if self.char_quality.value != air_quality: + self.char_quality.set_value(air_quality) + _LOGGER.debug("%s: Set air_quality to %d", self.entity_id, air_quality) + + +@TYPES.register("PM25Sensor") +class PM25Sensor(AirQualitySensor): + """Generate a PM25Sensor accessory as PM 2.5 sensor.""" + + def create_services(self): + """Override the init function for PM 2.5 Sensor.""" + serv_air_quality = self.add_preload_service( + SERV_AIR_QUALITY_SENSOR, [CHAR_PM25_DENSITY] + ) + self.char_quality = serv_air_quality.configure_char(CHAR_AIR_QUALITY, value=0) + self.char_density = serv_air_quality.configure_char(CHAR_PM25_DENSITY, value=0) + + @callback + def async_update_state(self, new_state): + """Update accessory after state change.""" + density = convert_to_float(new_state.state) + if not density: + return + if self.char_density.value != density: + self.char_density.set_value(density) + _LOGGER.debug("%s: Set density to %d", self.entity_id, density) + air_quality = density_to_air_quality_pm25(density) + if self.char_quality.value != air_quality: + self.char_quality.set_value(air_quality) + _LOGGER.debug("%s: Set air_quality to %d", self.entity_id, air_quality) + + @TYPES.register("CarbonMonoxideSensor") class CarbonMonoxideSensor(HomeAccessory): """Generate a CarbonMonoxidSensor accessory as CO sensor.""" diff --git a/homeassistant/components/homekit/util.py b/homeassistant/components/homekit/util.py index 7fa4ffa8bf6..b31c55db767 100644 --- a/homeassistant/components/homekit/util.py +++ b/homeassistant/components/homekit/util.py @@ -407,6 +407,32 @@ def density_to_air_quality(density): return 5 +def density_to_air_quality_pm10(density): + """Map PM10 density to HomeKit AirQuality level.""" + if density <= 40: + return 1 + if density <= 80: + return 2 + if density <= 120: + return 3 + if density <= 300: + return 4 + return 5 + + +def density_to_air_quality_pm25(density): + """Map PM2.5 density to HomeKit AirQuality level.""" + if density <= 25: + return 1 + if density <= 50: + return 2 + if density <= 100: + return 3 + if density <= 300: + return 4 + return 5 + + def get_persist_filename_for_entry_id(entry_id: str): """Determine the filename of the homekit state file.""" return f"{DOMAIN}.{entry_id}.state" diff --git a/tests/components/homekit/test_get_accessories.py b/tests/components/homekit/test_get_accessories.py index 31f7b0f3bcc..32f4abe98f1 100644 --- a/tests/components/homekit/test_get_accessories.py +++ b/tests/components/homekit/test_get_accessories.py @@ -212,8 +212,20 @@ def test_type_media_player(type_name, entity_id, state, attrs, config): ("BinarySensor", "binary_sensor.opening", "on", {ATTR_DEVICE_CLASS: "opening"}), ("BinarySensor", "device_tracker.someone", "not_home", {}), ("BinarySensor", "person.someone", "home", {}), - ("AirQualitySensor", "sensor.air_quality_pm25", "40", {}), - ("AirQualitySensor", "sensor.air_quality", "40", {ATTR_DEVICE_CLASS: "pm25"}), + ("PM10Sensor", "sensor.air_quality_pm10", "30", {}), + ( + "PM10Sensor", + "sensor.air_quality", + "30", + {ATTR_DEVICE_CLASS: "pm10"}, + ), + ("PM25Sensor", "sensor.air_quality_pm25", "40", {}), + ( + "PM25Sensor", + "sensor.air_quality", + "40", + {ATTR_DEVICE_CLASS: "pm25"}, + ), ( "CarbonMonoxideSensor", "sensor.co", diff --git a/tests/components/homekit/test_type_sensors.py b/tests/components/homekit/test_type_sensors.py index 9b6d1c9cee2..75069ce9467 100644 --- a/tests/components/homekit/test_type_sensors.py +++ b/tests/components/homekit/test_type_sensors.py @@ -15,6 +15,8 @@ from homeassistant.components.homekit.type_sensors import ( CarbonMonoxideSensor, HumiditySensor, LightSensor, + PM10Sensor, + PM25Sensor, TemperatureSensor, ) from homeassistant.const import ( @@ -132,6 +134,100 @@ async def test_air_quality(hass, hk_driver): assert acc.char_quality.value == 5 +async def test_pm10(hass, hk_driver): + """Test if accessory is updated after state change.""" + entity_id = "sensor.air_quality_pm10" + + hass.states.async_set(entity_id, None) + await hass.async_block_till_done() + acc = PM10Sensor(hass, hk_driver, "PM10 Sensor", entity_id, 2, None) + await acc.run() + await hass.async_block_till_done() + + assert acc.aid == 2 + assert acc.category == 10 # Sensor + + assert acc.char_density.value == 0 + assert acc.char_quality.value == 0 + + hass.states.async_set(entity_id, STATE_UNKNOWN) + await hass.async_block_till_done() + assert acc.char_density.value == 0 + assert acc.char_quality.value == 0 + + hass.states.async_set(entity_id, "34") + await hass.async_block_till_done() + assert acc.char_density.value == 34 + assert acc.char_quality.value == 1 + + hass.states.async_set(entity_id, "70") + await hass.async_block_till_done() + assert acc.char_density.value == 70 + assert acc.char_quality.value == 2 + + hass.states.async_set(entity_id, "110") + await hass.async_block_till_done() + assert acc.char_density.value == 110 + assert acc.char_quality.value == 3 + + hass.states.async_set(entity_id, "200") + await hass.async_block_till_done() + assert acc.char_density.value == 200 + assert acc.char_quality.value == 4 + + hass.states.async_set(entity_id, "400") + await hass.async_block_till_done() + assert acc.char_density.value == 400 + assert acc.char_quality.value == 5 + + +async def test_pm25(hass, hk_driver): + """Test if accessory is updated after state change.""" + entity_id = "sensor.air_quality_pm25" + + hass.states.async_set(entity_id, None) + await hass.async_block_till_done() + acc = PM25Sensor(hass, hk_driver, "PM25 Sensor", entity_id, 2, None) + await acc.run() + await hass.async_block_till_done() + + assert acc.aid == 2 + assert acc.category == 10 # Sensor + + assert acc.char_density.value == 0 + assert acc.char_quality.value == 0 + + hass.states.async_set(entity_id, STATE_UNKNOWN) + await hass.async_block_till_done() + assert acc.char_density.value == 0 + assert acc.char_quality.value == 0 + + hass.states.async_set(entity_id, "23") + await hass.async_block_till_done() + assert acc.char_density.value == 23 + assert acc.char_quality.value == 1 + + hass.states.async_set(entity_id, "34") + await hass.async_block_till_done() + assert acc.char_density.value == 34 + assert acc.char_quality.value == 2 + + hass.states.async_set(entity_id, "90") + await hass.async_block_till_done() + assert acc.char_density.value == 90 + assert acc.char_quality.value == 3 + + hass.states.async_set(entity_id, "200") + await hass.async_block_till_done() + assert acc.char_density.value == 200 + assert acc.char_quality.value == 4 + + hass.states.async_set(entity_id, "400") + await hass.async_block_till_done() + assert acc.char_density.value == 400 + assert acc.char_quality.value == 5 + + async def test_co(hass, hk_driver): """Test if accessory is updated after state change.""" entity_id = "sensor.co" -- GitLab