mirror of
https://github.com/home-assistant/core.git
synced 2026-08-24 10:13:52 -05:00
Switchbot Cloud:Add a night light control to the fan (#177514)
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> Co-authored-by: Joost Lekkerkerker <joostlek@outlook.com>
This commit is contained in:
co-authored by
Copilot Autofix powered by AI
Joost Lekkerkerker
parent
4763a19826
commit
2cc0f1a96d
@@ -43,6 +43,7 @@ PLATFORMS: list[Platform] = [
|
||||
Platform.IMAGE,
|
||||
Platform.LIGHT,
|
||||
Platform.LOCK,
|
||||
Platform.SELECT,
|
||||
Platform.SENSOR,
|
||||
Platform.SWITCH,
|
||||
Platform.VACUUM,
|
||||
@@ -64,6 +65,7 @@ class SwitchbotDevices:
|
||||
switches: list[tuple[Device | Remote, SwitchBotCoordinator]] = field(
|
||||
default_factory=list
|
||||
)
|
||||
selects: 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)
|
||||
locks: list[tuple[Device, SwitchBotCoordinator]] = field(default_factory=list)
|
||||
@@ -261,6 +263,7 @@ async def make_new_device_data(
|
||||
Platform.IMAGE: devices_data.images,
|
||||
Platform.LIGHT: devices_data.lights,
|
||||
Platform.LOCK: devices_data.locks,
|
||||
Platform.SELECT: devices_data.selects,
|
||||
Platform.SENSOR: devices_data.sensors,
|
||||
Platform.SWITCH: devices_data.switches,
|
||||
Platform.VACUUM: devices_data.vacuums,
|
||||
|
||||
@@ -36,6 +36,25 @@ HUMIDITY_LEVELS = {
|
||||
100: 103, # High humidity mode
|
||||
}
|
||||
|
||||
NIGHT_LIGHT_ON = "on"
|
||||
NIGHT_LIGHT_OFF = "off"
|
||||
NIGHT_LIGHT_BRIGHT = "bright"
|
||||
NIGHT_LIGHT_SOFT = "soft"
|
||||
|
||||
STANDING_FAN_NIGHT_LIGHT_PARAMETERS_MAP = {
|
||||
NIGHT_LIGHT_ON: "on",
|
||||
NIGHT_LIGHT_OFF: "off",
|
||||
NIGHT_LIGHT_BRIGHT: "1",
|
||||
NIGHT_LIGHT_SOFT: "2",
|
||||
}
|
||||
|
||||
BATTERY_CIRCULATOR_FAN_2_PRO_NIGHT_LIGHT_PARAMETERS_MAP = {
|
||||
NIGHT_LIGHT_ON: "on",
|
||||
NIGHT_LIGHT_OFF: "off",
|
||||
NIGHT_LIGHT_BRIGHT: "0",
|
||||
NIGHT_LIGHT_SOFT: "1",
|
||||
}
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class SwitchbotCloudDeviceConfig:
|
||||
@@ -128,13 +147,13 @@ DEVICE_SUPPORT_MAP: Final[dict[str, SwitchbotCloudDeviceConfig]] = {
|
||||
),
|
||||
"Circulator Fan": SwitchbotCloudDeviceConfig(True, entity_config=(Platform.FAN,)),
|
||||
"Standing Fan": SwitchbotCloudDeviceConfig(
|
||||
True, entity_config=(Platform.SENSOR, Platform.FAN)
|
||||
True, entity_config=(Platform.SENSOR, Platform.FAN, Platform.SELECT)
|
||||
),
|
||||
"Battery Circulator Fan": SwitchbotCloudDeviceConfig(
|
||||
True, entity_config=(Platform.SENSOR, Platform.FAN)
|
||||
True, entity_config=(Platform.SENSOR, Platform.FAN, Platform.SELECT)
|
||||
),
|
||||
"Battery Circulator Fan 2 Pro": SwitchbotCloudDeviceConfig(
|
||||
True, entity_config=(Platform.SENSOR, Platform.FAN)
|
||||
True, entity_config=(Platform.SENSOR, Platform.FAN, Platform.SELECT)
|
||||
),
|
||||
"Water Detector": SwitchbotCloudDeviceConfig(
|
||||
True, entity_config=(Platform.SENSOR, Platform.BINARY_SENSOR)
|
||||
|
||||
@@ -53,6 +53,11 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"select": {
|
||||
"night_light_control": {
|
||||
"default": "mdi:lightbulb-night"
|
||||
}
|
||||
},
|
||||
"sensor": {
|
||||
"light_level": {
|
||||
"default": "mdi:brightness-7",
|
||||
|
||||
@@ -0,0 +1,106 @@
|
||||
"""SwitchBotCloudSelect entity."""
|
||||
|
||||
from typing import TYPE_CHECKING, override
|
||||
|
||||
from switchbot_api import BatteryCirculatorFanCommands, Device, Remote, SwitchBotAPI
|
||||
|
||||
from homeassistant.components.select import SelectEntity
|
||||
from homeassistant.const import EntityCategory
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
|
||||
from . import SwitchbotCloudConfigEntry, SwitchBotCoordinator
|
||||
from .const import (
|
||||
BATTERY_CIRCULATOR_FAN_2_PRO_NIGHT_LIGHT_PARAMETERS_MAP,
|
||||
NIGHT_LIGHT_BRIGHT,
|
||||
NIGHT_LIGHT_ON,
|
||||
NIGHT_LIGHT_SOFT,
|
||||
STANDING_FAN_NIGHT_LIGHT_PARAMETERS_MAP,
|
||||
)
|
||||
from .entity import SwitchBotCloudEntity
|
||||
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant,
|
||||
config: SwitchbotCloudConfigEntry,
|
||||
async_add_entities: AddConfigEntryEntitiesCallback,
|
||||
) -> None:
|
||||
"""Set up SwitchBot Cloud entry."""
|
||||
data = config.runtime_data
|
||||
async_add_entities(
|
||||
_async_make_entity(data.api, device, coordinator)
|
||||
for device, coordinator in data.devices.selects
|
||||
)
|
||||
|
||||
|
||||
class SwitchBotCloudStandingFanNightLight(SwitchBotCloudEntity, SelectEntity):
|
||||
"""SwitchBotCloud Standing Fan Night Light."""
|
||||
|
||||
_night_light_parameters_map: dict[str, str] = (
|
||||
STANDING_FAN_NIGHT_LIGHT_PARAMETERS_MAP
|
||||
)
|
||||
_attr_entity_category = EntityCategory.CONFIG
|
||||
_attr_current_option: str | None = None
|
||||
|
||||
_attr_translation_key = "night_light_control"
|
||||
_attr_options = list(_night_light_parameters_map)
|
||||
|
||||
@override
|
||||
async def async_select_option(self, option: str) -> None:
|
||||
"""Select the night light mode."""
|
||||
if option == NIGHT_LIGHT_ON:
|
||||
para = self._night_light_parameters_map.get(
|
||||
NIGHT_LIGHT_BRIGHT
|
||||
) or self._night_light_parameters_map.get(NIGHT_LIGHT_SOFT)
|
||||
if TYPE_CHECKING:
|
||||
assert para is not None
|
||||
await self.send_api_command(
|
||||
BatteryCirculatorFanCommands.SET_NIGHT_LIGHT_MODE,
|
||||
parameters=para,
|
||||
)
|
||||
else:
|
||||
await self.send_api_command(
|
||||
BatteryCirculatorFanCommands.SET_NIGHT_LIGHT_MODE,
|
||||
parameters=self._night_light_parameters_map[option],
|
||||
)
|
||||
self._attr_current_option = option
|
||||
self.async_write_ha_state()
|
||||
|
||||
@override
|
||||
def _set_attributes(self) -> None:
|
||||
"""Set attributes from coordinator data."""
|
||||
if self.coordinator.data is None:
|
||||
return
|
||||
night_status = self.coordinator.data.get("nightStatus")
|
||||
for key, value in self._night_light_parameters_map.items():
|
||||
if value == night_status:
|
||||
self._attr_current_option = key
|
||||
return
|
||||
self._attr_current_option = None
|
||||
|
||||
|
||||
class SwitchBotCloudBatteryCirculatorFan2ProNightLight(
|
||||
SwitchBotCloudStandingFanNightLight
|
||||
):
|
||||
"""SwitchBotCloud Battery Circulator Fan 2 Pro Night Light."""
|
||||
|
||||
_night_light_parameters_map: dict[str, str] = (
|
||||
BATTERY_CIRCULATOR_FAN_2_PRO_NIGHT_LIGHT_PARAMETERS_MAP
|
||||
)
|
||||
|
||||
|
||||
@callback
|
||||
def _async_make_entity(
|
||||
api: SwitchBotAPI, device: Device | Remote, coordinator: SwitchBotCoordinator
|
||||
) -> (
|
||||
SwitchBotCloudStandingFanNightLight
|
||||
| SwitchBotCloudBatteryCirculatorFan2ProNightLight
|
||||
):
|
||||
"""Make a SwitchBotCloudSelect entity."""
|
||||
if device.device_type in ["Standing Fan", "Battery Circulator Fan"]:
|
||||
return SwitchBotCloudStandingFanNightLight(api, device, coordinator)
|
||||
if device.device_type == "Battery Circulator Fan 2 Pro":
|
||||
return SwitchBotCloudBatteryCirculatorFan2ProNightLight(
|
||||
api, device, coordinator
|
||||
)
|
||||
raise NotImplementedError
|
||||
@@ -72,7 +72,17 @@
|
||||
"name": "Display"
|
||||
}
|
||||
},
|
||||
|
||||
"select": {
|
||||
"night_light_control": {
|
||||
"name": "Night light",
|
||||
"state": {
|
||||
"bright": "Bright",
|
||||
"off": "[%key:common::state::off%]",
|
||||
"on": "[%key:common::state::on%]",
|
||||
"soft": "Soft"
|
||||
}
|
||||
}
|
||||
},
|
||||
"sensor": {
|
||||
"light_level": {
|
||||
"name": "Light level"
|
||||
|
||||
@@ -65,7 +65,6 @@ BATTERY_CIRCULATOR_FAN_2_PRO_INFO = Device(
|
||||
hubDeviceId="test-hub-id",
|
||||
)
|
||||
|
||||
|
||||
METER_INFO = Device(
|
||||
version="V1.0",
|
||||
deviceId="meter-id-1",
|
||||
|
||||
@@ -47,6 +47,19 @@
|
||||
"fanSpeed": 3,
|
||||
"battery": 22
|
||||
},
|
||||
{
|
||||
"deviceId": "A1C3E5F7D9B0",
|
||||
"deviceType": "Battery Circulator Fan 2 Pro",
|
||||
"hubDeviceId": "FFFFFFFFFFF",
|
||||
"mode": "direct",
|
||||
"version": "V6.3",
|
||||
"power": "on",
|
||||
"nightStatus": "off",
|
||||
"oscillation": "on",
|
||||
"verticalOscillation": "on",
|
||||
"fanSpeed": 3,
|
||||
"battery": 22
|
||||
},
|
||||
{
|
||||
"deviceId": "9B0D2F4A6C8E",
|
||||
"deviceType": "Meter",
|
||||
|
||||
@@ -0,0 +1,144 @@
|
||||
"""Test for the switchbot_cloud select."""
|
||||
|
||||
from unittest.mock import AsyncMock, patch
|
||||
|
||||
import pytest
|
||||
from switchbot_api import Device, SwitchBotAPI
|
||||
|
||||
from homeassistant.components.select import (
|
||||
DOMAIN as SELECT_DOMAIN,
|
||||
SERVICE_SELECT_OPTION,
|
||||
)
|
||||
from homeassistant.config_entries import ConfigEntryState
|
||||
from homeassistant.const import ATTR_ENTITY_ID
|
||||
from homeassistant.core import HomeAssistant
|
||||
|
||||
from . import configure_integration
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"device",
|
||||
[
|
||||
"Standing Fan",
|
||||
"Battery Circulator Fan",
|
||||
"Battery Circulator Fan 2 Pro",
|
||||
],
|
||||
)
|
||||
async def test_night_light_coordinator_data_is_none(
|
||||
hass: HomeAssistant,
|
||||
mock_list_devices: AsyncMock,
|
||||
mock_get_status: AsyncMock,
|
||||
device: str,
|
||||
) -> None:
|
||||
"""Test coordinator data is none."""
|
||||
|
||||
mock_list_devices.return_value = [
|
||||
Device(
|
||||
version="V1.0",
|
||||
deviceId="device-id-1",
|
||||
deviceName="device-1",
|
||||
deviceType=device,
|
||||
hubDeviceId="test-hub-id",
|
||||
),
|
||||
]
|
||||
mock_get_status.side_effect = [None, None]
|
||||
entry = await configure_integration(hass)
|
||||
assert entry.state is ConfigEntryState.LOADED
|
||||
entity_id = "select.device_1_night_light"
|
||||
state = hass.states.get(entity_id)
|
||||
assert state.state == "unknown"
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("device", "key_type", "expected"),
|
||||
[
|
||||
("Standing Fan", "on", "1"),
|
||||
("Standing Fan", "off", "off"),
|
||||
("Standing Fan", "bright", "1"),
|
||||
("Standing Fan", "soft", "2"),
|
||||
("Battery Circulator Fan 2 Pro", "bright", "0"),
|
||||
("Battery Circulator Fan 2 Pro", "soft", "1"),
|
||||
],
|
||||
)
|
||||
async def test_night_light_options(
|
||||
hass: HomeAssistant,
|
||||
mock_list_devices: AsyncMock,
|
||||
mock_get_status: AsyncMock,
|
||||
device: str,
|
||||
key_type: str,
|
||||
expected: str,
|
||||
) -> None:
|
||||
"""Test night light options."""
|
||||
mock_list_devices.return_value = [
|
||||
Device(
|
||||
version="V1.0",
|
||||
deviceId="device-id-1",
|
||||
deviceName="device-1",
|
||||
deviceType=device,
|
||||
hubDeviceId="test-hub-id",
|
||||
),
|
||||
]
|
||||
|
||||
mock_get_status.side_effect = [
|
||||
{
|
||||
"deviceId": "B0E9FEDEB68C",
|
||||
"deviceType": device,
|
||||
"power": "on",
|
||||
"fanSpeed": 3,
|
||||
"mode": "direct",
|
||||
"nightStatus": expected,
|
||||
},
|
||||
]
|
||||
entry = await configure_integration(hass)
|
||||
assert entry.state is ConfigEntryState.LOADED
|
||||
entity_id = "select.device_1_night_light"
|
||||
|
||||
with (
|
||||
patch.object(SwitchBotAPI, "send_command") as mocked_send_command,
|
||||
):
|
||||
await hass.services.async_call(
|
||||
SELECT_DOMAIN,
|
||||
SERVICE_SELECT_OPTION,
|
||||
{ATTR_ENTITY_ID: entity_id, "option": key_type},
|
||||
blocking=True,
|
||||
)
|
||||
|
||||
mocked_send_command.assert_awaited_once()
|
||||
assert mocked_send_command.await_args.args[3] == expected
|
||||
|
||||
state = hass.states.get(entity_id)
|
||||
assert state.state == key_type
|
||||
|
||||
|
||||
async def test_night_light_options_not_exist(
|
||||
hass: HomeAssistant,
|
||||
mock_list_devices: AsyncMock,
|
||||
mock_get_status: AsyncMock,
|
||||
) -> None:
|
||||
"""Test night light options."""
|
||||
mock_list_devices.return_value = [
|
||||
Device(
|
||||
version="V1.0",
|
||||
deviceId="standing-fan-id-1",
|
||||
deviceName="standing-fan-1",
|
||||
deviceType="Standing Fan",
|
||||
hubDeviceId="test-hub-id",
|
||||
),
|
||||
]
|
||||
|
||||
mock_get_status.side_effect = [
|
||||
{
|
||||
"deviceId": "B0E9FEDEB68C",
|
||||
"deviceType": "Standing Fan",
|
||||
"power": "on",
|
||||
"fanSpeed": 3,
|
||||
"mode": "direct",
|
||||
"nightStatus": "fake_option",
|
||||
},
|
||||
]
|
||||
entry = await configure_integration(hass)
|
||||
assert entry.state is ConfigEntryState.LOADED
|
||||
entity_id = "select.standing_fan_1_night_light"
|
||||
|
||||
state = hass.states.get(entity_id)
|
||||
assert state.state == "unknown"
|
||||
Reference in New Issue
Block a user