mirror of
https://github.com/home-assistant/core.git
synced 2025-07-18 18:57:06 +00:00
Add Fan platform to Switchbot cloud (#148304)
Co-authored-by: Joost Lekkerkerker <joostlek@outlook.com>
This commit is contained in:
parent
0acfb81d50
commit
cd94685b7d
@ -29,6 +29,7 @@ PLATFORMS: list[Platform] = [
|
|||||||
Platform.BINARY_SENSOR,
|
Platform.BINARY_SENSOR,
|
||||||
Platform.BUTTON,
|
Platform.BUTTON,
|
||||||
Platform.CLIMATE,
|
Platform.CLIMATE,
|
||||||
|
Platform.FAN,
|
||||||
Platform.LOCK,
|
Platform.LOCK,
|
||||||
Platform.SENSOR,
|
Platform.SENSOR,
|
||||||
Platform.SWITCH,
|
Platform.SWITCH,
|
||||||
@ -51,6 +52,7 @@ class SwitchbotDevices:
|
|||||||
sensors: list[tuple[Device, SwitchBotCoordinator]] = field(default_factory=list)
|
sensors: list[tuple[Device, SwitchBotCoordinator]] = field(default_factory=list)
|
||||||
vacuums: list[tuple[Device, SwitchBotCoordinator]] = field(default_factory=list)
|
vacuums: list[tuple[Device, SwitchBotCoordinator]] = field(default_factory=list)
|
||||||
locks: list[tuple[Device, SwitchBotCoordinator]] = field(default_factory=list)
|
locks: list[tuple[Device, SwitchBotCoordinator]] = field(default_factory=list)
|
||||||
|
fans: list[tuple[Device, SwitchBotCoordinator]] = field(default_factory=list)
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
@ -96,7 +98,6 @@ async def make_switchbot_devices(
|
|||||||
for device in devices
|
for device in devices
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
|
|
||||||
return devices_data
|
return devices_data
|
||||||
|
|
||||||
|
|
||||||
@ -177,6 +178,16 @@ async def make_device_data(
|
|||||||
else:
|
else:
|
||||||
devices_data.switches.append((device, coordinator))
|
devices_data.switches.append((device, coordinator))
|
||||||
|
|
||||||
|
if isinstance(device, Device) and device.device_type in [
|
||||||
|
"Battery Circulator Fan",
|
||||||
|
"Circulator Fan",
|
||||||
|
]:
|
||||||
|
coordinator = await coordinator_for_device(
|
||||||
|
hass, entry, api, device, coordinators_by_id
|
||||||
|
)
|
||||||
|
devices_data.fans.append((device, coordinator))
|
||||||
|
devices_data.sensors.append((device, coordinator))
|
||||||
|
|
||||||
|
|
||||||
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||||
"""Set up SwitchBot via API from a config entry."""
|
"""Set up SwitchBot via API from a config entry."""
|
||||||
|
120
homeassistant/components/switchbot_cloud/fan.py
Normal file
120
homeassistant/components/switchbot_cloud/fan.py
Normal file
@ -0,0 +1,120 @@
|
|||||||
|
"""Support for the Switchbot Battery Circulator fan."""
|
||||||
|
|
||||||
|
import asyncio
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
|
from switchbot_api import (
|
||||||
|
BatteryCirculatorFanCommands,
|
||||||
|
BatteryCirculatorFanMode,
|
||||||
|
CommonCommands,
|
||||||
|
)
|
||||||
|
|
||||||
|
from homeassistant.components.fan import FanEntity, FanEntityFeature
|
||||||
|
from homeassistant.config_entries import ConfigEntry
|
||||||
|
from homeassistant.core import HomeAssistant
|
||||||
|
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||||
|
|
||||||
|
from . import SwitchbotCloudData
|
||||||
|
from .const import DOMAIN
|
||||||
|
from .entity import SwitchBotCloudEntity
|
||||||
|
|
||||||
|
|
||||||
|
async def async_setup_entry(
|
||||||
|
hass: HomeAssistant,
|
||||||
|
config: ConfigEntry,
|
||||||
|
async_add_entities: AddConfigEntryEntitiesCallback,
|
||||||
|
) -> None:
|
||||||
|
"""Set up SwitchBot Cloud entry."""
|
||||||
|
data: SwitchbotCloudData = hass.data[DOMAIN][config.entry_id]
|
||||||
|
async_add_entities(
|
||||||
|
SwitchBotCloudFan(data.api, device, coordinator)
|
||||||
|
for device, coordinator in data.devices.fans
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class SwitchBotCloudFan(SwitchBotCloudEntity, FanEntity):
|
||||||
|
"""Representation of a SwitchBot Battery Circulator Fan."""
|
||||||
|
|
||||||
|
_attr_name = None
|
||||||
|
|
||||||
|
_attr_supported_features = (
|
||||||
|
FanEntityFeature.SET_SPEED
|
||||||
|
| FanEntityFeature.PRESET_MODE
|
||||||
|
| FanEntityFeature.TURN_OFF
|
||||||
|
| FanEntityFeature.TURN_ON
|
||||||
|
)
|
||||||
|
_attr_preset_modes = list(BatteryCirculatorFanMode)
|
||||||
|
|
||||||
|
_attr_is_on: bool | None = None
|
||||||
|
|
||||||
|
@property
|
||||||
|
def is_on(self) -> bool | None:
|
||||||
|
"""Return true if the entity is on."""
|
||||||
|
return self._attr_is_on
|
||||||
|
|
||||||
|
def _set_attributes(self) -> None:
|
||||||
|
"""Set attributes from coordinator data."""
|
||||||
|
if self.coordinator.data is None:
|
||||||
|
return
|
||||||
|
|
||||||
|
power: str = self.coordinator.data["power"]
|
||||||
|
mode: str = self.coordinator.data["mode"]
|
||||||
|
fan_speed: str = self.coordinator.data["fanSpeed"]
|
||||||
|
self._attr_is_on = power == "on"
|
||||||
|
self._attr_preset_mode = mode
|
||||||
|
self._attr_percentage = int(fan_speed)
|
||||||
|
self._attr_supported_features = (
|
||||||
|
FanEntityFeature.PRESET_MODE
|
||||||
|
| FanEntityFeature.TURN_OFF
|
||||||
|
| FanEntityFeature.TURN_ON
|
||||||
|
)
|
||||||
|
if self.is_on and self.preset_mode == BatteryCirculatorFanMode.DIRECT.value:
|
||||||
|
self._attr_supported_features |= FanEntityFeature.SET_SPEED
|
||||||
|
|
||||||
|
async def async_turn_on(
|
||||||
|
self,
|
||||||
|
percentage: int | None = None,
|
||||||
|
preset_mode: str | None = None,
|
||||||
|
**kwargs: Any,
|
||||||
|
) -> None:
|
||||||
|
"""Turn on the fan."""
|
||||||
|
await self.send_api_command(CommonCommands.ON)
|
||||||
|
await self.send_api_command(
|
||||||
|
command=BatteryCirculatorFanCommands.SET_WIND_MODE,
|
||||||
|
parameters=str(self.preset_mode),
|
||||||
|
)
|
||||||
|
if self.preset_mode == BatteryCirculatorFanMode.DIRECT.value:
|
||||||
|
await self.send_api_command(
|
||||||
|
command=BatteryCirculatorFanCommands.SET_WIND_SPEED,
|
||||||
|
parameters=str(self.percentage),
|
||||||
|
)
|
||||||
|
await asyncio.sleep(5)
|
||||||
|
await self.coordinator.async_request_refresh()
|
||||||
|
|
||||||
|
async def async_turn_off(self, **kwargs: Any) -> None:
|
||||||
|
"""Turn off the fan."""
|
||||||
|
await self.send_api_command(CommonCommands.OFF)
|
||||||
|
await asyncio.sleep(5)
|
||||||
|
await self.coordinator.async_request_refresh()
|
||||||
|
|
||||||
|
async def async_set_percentage(self, percentage: int) -> None:
|
||||||
|
"""Set the speed of the fan, as a percentage."""
|
||||||
|
await self.send_api_command(
|
||||||
|
command=BatteryCirculatorFanCommands.SET_WIND_MODE,
|
||||||
|
parameters=str(BatteryCirculatorFanMode.DIRECT.value),
|
||||||
|
)
|
||||||
|
await self.send_api_command(
|
||||||
|
command=BatteryCirculatorFanCommands.SET_WIND_SPEED,
|
||||||
|
parameters=str(percentage),
|
||||||
|
)
|
||||||
|
await asyncio.sleep(5)
|
||||||
|
await self.coordinator.async_request_refresh()
|
||||||
|
|
||||||
|
async def async_set_preset_mode(self, preset_mode: str) -> None:
|
||||||
|
"""Set new preset mode."""
|
||||||
|
await self.send_api_command(
|
||||||
|
command=BatteryCirculatorFanCommands.SET_WIND_MODE,
|
||||||
|
parameters=preset_mode,
|
||||||
|
)
|
||||||
|
await asyncio.sleep(5)
|
||||||
|
await self.coordinator.async_request_refresh()
|
@ -91,6 +91,7 @@ CO2_DESCRIPTION = SensorEntityDescription(
|
|||||||
|
|
||||||
SENSOR_DESCRIPTIONS_BY_DEVICE_TYPES = {
|
SENSOR_DESCRIPTIONS_BY_DEVICE_TYPES = {
|
||||||
"Bot": (BATTERY_DESCRIPTION,),
|
"Bot": (BATTERY_DESCRIPTION,),
|
||||||
|
"Battery Circulator Fan": (BATTERY_DESCRIPTION,),
|
||||||
"Meter": (
|
"Meter": (
|
||||||
TEMPERATURE_DESCRIPTION,
|
TEMPERATURE_DESCRIPTION,
|
||||||
HUMIDITY_DESCRIPTION,
|
HUMIDITY_DESCRIPTION,
|
||||||
|
187
tests/components/switchbot_cloud/test_fan.py
Normal file
187
tests/components/switchbot_cloud/test_fan.py
Normal file
@ -0,0 +1,187 @@
|
|||||||
|
"""Test for the Switchbot Battery Circulator Fan."""
|
||||||
|
|
||||||
|
from unittest.mock import patch
|
||||||
|
|
||||||
|
from switchbot_api import Device, SwitchBotAPI
|
||||||
|
|
||||||
|
from homeassistant.components.fan import (
|
||||||
|
ATTR_PERCENTAGE,
|
||||||
|
ATTR_PRESET_MODE,
|
||||||
|
DOMAIN as FAN_DOMAIN,
|
||||||
|
SERVICE_SET_PERCENTAGE,
|
||||||
|
SERVICE_SET_PRESET_MODE,
|
||||||
|
SERVICE_TURN_ON,
|
||||||
|
)
|
||||||
|
from homeassistant.config_entries import ConfigEntryState
|
||||||
|
from homeassistant.const import (
|
||||||
|
ATTR_ENTITY_ID,
|
||||||
|
SERVICE_TURN_OFF,
|
||||||
|
STATE_OFF,
|
||||||
|
STATE_ON,
|
||||||
|
STATE_UNKNOWN,
|
||||||
|
)
|
||||||
|
from homeassistant.core import HomeAssistant
|
||||||
|
|
||||||
|
from . import configure_integration
|
||||||
|
|
||||||
|
|
||||||
|
async def test_coordinator_data_is_none(
|
||||||
|
hass: HomeAssistant, mock_list_devices, mock_get_status
|
||||||
|
) -> None:
|
||||||
|
"""Test coordinator data is none."""
|
||||||
|
mock_list_devices.return_value = [
|
||||||
|
Device(
|
||||||
|
version="V1.0",
|
||||||
|
deviceId="battery-fan-id-1",
|
||||||
|
deviceName="battery-fan-1",
|
||||||
|
deviceType="Battery Circulator Fan",
|
||||||
|
hubDeviceId="test-hub-id",
|
||||||
|
),
|
||||||
|
]
|
||||||
|
mock_get_status.side_effect = [
|
||||||
|
None,
|
||||||
|
]
|
||||||
|
entry = await configure_integration(hass)
|
||||||
|
assert entry.state is ConfigEntryState.LOADED
|
||||||
|
entity_id = "fan.battery_fan_1"
|
||||||
|
state = hass.states.get(entity_id)
|
||||||
|
|
||||||
|
assert state.state == STATE_UNKNOWN
|
||||||
|
|
||||||
|
|
||||||
|
async def test_turn_on(hass: HomeAssistant, mock_list_devices, mock_get_status) -> None:
|
||||||
|
"""Test turning on the fan."""
|
||||||
|
mock_list_devices.return_value = [
|
||||||
|
Device(
|
||||||
|
version="V1.0",
|
||||||
|
deviceId="battery-fan-id-1",
|
||||||
|
deviceName="battery-fan-1",
|
||||||
|
deviceType="Battery Circulator Fan",
|
||||||
|
hubDeviceId="test-hub-id",
|
||||||
|
),
|
||||||
|
]
|
||||||
|
mock_get_status.side_effect = [
|
||||||
|
{"power": "off", "mode": "direct", "fanSpeed": "0"},
|
||||||
|
{"power": "on", "mode": "direct", "fanSpeed": "0"},
|
||||||
|
{"power": "on", "mode": "direct", "fanSpeed": "0"},
|
||||||
|
]
|
||||||
|
entry = await configure_integration(hass)
|
||||||
|
assert entry.state is ConfigEntryState.LOADED
|
||||||
|
entity_id = "fan.battery_fan_1"
|
||||||
|
state = hass.states.get(entity_id)
|
||||||
|
|
||||||
|
assert state.state == STATE_OFF
|
||||||
|
|
||||||
|
with patch.object(SwitchBotAPI, "send_command") as mock_send_command:
|
||||||
|
await hass.services.async_call(
|
||||||
|
FAN_DOMAIN, SERVICE_TURN_ON, {ATTR_ENTITY_ID: entity_id}, blocking=True
|
||||||
|
)
|
||||||
|
mock_send_command.assert_called()
|
||||||
|
|
||||||
|
state = hass.states.get(entity_id)
|
||||||
|
assert state.state == STATE_ON
|
||||||
|
|
||||||
|
|
||||||
|
async def test_turn_off(
|
||||||
|
hass: HomeAssistant, mock_list_devices, mock_get_status
|
||||||
|
) -> None:
|
||||||
|
"""Test turning off the fan."""
|
||||||
|
mock_list_devices.return_value = [
|
||||||
|
Device(
|
||||||
|
version="V1.0",
|
||||||
|
deviceId="battery-fan-id-1",
|
||||||
|
deviceName="battery-fan-1",
|
||||||
|
deviceType="Battery Circulator Fan",
|
||||||
|
hubDeviceId="test-hub-id",
|
||||||
|
),
|
||||||
|
]
|
||||||
|
mock_get_status.side_effect = [
|
||||||
|
{"power": "on", "mode": "direct", "fanSpeed": "0"},
|
||||||
|
{"power": "off", "mode": "direct", "fanSpeed": "0"},
|
||||||
|
{"power": "off", "mode": "direct", "fanSpeed": "0"},
|
||||||
|
]
|
||||||
|
entry = await configure_integration(hass)
|
||||||
|
assert entry.state is ConfigEntryState.LOADED
|
||||||
|
entity_id = "fan.battery_fan_1"
|
||||||
|
state = hass.states.get(entity_id)
|
||||||
|
|
||||||
|
assert state.state == STATE_ON
|
||||||
|
|
||||||
|
with patch.object(SwitchBotAPI, "send_command") as mock_send_command:
|
||||||
|
await hass.services.async_call(
|
||||||
|
FAN_DOMAIN, SERVICE_TURN_OFF, {ATTR_ENTITY_ID: entity_id}, blocking=True
|
||||||
|
)
|
||||||
|
mock_send_command.assert_called()
|
||||||
|
|
||||||
|
state = hass.states.get(entity_id)
|
||||||
|
assert state.state == STATE_OFF
|
||||||
|
|
||||||
|
|
||||||
|
async def test_set_percentage(
|
||||||
|
hass: HomeAssistant, mock_list_devices, mock_get_status
|
||||||
|
) -> None:
|
||||||
|
"""Test set percentage."""
|
||||||
|
mock_list_devices.return_value = [
|
||||||
|
Device(
|
||||||
|
version="V1.0",
|
||||||
|
deviceId="battery-fan-id-1",
|
||||||
|
deviceName="battery-fan-1",
|
||||||
|
deviceType="Battery Circulator Fan",
|
||||||
|
hubDeviceId="test-hub-id",
|
||||||
|
),
|
||||||
|
]
|
||||||
|
mock_get_status.side_effect = [
|
||||||
|
{"power": "on", "mode": "direct", "fanSpeed": "0"},
|
||||||
|
{"power": "on", "mode": "direct", "fanSpeed": "0"},
|
||||||
|
{"power": "off", "mode": "direct", "fanSpeed": "5"},
|
||||||
|
]
|
||||||
|
entry = await configure_integration(hass)
|
||||||
|
assert entry.state is ConfigEntryState.LOADED
|
||||||
|
entity_id = "fan.battery_fan_1"
|
||||||
|
state = hass.states.get(entity_id)
|
||||||
|
|
||||||
|
assert state.state == STATE_ON
|
||||||
|
|
||||||
|
with patch.object(SwitchBotAPI, "send_command") as mock_send_command:
|
||||||
|
await hass.services.async_call(
|
||||||
|
FAN_DOMAIN,
|
||||||
|
SERVICE_SET_PERCENTAGE,
|
||||||
|
{ATTR_ENTITY_ID: entity_id, ATTR_PERCENTAGE: 5},
|
||||||
|
blocking=True,
|
||||||
|
)
|
||||||
|
mock_send_command.assert_called()
|
||||||
|
|
||||||
|
|
||||||
|
async def test_set_preset_mode(
|
||||||
|
hass: HomeAssistant, mock_list_devices, mock_get_status
|
||||||
|
) -> None:
|
||||||
|
"""Test set preset mode."""
|
||||||
|
mock_list_devices.return_value = [
|
||||||
|
Device(
|
||||||
|
version="V1.0",
|
||||||
|
deviceId="battery-fan-id-1",
|
||||||
|
deviceName="battery-fan-1",
|
||||||
|
deviceType="Battery Circulator Fan",
|
||||||
|
hubDeviceId="test-hub-id",
|
||||||
|
),
|
||||||
|
]
|
||||||
|
mock_get_status.side_effect = [
|
||||||
|
{"power": "on", "mode": "direct", "fanSpeed": "0"},
|
||||||
|
{"power": "on", "mode": "direct", "fanSpeed": "0"},
|
||||||
|
{"power": "on", "mode": "baby", "fanSpeed": "0"},
|
||||||
|
]
|
||||||
|
entry = await configure_integration(hass)
|
||||||
|
assert entry.state is ConfigEntryState.LOADED
|
||||||
|
entity_id = "fan.battery_fan_1"
|
||||||
|
state = hass.states.get(entity_id)
|
||||||
|
|
||||||
|
assert state.state == STATE_ON
|
||||||
|
|
||||||
|
with patch.object(SwitchBotAPI, "send_command") as mock_send_command:
|
||||||
|
await hass.services.async_call(
|
||||||
|
FAN_DOMAIN,
|
||||||
|
SERVICE_SET_PRESET_MODE,
|
||||||
|
{ATTR_ENTITY_ID: entity_id, ATTR_PRESET_MODE: "baby"},
|
||||||
|
blocking=True,
|
||||||
|
)
|
||||||
|
mock_send_command.assert_called_once()
|
Loading…
x
Reference in New Issue
Block a user