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