Expose NO2 and VOCs sensors to homekit (#81217)

Co-authored-by: J. Nick Koston <nick@koston.org>
This commit is contained in:
Stackie Jia 2022-10-31 00:33:06 +08:00 committed by GitHub
parent 5d282db439
commit a1eec7b55d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 219 additions and 15 deletions

View File

@ -200,6 +200,10 @@ def get_accessory( # noqa: C901
or SensorDeviceClass.PM25 in state.entity_id
):
a_type = "PM25Sensor"
elif device_class == SensorDeviceClass.NITROGEN_DIOXIDE:
a_type = "NitrogenDioxideSensor"
elif device_class == SensorDeviceClass.VOLATILE_ORGANIC_COMPOUNDS:
a_type = "VolatileOrganicCompoundsSensor"
elif (
device_class == SensorDeviceClass.GAS
or SensorDeviceClass.GAS in state.entity_id

View File

@ -196,6 +196,7 @@ CHAR_MODEL = "Model"
CHAR_MOTION_DETECTED = "MotionDetected"
CHAR_MUTE = "Mute"
CHAR_NAME = "Name"
CHAR_NITROGEN_DIOXIDE_DENSITY = "NitrogenDioxideDensity"
CHAR_OBSTRUCTION_DETECTED = "ObstructionDetected"
CHAR_OCCUPANCY_DETECTED = "OccupancyDetected"
CHAR_ON = "On"
@ -226,6 +227,7 @@ CHAR_TARGET_TILT_ANGLE = "TargetHorizontalTiltAngle"
CHAR_HOLD_POSITION = "HoldPosition"
CHAR_TEMP_DISPLAY_UNITS = "TemperatureDisplayUnits"
CHAR_VALVE_TYPE = "ValveType"
CHAR_VOC_DENSITY = "VOCDensity"
CHAR_VOLUME = "Volume"
CHAR_VOLUME_SELECTOR = "VolumeSelector"
CHAR_VOLUME_CONTROL_TYPE = "VolumeControlType"

View File

@ -33,10 +33,12 @@ from .const import (
CHAR_CURRENT_TEMPERATURE,
CHAR_LEAK_DETECTED,
CHAR_MOTION_DETECTED,
CHAR_NITROGEN_DIOXIDE_DENSITY,
CHAR_OCCUPANCY_DETECTED,
CHAR_PM10_DENSITY,
CHAR_PM25_DENSITY,
CHAR_SMOKE_DETECTED,
CHAR_VOC_DENSITY,
PROP_CELSIUS,
SERV_AIR_QUALITY_SENSOR,
SERV_CARBON_DIOXIDE_SENSOR,
@ -55,7 +57,9 @@ from .const import (
from .util import (
convert_to_float,
density_to_air_quality,
density_to_air_quality_nitrogen_dioxide,
density_to_air_quality_pm10,
density_to_air_quality_voc,
temperature_to_homekit,
)
@ -206,7 +210,7 @@ class PM10Sensor(AirQualitySensor):
def async_update_state(self, new_state):
"""Update accessory after state change."""
density = convert_to_float(new_state.state)
if not density:
if density is None:
return
if self.char_density.value != density:
self.char_density.set_value(density)
@ -233,7 +237,7 @@ class PM25Sensor(AirQualitySensor):
def async_update_state(self, new_state):
"""Update accessory after state change."""
density = convert_to_float(new_state.state)
if not density:
if density is None:
return
if self.char_density.value != density:
self.char_density.set_value(density)
@ -244,6 +248,62 @@ class PM25Sensor(AirQualitySensor):
_LOGGER.debug("%s: Set air_quality to %d", self.entity_id, air_quality)
@TYPES.register("NitrogenDioxideSensor")
class NitrogenDioxideSensor(AirQualitySensor):
"""Generate a NitrogenDioxideSensor accessory as NO2 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_NITROGEN_DIOXIDE_DENSITY]
)
self.char_quality = serv_air_quality.configure_char(CHAR_AIR_QUALITY, value=0)
self.char_density = serv_air_quality.configure_char(
CHAR_NITROGEN_DIOXIDE_DENSITY, value=0
)
@callback
def async_update_state(self, new_state):
"""Update accessory after state change."""
density = convert_to_float(new_state.state)
if density is None:
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_nitrogen_dioxide(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("VolatileOrganicCompoundsSensor")
class VolatileOrganicCompoundsSensor(AirQualitySensor):
"""Generate a VolatileOrganicCompoundsSensor accessory as VOCs 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_VOC_DENSITY]
)
self.char_quality = serv_air_quality.configure_char(CHAR_AIR_QUALITY, value=0)
self.char_density = serv_air_quality.configure_char(CHAR_VOC_DENSITY, value=0)
@callback
def async_update_state(self, new_state):
"""Update accessory after state change."""
density = convert_to_float(new_state.state)
if density is None:
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_voc(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."""

View File

@ -413,14 +413,40 @@ def density_to_air_quality(density: float) -> int:
def density_to_air_quality_pm10(density: float) -> int:
"""Map PM10 density to HomeKit AirQuality level."""
if density <= 40:
"""Map PM10 µg/m3 density to HomeKit AirQuality level."""
if density <= 54: # US AQI 0-50 (HomeKit: Excellent)
return 1
if density <= 80:
if density <= 154: # US AQI 51-100 (HomeKit: Good)
return 2
if density <= 120:
if density <= 254: # US AQI 101-150 (HomeKit: Fair)
return 3
if density <= 300:
if density <= 354: # US AQI 151-200 (HomeKit: Inferior)
return 4
return 5 # US AQI 201+ (HomeKit: Poor)
def density_to_air_quality_nitrogen_dioxide(density: float) -> int:
"""Map nitrogen dioxide µg/m3 to HomeKit AirQuality level."""
if density <= 30:
return 1
if density <= 60:
return 2
if density <= 80:
return 3
if density <= 90:
return 4
return 5
def density_to_air_quality_voc(density: float) -> int:
"""Map VOCs µg/m3 to HomeKit AirQuality level."""
if density <= 24:
return 1
if density <= 48:
return 2
if density <= 64:
return 3
if density <= 96:
return 4
return 5

View File

@ -226,6 +226,18 @@ def test_type_media_player(type_name, entity_id, state, attrs, config):
"40",
{ATTR_DEVICE_CLASS: "pm25"},
),
(
"NitrogenDioxideSensor",
"sensor.air_quality_nitrogen_dioxide",
"50",
{ATTR_DEVICE_CLASS: SensorDeviceClass.NITROGEN_DIOXIDE},
),
(
"VolatileOrganicCompoundsSensor",
"sensor.air_quality_volatile_organic_compounds",
"55",
{ATTR_DEVICE_CLASS: SensorDeviceClass.VOLATILE_ORGANIC_COMPOUNDS},
),
(
"CarbonMonoxideSensor",
"sensor.co",

View File

@ -15,9 +15,11 @@ from homeassistant.components.homekit.type_sensors import (
CarbonMonoxideSensor,
HumiditySensor,
LightSensor,
NitrogenDioxideSensor,
PM10Sensor,
PM25Sensor,
TemperatureSensor,
VolatileOrganicCompoundsSensor,
)
from homeassistant.const import (
ATTR_DEVICE_CLASS,
@ -155,24 +157,24 @@ async def test_pm10(hass, hk_driver):
assert acc.char_density.value == 0
assert acc.char_quality.value == 0
hass.states.async_set(entity_id, "34")
hass.states.async_set(entity_id, "54")
await hass.async_block_till_done()
assert acc.char_density.value == 34
assert acc.char_density.value == 54
assert acc.char_quality.value == 1
hass.states.async_set(entity_id, "70")
hass.states.async_set(entity_id, "154")
await hass.async_block_till_done()
assert acc.char_density.value == 70
assert acc.char_density.value == 154
assert acc.char_quality.value == 2
hass.states.async_set(entity_id, "110")
hass.states.async_set(entity_id, "254")
await hass.async_block_till_done()
assert acc.char_density.value == 110
assert acc.char_density.value == 254
assert acc.char_quality.value == 3
hass.states.async_set(entity_id, "200")
hass.states.async_set(entity_id, "354")
await hass.async_block_till_done()
assert acc.char_density.value == 200
assert acc.char_density.value == 354
assert acc.char_quality.value == 4
hass.states.async_set(entity_id, "400")
@ -228,6 +230,104 @@ async def test_pm25(hass, hk_driver):
assert acc.char_quality.value == 5
async def test_no2(hass, hk_driver):
"""Test if accessory is updated after state change."""
entity_id = "sensor.air_quality_nitrogen_dioxide"
hass.states.async_set(entity_id, None)
await hass.async_block_till_done()
acc = NitrogenDioxideSensor(
hass, hk_driver, "Nitrogen Dioxide 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, "30")
await hass.async_block_till_done()
assert acc.char_density.value == 30
assert acc.char_quality.value == 1
hass.states.async_set(entity_id, "60")
await hass.async_block_till_done()
assert acc.char_density.value == 60
assert acc.char_quality.value == 2
hass.states.async_set(entity_id, "80")
await hass.async_block_till_done()
assert acc.char_density.value == 80
assert acc.char_quality.value == 3
hass.states.async_set(entity_id, "90")
await hass.async_block_till_done()
assert acc.char_density.value == 90
assert acc.char_quality.value == 4
hass.states.async_set(entity_id, "100")
await hass.async_block_till_done()
assert acc.char_density.value == 100
assert acc.char_quality.value == 5
async def test_voc(hass, hk_driver):
"""Test if accessory is updated after state change."""
entity_id = "sensor.air_quality_volatile_organic_compounds"
hass.states.async_set(entity_id, None)
await hass.async_block_till_done()
acc = VolatileOrganicCompoundsSensor(
hass, hk_driver, "Volatile Organic Compounds 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, "24")
await hass.async_block_till_done()
assert acc.char_density.value == 24
assert acc.char_quality.value == 1
hass.states.async_set(entity_id, "48")
await hass.async_block_till_done()
assert acc.char_density.value == 48
assert acc.char_quality.value == 2
hass.states.async_set(entity_id, "64")
await hass.async_block_till_done()
assert acc.char_density.value == 64
assert acc.char_quality.value == 3
hass.states.async_set(entity_id, "96")
await hass.async_block_till_done()
assert acc.char_density.value == 96
assert acc.char_quality.value == 4
hass.states.async_set(entity_id, "128")
await hass.async_block_till_done()
assert acc.char_density.value == 128
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"