mirror of
https://github.com/home-assistant/core.git
synced 2025-07-20 03:37:07 +00:00
Add switchbot circulator fan support (#142980)
* add support for circulator fan * add fan unin tests * optimize unit tests * add fan mode translation and icon * optimize fan unit test
This commit is contained in:
parent
1626b3b7c9
commit
9732b8c0dd
@ -72,6 +72,7 @@ PLATFORMS_BY_TYPE = {
|
|||||||
Platform.SENSOR,
|
Platform.SENSOR,
|
||||||
],
|
],
|
||||||
SupportedModels.HUBMINI_MATTER.value: [Platform.SENSOR],
|
SupportedModels.HUBMINI_MATTER.value: [Platform.SENSOR],
|
||||||
|
SupportedModels.CIRCULATOR_FAN.value: [Platform.FAN, Platform.SENSOR],
|
||||||
}
|
}
|
||||||
CLASS_BY_DEVICE = {
|
CLASS_BY_DEVICE = {
|
||||||
SupportedModels.CEILING_LIGHT.value: switchbot.SwitchbotCeilingLight,
|
SupportedModels.CEILING_LIGHT.value: switchbot.SwitchbotCeilingLight,
|
||||||
@ -87,6 +88,7 @@ CLASS_BY_DEVICE = {
|
|||||||
SupportedModels.RELAY_SWITCH_1PM.value: switchbot.SwitchbotRelaySwitch,
|
SupportedModels.RELAY_SWITCH_1PM.value: switchbot.SwitchbotRelaySwitch,
|
||||||
SupportedModels.RELAY_SWITCH_1.value: switchbot.SwitchbotRelaySwitch,
|
SupportedModels.RELAY_SWITCH_1.value: switchbot.SwitchbotRelaySwitch,
|
||||||
SupportedModels.ROLLER_SHADE.value: switchbot.SwitchbotRollerShade,
|
SupportedModels.ROLLER_SHADE.value: switchbot.SwitchbotRollerShade,
|
||||||
|
SupportedModels.CIRCULATOR_FAN.value: switchbot.SwitchbotFan,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@ -37,6 +37,7 @@ class SupportedModels(StrEnum):
|
|||||||
REMOTE = "remote"
|
REMOTE = "remote"
|
||||||
ROLLER_SHADE = "roller_shade"
|
ROLLER_SHADE = "roller_shade"
|
||||||
HUBMINI_MATTER = "hubmini_matter"
|
HUBMINI_MATTER = "hubmini_matter"
|
||||||
|
CIRCULATOR_FAN = "circulator_fan"
|
||||||
|
|
||||||
|
|
||||||
CONNECTABLE_SUPPORTED_MODEL_TYPES = {
|
CONNECTABLE_SUPPORTED_MODEL_TYPES = {
|
||||||
@ -54,6 +55,7 @@ CONNECTABLE_SUPPORTED_MODEL_TYPES = {
|
|||||||
SwitchbotModel.RELAY_SWITCH_1PM: SupportedModels.RELAY_SWITCH_1PM,
|
SwitchbotModel.RELAY_SWITCH_1PM: SupportedModels.RELAY_SWITCH_1PM,
|
||||||
SwitchbotModel.RELAY_SWITCH_1: SupportedModels.RELAY_SWITCH_1,
|
SwitchbotModel.RELAY_SWITCH_1: SupportedModels.RELAY_SWITCH_1,
|
||||||
SwitchbotModel.ROLLER_SHADE: SupportedModels.ROLLER_SHADE,
|
SwitchbotModel.ROLLER_SHADE: SupportedModels.ROLLER_SHADE,
|
||||||
|
SwitchbotModel.CIRCULATOR_FAN: SupportedModels.CIRCULATOR_FAN,
|
||||||
}
|
}
|
||||||
|
|
||||||
NON_CONNECTABLE_SUPPORTED_MODEL_TYPES = {
|
NON_CONNECTABLE_SUPPORTED_MODEL_TYPES = {
|
||||||
|
122
homeassistant/components/switchbot/fan.py
Normal file
122
homeassistant/components/switchbot/fan.py
Normal file
@ -0,0 +1,122 @@
|
|||||||
|
"""Support for SwitchBot Fans."""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import logging
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
|
import switchbot
|
||||||
|
from switchbot import FanMode
|
||||||
|
|
||||||
|
from homeassistant.components.fan import FanEntity, FanEntityFeature
|
||||||
|
from homeassistant.core import HomeAssistant
|
||||||
|
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||||
|
from homeassistant.helpers.restore_state import RestoreEntity
|
||||||
|
|
||||||
|
from .coordinator import SwitchbotConfigEntry, SwitchbotDataUpdateCoordinator
|
||||||
|
from .entity import SwitchbotEntity
|
||||||
|
|
||||||
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
PARALLEL_UPDATES = 0
|
||||||
|
|
||||||
|
|
||||||
|
async def async_setup_entry(
|
||||||
|
hass: HomeAssistant,
|
||||||
|
entry: SwitchbotConfigEntry,
|
||||||
|
async_add_entities: AddConfigEntryEntitiesCallback,
|
||||||
|
) -> None:
|
||||||
|
"""Set up Switchbot fan based on a config entry."""
|
||||||
|
coordinator = entry.runtime_data
|
||||||
|
async_add_entities([SwitchBotFanEntity(coordinator)])
|
||||||
|
|
||||||
|
|
||||||
|
class SwitchBotFanEntity(SwitchbotEntity, FanEntity, RestoreEntity):
|
||||||
|
"""Representation of a Switchbot."""
|
||||||
|
|
||||||
|
_device: switchbot.SwitchbotFan
|
||||||
|
_attr_supported_features = (
|
||||||
|
FanEntityFeature.SET_SPEED
|
||||||
|
| FanEntityFeature.OSCILLATE
|
||||||
|
| FanEntityFeature.PRESET_MODE
|
||||||
|
| FanEntityFeature.TURN_OFF
|
||||||
|
| FanEntityFeature.TURN_ON
|
||||||
|
)
|
||||||
|
_attr_preset_modes = FanMode.get_modes()
|
||||||
|
_attr_translation_key = "fan"
|
||||||
|
_attr_name = None
|
||||||
|
|
||||||
|
def __init__(self, coordinator: SwitchbotDataUpdateCoordinator) -> None:
|
||||||
|
"""Initialize the switchbot."""
|
||||||
|
super().__init__(coordinator)
|
||||||
|
self._attr_is_on = False
|
||||||
|
|
||||||
|
@property
|
||||||
|
def is_on(self) -> bool | None:
|
||||||
|
"""Return true if device is on."""
|
||||||
|
return self._device.is_on()
|
||||||
|
|
||||||
|
@property
|
||||||
|
def percentage(self) -> int | None:
|
||||||
|
"""Return the current speed as a percentage."""
|
||||||
|
return self._device.get_current_percentage()
|
||||||
|
|
||||||
|
@property
|
||||||
|
def oscillating(self) -> bool | None:
|
||||||
|
"""Return whether or not the fan is currently oscillating."""
|
||||||
|
return self._device.get_oscillating_state()
|
||||||
|
|
||||||
|
@property
|
||||||
|
def preset_mode(self) -> str | None:
|
||||||
|
"""Return the current preset mode."""
|
||||||
|
return self._device.get_current_mode()
|
||||||
|
|
||||||
|
async def async_set_preset_mode(self, preset_mode: str) -> None:
|
||||||
|
"""Set the preset mode of the fan."""
|
||||||
|
|
||||||
|
_LOGGER.debug(
|
||||||
|
"Switchbot fan to set preset mode %s %s", preset_mode, self._address
|
||||||
|
)
|
||||||
|
self._last_run_success = bool(await self._device.set_preset_mode(preset_mode))
|
||||||
|
self.async_write_ha_state()
|
||||||
|
|
||||||
|
async def async_set_percentage(self, percentage: int) -> None:
|
||||||
|
"""Set the speed percentage of the fan."""
|
||||||
|
|
||||||
|
_LOGGER.debug(
|
||||||
|
"Switchbot fan to set percentage %d %s", percentage, self._address
|
||||||
|
)
|
||||||
|
self._last_run_success = bool(await self._device.set_percentage(percentage))
|
||||||
|
self.async_write_ha_state()
|
||||||
|
|
||||||
|
async def async_oscillate(self, oscillating: bool) -> None:
|
||||||
|
"""Oscillate the fan."""
|
||||||
|
|
||||||
|
_LOGGER.debug(
|
||||||
|
"Switchbot fan to set oscillating %s %s", oscillating, self._address
|
||||||
|
)
|
||||||
|
self._last_run_success = bool(await self._device.set_oscillation(oscillating))
|
||||||
|
self.async_write_ha_state()
|
||||||
|
|
||||||
|
async def async_turn_on(
|
||||||
|
self,
|
||||||
|
percentage: int | None = None,
|
||||||
|
preset_mode: str | None = None,
|
||||||
|
**kwargs: Any,
|
||||||
|
) -> None:
|
||||||
|
"""Turn on the fan."""
|
||||||
|
|
||||||
|
_LOGGER.debug(
|
||||||
|
"Switchbot fan to set turn on %s %s %s",
|
||||||
|
percentage,
|
||||||
|
preset_mode,
|
||||||
|
self._address,
|
||||||
|
)
|
||||||
|
self._last_run_success = bool(await self._device.turn_on())
|
||||||
|
self.async_write_ha_state()
|
||||||
|
|
||||||
|
async def async_turn_off(self, **kwargs: Any) -> None:
|
||||||
|
"""Turn off the fan."""
|
||||||
|
|
||||||
|
_LOGGER.debug("Switchbot fan to set turn off %s", self._address)
|
||||||
|
self._last_run_success = bool(await self._device.turn_off())
|
||||||
|
self.async_write_ha_state()
|
18
homeassistant/components/switchbot/icons.json
Normal file
18
homeassistant/components/switchbot/icons.json
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
{
|
||||||
|
"entity": {
|
||||||
|
"fan": {
|
||||||
|
"fan": {
|
||||||
|
"state_attributes": {
|
||||||
|
"preset_mode": {
|
||||||
|
"state": {
|
||||||
|
"normal": "mdi:fan",
|
||||||
|
"natural": "mdi:leaf",
|
||||||
|
"sleep": "mdi:power-sleep",
|
||||||
|
"baby": "mdi:baby-face-outline"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -160,6 +160,26 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"fan": {
|
||||||
|
"fan": {
|
||||||
|
"state_attributes": {
|
||||||
|
"last_run_success": {
|
||||||
|
"state": {
|
||||||
|
"true": "[%key:component::binary_sensor::entity_component::problem::state::off%]",
|
||||||
|
"false": "[%key:component::binary_sensor::entity_component::problem::state::on%]"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"preset_mode": {
|
||||||
|
"state": {
|
||||||
|
"normal": "Normal",
|
||||||
|
"natural": "Natural",
|
||||||
|
"sleep": "Sleep",
|
||||||
|
"baby": "Baby"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -530,3 +530,28 @@ LOCK_SERVICE_INFO = BluetoothServiceInfoBleak(
|
|||||||
connectable=True,
|
connectable=True,
|
||||||
tx_power=-127,
|
tx_power=-127,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
CIRCULATOR_FAN_SERVICE_INFO = BluetoothServiceInfoBleak(
|
||||||
|
name="CirculatorFan",
|
||||||
|
manufacturer_data={
|
||||||
|
2409: b"\xb0\xe9\xfeXY\xa8~LR9",
|
||||||
|
},
|
||||||
|
service_data={"0000fd3d-0000-1000-8000-00805f9b34fb": b"~\x00R"},
|
||||||
|
service_uuids=["cba20d00-224d-11e6-9fb8-0002a5d5c51b"],
|
||||||
|
address="AA:BB:CC:DD:EE:FF",
|
||||||
|
rssi=-60,
|
||||||
|
source="local",
|
||||||
|
advertisement=generate_advertisement_data(
|
||||||
|
local_name="CirculatorFan",
|
||||||
|
manufacturer_data={
|
||||||
|
2409: b"\xb0\xe9\xfeXY\xa8~LR9",
|
||||||
|
},
|
||||||
|
service_data={"0000fd3d-0000-1000-8000-00805f9b34fb": b"~\x00R"},
|
||||||
|
service_uuids=["cba20d00-224d-11e6-9fb8-0002a5d5c51b"],
|
||||||
|
),
|
||||||
|
device=generate_ble_device("AA:BB:CC:DD:EE:FF", "CirculatorFan"),
|
||||||
|
time=0,
|
||||||
|
connectable=True,
|
||||||
|
tx_power=-127,
|
||||||
|
)
|
||||||
|
91
tests/components/switchbot/test_fan.py
Normal file
91
tests/components/switchbot/test_fan.py
Normal file
@ -0,0 +1,91 @@
|
|||||||
|
"""Test the switchbot fan."""
|
||||||
|
|
||||||
|
from collections.abc import Callable
|
||||||
|
from unittest.mock import AsyncMock, patch
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
from homeassistant.components.fan import (
|
||||||
|
ATTR_OSCILLATING,
|
||||||
|
ATTR_PERCENTAGE,
|
||||||
|
ATTR_PRESET_MODE,
|
||||||
|
DOMAIN as FAN_DOMAIN,
|
||||||
|
SERVICE_OSCILLATE,
|
||||||
|
SERVICE_SET_PERCENTAGE,
|
||||||
|
SERVICE_SET_PRESET_MODE,
|
||||||
|
)
|
||||||
|
from homeassistant.const import ATTR_ENTITY_ID, SERVICE_TURN_OFF, SERVICE_TURN_ON
|
||||||
|
from homeassistant.core import HomeAssistant
|
||||||
|
|
||||||
|
from . import CIRCULATOR_FAN_SERVICE_INFO
|
||||||
|
|
||||||
|
from tests.common import MockConfigEntry
|
||||||
|
from tests.components.bluetooth import inject_bluetooth_service_info
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
(
|
||||||
|
"service",
|
||||||
|
"service_data",
|
||||||
|
"mock_method",
|
||||||
|
),
|
||||||
|
[
|
||||||
|
(
|
||||||
|
SERVICE_SET_PRESET_MODE,
|
||||||
|
{ATTR_PRESET_MODE: "baby"},
|
||||||
|
"set_preset_mode",
|
||||||
|
),
|
||||||
|
(
|
||||||
|
SERVICE_SET_PERCENTAGE,
|
||||||
|
{ATTR_PERCENTAGE: 27},
|
||||||
|
"set_percentage",
|
||||||
|
),
|
||||||
|
(
|
||||||
|
SERVICE_OSCILLATE,
|
||||||
|
{ATTR_OSCILLATING: True},
|
||||||
|
"set_oscillation",
|
||||||
|
),
|
||||||
|
(
|
||||||
|
SERVICE_TURN_OFF,
|
||||||
|
{},
|
||||||
|
"turn_off",
|
||||||
|
),
|
||||||
|
(
|
||||||
|
SERVICE_TURN_ON,
|
||||||
|
{},
|
||||||
|
"turn_on",
|
||||||
|
),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
async def test_circulator_fan_controlling(
|
||||||
|
hass: HomeAssistant,
|
||||||
|
mock_entry_factory: Callable[[str], MockConfigEntry],
|
||||||
|
service: str,
|
||||||
|
service_data: dict,
|
||||||
|
mock_method: str,
|
||||||
|
) -> None:
|
||||||
|
"""Test controlling the circulator fan with different services."""
|
||||||
|
inject_bluetooth_service_info(hass, CIRCULATOR_FAN_SERVICE_INFO)
|
||||||
|
|
||||||
|
entry = mock_entry_factory(sensor_type="circulator_fan")
|
||||||
|
entity_id = "fan.test_name"
|
||||||
|
entry.add_to_hass(hass)
|
||||||
|
|
||||||
|
mocked_instance = AsyncMock(return_value=True)
|
||||||
|
mcoked_none_instance = AsyncMock(return_value=None)
|
||||||
|
with patch.multiple(
|
||||||
|
"homeassistant.components.switchbot.fan.switchbot.SwitchbotFan",
|
||||||
|
get_basic_info=mcoked_none_instance,
|
||||||
|
**{mock_method: mocked_instance},
|
||||||
|
):
|
||||||
|
assert await hass.config_entries.async_setup(entry.entry_id)
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
|
||||||
|
await hass.services.async_call(
|
||||||
|
FAN_DOMAIN,
|
||||||
|
service,
|
||||||
|
{**service_data, ATTR_ENTITY_ID: entity_id},
|
||||||
|
blocking=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
mocked_instance.assert_awaited_once()
|
@ -22,6 +22,7 @@ from homeassistant.core import HomeAssistant
|
|||||||
from homeassistant.setup import async_setup_component
|
from homeassistant.setup import async_setup_component
|
||||||
|
|
||||||
from . import (
|
from . import (
|
||||||
|
CIRCULATOR_FAN_SERVICE_INFO,
|
||||||
HUBMINI_MATTER_SERVICE_INFO,
|
HUBMINI_MATTER_SERVICE_INFO,
|
||||||
LEAK_SERVICE_INFO,
|
LEAK_SERVICE_INFO,
|
||||||
REMOTE_SERVICE_INFO,
|
REMOTE_SERVICE_INFO,
|
||||||
@ -340,3 +341,47 @@ async def test_hubmini_matter_sensor(hass: HomeAssistant) -> None:
|
|||||||
|
|
||||||
assert await hass.config_entries.async_unload(entry.entry_id)
|
assert await hass.config_entries.async_unload(entry.entry_id)
|
||||||
await hass.async_block_till_done()
|
await hass.async_block_till_done()
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.usefixtures("entity_registry_enabled_by_default")
|
||||||
|
async def test_fan_sensors(hass: HomeAssistant) -> None:
|
||||||
|
"""Test setting up creates the sensors."""
|
||||||
|
await async_setup_component(hass, DOMAIN, {})
|
||||||
|
inject_bluetooth_service_info(hass, CIRCULATOR_FAN_SERVICE_INFO)
|
||||||
|
|
||||||
|
entry = MockConfigEntry(
|
||||||
|
domain=DOMAIN,
|
||||||
|
data={
|
||||||
|
CONF_ADDRESS: "aa:bb:cc:dd:ee:ff",
|
||||||
|
CONF_NAME: "test-name",
|
||||||
|
CONF_PASSWORD: "test-password",
|
||||||
|
CONF_SENSOR_TYPE: "circulator_fan",
|
||||||
|
},
|
||||||
|
unique_id="aabbccddeeff",
|
||||||
|
)
|
||||||
|
entry.add_to_hass(hass)
|
||||||
|
|
||||||
|
with patch(
|
||||||
|
"homeassistant.components.switchbot.fan.switchbot.SwitchbotFan.update",
|
||||||
|
return_value=True,
|
||||||
|
):
|
||||||
|
assert await hass.config_entries.async_setup(entry.entry_id)
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
|
||||||
|
assert len(hass.states.async_all("sensor")) == 2
|
||||||
|
|
||||||
|
battery_sensor = hass.states.get("sensor.test_name_battery")
|
||||||
|
battery_sensor_attrs = battery_sensor.attributes
|
||||||
|
assert battery_sensor.state == "82"
|
||||||
|
assert battery_sensor_attrs[ATTR_FRIENDLY_NAME] == "test-name Battery"
|
||||||
|
assert battery_sensor_attrs[ATTR_UNIT_OF_MEASUREMENT] == "%"
|
||||||
|
assert battery_sensor_attrs[ATTR_STATE_CLASS] == "measurement"
|
||||||
|
|
||||||
|
rssi_sensor = hass.states.get("sensor.test_name_bluetooth_signal")
|
||||||
|
rssi_sensor_attrs = rssi_sensor.attributes
|
||||||
|
assert rssi_sensor.state == "-60"
|
||||||
|
assert rssi_sensor_attrs[ATTR_FRIENDLY_NAME] == "test-name Bluetooth signal"
|
||||||
|
assert rssi_sensor_attrs[ATTR_UNIT_OF_MEASUREMENT] == "dBm"
|
||||||
|
|
||||||
|
assert await hass.config_entries.async_unload(entry.entry_id)
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
Loading…
x
Reference in New Issue
Block a user