Initial Home Assistant commit
This commit is contained in:
@@ -0,0 +1,391 @@
|
||||
"""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)
|
||||
Reference in New Issue
Block a user