1
0
mirror of https://github.com/home-assistant/core.git synced 2025-12-24 21:06:19 +00:00

Add smart radiator thermostat support to Switchbot Cloud (#154445)

Co-authored-by: Joostlek <joostlek@outlook.com>
This commit is contained in:
Samuel Xiao
2025-11-26 00:46:55 +08:00
committed by GitHub
parent f96996b27f
commit 6deff1c78f
6 changed files with 341 additions and 9 deletions

View File

@@ -48,7 +48,9 @@ class SwitchbotDevices:
default_factory=list
)
buttons: list[tuple[Device, SwitchBotCoordinator]] = field(default_factory=list)
climates: list[tuple[Remote, SwitchBotCoordinator]] = field(default_factory=list)
climates: list[tuple[Remote | Device, SwitchBotCoordinator]] = field(
default_factory=list
)
covers: list[tuple[Device, SwitchBotCoordinator]] = field(default_factory=list)
switches: list[tuple[Device | Remote, SwitchBotCoordinator]] = field(
default_factory=list
@@ -121,6 +123,17 @@ async def make_device_data(
hass, entry, api, device, coordinators_by_id
)
devices_data.climates.append((device, coordinator))
if (
isinstance(device, Remote | Device)
and device.device_type == "Smart Radiator Thermostat"
):
coordinator = await coordinator_for_device(
hass, entry, api, device, coordinators_by_id
)
devices_data.climates.append((device, coordinator))
devices_data.sensors.append((device, coordinator))
if (
isinstance(device, Device)
and (

View File

@@ -1,26 +1,46 @@
"""Support for SwitchBot Air Conditioner remotes."""
import asyncio
from logging import getLogger
from typing import Any
from switchbot_api import AirConditionerCommands
from switchbot_api import (
AirConditionerCommands,
Device,
Remote,
SmartRadiatorThermostatCommands,
SmartRadiatorThermostatMode,
SwitchBotAPI,
)
from homeassistant.components import climate as FanState
from homeassistant.components.climate import (
ATTR_FAN_MODE,
ATTR_TEMPERATURE,
PRESET_AWAY,
PRESET_BOOST,
PRESET_COMFORT,
PRESET_ECO,
PRESET_HOME,
PRESET_NONE,
PRESET_SLEEP,
ClimateEntity,
ClimateEntityFeature,
HVACMode,
)
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import STATE_UNAVAILABLE, STATE_UNKNOWN, UnitOfTemperature
from homeassistant.core import HomeAssistant
from homeassistant.const import (
PRECISION_TENTHS,
STATE_UNAVAILABLE,
STATE_UNKNOWN,
UnitOfTemperature,
)
from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from homeassistant.helpers.restore_state import RestoreEntity
from . import SwitchbotCloudData
from .const import DOMAIN
from . import SwitchbotCloudData, SwitchBotCoordinator
from .const import DOMAIN, SMART_RADIATOR_THERMOSTAT_AFTER_COMMAND_REFRESH
from .entity import SwitchBotCloudEntity
_LOGGER = getLogger(__name__)
@@ -53,7 +73,7 @@ async def async_setup_entry(
"""Set up SwitchBot Cloud entry."""
data: SwitchbotCloudData = hass.data[DOMAIN][config.entry_id]
async_add_entities(
SwitchBotCloudAirConditioner(data.api, device, coordinator)
_async_make_entity(data.api, device, coordinator)
for device, coordinator in data.devices.climates
)
@@ -178,3 +198,122 @@ class SwitchBotCloudAirConditioner(SwitchBotCloudEntity, ClimateEntity, RestoreE
if hvac_mode == HVACMode.OFF:
hvac_mode = HVACMode.FAN_ONLY
await self.async_set_hvac_mode(hvac_mode)
RADIATOR_PRESET_MODE_MAP: dict[str, SmartRadiatorThermostatMode] = {
PRESET_NONE: SmartRadiatorThermostatMode.OFF,
PRESET_ECO: SmartRadiatorThermostatMode.ENERGY_SAVING,
PRESET_BOOST: SmartRadiatorThermostatMode.FAST_HEATING,
PRESET_COMFORT: SmartRadiatorThermostatMode.COMFORT,
PRESET_HOME: SmartRadiatorThermostatMode.MANUAL,
}
RADIATOR_HA_PRESET_MODE_MAP = {
value: key for key, value in RADIATOR_PRESET_MODE_MAP.items()
}
class SwitchBotCloudSmartRadiatorThermostat(SwitchBotCloudEntity, ClimateEntity):
"""Representation of a Smart Radiator Thermostat."""
_attr_name = None
_attr_supported_features = (
ClimateEntityFeature.TARGET_TEMPERATURE | ClimateEntityFeature.PRESET_MODE
)
_attr_max_temp = 35
_attr_min_temp = 4
_attr_target_temperature_step = PRECISION_TENTHS
_attr_temperature_unit = UnitOfTemperature.CELSIUS
_attr_preset_modes = [
PRESET_NONE,
PRESET_ECO,
PRESET_AWAY,
PRESET_BOOST,
PRESET_COMFORT,
PRESET_HOME,
PRESET_SLEEP,
]
_attr_preset_mode = PRESET_HOME
_attr_hvac_modes = [
HVACMode.OFF,
HVACMode.HEAT,
]
async def async_set_temperature(self, **kwargs: Any) -> None:
"""Set target temperature."""
self._attr_target_temperature = kwargs["temperature"]
await self.send_api_command(
command=SmartRadiatorThermostatCommands.SET_MANUAL_MODE_TEMPERATURE,
parameters=str(self._attr_target_temperature),
)
await asyncio.sleep(SMART_RADIATOR_THERMOSTAT_AFTER_COMMAND_REFRESH)
await self.coordinator.async_request_refresh()
async def async_set_preset_mode(self, preset_mode: str) -> None:
"""Set preset mode."""
await self.send_api_command(
command=SmartRadiatorThermostatCommands.SET_MODE,
parameters=RADIATOR_PRESET_MODE_MAP[preset_mode].value,
)
self._attr_preset_mode = preset_mode
if self.preset_mode == PRESET_HOME:
self._attr_target_temperature = self.current_temperature
else:
self._attr_target_temperature = None
await asyncio.sleep(SMART_RADIATOR_THERMOSTAT_AFTER_COMMAND_REFRESH)
await self.coordinator.async_request_refresh()
async def async_set_hvac_mode(self, hvac_mode: HVACMode) -> None:
"""Set target hvac mode."""
if hvac_mode is HVACMode.OFF:
await self.send_api_command(
command=SmartRadiatorThermostatCommands.SET_MODE,
parameters=RADIATOR_PRESET_MODE_MAP[PRESET_NONE].value,
)
self._attr_preset_mode = PRESET_NONE
else:
await self.send_api_command(
command=SmartRadiatorThermostatCommands.SET_MODE,
parameters=RADIATOR_PRESET_MODE_MAP[PRESET_BOOST].value,
)
self._attr_preset_mode = PRESET_BOOST
self._attr_target_temperature = None
self._attr_hvac_mode = hvac_mode
await asyncio.sleep(SMART_RADIATOR_THERMOSTAT_AFTER_COMMAND_REFRESH)
await self.coordinator.async_request_refresh()
def _set_attributes(self) -> None:
"""Set attributes from coordinator data."""
if self.coordinator.data is None:
return
mode: int = self.coordinator.data["mode"]
temperature: str = self.coordinator.data["temperature"]
self._attr_current_temperature = float(temperature)
self._attr_preset_mode = RADIATOR_HA_PRESET_MODE_MAP[
SmartRadiatorThermostatMode(mode)
]
if self.preset_mode in [PRESET_NONE, PRESET_AWAY]:
self._attr_hvac_mode = HVACMode.OFF
else:
self._attr_hvac_mode = HVACMode.HEAT
if self.preset_mode == PRESET_HOME:
self._attr_target_temperature = self._attr_current_temperature
self.async_write_ha_state()
@callback
def _async_make_entity(
api: SwitchBotAPI, device: Device | Remote, coordinator: SwitchBotCoordinator
) -> SwitchBotCloudAirConditioner | SwitchBotCloudSmartRadiatorThermostat:
"""Make a climate entity."""
if device.device_type == "Smart Radiator Thermostat":
return SwitchBotCloudSmartRadiatorThermostat(api, device, coordinator)
return SwitchBotCloudAirConditioner(api, device, coordinator)

View File

@@ -19,6 +19,7 @@ VACUUM_FAN_SPEED_MAX = "max"
AFTER_COMMAND_REFRESH = 5
COVER_ENTITY_AFTER_COMMAND_REFRESH = 10
SMART_RADIATOR_THERMOSTAT_AFTER_COMMAND_REFRESH = 30
HUMIDITY_LEVELS = {
34: 101, # Low humidity mode

View File

@@ -245,6 +245,7 @@ SENSOR_DESCRIPTIONS_BY_DEVICE_TYPES = {
HUMIDITY_DESCRIPTION,
BATTERY_DESCRIPTION,
),
"Smart Radiator Thermostat": (BATTERY_DESCRIPTION,),
}

View File

@@ -49,3 +49,13 @@ def mock_after_command_refresh_for_cover():
0,
):
yield
@pytest.fixture(scope="package", autouse=True)
def mock_after_command_refresh_for_smart_radiator_thermostat():
"""Mock after command refresh."""
with patch(
"homeassistant.components.switchbot_cloud.const.SMART_RADIATOR_THERMOSTAT_AFTER_COMMAND_REFRESH",
0,
):
yield

View File

@@ -2,7 +2,7 @@
from unittest.mock import patch
from switchbot_api import Remote
from switchbot_api import Device, Remote, SmartRadiatorThermostatCommands, SwitchBotAPI
from homeassistant.components.climate import (
ATTR_FAN_MODE,
@@ -11,11 +11,12 @@ from homeassistant.components.climate import (
DOMAIN as CLIMATE_DOMAIN,
SERVICE_SET_FAN_MODE,
SERVICE_SET_HVAC_MODE,
SERVICE_SET_PRESET_MODE,
SERVICE_SET_TEMPERATURE,
SERVICE_TURN_OFF,
SERVICE_TURN_ON,
HVACMode,
)
from homeassistant.components.switchbot_cloud import SwitchBotAPI
from homeassistant.config_entries import ConfigEntryState
from homeassistant.const import ATTR_ENTITY_ID
from homeassistant.core import HomeAssistant, State
@@ -296,3 +297,170 @@ async def test_air_conditioner_turn_on_from_hvac_mode_off(
assert "25,4,4,on" in str(mock_turn_on_command.call_args)
assert hass.states.get(entity_id).state == "fan_only"
async def test_smart_radiator_thermostat_set_temperature(
hass: HomeAssistant, mock_list_devices, mock_get_status
) -> None:
"""Test smart radiator thermostat set temperature."""
mock_list_devices.return_value = [
Device(
version="V1.0",
deviceId="ac-device-id-1",
deviceName="climate-1",
deviceType="Smart Radiator Thermostat",
hubDeviceId="test-hub-id",
),
]
mock_get_status.side_effect = [
{
"mode": 1,
"temperature": 27.5,
},
{
"mode": 1,
"temperature": 27.5,
},
{
"mode": 2,
"temperature": 27.5,
},
]
entry = await configure_integration(hass)
assert entry.state is ConfigEntryState.LOADED
entity_id = "climate.climate_1"
with patch.object(SwitchBotAPI, "send_command") as mock_send_command:
await hass.services.async_call(
CLIMATE_DOMAIN,
SERVICE_SET_TEMPERATURE,
{ATTR_ENTITY_ID: entity_id, "temperature": 27},
)
mock_send_command.assert_called_once_with(
"ac-device-id-1",
SmartRadiatorThermostatCommands.SET_MANUAL_MODE_TEMPERATURE,
"command",
"27.0",
)
async def test_smart_radiator_thermostat_set_preset_mode(
hass: HomeAssistant, mock_list_devices, mock_get_status
) -> None:
"""Test smart radiator thermostat set preset mode."""
mock_list_devices.return_value = [
Device(
version="V1.0",
deviceId="ac-device-id-1",
deviceName="climate-1",
deviceType="Smart Radiator Thermostat",
hubDeviceId="test-hub-id",
),
]
mock_get_status.side_effect = [
{
"mode": 1,
"temperature": 27.5,
},
{
"mode": 1,
"temperature": 27.5,
},
{
"mode": 2,
"temperature": 27.5,
},
]
entry = await configure_integration(hass)
assert entry.state is ConfigEntryState.LOADED
entity_id = "climate.climate_1"
with patch.object(SwitchBotAPI, "send_command") as mock_send_command:
await hass.services.async_call(
CLIMATE_DOMAIN,
SERVICE_SET_PRESET_MODE,
{ATTR_ENTITY_ID: entity_id, "preset_mode": "none"},
)
mock_send_command.assert_called_once_with(
"ac-device-id-1",
SmartRadiatorThermostatCommands.SET_MODE,
"command",
2,
)
with patch.object(SwitchBotAPI, "send_command") as mock_send_command:
await hass.services.async_call(
CLIMATE_DOMAIN,
SERVICE_SET_PRESET_MODE,
{ATTR_ENTITY_ID: entity_id, "preset_mode": "home"},
)
mock_send_command.assert_called_once_with(
"ac-device-id-1",
SmartRadiatorThermostatCommands.SET_MODE,
"command",
1,
)
async def test_smart_radiator_thermostat_set_hvac_mode(
hass: HomeAssistant, mock_list_devices, mock_get_status
) -> None:
"""Test smart radiator thermostat set hvac mode."""
mock_list_devices.return_value = [
Device(
version="V1.0",
deviceId="ac-device-id-1",
deviceName="climate-1",
deviceType="Smart Radiator Thermostat",
hubDeviceId="test-hub-id",
),
]
mock_get_status.side_effect = [
{
"mode": 2,
"temperature": 27.5,
},
{
"mode": 2,
"temperature": 27.5,
},
{
"mode": 2,
"temperature": 27.5,
},
]
entry = await configure_integration(hass)
assert entry.state is ConfigEntryState.LOADED
entity_id = "climate.climate_1"
with patch.object(SwitchBotAPI, "send_command") as mock_send_command:
await hass.services.async_call(
CLIMATE_DOMAIN,
SERVICE_SET_HVAC_MODE,
{ATTR_ENTITY_ID: entity_id, "hvac_mode": HVACMode.OFF},
)
mock_send_command.assert_called_once_with(
"ac-device-id-1",
SmartRadiatorThermostatCommands.SET_MODE,
"command",
2,
)
with patch.object(SwitchBotAPI, "send_command") as mock_send_command:
await hass.services.async_call(
CLIMATE_DOMAIN,
SERVICE_SET_HVAC_MODE,
{ATTR_ENTITY_ID: entity_id, "hvac_mode": HVACMode.HEAT},
)
mock_send_command.assert_called_once_with(
"ac-device-id-1",
SmartRadiatorThermostatCommands.SET_MODE,
"command",
5,
)