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:
@@ -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 (
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -245,6 +245,7 @@ SENSOR_DESCRIPTIONS_BY_DEVICE_TYPES = {
|
||||
HUMIDITY_DESCRIPTION,
|
||||
BATTERY_DESCRIPTION,
|
||||
),
|
||||
"Smart Radiator Thermostat": (BATTERY_DESCRIPTION,),
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user