Skip to content
Snippets Groups Projects
Unverified Commit 56abd5f2 authored by Lester Lo's avatar Lester Lo Committed by GitHub
Browse files

Add homekit pm type sensor (#46060)


Co-authored-by: default avatarJ. Nick Koston <nick@koston.org>
parent e1b57d83
No related branches found
No related tags found
No related merge requests found
...@@ -173,9 +173,19 @@ def get_accessory(hass, driver, state, aid, config): # noqa: C901 ...@@ -173,9 +173,19 @@ def get_accessory(hass, driver, state, aid, config): # noqa: C901
a_type = "TemperatureSensor" a_type = "TemperatureSensor"
elif device_class == SensorDeviceClass.HUMIDITY and unit == PERCENTAGE: elif device_class == SensorDeviceClass.HUMIDITY and unit == PERCENTAGE:
a_type = "HumiditySensor" a_type = "HumiditySensor"
elif (
device_class == SensorDeviceClass.PM10
or SensorDeviceClass.PM10 in state.entity_id
):
a_type = "PM10Sensor"
elif ( elif (
device_class == SensorDeviceClass.PM25 device_class == SensorDeviceClass.PM25
or SensorDeviceClass.PM25 in state.entity_id 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" a_type = "AirQualitySensor"
elif device_class == SensorDeviceClass.CO: elif device_class == SensorDeviceClass.CO:
......
...@@ -150,6 +150,8 @@ SERV_WINDOW_COVERING = "WindowCovering" ...@@ -150,6 +150,8 @@ SERV_WINDOW_COVERING = "WindowCovering"
CHAR_ACTIVE = "Active" CHAR_ACTIVE = "Active"
CHAR_ACTIVE_IDENTIFIER = "ActiveIdentifier" CHAR_ACTIVE_IDENTIFIER = "ActiveIdentifier"
CHAR_AIR_PARTICULATE_DENSITY = "AirParticulateDensity" CHAR_AIR_PARTICULATE_DENSITY = "AirParticulateDensity"
CHAR_PM25_DENSITY = "PM2.5Density"
CHAR_PM10_DENSITY = "PM10Density"
CHAR_AIR_QUALITY = "AirQuality" CHAR_AIR_QUALITY = "AirQuality"
CHAR_BATTERY_LEVEL = "BatteryLevel" CHAR_BATTERY_LEVEL = "BatteryLevel"
CHAR_BRIGHTNESS = "Brightness" CHAR_BRIGHTNESS = "Brightness"
...@@ -235,7 +237,6 @@ PROP_MIN_VALUE = "minValue" ...@@ -235,7 +237,6 @@ PROP_MIN_VALUE = "minValue"
PROP_MIN_STEP = "minStep" PROP_MIN_STEP = "minStep"
PROP_CELSIUS = {"minValue": -273, "maxValue": 999} PROP_CELSIUS = {"minValue": -273, "maxValue": 999}
PROP_VALID_VALUES = "ValidValues" PROP_VALID_VALUES = "ValidValues"
# #### Thresholds #### # #### Thresholds ####
THRESHOLD_CO = 25 THRESHOLD_CO = 25
THRESHOLD_CO2 = 1000 THRESHOLD_CO2 = 1000
......
...@@ -35,6 +35,8 @@ from .const import ( ...@@ -35,6 +35,8 @@ from .const import (
CHAR_LEAK_DETECTED, CHAR_LEAK_DETECTED,
CHAR_MOTION_DETECTED, CHAR_MOTION_DETECTED,
CHAR_OCCUPANCY_DETECTED, CHAR_OCCUPANCY_DETECTED,
CHAR_PM10_DENSITY,
CHAR_PM25_DENSITY,
CHAR_SMOKE_DETECTED, CHAR_SMOKE_DETECTED,
PROP_CELSIUS, PROP_CELSIUS,
SERV_AIR_QUALITY_SENSOR, SERV_AIR_QUALITY_SENSOR,
...@@ -51,7 +53,13 @@ from .const import ( ...@@ -51,7 +53,13 @@ from .const import (
THRESHOLD_CO, THRESHOLD_CO,
THRESHOLD_CO2, 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__) _LOGGER = logging.getLogger(__name__)
...@@ -156,6 +164,15 @@ class AirQualitySensor(HomeAccessory): ...@@ -156,6 +164,15 @@ class AirQualitySensor(HomeAccessory):
"""Initialize a AirQualitySensor accessory object.""" """Initialize a AirQualitySensor accessory object."""
super().__init__(*args, category=CATEGORY_SENSOR) super().__init__(*args, category=CATEGORY_SENSOR)
state = self.hass.states.get(self.entity_id) 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 = self.add_preload_service(
SERV_AIR_QUALITY_SENSOR, [CHAR_AIR_PARTICULATE_DENSITY] SERV_AIR_QUALITY_SENSOR, [CHAR_AIR_PARTICULATE_DENSITY]
) )
...@@ -163,9 +180,6 @@ class AirQualitySensor(HomeAccessory): ...@@ -163,9 +180,6 @@ class AirQualitySensor(HomeAccessory):
self.char_density = serv_air_quality.configure_char( self.char_density = serv_air_quality.configure_char(
CHAR_AIR_PARTICULATE_DENSITY, value=0 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 @callback
def async_update_state(self, new_state): def async_update_state(self, new_state):
...@@ -179,6 +193,60 @@ class AirQualitySensor(HomeAccessory): ...@@ -179,6 +193,60 @@ class AirQualitySensor(HomeAccessory):
_LOGGER.debug("%s: Set air_quality to %d", self.entity_id, air_quality) _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") @TYPES.register("CarbonMonoxideSensor")
class CarbonMonoxideSensor(HomeAccessory): class CarbonMonoxideSensor(HomeAccessory):
"""Generate a CarbonMonoxidSensor accessory as CO sensor.""" """Generate a CarbonMonoxidSensor accessory as CO sensor."""
......
...@@ -407,6 +407,32 @@ def density_to_air_quality(density): ...@@ -407,6 +407,32 @@ def density_to_air_quality(density):
return 5 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): def get_persist_filename_for_entry_id(entry_id: str):
"""Determine the filename of the homekit state file.""" """Determine the filename of the homekit state file."""
return f"{DOMAIN}.{entry_id}.state" return f"{DOMAIN}.{entry_id}.state"
......
...@@ -212,8 +212,20 @@ def test_type_media_player(type_name, entity_id, state, attrs, config): ...@@ -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", "binary_sensor.opening", "on", {ATTR_DEVICE_CLASS: "opening"}),
("BinarySensor", "device_tracker.someone", "not_home", {}), ("BinarySensor", "device_tracker.someone", "not_home", {}),
("BinarySensor", "person.someone", "home", {}), ("BinarySensor", "person.someone", "home", {}),
("AirQualitySensor", "sensor.air_quality_pm25", "40", {}), ("PM10Sensor", "sensor.air_quality_pm10", "30", {}),
("AirQualitySensor", "sensor.air_quality", "40", {ATTR_DEVICE_CLASS: "pm25"}), (
"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", "CarbonMonoxideSensor",
"sensor.co", "sensor.co",
......
...@@ -15,6 +15,8 @@ from homeassistant.components.homekit.type_sensors import ( ...@@ -15,6 +15,8 @@ from homeassistant.components.homekit.type_sensors import (
CarbonMonoxideSensor, CarbonMonoxideSensor,
HumiditySensor, HumiditySensor,
LightSensor, LightSensor,
PM10Sensor,
PM25Sensor,
TemperatureSensor, TemperatureSensor,
) )
from homeassistant.const import ( from homeassistant.const import (
...@@ -132,6 +134,100 @@ async def test_air_quality(hass, hk_driver): ...@@ -132,6 +134,100 @@ async def test_air_quality(hass, hk_driver):
assert acc.char_quality.value == 5 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): async def test_co(hass, hk_driver):
"""Test if accessory is updated after state change.""" """Test if accessory is updated after state change."""
entity_id = "sensor.co" entity_id = "sensor.co"
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment