Add support for air purifiers in HomeKit (#142467)

* Add support for air purifier type in HomeKit.

Any fan and PM2.5 in the same device will be treated as an air purifier.

type_air_purifiers.py heavily based on type_fans.py -
I tried extending type_fans.py but this looked better to me.

* Refactor to make AirPurifier class extend Fan.

* Ensure all chars are added before creating service

* Add support for switching automatic mode.

* Add test for auto/manual switch

* Add support for air purifier type in HomeKit.

Any fan and PM2.5 in the same device will be treated as an air purifier.

type_air_purifiers.py heavily based on type_fans.py -
I tried extending type_fans.py but this looked better to me.

* Add support for air purifier type in HomeKit.

Any fan and PM2.5 in the same device will be treated as an air purifier.

type_air_purifiers.py heavily based on type_fans.py -
I tried extending type_fans.py but this looked better to me.

* Refactor to make AirPurifier class extend Fan.

* Ensure all chars are added before creating service

* Add support for switching automatic mode.

* Add test for auto/manual switch

* Add support for air purifier type in HomeKit.

Any fan and PM2.5 in the same device will be treated as an air purifier.

type_air_purifiers.py heavily based on type_fans.py -
I tried extending type_fans.py but this looked better to me.

* Improve fan config: allow setting fan type (fan or air purifier)

Be more explicit than assuming a fan is an air purifier if it has a PM2.5 sensor. Set defaults based on the presence of sensors.

* Fix return type annotation for fan/air purifier create_services

* Allow linking air purifier filter level/change indicator

* Remove no longer needed if statement in fan init

* Fix up types and clean up code

* Update homekit tests to account for air purifiers

* Fix pylint errors

* Fix mypy errors

* Improve type annotations

* Improve readability of auto preset mode discovery

* Test air purifier with 'Auto' preset mode

* Handle case with a single preset mode

* Test air purifier edge cases: state updates to same value, and removed linked entities

* Don't create 'auto mode' switch for air purifiers

This is already exposed as a target mode on the air purifier service itself

* Handle unavailable states in air purifier

Also don't remove device class when updating state in test

* Reduce branching in air purifier test

* Split up air purifier tests for with and without auto presets, to reduce branching

* Handle unavailable states in air purifier more explicitly

* Use constant for ignored state values

* Use a set for ignored_states

* Update tests/components/homekit/test_type_air_purifiers.py

---------

Co-authored-by: Andrew Kurowski <62596884+ak6i@users.noreply.github.com>
Co-authored-by: J. Nick Koston <nick+github@koston.org>
Co-authored-by: J. Nick Koston <nick@koston.org>
This commit is contained in:
Maarten Staa
2025-04-09 22:20:21 +02:00
committed by GitHub
parent 1b66278a68
commit 9fe306f056
10 changed files with 1407 additions and 13 deletions

View File

@@ -21,6 +21,7 @@ from homeassistant.components.homekit import (
STATUS_RUNNING,
STATUS_STOPPED,
STATUS_WAIT,
TYPE_AIR_PURIFIER,
HomeKit,
)
from homeassistant.components.homekit.accessories import HomeBridge
@@ -51,6 +52,7 @@ from homeassistant.const import (
ATTR_DEVICE_ID,
ATTR_ENTITY_ID,
ATTR_UNIT_OF_MEASUREMENT,
CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
CONF_NAME,
CONF_PORT,
EVENT_HOMEASSISTANT_STARTED,
@@ -58,6 +60,7 @@ from homeassistant.const import (
SERVICE_RELOAD,
STATE_ON,
EntityCategory,
UnitOfTemperature,
)
from homeassistant.core import HomeAssistant, State
from homeassistant.exceptions import HomeAssistantError
@@ -2162,6 +2165,109 @@ async def test_homekit_finds_linked_humidity_sensors(
)
@pytest.mark.usefixtures("mock_async_zeroconf")
async def test_homekit_finds_linked_air_purifier_sensors(
hass: HomeAssistant,
hk_driver,
device_registry: dr.DeviceRegistry,
entity_registry: er.EntityRegistry,
) -> None:
"""Test HomeKit start method."""
entry = await async_init_integration(hass)
homekit = _mock_homekit(hass, entry, HOMEKIT_MODE_BRIDGE)
homekit.driver = hk_driver
homekit.bridge = HomeBridge(hass, hk_driver, "mock_bridge")
config_entry = MockConfigEntry(domain="air_purifier", data={})
config_entry.add_to_hass(hass)
device_entry = device_registry.async_get_or_create(
config_entry_id=config_entry.entry_id,
sw_version="0.16.1",
model="Smart Air Purifier",
manufacturer="Home Assistant",
connections={(dr.CONNECTION_NETWORK_MAC, "12:34:56:AB:CD:EF")},
)
humidity_sensor = entity_registry.async_get_or_create(
"sensor",
"air_purifier",
"humidity_sensor",
device_id=device_entry.id,
original_device_class=SensorDeviceClass.HUMIDITY,
)
pm25_sensor = entity_registry.async_get_or_create(
"sensor",
"air_purifier",
"pm25_sensor",
device_id=device_entry.id,
original_device_class=SensorDeviceClass.PM25,
)
temperature_sensor = entity_registry.async_get_or_create(
"sensor",
"air_purifier",
"temperature_sensor",
device_id=device_entry.id,
original_device_class=SensorDeviceClass.TEMPERATURE,
)
air_purifier = entity_registry.async_get_or_create(
"fan", "air_purifier", "demo", device_id=device_entry.id
)
hass.states.async_set(
humidity_sensor.entity_id,
"42",
{
ATTR_DEVICE_CLASS: SensorDeviceClass.HUMIDITY,
ATTR_UNIT_OF_MEASUREMENT: PERCENTAGE,
},
)
hass.states.async_set(
pm25_sensor.entity_id,
8,
{
ATTR_DEVICE_CLASS: SensorDeviceClass.PM25,
ATTR_UNIT_OF_MEASUREMENT: CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
},
)
hass.states.async_set(
temperature_sensor.entity_id,
22,
{
ATTR_DEVICE_CLASS: SensorDeviceClass.TEMPERATURE,
ATTR_UNIT_OF_MEASUREMENT: UnitOfTemperature.CELSIUS,
},
)
hass.states.async_set(air_purifier.entity_id, STATE_ON)
with (
patch.object(homekit.bridge, "add_accessory"),
patch(f"{PATH_HOMEKIT}.async_show_setup_message"),
patch(f"{PATH_HOMEKIT}.get_accessory") as mock_get_acc,
patch("pyhap.accessory_driver.AccessoryDriver.async_start"),
):
await homekit.async_start()
await hass.async_block_till_done()
mock_get_acc.assert_called_with(
hass,
ANY,
ANY,
ANY,
{
"manufacturer": "Home Assistant",
"model": "Smart Air Purifier",
"platform": "air_purifier",
"sw_version": "0.16.1",
"type": TYPE_AIR_PURIFIER,
"linked_humidity_sensor": "sensor.air_purifier_humidity_sensor",
"linked_pm25_sensor": "sensor.air_purifier_pm25_sensor",
"linked_temperature_sensor": "sensor.air_purifier_temperature_sensor",
},
)
@pytest.mark.usefixtures("mock_async_zeroconf")
async def test_reload(hass: HomeAssistant) -> None:
"""Test we can reload from yaml."""