"""Platform for light integration.""" import logging # Import the device class from the component that you want to support from datetime import timedelta from typing import List, Optional, Callable, Any from aiohttp.client_exceptions import ClientConnectionError from homeassistant.components.climate import ( ClimateEntity, ClimateEntityFeature, HVACAction, HVACMode, ) from homeassistant.components.climate.const import ( FAN_AUTO, FAN_ON, PRESET_HOME, PRESET_AWAY, PRESET_SLEEP, ) from homeassistant.config_entries import ConfigEntry from homeassistant.const import UnitOfTemperature from homeassistant.core import HomeAssistant, callback from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers import device_registry as dr from wyzeapy import Wyzeapy, ThermostatService from wyzeapy.exceptions import AccessTokenError, ParameterError, UnknownApiError from wyzeapy.services.thermostat_service import ( Thermostat, TemperatureUnit, Preset, FanMode, HVACState, HVACMode as WyzeHVACMode, ) from .token_manager import token_exception_handler from .const import DOMAIN, CONF_CLIENT _LOGGER = logging.getLogger(__name__) ATTRIBUTION = "Data provided by Wyze" SCAN_INTERVAL = timedelta(seconds=30) @token_exception_handler async def async_setup_entry( hass: HomeAssistant, config_entry: ConfigEntry, async_add_entities: Callable[[List[Any], bool], None], ): """ This function sets up the config entry so that it is available to Home Assistant :param hass: The Home Assistant instance :param config_entry: The current config entry :param async_add_entities: A function to add entities :return: """ _LOGGER.debug("""Creating new WyzeApi thermostat component""") client: Wyzeapy = hass.data[DOMAIN][config_entry.entry_id][CONF_CLIENT] thermostat_service = await client.thermostat_service thermostats = [ WyzeThermostat(thermostat_service, thermostat) for thermostat in await thermostat_service.get_thermostats() ] async_add_entities(thermostats, True) class WyzeThermostat(ClimateEntity): """ This class defines a representation of a Wyze Thermostat that can be used for Home Assistant """ # pylint: disable=R0902 _server_out_of_sync = False def __init__(self, thermostat_service: ThermostatService, thermostat: Thermostat): self._thermostat_service = thermostat_service self._thermostat = thermostat def set_temperature(self, **kwargs) -> None: raise NotImplementedError def set_humidity(self, humidity: int) -> None: raise NotImplementedError def set_fan_mode(self, fan_mode: str) -> None: raise NotImplementedError def set_hvac_mode(self, hvac_mode: str) -> None: raise NotImplementedError def set_swing_mode(self, swing_mode: str) -> None: raise NotImplementedError def set_preset_mode(self, preset_mode: str) -> None: raise NotImplementedError def turn_aux_heat_on(self) -> None: raise NotImplementedError def turn_aux_heat_off(self) -> None: raise NotImplementedError @property def current_temperature(self) -> float: return self._thermostat.temperature @property def current_humidity(self) -> Optional[int]: return self._thermostat.humidity @property def temperature_unit(self) -> str: # if self._thermostat.temp_unit == TemperatureUnit.FAHRENHEIT: return UnitOfTemperature.FAHRENHEIT # return TEMP_CELSIUS @property def unit_of_measurement(self) -> str: if self._thermostat.temp_unit == TemperatureUnit.FAHRENHEIT: return UnitOfTemperature.FAHRENHEIT return UnitOfTemperature.CELSIUS @property def hvac_mode(self) -> str: # pylint: disable=R1705 if self._thermostat.hvac_mode == WyzeHVACMode.AUTO: return HVACMode.AUTO elif self._thermostat.hvac_mode == WyzeHVACMode.HEAT: return HVACMode.HEAT elif self._thermostat.hvac_mode == WyzeHVACMode.COOL: return HVACMode.COOL else: return HVACMode.OFF @property def hvac_modes(self) -> List[str]: return [HVACMode.AUTO, HVACMode.HEAT, HVACMode.COOL, HVACMode.OFF] @property def target_temperature_high(self) -> Optional[float]: return self._thermostat.cool_set_point @property def target_temperature_low(self) -> Optional[float]: return self._thermostat.heat_set_point @property def preset_mode(self) -> Optional[str]: match self._thermostat.preset: case Preset.HOME: return PRESET_HOME case Preset.AWAY: return PRESET_AWAY case Preset.SLEEP: return PRESET_SLEEP case _: raise NotImplementedError @property def preset_modes(self) -> Optional[List[str]]: return [PRESET_HOME, PRESET_AWAY, PRESET_SLEEP] @property def is_aux_heat(self) -> Optional[bool]: raise NotImplementedError @property def fan_mode(self) -> Optional[str]: if self._thermostat.fan_mode == FanMode.AUTO: return FAN_AUTO else: return FAN_ON @property def fan_modes(self) -> Optional[List[str]]: return [FAN_AUTO, FAN_ON] @property def swing_mode(self) -> Optional[str]: raise NotImplementedError @property def swing_modes(self) -> Optional[str]: raise NotImplementedError @property def hvac_action(self) -> str: # pylint: disable=R1705 if self._thermostat.hvac_state == HVACState.IDLE: return HVACAction.IDLE elif self._thermostat.hvac_state == HVACState.HEATING: return HVACAction.HEATING elif self._thermostat.hvac_state == HVACState.COOLING: return HVACAction.COOLING else: return HVACAction.OFF @token_exception_handler async def async_set_temperature(self, **kwargs) -> None: target_temp_low = kwargs["target_temp_low"] target_temp_high = kwargs["target_temp_high"] try: # Always send setpoints unconditionally rather than guarding against # the cached heat_set_point/cool_set_point value. The equality check # causes a silent no-op whenever the cache is stale (e.g. after a mode # change resets the physical device to firmware defaults, or after a # Wyze cloud/app-driven reset), leaving the physical thermostat at the # wrong setpoint while HA and the Wyze cloud both report the correct one. # The _server_out_of_sync flag compounds this by delaying the cache # refresh by up to 60 s after any command. The underlying API call # (set_iot_prop_by_topic) is idempotent, so always sending is safe. # See: https://github.com/SecKatie/ha-wyzeapi/issues/813 await self._thermostat_service.set_heat_point( self._thermostat, int(target_temp_low) ) self._thermostat.heat_set_point = int(target_temp_low) await self._thermostat_service.set_cool_point( self._thermostat, int(target_temp_high) ) self._thermostat.cool_set_point = int(target_temp_high) except (AccessTokenError, ParameterError, UnknownApiError) as err: raise HomeAssistantError(f"Wyze returned an error: {err.args}") from err except ClientConnectionError as err: raise HomeAssistantError(err) from err else: self._server_out_of_sync = True self.async_schedule_update_ha_state() async def async_set_humidity(self, humidity: int) -> None: raise NotImplementedError @token_exception_handler async def async_set_fan_mode(self, fan_mode: str) -> None: try: if fan_mode == FAN_ON: await self._thermostat_service.set_fan_mode( self._thermostat, FanMode.ON ) self._thermostat.fan_mode = FanMode.ON elif fan_mode == FAN_AUTO: await self._thermostat_service.set_fan_mode( self._thermostat, FanMode.AUTO ) self._thermostat.fan_mode = FanMode.AUTO except (AccessTokenError, ParameterError, UnknownApiError) as err: raise HomeAssistantError(f"Wyze returned an error: {err.args}") from err except ClientConnectionError as err: raise HomeAssistantError(err) from err else: self._server_out_of_sync = True self.async_schedule_update_ha_state() @token_exception_handler async def async_set_hvac_mode(self, hvac_mode: str) -> None: try: if hvac_mode == HVACMode.OFF: await self._thermostat_service.set_hvac_mode( self._thermostat, WyzeHVACMode.OFF ) self._thermostat.hvac_mode = HVACMode.OFF elif hvac_mode == HVACMode.HEAT: await self._thermostat_service.set_hvac_mode( self._thermostat, WyzeHVACMode.HEAT ) self._thermostat.hvac_mode = HVACMode.HEAT elif hvac_mode == HVACMode.COOL: await self._thermostat_service.set_hvac_mode( self._thermostat, WyzeHVACMode.COOL ) self._thermostat.hvac_mode = HVACMode.COOL elif hvac_mode == HVACMode.AUTO: await self._thermostat_service.set_hvac_mode( self._thermostat, WyzeHVACMode.AUTO ) self._thermostat.hvac_mode = HVACMode.AUTO except (AccessTokenError, ParameterError, UnknownApiError) as err: raise HomeAssistantError(f"Wyze returned an error: {err.args}") from err except ClientConnectionError as err: raise HomeAssistantError(err) from err else: self._server_out_of_sync = True self.async_schedule_update_ha_state() async def async_set_swing_mode(self, swing_mode: str) -> None: raise NotImplementedError @token_exception_handler async def async_set_preset_mode(self, preset_mode: str) -> None: try: if preset_mode == PRESET_SLEEP: await self._thermostat_service.set_preset( self._thermostat, Preset.SLEEP ) self._thermostat.preset = Preset.SLEEP elif preset_mode == PRESET_AWAY: await self._thermostat_service.set_preset(self._thermostat, Preset.AWAY) self._thermostat.preset = Preset.AWAY elif preset_mode == PRESET_HOME: await self._thermostat_service.set_preset(self._thermostat, Preset.HOME) self._thermostat.preset = Preset.HOME except (AccessTokenError, ParameterError, UnknownApiError) as err: raise HomeAssistantError(f"Wyze returned an error: {err.args}") from err except ClientConnectionError as err: raise HomeAssistantError(err) from err else: self._server_out_of_sync = True self.async_schedule_update_ha_state() async def async_turn_aux_heat_on(self) -> None: raise NotImplementedError async def async_turn_aux_heat_off(self) -> None: raise NotImplementedError @property def supported_features(self) -> int: return ( ClimateEntityFeature.TARGET_TEMPERATURE_RANGE | ClimateEntityFeature.FAN_MODE | ClimateEntityFeature.PRESET_MODE ) @property def device_info(self) -> dict: return { "identifiers": {(DOMAIN, self._thermostat.mac)}, "name": self._thermostat.nickname, "connections": { ( dr.CONNECTION_NETWORK_MAC, self._thermostat.mac, ) }, "manufacturer": "WyzeLabs", "model": self._thermostat.product_model, } @property def should_poll(self) -> bool: return False @property def name(self) -> str: """Return the display name of this lock.""" return self._thermostat.nickname @property def unique_id(self) -> str: return self._thermostat.mac @property def available(self) -> bool: """Return the connection status of this light""" return self._thermostat.available @token_exception_handler async def async_update(self) -> None: """ This function updates the state of the Thermostat :return: None """ if not self._server_out_of_sync: self._thermostat = await self._thermostat_service.update(self._thermostat) else: self._server_out_of_sync = False @callback def async_update_callback(self, thermostat: Thermostat): """Update the thermostat's state.""" self._thermostat = thermostat self.async_schedule_update_ha_state() async def async_added_to_hass(self) -> None: """Subscribe to update events.""" self._thermostat.callback_function = self.async_update_callback self._thermostat_service.register_updater(self._thermostat, 30) await self._thermostat_service.start_update_manager() return await super().async_added_to_hass() async def async_will_remove_from_hass(self) -> None: self._thermostat_service.unregister_updater(self._thermostat)