Add Switchbot Cloud AC Off (#138648)

Co-authored-by: Joost Lekkerkerker <joostlek@outlook.com>
Co-authored-by: Erik Montnemery <erik@montnemery.com>
This commit is contained in:
Ravaka Razafimanantsoa
2025-09-01 19:52:43 +02:00
committed by GitHub
co-authored by Joost Lekkerkerker Erik Montnemery
parent 3b60961f02
commit 095f73d84f
2 changed files with 229 additions and 6 deletions
@@ -1,24 +1,30 @@
"""Support for SwitchBot Air Conditioner remotes."""
from logging import getLogger
from typing import Any
from switchbot_api import AirConditionerCommands
from homeassistant.components import climate as FanState
from homeassistant.components.climate import (
ATTR_FAN_MODE,
ATTR_TEMPERATURE,
ClimateEntity,
ClimateEntityFeature,
HVACMode,
)
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import ATTR_TEMPERATURE, UnitOfTemperature
from homeassistant.const import STATE_UNAVAILABLE, STATE_UNKNOWN, UnitOfTemperature
from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from homeassistant.helpers.restore_state import RestoreEntity
from . import SwitchbotCloudData
from .const import DOMAIN
from .entity import SwitchBotCloudEntity
_LOGGER = getLogger(__name__)
_SWITCHBOT_HVAC_MODES: dict[HVACMode, int] = {
HVACMode.HEAT_COOL: 1,
HVACMode.COOL: 2,
@@ -52,7 +58,7 @@ async def async_setup_entry(
)
class SwitchBotCloudAirConditioner(SwitchBotCloudEntity, ClimateEntity):
class SwitchBotCloudAirConditioner(SwitchBotCloudEntity, ClimateEntity, RestoreEntity):
"""Representation of a SwitchBot air conditioner.
As it is an IR device, we don't know the actual state.
@@ -75,6 +81,7 @@ class SwitchBotCloudAirConditioner(SwitchBotCloudEntity, ClimateEntity):
HVACMode.DRY,
HVACMode.FAN_ONLY,
HVACMode.HEAT,
HVACMode.OFF,
]
_attr_hvac_mode = HVACMode.FAN_ONLY
_attr_temperature_unit = UnitOfTemperature.CELSIUS
@@ -83,6 +90,39 @@ class SwitchBotCloudAirConditioner(SwitchBotCloudEntity, ClimateEntity):
_attr_precision = 1
_attr_name = None
async def async_added_to_hass(self) -> None:
"""Run when entity about to be added."""
await super().async_added_to_hass()
if not (
last_state := await self.async_get_last_state()
) or last_state.state in (
STATE_UNAVAILABLE,
STATE_UNKNOWN,
):
return
_LOGGER.debug("Last state attributes: %s", last_state.attributes)
self._attr_hvac_mode = HVACMode(last_state.state)
self._attr_fan_mode = last_state.attributes.get(
ATTR_FAN_MODE, self._attr_fan_mode
)
self._attr_target_temperature = last_state.attributes.get(
ATTR_TEMPERATURE, self._attr_target_temperature
)
def _get_mode(self, hvac_mode: HVACMode | None) -> int:
new_hvac_mode = hvac_mode or self._attr_hvac_mode
_LOGGER.debug(
"Received hvac_mode: %s (Currently set as %s)",
hvac_mode,
self._attr_hvac_mode,
)
if new_hvac_mode == HVACMode.OFF:
return _SWITCHBOT_HVAC_MODES.get(
self._attr_hvac_mode, _DEFAULT_SWITCHBOT_HVAC_MODE
)
return _SWITCHBOT_HVAC_MODES.get(new_hvac_mode, _DEFAULT_SWITCHBOT_HVAC_MODE)
async def _do_send_command(
self,
hvac_mode: HVACMode | None = None,
@@ -90,15 +130,16 @@ class SwitchBotCloudAirConditioner(SwitchBotCloudEntity, ClimateEntity):
temperature: float | None = None,
) -> None:
new_temperature = temperature or self._attr_target_temperature
new_mode = _SWITCHBOT_HVAC_MODES.get(
hvac_mode or self._attr_hvac_mode, _DEFAULT_SWITCHBOT_HVAC_MODE
)
new_mode = self._get_mode(hvac_mode)
new_fan_speed = _SWITCHBOT_FAN_MODES.get(
fan_mode or self._attr_fan_mode, _DEFAULT_SWITCHBOT_FAN_MODE
)
new_power_state = "on" if hvac_mode != HVACMode.OFF else "off"
command = f"{int(new_temperature)},{new_mode},{new_fan_speed},{new_power_state}"
_LOGGER.debug("Sending command to %s: %s", self._attr_unique_id, command)
await self.send_api_command(
AirConditionerCommands.SET_ALL,
parameters=f"{int(new_temperature)},{new_mode},{new_fan_speed},on",
parameters=command,
)
async def async_set_hvac_mode(self, hvac_mode: HVACMode) -> None:
@@ -0,0 +1,182 @@
"""Test for the switchbot_cloud climate."""
from unittest.mock import patch
from switchbot_api import Remote
from homeassistant.components.climate import (
ATTR_FAN_MODE,
ATTR_HVAC_MODE,
ATTR_TEMPERATURE,
DOMAIN as CLIMATE_DOMAIN,
SERVICE_SET_FAN_MODE,
SERVICE_SET_HVAC_MODE,
SERVICE_SET_TEMPERATURE,
)
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
from . import configure_integration
from tests.common import mock_restore_cache
async def test_air_conditioner_set_hvac_mode(
hass: HomeAssistant, mock_list_devices, mock_get_status
) -> None:
"""Test setting HVAC mode for air conditioner."""
mock_list_devices.return_value = [
Remote(
deviceId="ac-device-id-1",
deviceName="climate-1",
remoteType="DIY Air Conditioner",
hubDeviceId="test-hub-id",
),
]
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, ATTR_HVAC_MODE: "cool"},
blocking=True,
)
mock_send_command.assert_called_once()
assert "21,2,1,on" in str(mock_send_command.call_args)
assert hass.states.get(entity_id).state == "cool"
# Test turning off
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, ATTR_HVAC_MODE: "off"},
blocking=True,
)
mock_send_command.assert_called_once()
assert "21,2,1,off" in str(mock_send_command.call_args)
assert hass.states.get(entity_id).state == "off"
async def test_air_conditioner_set_fan_mode(
hass: HomeAssistant, mock_list_devices, mock_get_status
) -> None:
"""Test setting fan mode for air conditioner."""
mock_list_devices.return_value = [
Remote(
deviceId="ac-device-id-1",
deviceName="climate-1",
remoteType="Air Conditioner",
hubDeviceId="test-hub-id",
),
]
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_FAN_MODE,
{ATTR_ENTITY_ID: entity_id, ATTR_FAN_MODE: "high"},
blocking=True,
)
mock_send_command.assert_called_once()
assert "21,4,4,on" in str(mock_send_command.call_args)
assert hass.states.get(entity_id).attributes[ATTR_FAN_MODE] == "high"
async def test_air_conditioner_set_temperature(
hass: HomeAssistant, mock_list_devices, mock_get_status
) -> None:
"""Test setting temperature for air conditioner."""
mock_list_devices.return_value = [
Remote(
deviceId="ac-device-id-1",
deviceName="climate-1",
remoteType="Air Conditioner",
hubDeviceId="test-hub-id",
),
]
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, ATTR_TEMPERATURE: 25},
blocking=True,
)
mock_send_command.assert_called_once()
assert "25,4,1,on" in str(mock_send_command.call_args)
assert hass.states.get(entity_id).attributes[ATTR_TEMPERATURE] == 25
async def test_air_conditioner_restore_state(
hass: HomeAssistant, mock_list_devices, mock_get_status
) -> None:
"""Test restoring state for air conditioner."""
mock_list_devices.return_value = [
Remote(
deviceId="ac-device-id-1",
deviceName="climate-1",
remoteType="Air Conditioner",
hubDeviceId="test-hub-id",
),
]
mock_state = State(
"climate.climate_1",
"cool",
{
ATTR_FAN_MODE: "high",
ATTR_TEMPERATURE: 25,
},
)
mock_restore_cache(hass, (mock_state,))
entry = await configure_integration(hass)
assert entry.state is ConfigEntryState.LOADED
entity_id = "climate.climate_1"
state = hass.states.get(entity_id)
assert state.state == "cool"
assert state.attributes[ATTR_FAN_MODE] == "high"
assert state.attributes[ATTR_TEMPERATURE] == 25
async def test_air_conditioner_no_last_state(
hass: HomeAssistant, mock_list_devices, mock_get_status
) -> None:
"""Test behavior when no previous state exists."""
mock_list_devices.return_value = [
Remote(
deviceId="ac-device-id-1",
deviceName="climate-1",
remoteType="Air Conditioner",
hubDeviceId="test-hub-id",
),
]
entry = await configure_integration(hass)
assert entry.state is ConfigEntryState.LOADED
entity_id = "climate.climate_1"
state = hass.states.get(entity_id)
assert state.state == "fan_only"
assert state.attributes[ATTR_FAN_MODE] == "auto"
assert state.attributes[ATTR_TEMPERATURE] == 21