399 lines
12 KiB
Python
399 lines
12 KiB
Python
"""Support for interface with a Gree climate systems."""
|
|
from datetime import timedelta
|
|
import logging
|
|
from typing import List
|
|
|
|
from greeclimate.device import (
|
|
FanSpeed,
|
|
HorizontalSwing,
|
|
Mode,
|
|
TemperatureUnits,
|
|
VerticalSwing,
|
|
)
|
|
from greeclimate.exceptions import DeviceTimeoutError
|
|
|
|
from homeassistant.components.climate import ClimateEntity
|
|
from homeassistant.components.climate.const import (
|
|
FAN_AUTO,
|
|
FAN_HIGH,
|
|
FAN_LOW,
|
|
FAN_MEDIUM,
|
|
HVAC_MODE_AUTO,
|
|
HVAC_MODE_COOL,
|
|
HVAC_MODE_DRY,
|
|
HVAC_MODE_FAN_ONLY,
|
|
HVAC_MODE_HEAT,
|
|
HVAC_MODE_OFF,
|
|
PRESET_AWAY,
|
|
PRESET_BOOST,
|
|
PRESET_ECO,
|
|
PRESET_NONE,
|
|
PRESET_SLEEP,
|
|
SUPPORT_FAN_MODE,
|
|
SUPPORT_PRESET_MODE,
|
|
SUPPORT_SWING_MODE,
|
|
SUPPORT_TARGET_TEMPERATURE,
|
|
SWING_BOTH,
|
|
SWING_HORIZONTAL,
|
|
SWING_OFF,
|
|
SWING_VERTICAL,
|
|
)
|
|
from homeassistant.const import (
|
|
ATTR_TEMPERATURE,
|
|
PRECISION_WHOLE,
|
|
TEMP_CELSIUS,
|
|
TEMP_FAHRENHEIT,
|
|
)
|
|
from homeassistant.helpers.device_registry import CONNECTION_NETWORK_MAC
|
|
|
|
from .const import (
|
|
DOMAIN,
|
|
FAN_MEDIUM_HIGH,
|
|
FAN_MEDIUM_LOW,
|
|
MAX_ERRORS,
|
|
MAX_TEMP,
|
|
MIN_TEMP,
|
|
TARGET_TEMPERATURE_STEP,
|
|
)
|
|
|
|
_LOGGER = logging.getLogger(__name__)
|
|
|
|
SCAN_INTERVAL = timedelta(seconds=60)
|
|
PARALLEL_UPDATES = 0
|
|
|
|
HVAC_MODES = {
|
|
Mode.Auto: HVAC_MODE_AUTO,
|
|
Mode.Cool: HVAC_MODE_COOL,
|
|
Mode.Dry: HVAC_MODE_DRY,
|
|
Mode.Fan: HVAC_MODE_FAN_ONLY,
|
|
Mode.Heat: HVAC_MODE_HEAT,
|
|
}
|
|
HVAC_MODES_REVERSE = {v: k for k, v in HVAC_MODES.items()}
|
|
|
|
PRESET_MODES = [
|
|
PRESET_ECO, # Power saving mode
|
|
PRESET_AWAY, # Steady heat, or 8C mode on gree units
|
|
PRESET_BOOST, # Turbo mode
|
|
PRESET_NONE, # Default operating mode
|
|
PRESET_SLEEP, # Sleep mode
|
|
]
|
|
|
|
FAN_MODES = {
|
|
FanSpeed.Auto: FAN_AUTO,
|
|
FanSpeed.Low: FAN_LOW,
|
|
FanSpeed.MediumLow: FAN_MEDIUM_LOW,
|
|
FanSpeed.Medium: FAN_MEDIUM,
|
|
FanSpeed.MediumHigh: FAN_MEDIUM_HIGH,
|
|
FanSpeed.High: FAN_HIGH,
|
|
}
|
|
FAN_MODES_REVERSE = {v: k for k, v in FAN_MODES.items()}
|
|
|
|
SWING_MODES = [SWING_OFF, SWING_VERTICAL, SWING_HORIZONTAL, SWING_BOTH]
|
|
|
|
SUPPORTED_FEATURES = (
|
|
SUPPORT_TARGET_TEMPERATURE
|
|
| SUPPORT_FAN_MODE
|
|
| SUPPORT_PRESET_MODE
|
|
| SUPPORT_SWING_MODE
|
|
)
|
|
|
|
|
|
async def async_setup_entry(hass, config_entry, async_add_entities):
|
|
"""Set up the Gree HVAC device from a config entry."""
|
|
async_add_entities(
|
|
GreeClimateEntity(device) for device in hass.data[DOMAIN].pop("pending")
|
|
)
|
|
|
|
|
|
class GreeClimateEntity(ClimateEntity):
|
|
"""Representation of a Gree HVAC device."""
|
|
|
|
def __init__(self, device):
|
|
"""Initialize the Gree device."""
|
|
self._device = device
|
|
self._name = device.device_info.name
|
|
self._mac = device.device_info.mac
|
|
self._available = False
|
|
self._error_count = 0
|
|
|
|
async def async_update(self):
|
|
"""Update the state of the device."""
|
|
try:
|
|
await self._device.update_state()
|
|
|
|
if not self._available and self._error_count:
|
|
_LOGGER.warning(
|
|
"Device is available: %s (%s)",
|
|
self._name,
|
|
str(self._device.device_info),
|
|
)
|
|
|
|
self._available = True
|
|
self._error_count = 0
|
|
except DeviceTimeoutError:
|
|
self._error_count += 1
|
|
|
|
# Under normal conditions GREE units timeout every once in a while
|
|
if self._available and self._error_count >= MAX_ERRORS:
|
|
self._available = False
|
|
_LOGGER.warning(
|
|
"Device is unavailable: %s (%s)",
|
|
self._name,
|
|
self._device.device_info,
|
|
)
|
|
except Exception: # pylint: disable=broad-except
|
|
# Under normal conditions GREE units timeout every once in a while
|
|
if self._available:
|
|
self._available = False
|
|
_LOGGER.exception(
|
|
"Unknown exception caught during update by gree device: %s (%s)",
|
|
self._name,
|
|
self._device.device_info,
|
|
)
|
|
|
|
async def _push_state_update(self):
|
|
"""Send state updates to the physical device."""
|
|
try:
|
|
return await self._device.push_state_update()
|
|
except DeviceTimeoutError:
|
|
self._error_count += 1
|
|
|
|
# Under normal conditions GREE units timeout every once in a while
|
|
if self._available and self._error_count >= MAX_ERRORS:
|
|
self._available = False
|
|
_LOGGER.warning(
|
|
"Device timedout while sending state update: %s (%s)",
|
|
self._name,
|
|
self._device.device_info,
|
|
)
|
|
except Exception: # pylint: disable=broad-except
|
|
# Under normal conditions GREE units timeout every once in a while
|
|
if self._available:
|
|
self._available = False
|
|
_LOGGER.exception(
|
|
"Unknown exception caught while sending state update to: %s (%s)",
|
|
self._name,
|
|
self._device.device_info,
|
|
)
|
|
|
|
@property
|
|
def available(self) -> bool:
|
|
"""Return if the device is available."""
|
|
return self._available
|
|
|
|
@property
|
|
def name(self) -> str:
|
|
"""Return the name of the device."""
|
|
return self._name
|
|
|
|
@property
|
|
def unique_id(self) -> str:
|
|
"""Return a unique id for the device."""
|
|
return self._mac
|
|
|
|
@property
|
|
def device_info(self):
|
|
"""Return device specific attributes."""
|
|
return {
|
|
"name": self._name,
|
|
"identifiers": {(DOMAIN, self._mac)},
|
|
"manufacturer": "Gree",
|
|
"connections": {(CONNECTION_NETWORK_MAC, self._mac)},
|
|
}
|
|
|
|
@property
|
|
def temperature_unit(self) -> str:
|
|
"""Return the temperature units for the device."""
|
|
units = self._device.temperature_units
|
|
return TEMP_CELSIUS if units == TemperatureUnits.C else TEMP_FAHRENHEIT
|
|
|
|
@property
|
|
def precision(self) -> float:
|
|
"""Return the precision of temperature for the device."""
|
|
return PRECISION_WHOLE
|
|
|
|
@property
|
|
def current_temperature(self) -> float:
|
|
"""Return the target temperature, gree devices don't provide internal temp."""
|
|
return self.target_temperature
|
|
|
|
@property
|
|
def target_temperature(self) -> float:
|
|
"""Return the target temperature for the device."""
|
|
return self._device.target_temperature
|
|
|
|
async def async_set_temperature(self, **kwargs):
|
|
"""Set new target temperature."""
|
|
if ATTR_TEMPERATURE not in kwargs:
|
|
raise ValueError(f"Missing parameter {ATTR_TEMPERATURE}")
|
|
|
|
temperature = kwargs[ATTR_TEMPERATURE]
|
|
_LOGGER.debug(
|
|
"Setting temperature to %d for %s",
|
|
temperature,
|
|
self._name,
|
|
)
|
|
|
|
self._device.target_temperature = round(temperature)
|
|
await self._push_state_update()
|
|
|
|
@property
|
|
def min_temp(self) -> float:
|
|
"""Return the minimum temperature supported by the device."""
|
|
return MIN_TEMP
|
|
|
|
@property
|
|
def max_temp(self) -> float:
|
|
"""Return the maximum temperature supported by the device."""
|
|
return MAX_TEMP
|
|
|
|
@property
|
|
def target_temperature_step(self) -> float:
|
|
"""Return the target temperature step support by the device."""
|
|
return TARGET_TEMPERATURE_STEP
|
|
|
|
@property
|
|
def hvac_mode(self) -> str:
|
|
"""Return the current HVAC mode for the device."""
|
|
if not self._device.power:
|
|
return HVAC_MODE_OFF
|
|
|
|
return HVAC_MODES.get(self._device.mode)
|
|
|
|
async def async_set_hvac_mode(self, hvac_mode):
|
|
"""Set new target hvac mode."""
|
|
if hvac_mode not in self.hvac_modes:
|
|
raise ValueError(f"Invalid hvac_mode: {hvac_mode}")
|
|
|
|
_LOGGER.debug(
|
|
"Setting HVAC mode to %s for device %s",
|
|
hvac_mode,
|
|
self._name,
|
|
)
|
|
|
|
if hvac_mode == HVAC_MODE_OFF:
|
|
self._device.power = False
|
|
await self._push_state_update()
|
|
return
|
|
|
|
if not self._device.power:
|
|
self._device.power = True
|
|
|
|
self._device.mode = HVAC_MODES_REVERSE.get(hvac_mode)
|
|
await self._push_state_update()
|
|
|
|
@property
|
|
def hvac_modes(self) -> List[str]:
|
|
"""Return the HVAC modes support by the device."""
|
|
modes = [*HVAC_MODES_REVERSE]
|
|
modes.append(HVAC_MODE_OFF)
|
|
return modes
|
|
|
|
@property
|
|
def preset_mode(self) -> str:
|
|
"""Return the current preset mode for the device."""
|
|
if self._device.steady_heat:
|
|
return PRESET_AWAY
|
|
if self._device.power_save:
|
|
return PRESET_ECO
|
|
if self._device.sleep:
|
|
return PRESET_SLEEP
|
|
if self._device.turbo:
|
|
return PRESET_BOOST
|
|
return PRESET_NONE
|
|
|
|
async def async_set_preset_mode(self, preset_mode):
|
|
"""Set new preset mode."""
|
|
if preset_mode not in PRESET_MODES:
|
|
raise ValueError(f"Invalid preset mode: {preset_mode}")
|
|
|
|
_LOGGER.debug(
|
|
"Setting preset mode to %s for device %s",
|
|
preset_mode,
|
|
self._name,
|
|
)
|
|
|
|
self._device.steady_heat = False
|
|
self._device.power_save = False
|
|
self._device.turbo = False
|
|
self._device.sleep = False
|
|
|
|
if preset_mode == PRESET_AWAY:
|
|
self._device.steady_heat = True
|
|
elif preset_mode == PRESET_ECO:
|
|
self._device.power_save = True
|
|
elif preset_mode == PRESET_BOOST:
|
|
self._device.turbo = True
|
|
elif preset_mode == PRESET_SLEEP:
|
|
self._device.sleep = True
|
|
|
|
await self._push_state_update()
|
|
|
|
@property
|
|
def preset_modes(self) -> List[str]:
|
|
"""Return the preset modes support by the device."""
|
|
return PRESET_MODES
|
|
|
|
@property
|
|
def fan_mode(self) -> str:
|
|
"""Return the current fan mode for the device."""
|
|
speed = self._device.fan_speed
|
|
return FAN_MODES.get(speed)
|
|
|
|
async def async_set_fan_mode(self, fan_mode):
|
|
"""Set new target fan mode."""
|
|
if fan_mode not in FAN_MODES_REVERSE:
|
|
raise ValueError(f"Invalid fan mode: {fan_mode}")
|
|
|
|
self._device.fan_speed = FAN_MODES_REVERSE.get(fan_mode)
|
|
await self._push_state_update()
|
|
|
|
@property
|
|
def fan_modes(self) -> List[str]:
|
|
"""Return the fan modes support by the device."""
|
|
return [*FAN_MODES_REVERSE]
|
|
|
|
@property
|
|
def swing_mode(self) -> str:
|
|
"""Return the current swing mode for the device."""
|
|
h_swing = self._device.horizontal_swing == HorizontalSwing.FullSwing
|
|
v_swing = self._device.vertical_swing == VerticalSwing.FullSwing
|
|
|
|
if h_swing and v_swing:
|
|
return SWING_BOTH
|
|
if h_swing:
|
|
return SWING_HORIZONTAL
|
|
if v_swing:
|
|
return SWING_VERTICAL
|
|
return SWING_OFF
|
|
|
|
async def async_set_swing_mode(self, swing_mode):
|
|
"""Set new target swing operation."""
|
|
if swing_mode not in SWING_MODES:
|
|
raise ValueError(f"Invalid swing mode: {swing_mode}")
|
|
|
|
_LOGGER.debug(
|
|
"Setting swing mode to %s for device %s",
|
|
swing_mode,
|
|
self._name,
|
|
)
|
|
|
|
self._device.horizontal_swing = HorizontalSwing.Center
|
|
self._device.vertical_swing = VerticalSwing.FixedMiddle
|
|
if swing_mode in (SWING_BOTH, SWING_HORIZONTAL):
|
|
self._device.horizontal_swing = HorizontalSwing.FullSwing
|
|
if swing_mode in (SWING_BOTH, SWING_VERTICAL):
|
|
self._device.vertical_swing = VerticalSwing.FullSwing
|
|
|
|
await self._push_state_update()
|
|
|
|
@property
|
|
def swing_modes(self) -> List[str]:
|
|
"""Return the swing modes currently supported for this device."""
|
|
return SWING_MODES
|
|
|
|
@property
|
|
def supported_features(self) -> int:
|
|
"""Return the supported features for this device integration."""
|
|
return SUPPORTED_FEATURES
|