Add Climate to switchbot cloud integration (#101660)

This commit is contained in:
Ravaka Razafimanantsoa 2023-10-25 13:46:00 +09:00 committed by GitHub
parent 0cb0e3ceeb
commit 7038bd67f7
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 178 additions and 27 deletions

View File

@ -1264,6 +1264,7 @@ omit =
homeassistant/components/switchbot/sensor.py
homeassistant/components/switchbot/switch.py
homeassistant/components/switchbot/lock.py
homeassistant/components/switchbot_cloud/climate.py
homeassistant/components/switchbot_cloud/coordinator.py
homeassistant/components/switchbot_cloud/entity.py
homeassistant/components/switchbot_cloud/switch.py

View File

@ -1,27 +1,28 @@
"""The SwitchBot via API integration."""
from asyncio import gather
from dataclasses import dataclass
from dataclasses import dataclass, field
from logging import getLogger
from switchbot_api import CannotConnect, Device, InvalidAuth, Remote, SwitchBotAPI
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import CONF_API_KEY, CONF_API_TOKEN, Platform
from homeassistant.core import HomeAssistant
from homeassistant.core import HomeAssistant, callback
from homeassistant.exceptions import ConfigEntryNotReady
from .const import DOMAIN
from .coordinator import SwitchBotCoordinator
_LOGGER = getLogger(__name__)
PLATFORMS: list[Platform] = [Platform.SWITCH]
PLATFORMS: list[Platform] = [Platform.CLIMATE, Platform.SWITCH]
@dataclass
class SwitchbotDevices:
"""Switchbot devices data."""
switches: list[Device | Remote]
climates: list[Remote] = field(default_factory=list)
switches: list[Device | Remote] = field(default_factory=list)
@dataclass
@ -32,18 +33,47 @@ class SwitchbotCloudData:
devices: SwitchbotDevices
@callback
def prepare_device(
hass: HomeAssistant,
api: SwitchBotAPI,
device: Device | Remote,
coordinators: list[SwitchBotCoordinator],
coordinators_by_id: dict[str, SwitchBotCoordinator],
) -> tuple[Device | Remote, SwitchBotCoordinator]:
"""Instantiate coordinator and adds to list for gathering."""
coordinator = SwitchBotCoordinator(hass, api, device)
coordinators.append(coordinator)
coordinator = coordinators_by_id.setdefault(
device.device_id, SwitchBotCoordinator(hass, api, device)
)
return (device, coordinator)
@callback
def make_device_data(
hass: HomeAssistant,
api: SwitchBotAPI,
devices: list[Device | Remote],
coordinators_by_id: dict[str, SwitchBotCoordinator],
) -> SwitchbotDevices:
"""Make device data."""
devices_data = SwitchbotDevices()
for device in devices:
if isinstance(device, Remote) and device.device_type.endswith(
"Air Conditioner"
):
devices_data.climates.append(
prepare_device(hass, api, device, coordinators_by_id)
)
if (
isinstance(device, Device)
and device.device_type.startswith("Plug")
or isinstance(device, Remote)
):
devices_data.switches.append(
prepare_device(hass, api, device, coordinators_by_id)
)
return devices_data
async def async_setup_entry(hass: HomeAssistant, config: ConfigEntry) -> bool:
"""Set up SwitchBot via API from a config entry."""
token = config.data[CONF_API_TOKEN]
@ -60,25 +90,15 @@ async def async_setup_entry(hass: HomeAssistant, config: ConfigEntry) -> bool:
except CannotConnect as ex:
raise ConfigEntryNotReady from ex
_LOGGER.debug("Devices: %s", devices)
coordinators: list[SwitchBotCoordinator] = []
coordinators_by_id: dict[str, SwitchBotCoordinator] = {}
hass.data.setdefault(DOMAIN, {})
data = SwitchbotCloudData(
api=api,
devices=SwitchbotDevices(
switches=[
prepare_device(hass, api, device, coordinators)
for device in devices
if isinstance(device, Device)
and device.device_type.startswith("Plug")
or isinstance(device, Remote)
],
),
hass.data[DOMAIN][config.entry_id] = SwitchbotCloudData(
api=api, devices=make_device_data(hass, api, devices, coordinators_by_id)
)
hass.data[DOMAIN][config.entry_id] = data
for device_type, devices in vars(data.devices).items():
_LOGGER.debug("%s: %s", device_type, devices)
await hass.config_entries.async_forward_entry_setups(config, PLATFORMS)
await gather(*[coordinator.async_refresh() for coordinator in coordinators])
await gather(
*[coordinator.async_refresh() for coordinator in coordinators_by_id.values()]
)
return True

View File

@ -0,0 +1,118 @@
"""Support for SwitchBot Air Conditioner remotes."""
from typing import Any
from switchbot_api import AirConditionerCommands
import homeassistant.components.climate as FanState
from homeassistant.components.climate import (
ClimateEntity,
ClimateEntityFeature,
HVACMode,
)
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import ATTR_TEMPERATURE, UnitOfTemperature
from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.helpers.typing import DiscoveryInfoType
from . import SwitchbotCloudData
from .const import DOMAIN
from .entity import SwitchBotCloudEntity
_SWITCHBOT_HVAC_MODES: dict[HVACMode, int] = {
HVACMode.HEAT_COOL: 1,
HVACMode.COOL: 2,
HVACMode.DRY: 3,
HVACMode.FAN_ONLY: 4,
HVACMode.HEAT: 5,
}
_DEFAULT_SWITCHBOT_HVAC_MODE = _SWITCHBOT_HVAC_MODES[HVACMode.FAN_ONLY]
_SWITCHBOT_FAN_MODES: dict[str, int] = {
FanState.FAN_AUTO: 1,
FanState.FAN_LOW: 2,
FanState.FAN_MEDIUM: 3,
FanState.FAN_HIGH: 4,
}
_DEFAULT_SWITCHBOT_FAN_MODE = _SWITCHBOT_FAN_MODES[FanState.FAN_AUTO]
async def async_setup_entry(
hass: HomeAssistant,
config: ConfigEntry,
async_add_entities: AddEntitiesCallback,
discovery_info: DiscoveryInfoType | None = None,
) -> None:
"""Set up SwitchBot Cloud entry."""
data: SwitchbotCloudData = hass.data[DOMAIN][config.entry_id]
async_add_entities(
SwitchBotCloudAirConditionner(data.api, device, coordinator)
for device, coordinator in data.devices.climates
)
class SwitchBotCloudAirConditionner(SwitchBotCloudEntity, ClimateEntity):
"""Representation of a SwitchBot air conditionner, as it is an IR device, we don't know the actual state."""
_attr_assumed_state = True
_attr_supported_features = (
ClimateEntityFeature.FAN_MODE | ClimateEntityFeature.TARGET_TEMPERATURE
)
_attr_fan_modes = [
FanState.FAN_AUTO,
FanState.FAN_LOW,
FanState.FAN_MEDIUM,
FanState.FAN_HIGH,
]
_attr_fan_mode = FanState.FAN_AUTO
_attr_hvac_modes = [
HVACMode.HEAT_COOL,
HVACMode.COOL,
HVACMode.DRY,
HVACMode.FAN_ONLY,
HVACMode.HEAT,
]
_attr_hvac_mode = HVACMode.FAN_ONLY
_attr_temperature_unit = UnitOfTemperature.CELSIUS
_attr_target_temperature = 21
_attr_name = None
async def _do_send_command(
self,
hvac_mode: HVACMode | None = None,
fan_mode: str | None = None,
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_fan_speed = _SWITCHBOT_FAN_MODES.get(
fan_mode or self._attr_fan_mode, _DEFAULT_SWITCHBOT_FAN_MODE
)
await self.send_command(
AirConditionerCommands.SET_ALL,
parameters=f"{new_temperature},{new_mode},{new_fan_speed},on",
)
async def async_set_hvac_mode(self, hvac_mode: HVACMode) -> None:
"""Set target hvac mode."""
await self._do_send_command(hvac_mode=hvac_mode)
self._attr_hvac_mode = hvac_mode
self.async_write_ha_state()
async def async_set_fan_mode(self, fan_mode: str) -> None:
"""Set target fan mode."""
await self._do_send_command(fan_mode=fan_mode)
self._attr_fan_mode = fan_mode
self.async_write_ha_state()
async def async_set_temperature(self, **kwargs: Any) -> None:
"""Set target temperature."""
if (temperature := kwargs.get(ATTR_TEMPERATURE)) is None:
return
await self._do_send_command(temperature=temperature)
self._attr_target_temperature = temperature

View File

@ -3,7 +3,7 @@
from unittest.mock import patch
import pytest
from switchbot_api import CannotConnect, Device, InvalidAuth, PowerState
from switchbot_api import CannotConnect, Device, InvalidAuth, PowerState, Remote
from homeassistant.components.switchbot_cloud import SwitchBotAPI
from homeassistant.config_entries import ConfigEntryState
@ -32,12 +32,24 @@ async def test_setup_entry_success(
) -> None:
"""Test successful setup of entry."""
mock_list_devices.return_value = [
Remote(
deviceId="air-conditonner-id-1",
deviceName="air-conditonner-name-1",
remoteType="Air Conditioner",
hubDeviceId="test-hub-id",
),
Device(
deviceId="test-id",
deviceName="test-name",
deviceId="plug-id-1",
deviceName="plug-name-1",
deviceType="Plug",
hubDeviceId="test-hub-id",
)
),
Remote(
deviceId="plug-id-2",
deviceName="plug-name-2",
remoteType="DIY Plug",
hubDeviceId="test-hub-id",
),
]
mock_get_status.return_value = {"power": PowerState.ON.value}
entry = configure_integration(hass)