diff --git a/.coveragerc b/.coveragerc index 45b9c8d1086..298bc9020ef 100644 --- a/.coveragerc +++ b/.coveragerc @@ -1232,6 +1232,7 @@ omit = homeassistant/components/swisscom/device_tracker.py homeassistant/components/switchbee/__init__.py homeassistant/components/switchbee/button.py + homeassistant/components/switchbee/climate.py homeassistant/components/switchbee/coordinator.py homeassistant/components/switchbee/cover.py homeassistant/components/switchbee/entity.py diff --git a/homeassistant/components/switchbee/__init__.py b/homeassistant/components/switchbee/__init__.py index 7a843697e8d..5848477ec71 100644 --- a/homeassistant/components/switchbee/__init__.py +++ b/homeassistant/components/switchbee/__init__.py @@ -15,6 +15,7 @@ from .coordinator import SwitchBeeCoordinator PLATFORMS: list[Platform] = [ Platform.BUTTON, + Platform.CLIMATE, Platform.COVER, Platform.LIGHT, Platform.SWITCH, @@ -27,7 +28,6 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: central_unit = entry.data[CONF_HOST] user = entry.data[CONF_USERNAME] password = entry.data[CONF_PASSWORD] - websession = async_get_clientsession(hass, verify_ssl=False) api = CentralUnitAPI(central_unit, user, password, websession) try: diff --git a/homeassistant/components/switchbee/climate.py b/homeassistant/components/switchbee/climate.py new file mode 100644 index 00000000000..8d0024b75ed --- /dev/null +++ b/homeassistant/components/switchbee/climate.py @@ -0,0 +1,182 @@ +"""Support for SwitchBee climate.""" +from __future__ import annotations + +from typing import Any + +from switchbee.api import SwitchBeeDeviceOfflineError, SwitchBeeError +from switchbee.const import ( + ApiAttribute, + ThermostatFanSpeed, + ThermostatMode, + ThermostatTemperatureUnit, +) +from switchbee.device import ApiStateCommand, SwitchBeeThermostat + +from homeassistant.components.climate import ( + FAN_AUTO, + FAN_HIGH, + FAN_LOW, + FAN_MEDIUM, + ClimateEntity, + ClimateEntityFeature, + HVACAction, + HVACMode, +) +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import ATTR_TEMPERATURE, TEMP_CELSIUS, TEMP_FAHRENHEIT +from homeassistant.core import HomeAssistant, callback +from homeassistant.exceptions import HomeAssistantError +from homeassistant.helpers.entity_platform import AddEntitiesCallback + +from .const import DOMAIN +from .coordinator import SwitchBeeCoordinator +from .entity import SwitchBeeDeviceEntity + +FAN_SB_TO_HASS = { + ThermostatFanSpeed.AUTO: FAN_AUTO, + ThermostatFanSpeed.LOW: FAN_LOW, + ThermostatFanSpeed.MEDIUM: FAN_MEDIUM, + ThermostatFanSpeed.HIGH: FAN_HIGH, +} + +FAN_HASS_TO_SB: dict[str | None, str] = { + FAN_AUTO: ThermostatFanSpeed.AUTO, + FAN_LOW: ThermostatFanSpeed.LOW, + FAN_MEDIUM: ThermostatFanSpeed.MEDIUM, + FAN_HIGH: ThermostatFanSpeed.HIGH, +} + +HVAC_MODE_SB_TO_HASS = { + ThermostatMode.COOL: HVACMode.COOL, + ThermostatMode.HEAT: HVACMode.HEAT, + ThermostatMode.FAN: HVACMode.FAN_ONLY, +} + +HVAC_MODE_HASS_TO_SB: dict[HVACMode | str | None, str] = { + HVACMode.COOL: ThermostatMode.COOL, + HVACMode.HEAT: ThermostatMode.HEAT, + HVACMode.FAN_ONLY: ThermostatMode.FAN, +} + +HVAC_ACTION_SB_TO_HASS = { + ThermostatMode.COOL: HVACAction.COOLING, + ThermostatMode.HEAT: HVACAction.HEATING, + ThermostatMode.FAN: HVACAction.FAN, +} + +HVAC_UNIT_SB_TO_HASS = { + ThermostatTemperatureUnit.CELSIUS: TEMP_CELSIUS, + ThermostatTemperatureUnit.FAHRENHEIT: TEMP_FAHRENHEIT, +} + +SUPPORTED_FAN_MODES = [FAN_AUTO, FAN_HIGH, FAN_MEDIUM, FAN_LOW] + + +async def async_setup_entry( + hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback +) -> None: + """Set up SwitchBee climate.""" + coordinator: SwitchBeeCoordinator = hass.data[DOMAIN][entry.entry_id] + async_add_entities( + SwitchBeeClimateEntity(switchbee_device, coordinator) + for switchbee_device in coordinator.data.values() + if isinstance(switchbee_device, SwitchBeeThermostat) + ) + + +class SwitchBeeClimateEntity(SwitchBeeDeviceEntity[SwitchBeeThermostat], ClimateEntity): + """Representation of a SwitchBee climate.""" + + _attr_supported_features = ( + ClimateEntityFeature.TARGET_TEMPERATURE | ClimateEntityFeature.FAN_MODE + ) + _attr_fan_modes = SUPPORTED_FAN_MODES + _attr_target_temperature_step = 1 + + def __init__( + self, + device: SwitchBeeThermostat, + coordinator: SwitchBeeCoordinator, + ) -> None: + """Initialize the Switchbee switch.""" + super().__init__(device, coordinator) + # set HVAC capabilities + self._attr_max_temp = device.max_temperature + self._attr_min_temp = device.min_temperature + self._attr_temperature_unit = HVAC_UNIT_SB_TO_HASS[device.unit] + self._attr_hvac_modes = [HVAC_MODE_SB_TO_HASS[mode] for mode in device.modes] + self._attr_hvac_modes.append(HVACMode.OFF) + self._update_attrs_from_coordinator() + + @callback + def _handle_coordinator_update(self) -> None: + """Handle updated data from the coordinator.""" + self._update_attrs_from_coordinator() + super()._handle_coordinator_update() + + def _update_attrs_from_coordinator(self) -> None: + + coordinator_device = self._get_coordinator_device() + + self._attr_hvac_mode: HVACMode = ( + HVACMode.OFF + if coordinator_device.state == ApiStateCommand.OFF + else HVAC_MODE_SB_TO_HASS[coordinator_device.mode] + ) + self._attr_fan_mode = FAN_SB_TO_HASS[coordinator_device.fan] + self._attr_current_temperature = coordinator_device.temperature + self._attr_target_temperature = coordinator_device.target_temperature + + async def async_set_hvac_mode(self, hvac_mode: HVACMode) -> None: + """Set hvac mode.""" + + if hvac_mode == HVACMode.OFF: + await self._operate(power=ApiStateCommand.OFF) + else: + await self._operate( + power=ApiStateCommand.ON, mode=HVAC_MODE_HASS_TO_SB[hvac_mode] + ) + + async def async_set_temperature(self, **kwargs: Any) -> None: + """Set new target temperature.""" + await self._operate(target_temperature=kwargs[ATTR_TEMPERATURE]) + + async def async_set_fan_mode(self, fan_mode: str) -> None: + """Set AC fan mode.""" + await self._operate(fan=FAN_HASS_TO_SB[fan_mode]) + + async def _operate( + self, + power: str | None = None, + mode: str | None = None, + fan: str | None = None, + target_temperature: int | None = None, + ) -> None: + """Send request to central unit.""" + + if power is None: + power = ApiStateCommand.ON + if self.hvac_mode == HVACMode.OFF: + power = ApiStateCommand.OFF + if mode is None: + mode = HVAC_MODE_HASS_TO_SB[self.hvac_mode] + if fan is None: + fan = FAN_HASS_TO_SB[self.fan_mode] + if target_temperature is None: + target_temperature = int(self.target_temperature or 0) + + state: dict[str, int | str] = { + ApiAttribute.POWER: power, + ApiAttribute.MODE: mode, + ApiAttribute.FAN: fan, + ApiAttribute.CONFIGURED_TEMPERATURE: target_temperature, + } + + try: + await self.coordinator.api.set_state(self._device.id, state) + except (SwitchBeeError, SwitchBeeDeviceOfflineError) as exp: + raise HomeAssistantError( + f"Failed to set {self.name} state {state}, error: {str(exp)}" + ) from exp + else: + await self.coordinator.async_refresh() diff --git a/homeassistant/components/switchbee/entity.py b/homeassistant/components/switchbee/entity.py index 4fed0c61393..5129446a204 100644 --- a/homeassistant/components/switchbee/entity.py +++ b/homeassistant/components/switchbee/entity.py @@ -4,7 +4,7 @@ from typing import Generic, TypeVar, cast from switchbee import SWITCHBEE_BRAND from switchbee.api import SwitchBeeDeviceOfflineError, SwitchBeeError -from switchbee.device import SwitchBeeBaseDevice +from switchbee.device import DeviceType, SwitchBeeBaseDevice from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.update_coordinator import CoordinatorEntity @@ -46,12 +46,15 @@ class SwitchBeeDeviceEntity(SwitchBeeEntity[_DeviceTypeT]): """Initialize the Switchbee device.""" super().__init__(device, coordinator) self._is_online: bool = True + identifier = ( + device.id if device.type == DeviceType.Thermostat else device.unit_id + ) self._attr_device_info = DeviceInfo( - name=f"SwitchBee {device.unit_id}", + name=f"SwitchBee {identifier}", identifiers={ ( DOMAIN, - f"{device.unit_id}-{coordinator.mac_formatted}", + f"{identifier}-{coordinator.mac_formatted}", ) }, manufacturer=SWITCHBEE_BRAND,