Add homekit pm type sensor (#46060)
Co-authored-by: J. Nick Koston <nick@koston.org>
This commit is contained in:
parent
e1b57d83c7
commit
56abd5f2cf
6 changed files with 220 additions and 7 deletions
|
@ -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"
|
||||||
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"
|
||||||
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 (
|
||||||
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 (
|
||||||
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):
|
||||||
"""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):
|
||||||
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):
|
||||||
_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):
|
||||||
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):
|
||||||
("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 (
|
||||||
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):
|
||||||
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"
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue