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/sensor.py
homeassistant/components/switchbot/switch.py homeassistant/components/switchbot/switch.py
homeassistant/components/switchbot/lock.py homeassistant/components/switchbot/lock.py
homeassistant/components/switchbot_cloud/climate.py
homeassistant/components/switchbot_cloud/coordinator.py homeassistant/components/switchbot_cloud/coordinator.py
homeassistant/components/switchbot_cloud/entity.py homeassistant/components/switchbot_cloud/entity.py
homeassistant/components/switchbot_cloud/switch.py homeassistant/components/switchbot_cloud/switch.py

View File

@ -1,27 +1,28 @@
"""The SwitchBot via API integration.""" """The SwitchBot via API integration."""
from asyncio import gather from asyncio import gather
from dataclasses import dataclass from dataclasses import dataclass, field
from logging import getLogger from logging import getLogger
from switchbot_api import CannotConnect, Device, InvalidAuth, Remote, SwitchBotAPI from switchbot_api import CannotConnect, Device, InvalidAuth, Remote, SwitchBotAPI
from homeassistant.config_entries import ConfigEntry from homeassistant.config_entries import ConfigEntry
from homeassistant.const import CONF_API_KEY, CONF_API_TOKEN, Platform 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 homeassistant.exceptions import ConfigEntryNotReady
from .const import DOMAIN from .const import DOMAIN
from .coordinator import SwitchBotCoordinator from .coordinator import SwitchBotCoordinator
_LOGGER = getLogger(__name__) _LOGGER = getLogger(__name__)
PLATFORMS: list[Platform] = [Platform.SWITCH] PLATFORMS: list[Platform] = [Platform.CLIMATE, Platform.SWITCH]
@dataclass @dataclass
class SwitchbotDevices: class SwitchbotDevices:
"""Switchbot devices data.""" """Switchbot devices data."""
switches: list[Device | Remote] climates: list[Remote] = field(default_factory=list)
switches: list[Device | Remote] = field(default_factory=list)
@dataclass @dataclass
@ -32,18 +33,47 @@ class SwitchbotCloudData:
devices: SwitchbotDevices devices: SwitchbotDevices
@callback
def prepare_device( def prepare_device(
hass: HomeAssistant, hass: HomeAssistant,
api: SwitchBotAPI, api: SwitchBotAPI,
device: Device | Remote, device: Device | Remote,
coordinators: list[SwitchBotCoordinator], coordinators_by_id: dict[str, SwitchBotCoordinator],
) -> tuple[Device | Remote, SwitchBotCoordinator]: ) -> tuple[Device | Remote, SwitchBotCoordinator]:
"""Instantiate coordinator and adds to list for gathering.""" """Instantiate coordinator and adds to list for gathering."""
coordinator = SwitchBotCoordinator(hass, api, device) coordinator = coordinators_by_id.setdefault(
coordinators.append(coordinator) device.device_id, SwitchBotCoordinator(hass, api, device)
)
return (device, coordinator) 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: async def async_setup_entry(hass: HomeAssistant, config: ConfigEntry) -> bool:
"""Set up SwitchBot via API from a config entry.""" """Set up SwitchBot via API from a config entry."""
token = config.data[CONF_API_TOKEN] token = config.data[CONF_API_TOKEN]
@ -60,25 +90,15 @@ async def async_setup_entry(hass: HomeAssistant, config: ConfigEntry) -> bool:
except CannotConnect as ex: except CannotConnect as ex:
raise ConfigEntryNotReady from ex raise ConfigEntryNotReady from ex
_LOGGER.debug("Devices: %s", devices) _LOGGER.debug("Devices: %s", devices)
coordinators: list[SwitchBotCoordinator] = [] coordinators_by_id: dict[str, SwitchBotCoordinator] = {}
hass.data.setdefault(DOMAIN, {}) hass.data.setdefault(DOMAIN, {})
data = SwitchbotCloudData( hass.data[DOMAIN][config.entry_id] = SwitchbotCloudData(
api=api, api=api, devices=make_device_data(hass, api, devices, coordinators_by_id)
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] = 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 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 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 from unittest.mock import patch
import pytest 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.components.switchbot_cloud import SwitchBotAPI
from homeassistant.config_entries import ConfigEntryState from homeassistant.config_entries import ConfigEntryState
@ -32,12 +32,24 @@ async def test_setup_entry_success(
) -> None: ) -> None:
"""Test successful setup of entry.""" """Test successful setup of entry."""
mock_list_devices.return_value = [ mock_list_devices.return_value = [
Remote(
deviceId="air-conditonner-id-1",
deviceName="air-conditonner-name-1",
remoteType="Air Conditioner",
hubDeviceId="test-hub-id",
),
Device( Device(
deviceId="test-id", deviceId="plug-id-1",
deviceName="test-name", deviceName="plug-name-1",
deviceType="Plug", deviceType="Plug",
hubDeviceId="test-hub-id", 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} mock_get_status.return_value = {"power": PowerState.ON.value}
entry = configure_integration(hass) entry = configure_integration(hass)