From ba33e5340947780fa9f2fc446ea0997b3e588fd5 Mon Sep 17 00:00:00 2001 From: Onero-testdev Date: Fri, 19 Jun 2026 02:53:06 +0800 Subject: [PATCH] Add SwitchBot Standing Fan switch platform (#173579) Co-authored-by: Claude Opus 4.8 --- .../components/switchbot/__init__.py | 2 +- .../components/switchbot/strings.json | 6 ++ homeassistant/components/switchbot/switch.py | 84 +++++++++++++++++++ tests/components/switchbot/test_switch.py | 59 +++++++++++++ 4 files changed, 150 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/switchbot/__init__.py b/homeassistant/components/switchbot/__init__.py index 6a0c1ef2f0c..bf4580bde69 100644 --- a/homeassistant/components/switchbot/__init__.py +++ b/homeassistant/components/switchbot/__init__.py @@ -96,7 +96,7 @@ PLATFORMS_BY_TYPE = { ], SupportedModels.HUBMINI_MATTER.value: [Platform.SENSOR], SupportedModels.CIRCULATOR_FAN.value: [Platform.FAN, Platform.SENSOR], - SupportedModels.STANDING_FAN.value: [Platform.SENSOR], + SupportedModels.STANDING_FAN.value: [Platform.SWITCH, Platform.SENSOR], SupportedModels.S10_VACUUM.value: [Platform.VACUUM, Platform.SENSOR], SupportedModels.S20_VACUUM.value: [Platform.VACUUM, Platform.SENSOR], SupportedModels.K10_VACUUM.value: [Platform.VACUUM, Platform.SENSOR], diff --git a/homeassistant/components/switchbot/strings.json b/homeassistant/components/switchbot/strings.json index 076be9c30db..3981c7e6e8d 100644 --- a/homeassistant/components/switchbot/strings.json +++ b/homeassistant/components/switchbot/strings.json @@ -356,6 +356,12 @@ "child_lock": { "name": "Child lock" }, + "horizontal_oscillation": { + "name": "Horizontal oscillation" + }, + "vertical_oscillation": { + "name": "Vertical oscillation" + }, "wireless_charging": { "name": "Wireless charging" } diff --git a/homeassistant/components/switchbot/switch.py b/homeassistant/components/switchbot/switch.py index 5d69696f081..4f742b556fb 100644 --- a/homeassistant/components/switchbot/switch.py +++ b/homeassistant/components/switchbot/switch.py @@ -1,5 +1,6 @@ """Support for Switchbot bot.""" +import abc from collections.abc import Awaitable, Callable from dataclasses import dataclass import logging @@ -87,6 +88,13 @@ async def async_setup_entry( for desc in AIRPURIFIER_TABLE_SWITCHES ] ) + elif isinstance(coordinator.device, switchbot.SwitchbotStandingFan): + async_add_entities( + [ + SwitchbotFanHorizontalOscillationSwitch(coordinator), + SwitchbotFanVerticalOscillationSwitch(coordinator), + ] + ) else: async_add_entities([SwitchBotSwitch(coordinator)]) @@ -217,3 +225,79 @@ class SwitchbotMultiChannelSwitch(SwitchbotSwitchedEntity, SwitchEntity): ) await self._device.turn_off(self._channel) self.async_write_ha_state() + + +class SwitchbotFanOscillationSwitch(SwitchbotSwitchedEntity, SwitchEntity, abc.ABC): + """Base class for fan oscillation switch entities.""" + + _attr_device_class = SwitchDeviceClass.SWITCH + _device: switchbot.SwitchbotStandingFan + + @property + @abc.abstractmethod + def is_on(self) -> bool | None: + """Return true if oscillation is active.""" + + @abc.abstractmethod + async def async_turn_on(self, **kwargs: Any) -> None: + """Enable oscillation.""" + + @abc.abstractmethod + async def async_turn_off(self, **kwargs: Any) -> None: + """Disable oscillation.""" + + +class SwitchbotFanHorizontalOscillationSwitch(SwitchbotFanOscillationSwitch): + """Switch entity for fan horizontal (left-right) oscillation.""" + + _attr_translation_key = "horizontal_oscillation" + + def __init__(self, coordinator: SwitchbotDataUpdateCoordinator) -> None: + """Initialize the horizontal oscillation switch.""" + super().__init__(coordinator) + self._attr_unique_id = f"{coordinator.base_unique_id}-horizontal-oscillation" + + @property + def is_on(self) -> bool | None: + """Return true if horizontal oscillation is active.""" + return self._device.get_horizontal_oscillating_state() + + @exception_handler + async def async_turn_on(self, **kwargs: Any) -> None: + """Enable horizontal oscillation.""" + await self._device.set_horizontal_oscillation(True) + self.async_write_ha_state() + + @exception_handler + async def async_turn_off(self, **kwargs: Any) -> None: + """Disable horizontal oscillation.""" + await self._device.set_horizontal_oscillation(False) + self.async_write_ha_state() + + +class SwitchbotFanVerticalOscillationSwitch(SwitchbotFanOscillationSwitch): + """Switch entity for fan vertical (up-down) oscillation.""" + + _attr_translation_key = "vertical_oscillation" + + def __init__(self, coordinator: SwitchbotDataUpdateCoordinator) -> None: + """Initialize the vertical oscillation switch.""" + super().__init__(coordinator) + self._attr_unique_id = f"{coordinator.base_unique_id}-vertical-oscillation" + + @property + def is_on(self) -> bool | None: + """Return true if vertical oscillation is active.""" + return self._device.get_vertical_oscillating_state() + + @exception_handler + async def async_turn_on(self, **kwargs: Any) -> None: + """Enable vertical oscillation.""" + await self._device.set_vertical_oscillation(True) + self.async_write_ha_state() + + @exception_handler + async def async_turn_off(self, **kwargs: Any) -> None: + """Disable vertical oscillation.""" + await self._device.set_vertical_oscillation(False) + self.async_write_ha_state() diff --git a/tests/components/switchbot/test_switch.py b/tests/components/switchbot/test_switch.py index 1ee0052ac7c..fdbd43206c2 100644 --- a/tests/components/switchbot/test_switch.py +++ b/tests/components/switchbot/test_switch.py @@ -25,6 +25,7 @@ from . import ( PLUG_MINI_EU_SERVICE_INFO, RELAY_SWITCH_1_SERVICE_INFO, RELAY_SWITCH_2PM_SERVICE_INFO, + STANDING_FAN_SERVICE_INFO, WOHAND_SERVICE_INFO, WORELAY_SWITCH_1PM_SERVICE_INFO, ) @@ -396,3 +397,61 @@ async def test_air_purifier_switch_control( ) mocked_turn_off.assert_awaited_once() + + +@pytest.mark.parametrize( + ("entity_id", "set_method", "get_state_method"), + [ + ( + "switch.test_name_horizontal_oscillation", + "set_horizontal_oscillation", + "get_horizontal_oscillating_state", + ), + ( + "switch.test_name_vertical_oscillation", + "set_vertical_oscillation", + "get_vertical_oscillating_state", + ), + ], +) +async def test_standing_fan_oscillation_switches( + hass: HomeAssistant, + mock_entry_factory: Callable[[str], MockConfigEntry], + entity_id: str, + set_method: str, + get_state_method: str, +) -> None: + """Test horizontal/vertical oscillation switches for the standing fan.""" + inject_bluetooth_service_info(hass, STANDING_FAN_SERVICE_INFO) + + entry = mock_entry_factory(sensor_type="standing_fan") + entry.add_to_hass(hass) + + mocked_set = AsyncMock(return_value=True) + with patch.multiple( + "homeassistant.components.switchbot.switch.switchbot.SwitchbotStandingFan", + get_basic_info=AsyncMock(return_value=None), + **{ + set_method: mocked_set, + get_state_method: lambda self: False, + }, + ): + assert await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + + await hass.services.async_call( + SWITCH_DOMAIN, + SERVICE_TURN_ON, + {ATTR_ENTITY_ID: entity_id}, + blocking=True, + ) + mocked_set.assert_awaited_once_with(True) + + await hass.services.async_call( + SWITCH_DOMAIN, + SERVICE_TURN_OFF, + {ATTR_ENTITY_ID: entity_id}, + blocking=True, + ) + assert mocked_set.await_count == 2 + mocked_set.assert_awaited_with(False)