From c1a9f293a70f6ddd55627b8c3a8533829597c2f4 Mon Sep 17 00:00:00 2001 From: Retha Runolfsson <137745329+zerzhang@users.noreply.github.com> Date: Wed, 1 Apr 2026 22:08:40 +0800 Subject: [PATCH] Add fan speed percentage control to SwitchBot Air Purifier (#166953) --- .../components/switchbot/__init__.py | 7 ++-- homeassistant/components/switchbot/const.py | 13 ++++++++ homeassistant/components/switchbot/fan.py | 16 ++++++++++ tests/components/switchbot/__init__.py | 32 +++++++++---------- tests/components/switchbot/test_fan.py | 29 ++++++++++------- tests/components/switchbot/test_init.py | 16 +++++----- 6 files changed, 75 insertions(+), 38 deletions(-) diff --git a/homeassistant/components/switchbot/__init__.py b/homeassistant/components/switchbot/__init__.py index a2768c202b7..0d47f7752a7 100644 --- a/homeassistant/components/switchbot/__init__.py +++ b/homeassistant/components/switchbot/__init__.py @@ -114,7 +114,10 @@ PLATFORMS_BY_TYPE = { SupportedModels.AIR_PURIFIER_US.value: [Platform.FAN, Platform.SENSOR], SupportedModels.AIR_PURIFIER_TABLE_JP.value: [Platform.FAN, Platform.SENSOR], SupportedModels.AIR_PURIFIER_TABLE_US.value: [Platform.FAN, Platform.SENSOR], - SupportedModels.EVAPORATIVE_HUMIDIFIER: [Platform.HUMIDIFIER, Platform.SENSOR], + SupportedModels.EVAPORATIVE_HUMIDIFIER.value: [ + Platform.HUMIDIFIER, + Platform.SENSOR, + ], SupportedModels.FLOOR_LAMP.value: [Platform.LIGHT, Platform.SENSOR], SupportedModels.STRIP_LIGHT_3.value: [Platform.LIGHT, Platform.SENSOR], SupportedModels.RGBICWW_FLOOR_LAMP.value: [Platform.LIGHT, Platform.SENSOR], @@ -171,7 +174,7 @@ CLASS_BY_DEVICE = { SupportedModels.AIR_PURIFIER_US.value: switchbot.SwitchbotAirPurifier, SupportedModels.AIR_PURIFIER_TABLE_JP.value: switchbot.SwitchbotAirPurifier, SupportedModels.AIR_PURIFIER_TABLE_US.value: switchbot.SwitchbotAirPurifier, - SupportedModels.EVAPORATIVE_HUMIDIFIER: switchbot.SwitchbotEvaporativeHumidifier, + SupportedModels.EVAPORATIVE_HUMIDIFIER.value: switchbot.SwitchbotEvaporativeHumidifier, SupportedModels.FLOOR_LAMP.value: switchbot.SwitchbotStripLight3, SupportedModels.STRIP_LIGHT_3.value: switchbot.SwitchbotStripLight3, SupportedModels.RGBICWW_FLOOR_LAMP.value: switchbot.SwitchbotRgbicLight, diff --git a/homeassistant/components/switchbot/const.py b/homeassistant/components/switchbot/const.py index d871f18d964..142b13befcc 100644 --- a/homeassistant/components/switchbot/const.py +++ b/homeassistant/components/switchbot/const.py @@ -206,3 +206,16 @@ CONF_KEY_ID = "key_id" CONF_ENCRYPTION_KEY = "encryption_key" CONF_LOCK_NIGHTLATCH = "lock_force_nightlatch" CONF_CURTAIN_SPEED = "curtain_speed" + +AIRPURIFIER_BASIC_MODELS = { + SwitchbotModel.AIR_PURIFIER_JP, + SwitchbotModel.AIR_PURIFIER_US, +} +AIRPURIFIER_TABLE_MODELS = { + SwitchbotModel.AIR_PURIFIER_TABLE_JP, + SwitchbotModel.AIR_PURIFIER_TABLE_US, +} +AIRPURIFIER_PM25_MODELS = { + SwitchbotModel.AIR_PURIFIER_US, + SwitchbotModel.AIR_PURIFIER_TABLE_US, +} diff --git a/homeassistant/components/switchbot/fan.py b/homeassistant/components/switchbot/fan.py index 9a7260f5925..66d407eed2e 100644 --- a/homeassistant/components/switchbot/fan.py +++ b/homeassistant/components/switchbot/fan.py @@ -131,6 +131,7 @@ class SwitchBotAirPurifierEntity(SwitchbotEntity, FanEntity): _device: switchbot.SwitchbotAirPurifier _attr_supported_features = ( FanEntityFeature.PRESET_MODE + | FanEntityFeature.SET_SPEED | FanEntityFeature.TURN_OFF | FanEntityFeature.TURN_ON ) @@ -148,6 +149,11 @@ class SwitchBotAirPurifierEntity(SwitchbotEntity, FanEntity): """Return the current preset mode.""" return self._device.get_current_mode() + @property + def percentage(self) -> int | None: + """Return the speed percentage of the air purifier.""" + return self._device.get_current_percentage() + @exception_handler async def async_set_preset_mode(self, preset_mode: str) -> None: """Set the preset mode of the air purifier.""" @@ -160,6 +166,16 @@ class SwitchBotAirPurifierEntity(SwitchbotEntity, FanEntity): self._last_run_success = bool(await self._device.set_preset_mode(preset_mode)) self.async_write_ha_state() + @exception_handler + async def async_set_percentage(self, percentage: int) -> None: + """Set the speed percentage of the air purifier.""" + + _LOGGER.debug( + "Switchbot air purifier to set percentage %d %s", percentage, self._address + ) + await self._device.set_percentage(percentage) + self.async_write_ha_state() + @exception_handler async def async_turn_on( self, diff --git a/tests/components/switchbot/__init__.py b/tests/components/switchbot/__init__.py index 00f10ed1a72..40038d7d7c3 100644 --- a/tests/components/switchbot/__init__.py +++ b/tests/components/switchbot/__init__.py @@ -785,8 +785,8 @@ LOCK_ULTRA_SERVICE_INFO = BluetoothServiceInfoBleak( ) -AIR_PURIFIER_TABLE_PM25_SERVICE_INFO = BluetoothServiceInfoBleak( - name="Air Purifier Table PM25", +AIR_PURIFIER_TABLE_US_SERVICE_INFO = BluetoothServiceInfoBleak( + name="Air Purifier Table US", manufacturer_data={ 2409: b"\xf0\x9e\x9e\x96j\xd6\xa1\x81\x88\xe4\x00\x01\x95\x00\x00", }, @@ -796,22 +796,22 @@ AIR_PURIFIER_TABLE_PM25_SERVICE_INFO = BluetoothServiceInfoBleak( rssi=-60, source="local", advertisement=generate_advertisement_data( - local_name="Air Purifier Table PM25", + local_name="Air Purifier Table US", manufacturer_data={ 2409: b"\xf0\x9e\x9e\x96j\xd6\xa1\x81\x88\xe4\x00\x01\x95\x00\x00", }, service_data={"0000fd3d-0000-1000-8000-00805f9b34fb": b"7\x00\x00\x95-\x00"}, service_uuids=["cba20d00-224d-11e6-9fb8-0002a5d5c51b"], ), - device=generate_ble_device("AA:BB:CC:DD:EE:FF", "Air Purifier Table PM25"), + device=generate_ble_device("AA:BB:CC:DD:EE:FF", "Air Purifier Table US"), time=0, connectable=True, tx_power=-127, ) -AIR_PURIFIER_PM25_SERVICE_INFO = BluetoothServiceInfoBleak( - name="Air Purifier PM25", +AIR_PURIFIER_US_SERVICE_INFO = BluetoothServiceInfoBleak( + name="Air Purifier US", manufacturer_data={ 2409: b'\xcc\x8d\xa2\xa7\x92>\t"\x80\x000\x00\x0f\x00\x00', }, @@ -821,22 +821,22 @@ AIR_PURIFIER_PM25_SERVICE_INFO = BluetoothServiceInfoBleak( rssi=-60, source="local", advertisement=generate_advertisement_data( - local_name="Air Purifier PM25", + local_name="Air Purifier US", manufacturer_data={ 2409: b'\xcc\x8d\xa2\xa7\x92>\t"\x80\x000\x00\x0f\x00\x00', }, service_data={"0000fd3d-0000-1000-8000-00805f9b34fb": b"*\x00\x00\x15\x04\x00"}, service_uuids=["cba20d00-224d-11e6-9fb8-0002a5d5c51b"], ), - device=generate_ble_device("AA:BB:CC:DD:EE:FF", "Air Purifier PM25"), + device=generate_ble_device("AA:BB:CC:DD:EE:FF", "Air Purifier US"), time=0, connectable=True, tx_power=-127, ) -AIR_PURIFIER_VOC_SERVICE_INFO = BluetoothServiceInfoBleak( - name="Air Purifier VOC", +AIR_PURIFIER_JP_SERVICE_INFO = BluetoothServiceInfoBleak( + name="Air Purifier JP", manufacturer_data={ 2409: b"\xcc\x8d\xa2\xa7\xe4\xa6\x0b\x83\x88d\x00\xea`\x00\x00", }, @@ -846,22 +846,22 @@ AIR_PURIFIER_VOC_SERVICE_INFO = BluetoothServiceInfoBleak( rssi=-60, source="local", advertisement=generate_advertisement_data( - local_name="Air Purifier VOC", + local_name="Air Purifier JP", manufacturer_data={ 2409: b"\xcc\x8d\xa2\xa7\xe4\xa6\x0b\x83\x88d\x00\xea`\x00\x00", }, service_data={"0000fd3d-0000-1000-8000-00805f9b34fb": b"+\x00\x00\x15\x04\x00"}, service_uuids=["cba20d00-224d-11e6-9fb8-0002a5d5c51b"], ), - device=generate_ble_device("AA:BB:CC:DD:EE:FF", "Air Purifier VOC"), + device=generate_ble_device("AA:BB:CC:DD:EE:FF", "Air Purifier JP"), time=0, connectable=True, tx_power=-127, ) -AIR_PURIFIER_TABLE_VOC_SERVICE_INFO = BluetoothServiceInfoBleak( - name="Air Purifier Table VOC", +AIR_PURIFIER_TABLE_JP_SERVICE_INFO = BluetoothServiceInfoBleak( + name="Air Purifier Table JP", manufacturer_data={ 2409: b"\xcc\x8d\xa2\xa7\xc1\xae\x9b\x81\x8c\xb2\x00\x01\x94\x00\x00", }, @@ -871,14 +871,14 @@ AIR_PURIFIER_TABLE_VOC_SERVICE_INFO = BluetoothServiceInfoBleak( rssi=-60, source="local", advertisement=generate_advertisement_data( - local_name="Air Purifier Table VOC", + local_name="Air Purifier Table JP", manufacturer_data={ 2409: b"\xcc\x8d\xa2\xa7\xc1\xae\x9b\x81\x8c\xb2\x00\x01\x94\x00\x00", }, service_data={"0000fd3d-0000-1000-8000-00805f9b34fb": b"8\x00\x00\x95-\x00"}, service_uuids=["cba20d00-224d-11e6-9fb8-0002a5d5c51b"], ), - device=generate_ble_device("AA:BB:CC:DD:EE:FF", "Air Purifier Table VOC"), + device=generate_ble_device("AA:BB:CC:DD:EE:FF", "Air Purifier Table JP"), time=0, connectable=True, tx_power=-127, diff --git a/tests/components/switchbot/test_fan.py b/tests/components/switchbot/test_fan.py index 4e8fe669fd2..f70899c37c7 100644 --- a/tests/components/switchbot/test_fan.py +++ b/tests/components/switchbot/test_fan.py @@ -21,10 +21,10 @@ from homeassistant.core import HomeAssistant from homeassistant.exceptions import HomeAssistantError from . import ( - AIR_PURIFIER_PM25_SERVICE_INFO, - AIR_PURIFIER_TABLE_PM25_SERVICE_INFO, - AIR_PURIFIER_TABLE_VOC_SERVICE_INFO, - AIR_PURIFIER_VOC_SERVICE_INFO, + AIR_PURIFIER_JP_SERVICE_INFO, + AIR_PURIFIER_TABLE_JP_SERVICE_INFO, + AIR_PURIFIER_TABLE_US_SERVICE_INFO, + AIR_PURIFIER_US_SERVICE_INFO, CIRCULATOR_FAN_SERVICE_INFO, ) @@ -103,10 +103,10 @@ async def test_circulator_fan_controlling( @pytest.mark.parametrize( ("service_info", "sensor_type"), [ - (AIR_PURIFIER_VOC_SERVICE_INFO, "air_purifier_jp"), - (AIR_PURIFIER_TABLE_VOC_SERVICE_INFO, "air_purifier_table_jp"), - (AIR_PURIFIER_PM25_SERVICE_INFO, "air_purifier_us"), - (AIR_PURIFIER_TABLE_PM25_SERVICE_INFO, "air_purifier_table_us"), + (AIR_PURIFIER_JP_SERVICE_INFO, "air_purifier_jp"), + (AIR_PURIFIER_TABLE_JP_SERVICE_INFO, "air_purifier_table_jp"), + (AIR_PURIFIER_US_SERVICE_INFO, "air_purifier_us"), + (AIR_PURIFIER_TABLE_US_SERVICE_INFO, "air_purifier_table_us"), ], ) @pytest.mark.parametrize( @@ -117,6 +117,11 @@ async def test_circulator_fan_controlling( {ATTR_PRESET_MODE: "sleep"}, "set_preset_mode", ), + ( + SERVICE_SET_PERCENTAGE, + {ATTR_PERCENTAGE: 27}, + "set_percentage", + ), ( SERVICE_TURN_OFF, {}, @@ -169,10 +174,10 @@ async def test_air_purifier_controlling( @pytest.mark.parametrize( ("service_info", "sensor_type"), [ - (AIR_PURIFIER_VOC_SERVICE_INFO, "air_purifier_jp"), - (AIR_PURIFIER_TABLE_VOC_SERVICE_INFO, "air_purifier_table_jp"), - (AIR_PURIFIER_PM25_SERVICE_INFO, "air_purifier_us"), - (AIR_PURIFIER_TABLE_PM25_SERVICE_INFO, "air_purifier_table_us"), + (AIR_PURIFIER_JP_SERVICE_INFO, "air_purifier_jp"), + (AIR_PURIFIER_TABLE_JP_SERVICE_INFO, "air_purifier_table_jp"), + (AIR_PURIFIER_US_SERVICE_INFO, "air_purifier_us"), + (AIR_PURIFIER_TABLE_US_SERVICE_INFO, "air_purifier_table_us"), ], ) @pytest.mark.parametrize( diff --git a/tests/components/switchbot/test_init.py b/tests/components/switchbot/test_init.py index 15aae12800d..a0bdea2d0a3 100644 --- a/tests/components/switchbot/test_init.py +++ b/tests/components/switchbot/test_init.py @@ -20,10 +20,10 @@ from homeassistant.const import CONF_ADDRESS, CONF_NAME, CONF_SENSOR_TYPE from homeassistant.core import HomeAssistant from . import ( - AIR_PURIFIER_PM25_SERVICE_INFO, - AIR_PURIFIER_TABLE_PM25_SERVICE_INFO, - AIR_PURIFIER_TABLE_VOC_SERVICE_INFO, - AIR_PURIFIER_VOC_SERVICE_INFO, + AIR_PURIFIER_JP_SERVICE_INFO, + AIR_PURIFIER_TABLE_JP_SERVICE_INFO, + AIR_PURIFIER_TABLE_US_SERVICE_INFO, + AIR_PURIFIER_US_SERVICE_INFO, HUBMINI_MATTER_SERVICE_INFO, LOCK_SERVICE_INFO, WOCURTAIN_SERVICE_INFO, @@ -253,22 +253,22 @@ async def test_migrate_entry_fails_for_future_version( [ ( DEPRECATED_SENSOR_TYPE_AIR_PURIFIER, - AIR_PURIFIER_VOC_SERVICE_INFO, + AIR_PURIFIER_JP_SERVICE_INFO, "air_purifier_jp", ), ( DEPRECATED_SENSOR_TYPE_AIR_PURIFIER, - AIR_PURIFIER_PM25_SERVICE_INFO, + AIR_PURIFIER_US_SERVICE_INFO, "air_purifier_us", ), ( DEPRECATED_SENSOR_TYPE_AIR_PURIFIER_TABLE, - AIR_PURIFIER_TABLE_VOC_SERVICE_INFO, + AIR_PURIFIER_TABLE_JP_SERVICE_INFO, "air_purifier_table_jp", ), ( DEPRECATED_SENSOR_TYPE_AIR_PURIFIER_TABLE, - AIR_PURIFIER_TABLE_PM25_SERVICE_INFO, + AIR_PURIFIER_TABLE_US_SERVICE_INFO, "air_purifier_table_us", ), ],