Skip to content

Commit 095f73d

Browse files
SeraphicRavjoostlekemontnemery
authored
Add Switchbot Cloud AC Off (home-assistant#138648)
Co-authored-by: Joost Lekkerkerker <[email protected]> Co-authored-by: Erik Montnemery <[email protected]>
1 parent 3b60961 commit 095f73d

File tree

2 files changed

+229
-6
lines changed

2 files changed

+229
-6
lines changed

homeassistant/components/switchbot_cloud/climate.py

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

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

56
from switchbot_api import AirConditionerCommands
67

78
from homeassistant.components import climate as FanState
89
from homeassistant.components.climate import (
10+
ATTR_FAN_MODE,
11+
ATTR_TEMPERATURE,
912
ClimateEntity,
1013
ClimateEntityFeature,
1114
HVACMode,
1215
)
1316
from homeassistant.config_entries import ConfigEntry
14-
from homeassistant.const import ATTR_TEMPERATURE, UnitOfTemperature
17+
from homeassistant.const import STATE_UNAVAILABLE, STATE_UNKNOWN, UnitOfTemperature
1518
from homeassistant.core import HomeAssistant
1619
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
20+
from homeassistant.helpers.restore_state import RestoreEntity
1721

1822
from . import SwitchbotCloudData
1923
from .const import DOMAIN
2024
from .entity import SwitchBotCloudEntity
2125

26+
_LOGGER = getLogger(__name__)
27+
2228
_SWITCHBOT_HVAC_MODES: dict[HVACMode, int] = {
2329
HVACMode.HEAT_COOL: 1,
2430
HVACMode.COOL: 2,
@@ -52,7 +58,7 @@ async def async_setup_entry(
5258
)
5359

5460

55-
class SwitchBotCloudAirConditioner(SwitchBotCloudEntity, ClimateEntity):
61+
class SwitchBotCloudAirConditioner(SwitchBotCloudEntity, ClimateEntity, RestoreEntity):
5662
"""Representation of a SwitchBot air conditioner.
5763
5864
As it is an IR device, we don't know the actual state.
@@ -75,6 +81,7 @@ class SwitchBotCloudAirConditioner(SwitchBotCloudEntity, ClimateEntity):
7581
HVACMode.DRY,
7682
HVACMode.FAN_ONLY,
7783
HVACMode.HEAT,
84+
HVACMode.OFF,
7885
]
7986
_attr_hvac_mode = HVACMode.FAN_ONLY
8087
_attr_temperature_unit = UnitOfTemperature.CELSIUS
@@ -83,22 +90,56 @@ class SwitchBotCloudAirConditioner(SwitchBotCloudEntity, ClimateEntity):
8390
_attr_precision = 1
8491
_attr_name = None
8592

93+
async def async_added_to_hass(self) -> None:
94+
"""Run when entity about to be added."""
95+
await super().async_added_to_hass()
96+
97+
if not (
98+
last_state := await self.async_get_last_state()
99+
) or last_state.state in (
100+
STATE_UNAVAILABLE,
101+
STATE_UNKNOWN,
102+
):
103+
return
104+
_LOGGER.debug("Last state attributes: %s", last_state.attributes)
105+
self._attr_hvac_mode = HVACMode(last_state.state)
106+
self._attr_fan_mode = last_state.attributes.get(
107+
ATTR_FAN_MODE, self._attr_fan_mode
108+
)
109+
self._attr_target_temperature = last_state.attributes.get(
110+
ATTR_TEMPERATURE, self._attr_target_temperature
111+
)
112+
113+
def _get_mode(self, hvac_mode: HVACMode | None) -> int:
114+
new_hvac_mode = hvac_mode or self._attr_hvac_mode
115+
_LOGGER.debug(
116+
"Received hvac_mode: %s (Currently set as %s)",
117+
hvac_mode,
118+
self._attr_hvac_mode,
119+
)
120+
if new_hvac_mode == HVACMode.OFF:
121+
return _SWITCHBOT_HVAC_MODES.get(
122+
self._attr_hvac_mode, _DEFAULT_SWITCHBOT_HVAC_MODE
123+
)
124+
return _SWITCHBOT_HVAC_MODES.get(new_hvac_mode, _DEFAULT_SWITCHBOT_HVAC_MODE)
125+
86126
async def _do_send_command(
87127
self,
88128
hvac_mode: HVACMode | None = None,
89129
fan_mode: str | None = None,
90130
temperature: float | None = None,
91131
) -> None:
92132
new_temperature = temperature or self._attr_target_temperature
93-
new_mode = _SWITCHBOT_HVAC_MODES.get(
94-
hvac_mode or self._attr_hvac_mode, _DEFAULT_SWITCHBOT_HVAC_MODE
95-
)
133+
new_mode = self._get_mode(hvac_mode)
96134
new_fan_speed = _SWITCHBOT_FAN_MODES.get(
97135
fan_mode or self._attr_fan_mode, _DEFAULT_SWITCHBOT_FAN_MODE
98136
)
137+
new_power_state = "on" if hvac_mode != HVACMode.OFF else "off"
138+
command = f"{int(new_temperature)},{new_mode},{new_fan_speed},{new_power_state}"
139+
_LOGGER.debug("Sending command to %s: %s", self._attr_unique_id, command)
99140
await self.send_api_command(
100141
AirConditionerCommands.SET_ALL,
101-
parameters=f"{int(new_temperature)},{new_mode},{new_fan_speed},on",
142+
parameters=command,
102143
)
103144

104145
async def async_set_hvac_mode(self, hvac_mode: HVACMode) -> None:
Lines changed: 182 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,182 @@
1+
"""Test for the switchbot_cloud climate."""
2+
3+
from unittest.mock import patch
4+
5+
from switchbot_api import Remote
6+
7+
from homeassistant.components.climate import (
8+
ATTR_FAN_MODE,
9+
ATTR_HVAC_MODE,
10+
ATTR_TEMPERATURE,
11+
DOMAIN as CLIMATE_DOMAIN,
12+
SERVICE_SET_FAN_MODE,
13+
SERVICE_SET_HVAC_MODE,
14+
SERVICE_SET_TEMPERATURE,
15+
)
16+
from homeassistant.components.switchbot_cloud import SwitchBotAPI
17+
from homeassistant.config_entries import ConfigEntryState
18+
from homeassistant.const import ATTR_ENTITY_ID
19+
from homeassistant.core import HomeAssistant, State
20+
21+
from . import configure_integration
22+
23+
from tests.common import mock_restore_cache
24+
25+
26+
async def test_air_conditioner_set_hvac_mode(
27+
hass: HomeAssistant, mock_list_devices, mock_get_status
28+
) -> None:
29+
"""Test setting HVAC mode for air conditioner."""
30+
mock_list_devices.return_value = [
31+
Remote(
32+
deviceId="ac-device-id-1",
33+
deviceName="climate-1",
34+
remoteType="DIY Air Conditioner",
35+
hubDeviceId="test-hub-id",
36+
),
37+
]
38+
39+
entry = await configure_integration(hass)
40+
assert entry.state is ConfigEntryState.LOADED
41+
42+
entity_id = "climate.climate_1"
43+
44+
with patch.object(SwitchBotAPI, "send_command") as mock_send_command:
45+
await hass.services.async_call(
46+
CLIMATE_DOMAIN,
47+
SERVICE_SET_HVAC_MODE,
48+
{ATTR_ENTITY_ID: entity_id, ATTR_HVAC_MODE: "cool"},
49+
blocking=True,
50+
)
51+
mock_send_command.assert_called_once()
52+
assert "21,2,1,on" in str(mock_send_command.call_args)
53+
54+
assert hass.states.get(entity_id).state == "cool"
55+
56+
# Test turning off
57+
with patch.object(SwitchBotAPI, "send_command") as mock_send_command:
58+
await hass.services.async_call(
59+
CLIMATE_DOMAIN,
60+
SERVICE_SET_HVAC_MODE,
61+
{ATTR_ENTITY_ID: entity_id, ATTR_HVAC_MODE: "off"},
62+
blocking=True,
63+
)
64+
mock_send_command.assert_called_once()
65+
assert "21,2,1,off" in str(mock_send_command.call_args)
66+
67+
assert hass.states.get(entity_id).state == "off"
68+
69+
70+
async def test_air_conditioner_set_fan_mode(
71+
hass: HomeAssistant, mock_list_devices, mock_get_status
72+
) -> None:
73+
"""Test setting fan mode for air conditioner."""
74+
mock_list_devices.return_value = [
75+
Remote(
76+
deviceId="ac-device-id-1",
77+
deviceName="climate-1",
78+
remoteType="Air Conditioner",
79+
hubDeviceId="test-hub-id",
80+
),
81+
]
82+
83+
entry = await configure_integration(hass)
84+
assert entry.state is ConfigEntryState.LOADED
85+
entity_id = "climate.climate_1"
86+
87+
with patch.object(SwitchBotAPI, "send_command") as mock_send_command:
88+
await hass.services.async_call(
89+
CLIMATE_DOMAIN,
90+
SERVICE_SET_FAN_MODE,
91+
{ATTR_ENTITY_ID: entity_id, ATTR_FAN_MODE: "high"},
92+
blocking=True,
93+
)
94+
mock_send_command.assert_called_once()
95+
assert "21,4,4,on" in str(mock_send_command.call_args)
96+
97+
assert hass.states.get(entity_id).attributes[ATTR_FAN_MODE] == "high"
98+
99+
100+
async def test_air_conditioner_set_temperature(
101+
hass: HomeAssistant, mock_list_devices, mock_get_status
102+
) -> None:
103+
"""Test setting temperature for air conditioner."""
104+
mock_list_devices.return_value = [
105+
Remote(
106+
deviceId="ac-device-id-1",
107+
deviceName="climate-1",
108+
remoteType="Air Conditioner",
109+
hubDeviceId="test-hub-id",
110+
),
111+
]
112+
113+
entry = await configure_integration(hass)
114+
assert entry.state is ConfigEntryState.LOADED
115+
entity_id = "climate.climate_1"
116+
117+
with patch.object(SwitchBotAPI, "send_command") as mock_send_command:
118+
await hass.services.async_call(
119+
CLIMATE_DOMAIN,
120+
SERVICE_SET_TEMPERATURE,
121+
{ATTR_ENTITY_ID: entity_id, ATTR_TEMPERATURE: 25},
122+
blocking=True,
123+
)
124+
mock_send_command.assert_called_once()
125+
assert "25,4,1,on" in str(mock_send_command.call_args)
126+
127+
assert hass.states.get(entity_id).attributes[ATTR_TEMPERATURE] == 25
128+
129+
130+
async def test_air_conditioner_restore_state(
131+
hass: HomeAssistant, mock_list_devices, mock_get_status
132+
) -> None:
133+
"""Test restoring state for air conditioner."""
134+
mock_list_devices.return_value = [
135+
Remote(
136+
deviceId="ac-device-id-1",
137+
deviceName="climate-1",
138+
remoteType="Air Conditioner",
139+
hubDeviceId="test-hub-id",
140+
),
141+
]
142+
143+
mock_state = State(
144+
"climate.climate_1",
145+
"cool",
146+
{
147+
ATTR_FAN_MODE: "high",
148+
ATTR_TEMPERATURE: 25,
149+
},
150+
)
151+
152+
mock_restore_cache(hass, (mock_state,))
153+
entry = await configure_integration(hass)
154+
assert entry.state is ConfigEntryState.LOADED
155+
entity_id = "climate.climate_1"
156+
state = hass.states.get(entity_id)
157+
assert state.state == "cool"
158+
assert state.attributes[ATTR_FAN_MODE] == "high"
159+
assert state.attributes[ATTR_TEMPERATURE] == 25
160+
161+
162+
async def test_air_conditioner_no_last_state(
163+
hass: HomeAssistant, mock_list_devices, mock_get_status
164+
) -> None:
165+
"""Test behavior when no previous state exists."""
166+
mock_list_devices.return_value = [
167+
Remote(
168+
deviceId="ac-device-id-1",
169+
deviceName="climate-1",
170+
remoteType="Air Conditioner",
171+
hubDeviceId="test-hub-id",
172+
),
173+
]
174+
175+
entry = await configure_integration(hass)
176+
assert entry.state is ConfigEntryState.LOADED
177+
178+
entity_id = "climate.climate_1"
179+
state = hass.states.get(entity_id)
180+
assert state.state == "fan_only"
181+
assert state.attributes[ATTR_FAN_MODE] == "auto"
182+
assert state.attributes[ATTR_TEMPERATURE] == 21

0 commit comments

Comments
 (0)