mirror of
https://github.com/home-assistant/core.git
synced 2025-07-19 03:07:37 +00:00
Expose NO2 and VOCs sensors to homekit (#81217)
Co-authored-by: J. Nick Koston <nick@koston.org>
This commit is contained in:
parent
5d282db439
commit
a1eec7b55d
@ -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
|
||||
|
@ -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"
|
||||
|
@ -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."""
|
||||
|
@ -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
|
||||
|
||||
|
@ -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",
|
||||
|
@ -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"
|
||||
|
Loading…
x
Reference in New Issue
Block a user