Skip to content

Commit 6deff1c

Browse files
Add smart radiator thermostat support to Switchbot Cloud (home-assistant#154445)
Co-authored-by: Joostlek <[email protected]>
1 parent f96996b commit 6deff1c

File tree

6 files changed

+341
-9
lines changed

6 files changed

+341
-9
lines changed

homeassistant/components/switchbot_cloud/__init__.py

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,9 @@ class SwitchbotDevices:
4848
default_factory=list
4949
)
5050
buttons: list[tuple[Device, SwitchBotCoordinator]] = field(default_factory=list)
51-
climates: list[tuple[Remote, SwitchBotCoordinator]] = field(default_factory=list)
51+
climates: list[tuple[Remote | Device, SwitchBotCoordinator]] = field(
52+
default_factory=list
53+
)
5254
covers: list[tuple[Device, SwitchBotCoordinator]] = field(default_factory=list)
5355
switches: list[tuple[Device | Remote, SwitchBotCoordinator]] = field(
5456
default_factory=list
@@ -121,6 +123,17 @@ async def make_device_data(
121123
hass, entry, api, device, coordinators_by_id
122124
)
123125
devices_data.climates.append((device, coordinator))
126+
127+
if (
128+
isinstance(device, Remote | Device)
129+
and device.device_type == "Smart Radiator Thermostat"
130+
):
131+
coordinator = await coordinator_for_device(
132+
hass, entry, api, device, coordinators_by_id
133+
)
134+
devices_data.climates.append((device, coordinator))
135+
devices_data.sensors.append((device, coordinator))
136+
124137
if (
125138
isinstance(device, Device)
126139
and (

homeassistant/components/switchbot_cloud/climate.py

Lines changed: 145 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,26 +1,46 @@
11
"""Support for SwitchBot Air Conditioner remotes."""
22

3+
import asyncio
34
from logging import getLogger
45
from typing import Any
56

6-
from switchbot_api import AirConditionerCommands
7+
from switchbot_api import (
8+
AirConditionerCommands,
9+
Device,
10+
Remote,
11+
SmartRadiatorThermostatCommands,
12+
SmartRadiatorThermostatMode,
13+
SwitchBotAPI,
14+
)
715

816
from homeassistant.components import climate as FanState
917
from homeassistant.components.climate import (
1018
ATTR_FAN_MODE,
1119
ATTR_TEMPERATURE,
20+
PRESET_AWAY,
21+
PRESET_BOOST,
22+
PRESET_COMFORT,
23+
PRESET_ECO,
24+
PRESET_HOME,
25+
PRESET_NONE,
26+
PRESET_SLEEP,
1227
ClimateEntity,
1328
ClimateEntityFeature,
1429
HVACMode,
1530
)
1631
from homeassistant.config_entries import ConfigEntry
17-
from homeassistant.const import STATE_UNAVAILABLE, STATE_UNKNOWN, UnitOfTemperature
18-
from homeassistant.core import HomeAssistant
32+
from homeassistant.const import (
33+
PRECISION_TENTHS,
34+
STATE_UNAVAILABLE,
35+
STATE_UNKNOWN,
36+
UnitOfTemperature,
37+
)
38+
from homeassistant.core import HomeAssistant, callback
1939
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
2040
from homeassistant.helpers.restore_state import RestoreEntity
2141

22-
from . import SwitchbotCloudData
23-
from .const import DOMAIN
42+
from . import SwitchbotCloudData, SwitchBotCoordinator
43+
from .const import DOMAIN, SMART_RADIATOR_THERMOSTAT_AFTER_COMMAND_REFRESH
2444
from .entity import SwitchBotCloudEntity
2545

2646
_LOGGER = getLogger(__name__)
@@ -53,7 +73,7 @@ async def async_setup_entry(
5373
"""Set up SwitchBot Cloud entry."""
5474
data: SwitchbotCloudData = hass.data[DOMAIN][config.entry_id]
5575
async_add_entities(
56-
SwitchBotCloudAirConditioner(data.api, device, coordinator)
76+
_async_make_entity(data.api, device, coordinator)
5777
for device, coordinator in data.devices.climates
5878
)
5979

@@ -178,3 +198,122 @@ async def async_turn_on(self) -> None:
178198
if hvac_mode == HVACMode.OFF:
179199
hvac_mode = HVACMode.FAN_ONLY
180200
await self.async_set_hvac_mode(hvac_mode)
201+
202+
203+
RADIATOR_PRESET_MODE_MAP: dict[str, SmartRadiatorThermostatMode] = {
204+
PRESET_NONE: SmartRadiatorThermostatMode.OFF,
205+
PRESET_ECO: SmartRadiatorThermostatMode.ENERGY_SAVING,
206+
PRESET_BOOST: SmartRadiatorThermostatMode.FAST_HEATING,
207+
PRESET_COMFORT: SmartRadiatorThermostatMode.COMFORT,
208+
PRESET_HOME: SmartRadiatorThermostatMode.MANUAL,
209+
}
210+
211+
RADIATOR_HA_PRESET_MODE_MAP = {
212+
value: key for key, value in RADIATOR_PRESET_MODE_MAP.items()
213+
}
214+
215+
216+
class SwitchBotCloudSmartRadiatorThermostat(SwitchBotCloudEntity, ClimateEntity):
217+
"""Representation of a Smart Radiator Thermostat."""
218+
219+
_attr_name = None
220+
221+
_attr_supported_features = (
222+
ClimateEntityFeature.TARGET_TEMPERATURE | ClimateEntityFeature.PRESET_MODE
223+
)
224+
225+
_attr_max_temp = 35
226+
_attr_min_temp = 4
227+
_attr_target_temperature_step = PRECISION_TENTHS
228+
_attr_temperature_unit = UnitOfTemperature.CELSIUS
229+
230+
_attr_preset_modes = [
231+
PRESET_NONE,
232+
PRESET_ECO,
233+
PRESET_AWAY,
234+
PRESET_BOOST,
235+
PRESET_COMFORT,
236+
PRESET_HOME,
237+
PRESET_SLEEP,
238+
]
239+
_attr_preset_mode = PRESET_HOME
240+
241+
_attr_hvac_modes = [
242+
HVACMode.OFF,
243+
HVACMode.HEAT,
244+
]
245+
246+
async def async_set_temperature(self, **kwargs: Any) -> None:
247+
"""Set target temperature."""
248+
self._attr_target_temperature = kwargs["temperature"]
249+
await self.send_api_command(
250+
command=SmartRadiatorThermostatCommands.SET_MANUAL_MODE_TEMPERATURE,
251+
parameters=str(self._attr_target_temperature),
252+
)
253+
254+
await asyncio.sleep(SMART_RADIATOR_THERMOSTAT_AFTER_COMMAND_REFRESH)
255+
await self.coordinator.async_request_refresh()
256+
257+
async def async_set_preset_mode(self, preset_mode: str) -> None:
258+
"""Set preset mode."""
259+
await self.send_api_command(
260+
command=SmartRadiatorThermostatCommands.SET_MODE,
261+
parameters=RADIATOR_PRESET_MODE_MAP[preset_mode].value,
262+
)
263+
self._attr_preset_mode = preset_mode
264+
265+
if self.preset_mode == PRESET_HOME:
266+
self._attr_target_temperature = self.current_temperature
267+
else:
268+
self._attr_target_temperature = None
269+
270+
await asyncio.sleep(SMART_RADIATOR_THERMOSTAT_AFTER_COMMAND_REFRESH)
271+
await self.coordinator.async_request_refresh()
272+
273+
async def async_set_hvac_mode(self, hvac_mode: HVACMode) -> None:
274+
"""Set target hvac mode."""
275+
if hvac_mode is HVACMode.OFF:
276+
await self.send_api_command(
277+
command=SmartRadiatorThermostatCommands.SET_MODE,
278+
parameters=RADIATOR_PRESET_MODE_MAP[PRESET_NONE].value,
279+
)
280+
self._attr_preset_mode = PRESET_NONE
281+
else:
282+
await self.send_api_command(
283+
command=SmartRadiatorThermostatCommands.SET_MODE,
284+
parameters=RADIATOR_PRESET_MODE_MAP[PRESET_BOOST].value,
285+
)
286+
self._attr_preset_mode = PRESET_BOOST
287+
self._attr_target_temperature = None
288+
self._attr_hvac_mode = hvac_mode
289+
await asyncio.sleep(SMART_RADIATOR_THERMOSTAT_AFTER_COMMAND_REFRESH)
290+
await self.coordinator.async_request_refresh()
291+
292+
def _set_attributes(self) -> None:
293+
"""Set attributes from coordinator data."""
294+
if self.coordinator.data is None:
295+
return
296+
mode: int = self.coordinator.data["mode"]
297+
temperature: str = self.coordinator.data["temperature"]
298+
self._attr_current_temperature = float(temperature)
299+
self._attr_preset_mode = RADIATOR_HA_PRESET_MODE_MAP[
300+
SmartRadiatorThermostatMode(mode)
301+
]
302+
303+
if self.preset_mode in [PRESET_NONE, PRESET_AWAY]:
304+
self._attr_hvac_mode = HVACMode.OFF
305+
else:
306+
self._attr_hvac_mode = HVACMode.HEAT
307+
if self.preset_mode == PRESET_HOME:
308+
self._attr_target_temperature = self._attr_current_temperature
309+
self.async_write_ha_state()
310+
311+
312+
@callback
313+
def _async_make_entity(
314+
api: SwitchBotAPI, device: Device | Remote, coordinator: SwitchBotCoordinator
315+
) -> SwitchBotCloudAirConditioner | SwitchBotCloudSmartRadiatorThermostat:
316+
"""Make a climate entity."""
317+
if device.device_type == "Smart Radiator Thermostat":
318+
return SwitchBotCloudSmartRadiatorThermostat(api, device, coordinator)
319+
return SwitchBotCloudAirConditioner(api, device, coordinator)

homeassistant/components/switchbot_cloud/const.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919

2020
AFTER_COMMAND_REFRESH = 5
2121
COVER_ENTITY_AFTER_COMMAND_REFRESH = 10
22+
SMART_RADIATOR_THERMOSTAT_AFTER_COMMAND_REFRESH = 30
2223

2324
HUMIDITY_LEVELS = {
2425
34: 101, # Low humidity mode

homeassistant/components/switchbot_cloud/sensor.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -245,6 +245,7 @@ class SwitchbotCloudSensorEntityDescription(SensorEntityDescription):
245245
HUMIDITY_DESCRIPTION,
246246
BATTERY_DESCRIPTION,
247247
),
248+
"Smart Radiator Thermostat": (BATTERY_DESCRIPTION,),
248249
}
249250

250251

tests/components/switchbot_cloud/conftest.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,3 +49,13 @@ def mock_after_command_refresh_for_cover():
4949
0,
5050
):
5151
yield
52+
53+
54+
@pytest.fixture(scope="package", autouse=True)
55+
def mock_after_command_refresh_for_smart_radiator_thermostat():
56+
"""Mock after command refresh."""
57+
with patch(
58+
"homeassistant.components.switchbot_cloud.const.SMART_RADIATOR_THERMOSTAT_AFTER_COMMAND_REFRESH",
59+
0,
60+
):
61+
yield

0 commit comments

Comments
 (0)