mirror of
https://github.com/home-assistant/core.git
synced 2025-06-25 01:21:51 +02:00
Add gree climate integration (#37498)
Co-authored-by: Martin Hjelmare <marhje52@gmail.com>
This commit is contained in:
@ -164,6 +164,7 @@ homeassistant/components/google_assistant/* @home-assistant/cloud
|
||||
homeassistant/components/google_cloud/* @lufton
|
||||
homeassistant/components/google_translate/* @awarecan
|
||||
homeassistant/components/gpsd/* @fabaff
|
||||
homeassistant/components/gree/* @cmroche
|
||||
homeassistant/components/greeneye_monitor/* @jkeljo
|
||||
homeassistant/components/griddy/* @bdraco
|
||||
homeassistant/components/group/* @home-assistant/core
|
||||
|
62
homeassistant/components/gree/__init__.py
Normal file
62
homeassistant/components/gree/__init__.py
Normal file
@ -0,0 +1,62 @@
|
||||
"""The Gree Climate integration."""
|
||||
import logging
|
||||
|
||||
from homeassistant.components.climate import DOMAIN as CLIMATE_DOMAIN
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.core import HomeAssistant
|
||||
|
||||
from .bridge import CannotConnect, DeviceHelper
|
||||
from .const import DOMAIN
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
async def async_setup(hass: HomeAssistant, config: dict):
|
||||
"""Set up the Gree Climate component."""
|
||||
hass.data[DOMAIN] = {}
|
||||
return True
|
||||
|
||||
|
||||
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry):
|
||||
"""Set up Gree Climate from a config entry."""
|
||||
devices = []
|
||||
|
||||
# First we'll grab as many devices as we can find on the network
|
||||
# it's necessary to bind static devices anyway
|
||||
_LOGGER.debug("Scanning network for Gree devices")
|
||||
|
||||
for device_info in await DeviceHelper.find_devices():
|
||||
try:
|
||||
device = await DeviceHelper.try_bind_device(device_info)
|
||||
except CannotConnect:
|
||||
_LOGGER.error("Unable to bind to gree device: %s", device_info)
|
||||
continue
|
||||
|
||||
_LOGGER.debug(
|
||||
"Adding Gree device at %s:%i (%s)",
|
||||
device.device_info.ip,
|
||||
device.device_info.port,
|
||||
device.device_info.name,
|
||||
)
|
||||
devices.append(device)
|
||||
|
||||
hass.data[DOMAIN]["devices"] = devices
|
||||
hass.data[DOMAIN]["pending"] = devices
|
||||
hass.async_create_task(
|
||||
hass.config_entries.async_forward_entry_setup(entry, CLIMATE_DOMAIN)
|
||||
)
|
||||
|
||||
return True
|
||||
|
||||
|
||||
async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry):
|
||||
"""Unload a config entry."""
|
||||
unload_ok = await hass.config_entries.async_forward_entry_unload(
|
||||
entry, CLIMATE_DOMAIN
|
||||
)
|
||||
|
||||
if unload_ok:
|
||||
hass.data[DOMAIN].pop("devices", None)
|
||||
hass.data[DOMAIN].pop("pending", None)
|
||||
|
||||
return unload_ok
|
38
homeassistant/components/gree/bridge.py
Normal file
38
homeassistant/components/gree/bridge.py
Normal file
@ -0,0 +1,38 @@
|
||||
"""Helper and wrapper classes for Gree module."""
|
||||
import logging
|
||||
from typing import List
|
||||
|
||||
from greeclimate.device import Device, DeviceInfo
|
||||
from greeclimate.discovery import Discovery
|
||||
from greeclimate.exceptions import DeviceNotBoundError
|
||||
|
||||
from homeassistant import exceptions
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class DeviceHelper:
|
||||
"""Device search and bind wrapper for Gree platform."""
|
||||
|
||||
@staticmethod
|
||||
async def try_bind_device(device_info: DeviceInfo) -> Device:
|
||||
"""Try and bing with a discovered device.
|
||||
|
||||
Note the you must bind with the device very quickly after it is discovered, or the
|
||||
process may not be completed correctly, raising a `CannotConnect` error.
|
||||
"""
|
||||
device = Device(device_info)
|
||||
try:
|
||||
await device.bind()
|
||||
except DeviceNotBoundError as exception:
|
||||
raise CannotConnect from exception
|
||||
return device
|
||||
|
||||
@staticmethod
|
||||
async def find_devices() -> List[DeviceInfo]:
|
||||
"""Gather a list of device infos from the local network."""
|
||||
return await Discovery.search_devices()
|
||||
|
||||
|
||||
class CannotConnect(exceptions.HomeAssistantError):
|
||||
"""Error to indicate we cannot connect."""
|
398
homeassistant/components/gree/climate.py
Normal file
398
homeassistant/components/gree/climate.py
Normal file
@ -0,0 +1,398 @@
|
||||
"""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
|
17
homeassistant/components/gree/config_flow.py
Normal file
17
homeassistant/components/gree/config_flow.py
Normal file
@ -0,0 +1,17 @@
|
||||
"""Config flow for Gree."""
|
||||
from homeassistant import config_entries
|
||||
from homeassistant.helpers import config_entry_flow
|
||||
|
||||
from .bridge import DeviceHelper
|
||||
from .const import DOMAIN
|
||||
|
||||
|
||||
async def _async_has_devices(hass) -> bool:
|
||||
"""Return if there are devices that can be discovered."""
|
||||
devices = await DeviceHelper.find_devices()
|
||||
return len(devices) > 0
|
||||
|
||||
|
||||
config_entry_flow.register_discovery_flow(
|
||||
DOMAIN, "Gree Climate", _async_has_devices, config_entries.CONN_CLASS_LOCAL_POLL
|
||||
)
|
13
homeassistant/components/gree/const.py
Normal file
13
homeassistant/components/gree/const.py
Normal file
@ -0,0 +1,13 @@
|
||||
"""Constants for the Gree Climate integration."""
|
||||
|
||||
DOMAIN = "gree"
|
||||
|
||||
FAN_MEDIUM_LOW = "medium low"
|
||||
FAN_MEDIUM_HIGH = "medium high"
|
||||
|
||||
MIN_TEMP = 16
|
||||
MAX_TEMP = 30
|
||||
|
||||
MAX_ERRORS = 2
|
||||
|
||||
TARGET_TEMPERATURE_STEP = 1
|
8
homeassistant/components/gree/manifest.json
Normal file
8
homeassistant/components/gree/manifest.json
Normal file
@ -0,0 +1,8 @@
|
||||
{
|
||||
"domain": "gree",
|
||||
"name": "Gree Climate",
|
||||
"config_flow": true,
|
||||
"documentation": "https://www.home-assistant.io/integrations/gree",
|
||||
"requirements": ["greeclimate==0.9.0"],
|
||||
"codeowners": ["@cmroche"]
|
||||
}
|
14
homeassistant/components/gree/strings.json
Normal file
14
homeassistant/components/gree/strings.json
Normal file
@ -0,0 +1,14 @@
|
||||
{
|
||||
"title": "Gree Climate",
|
||||
"config": {
|
||||
"step": {
|
||||
"confirm": {
|
||||
"description": "[%key:common::config_flow::description::confirm_setup%]"
|
||||
}
|
||||
},
|
||||
"abort": {
|
||||
"single_instance_allowed": "[%key:common::config_flow::abort::single_instance_allowed%]",
|
||||
"no_devices_found": "[%key:common::config_flow::abort::no_devices_found%]"
|
||||
}
|
||||
}
|
||||
}
|
14
homeassistant/components/gree/translations/en.json
Normal file
14
homeassistant/components/gree/translations/en.json
Normal file
@ -0,0 +1,14 @@
|
||||
{
|
||||
"config": {
|
||||
"abort": {
|
||||
"no_devices_found": "No devices found on the network",
|
||||
"single_instance_allowed": "Already configured. Only a single configuration possible."
|
||||
},
|
||||
"step": {
|
||||
"confirm": {
|
||||
"description": "Do you want to start set up?"
|
||||
}
|
||||
}
|
||||
},
|
||||
"title": "Gree Climate"
|
||||
}
|
@ -72,6 +72,7 @@ FLOWS = [
|
||||
"goalzero",
|
||||
"gogogate2",
|
||||
"gpslogger",
|
||||
"gree",
|
||||
"griddy",
|
||||
"guardian",
|
||||
"hangouts",
|
||||
|
@ -698,6 +698,9 @@ gpiozero==1.5.1
|
||||
# homeassistant.components.gpsd
|
||||
gps3==0.33.3
|
||||
|
||||
# homeassistant.components.gree
|
||||
greeclimate==0.9.0
|
||||
|
||||
# homeassistant.components.greeneye_monitor
|
||||
greeneye_monitor==2.1
|
||||
|
||||
|
@ -351,6 +351,9 @@ google-api-python-client==1.6.4
|
||||
# homeassistant.components.google_pubsub
|
||||
google-cloud-pubsub==0.39.1
|
||||
|
||||
# homeassistant.components.gree
|
||||
greeclimate==0.9.0
|
||||
|
||||
# homeassistant.components.griddy
|
||||
griddypower==0.1.0
|
||||
|
||||
|
1
tests/components/gree/__init__.py
Normal file
1
tests/components/gree/__init__.py
Normal file
@ -0,0 +1 @@
|
||||
"""Tests for the Gree Climate integration."""
|
35
tests/components/gree/common.py
Normal file
35
tests/components/gree/common.py
Normal file
@ -0,0 +1,35 @@
|
||||
"""Common helpers for gree test cases."""
|
||||
from tests.async_mock import AsyncMock, Mock
|
||||
|
||||
|
||||
def build_device_info_mock(
|
||||
name="fake-device-1", ipAddress="1.1.1.1", mac="aabbcc112233"
|
||||
):
|
||||
"""Build mock device info structure."""
|
||||
mock = Mock(ip=ipAddress, port=7000, mac=mac)
|
||||
mock.name = name
|
||||
return mock
|
||||
|
||||
|
||||
def build_device_mock(name="fake-device-1", ipAddress="1.1.1.1", mac="aabbcc112233"):
|
||||
"""Build mock device object."""
|
||||
mock = Mock(
|
||||
device_info=build_device_info_mock(name, ipAddress, mac),
|
||||
name=name,
|
||||
bind=AsyncMock(),
|
||||
update_state=AsyncMock(),
|
||||
push_state_update=AsyncMock(),
|
||||
temperature_units=0,
|
||||
mode=0,
|
||||
fan_speed=0,
|
||||
horizontal_swing=0,
|
||||
vertical_swing=0,
|
||||
target_temperature=25,
|
||||
power=False,
|
||||
sleep=False,
|
||||
quiet=False,
|
||||
turbo=False,
|
||||
power_save=False,
|
||||
steady_heat=False,
|
||||
)
|
||||
return mock
|
36
tests/components/gree/conftest.py
Normal file
36
tests/components/gree/conftest.py
Normal file
@ -0,0 +1,36 @@
|
||||
"""Pytest module configuration."""
|
||||
import pytest
|
||||
|
||||
from .common import build_device_info_mock, build_device_mock
|
||||
|
||||
from tests.async_mock import AsyncMock, patch
|
||||
|
||||
|
||||
@pytest.fixture(name="discovery")
|
||||
def discovery_fixture():
|
||||
"""Patch the discovery service."""
|
||||
with patch(
|
||||
"homeassistant.components.gree.bridge.Discovery.search_devices",
|
||||
new_callable=AsyncMock,
|
||||
return_value=[build_device_info_mock()],
|
||||
) as mock:
|
||||
yield mock
|
||||
|
||||
|
||||
@pytest.fixture(name="device")
|
||||
def device_fixture():
|
||||
"""Path the device search and bind."""
|
||||
with patch(
|
||||
"homeassistant.components.gree.bridge.Device",
|
||||
return_value=build_device_mock(),
|
||||
) as mock:
|
||||
yield mock
|
||||
|
||||
|
||||
@pytest.fixture(name="setup")
|
||||
def setup_fixture():
|
||||
"""Patch the climate setup."""
|
||||
with patch(
|
||||
"homeassistant.components.gree.climate.async_setup_entry", return_value=True
|
||||
) as setup:
|
||||
yield setup
|
781
tests/components/gree/test_climate.py
Normal file
781
tests/components/gree/test_climate.py
Normal file
@ -0,0 +1,781 @@
|
||||
"""Tests for gree component."""
|
||||
from datetime import timedelta
|
||||
|
||||
from greeclimate.device import HorizontalSwing, VerticalSwing
|
||||
from greeclimate.exceptions import DeviceNotBoundError, DeviceTimeoutError
|
||||
import pytest
|
||||
|
||||
from homeassistant.components.climate.const import (
|
||||
ATTR_FAN_MODE,
|
||||
ATTR_HVAC_MODE,
|
||||
ATTR_PRESET_MODE,
|
||||
ATTR_SWING_MODE,
|
||||
DOMAIN,
|
||||
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,
|
||||
SERVICE_SET_FAN_MODE,
|
||||
SERVICE_SET_HVAC_MODE,
|
||||
SERVICE_SET_PRESET_MODE,
|
||||
SERVICE_SET_SWING_MODE,
|
||||
SERVICE_SET_TEMPERATURE,
|
||||
SWING_BOTH,
|
||||
SWING_HORIZONTAL,
|
||||
SWING_OFF,
|
||||
SWING_VERTICAL,
|
||||
)
|
||||
from homeassistant.components.gree.climate import (
|
||||
FAN_MODES_REVERSE,
|
||||
HVAC_MODES_REVERSE,
|
||||
SUPPORTED_FEATURES,
|
||||
)
|
||||
from homeassistant.components.gree.const import (
|
||||
DOMAIN as GREE_DOMAIN,
|
||||
FAN_MEDIUM_HIGH,
|
||||
FAN_MEDIUM_LOW,
|
||||
)
|
||||
from homeassistant.const import (
|
||||
ATTR_ENTITY_ID,
|
||||
ATTR_FRIENDLY_NAME,
|
||||
ATTR_SUPPORTED_FEATURES,
|
||||
ATTR_TEMPERATURE,
|
||||
STATE_UNAVAILABLE,
|
||||
)
|
||||
from homeassistant.setup import async_setup_component
|
||||
import homeassistant.util.dt as dt_util
|
||||
|
||||
from .common import build_device_mock
|
||||
|
||||
from tests.async_mock import DEFAULT as DEFAULT_MOCK, AsyncMock, patch
|
||||
from tests.common import MockConfigEntry, async_fire_time_changed
|
||||
|
||||
ENTITY_ID = f"{DOMAIN}.fake_device_1"
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def mock_now():
|
||||
"""Fixture for dtutil.now."""
|
||||
return dt_util.utcnow()
|
||||
|
||||
|
||||
async def async_setup_gree(hass):
|
||||
"""Set up the gree platform."""
|
||||
MockConfigEntry(domain=GREE_DOMAIN).add_to_hass(hass)
|
||||
await async_setup_component(hass, GREE_DOMAIN, {GREE_DOMAIN: {"climate": {}}})
|
||||
await hass.async_block_till_done()
|
||||
|
||||
|
||||
async def test_discovery_called_once(hass, discovery, device):
|
||||
"""Test discovery is only ever called once."""
|
||||
await async_setup_gree(hass)
|
||||
assert discovery.call_count == 1
|
||||
|
||||
await async_setup_gree(hass)
|
||||
assert discovery.call_count == 1
|
||||
|
||||
|
||||
async def test_discovery_setup(hass, discovery, device):
|
||||
"""Test setup of platform."""
|
||||
MockDevice1 = build_device_mock(
|
||||
name="fake-device-1", ipAddress="1.1.1.1", mac="aabbcc112233"
|
||||
)
|
||||
MockDevice2 = build_device_mock(
|
||||
name="fake-device-2", ipAddress="2.2.2.2", mac="bbccdd223344"
|
||||
)
|
||||
|
||||
discovery.return_value = [MockDevice1.device_info, MockDevice2.device_info]
|
||||
device.side_effect = [MockDevice1, MockDevice2]
|
||||
|
||||
await async_setup_gree(hass)
|
||||
await hass.async_block_till_done()
|
||||
assert discovery.call_count == 1
|
||||
assert len(hass.states.async_all(DOMAIN)) == 2
|
||||
|
||||
|
||||
async def test_discovery_setup_connection_error(hass, discovery, device):
|
||||
"""Test gree integration is setup."""
|
||||
MockDevice1 = build_device_mock(name="fake-device-1")
|
||||
MockDevice1.bind = AsyncMock(side_effect=DeviceNotBoundError)
|
||||
|
||||
MockDevice2 = build_device_mock(name="fake-device-2")
|
||||
MockDevice2.bind = AsyncMock(side_effect=DeviceNotBoundError)
|
||||
|
||||
device.side_effect = [MockDevice1, MockDevice2]
|
||||
|
||||
await async_setup_gree(hass)
|
||||
await hass.async_block_till_done()
|
||||
assert discovery.call_count == 1
|
||||
|
||||
assert not hass.states.async_all(DOMAIN)
|
||||
|
||||
|
||||
async def test_update_connection_failure(hass, discovery, device, mock_now):
|
||||
"""Testing update hvac connection failure exception."""
|
||||
device().update_state.side_effect = [
|
||||
DEFAULT_MOCK,
|
||||
DeviceTimeoutError,
|
||||
DeviceTimeoutError,
|
||||
]
|
||||
|
||||
await async_setup_gree(hass)
|
||||
|
||||
next_update = mock_now + timedelta(minutes=5)
|
||||
with patch("homeassistant.util.dt.utcnow", return_value=next_update):
|
||||
async_fire_time_changed(hass, next_update)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
# First update to make the device available
|
||||
state = hass.states.get(ENTITY_ID)
|
||||
assert state.name == "fake-device-1"
|
||||
assert state.state != STATE_UNAVAILABLE
|
||||
|
||||
next_update = mock_now + timedelta(minutes=10)
|
||||
with patch("homeassistant.util.dt.utcnow", return_value=next_update):
|
||||
async_fire_time_changed(hass, next_update)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
next_update = mock_now + timedelta(minutes=15)
|
||||
with patch("homeassistant.util.dt.utcnow", return_value=next_update):
|
||||
async_fire_time_changed(hass, next_update)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
# Then two more update failures to make the device unavailable
|
||||
state = hass.states.get(ENTITY_ID)
|
||||
assert state.name == "fake-device-1"
|
||||
assert state.state == STATE_UNAVAILABLE
|
||||
|
||||
|
||||
async def test_update_connection_failure_recovery(hass, discovery, device, mock_now):
|
||||
"""Testing update hvac connection failure recovery."""
|
||||
device().update_state.side_effect = [DeviceTimeoutError, DEFAULT_MOCK]
|
||||
|
||||
await async_setup_gree(hass)
|
||||
|
||||
next_update = mock_now + timedelta(minutes=5)
|
||||
with patch("homeassistant.util.dt.utcnow", return_value=next_update):
|
||||
async_fire_time_changed(hass, next_update)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
state = hass.states.get(ENTITY_ID)
|
||||
assert state.name == "fake-device-1"
|
||||
assert state.state == STATE_UNAVAILABLE
|
||||
|
||||
next_update = mock_now + timedelta(minutes=10)
|
||||
with patch("homeassistant.util.dt.utcnow", return_value=next_update):
|
||||
async_fire_time_changed(hass, next_update)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
state = hass.states.get(ENTITY_ID)
|
||||
assert state.name == "fake-device-1"
|
||||
assert state.state != STATE_UNAVAILABLE
|
||||
|
||||
|
||||
async def test_update_unhandled_exception(hass, discovery, device, mock_now):
|
||||
"""Testing update hvac connection unhandled response exception."""
|
||||
device().update_state.side_effect = [DEFAULT_MOCK, Exception]
|
||||
|
||||
await async_setup_gree(hass)
|
||||
|
||||
next_update = mock_now + timedelta(minutes=5)
|
||||
with patch("homeassistant.util.dt.utcnow", return_value=next_update):
|
||||
async_fire_time_changed(hass, next_update)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
state = hass.states.get(ENTITY_ID)
|
||||
assert state.name == "fake-device-1"
|
||||
assert state.state != STATE_UNAVAILABLE
|
||||
|
||||
next_update = mock_now + timedelta(minutes=10)
|
||||
with patch("homeassistant.util.dt.utcnow", return_value=next_update):
|
||||
async_fire_time_changed(hass, next_update)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
state = hass.states.get(ENTITY_ID)
|
||||
assert state.name == "fake-device-1"
|
||||
assert state.state == STATE_UNAVAILABLE
|
||||
|
||||
|
||||
async def test_send_command_device_timeout(hass, discovery, device, mock_now):
|
||||
"""Test for sending power on command to the device with a device timeout."""
|
||||
await async_setup_gree(hass)
|
||||
|
||||
# First update to make the device available
|
||||
next_update = mock_now + timedelta(minutes=5)
|
||||
with patch("homeassistant.util.dt.utcnow", return_value=next_update):
|
||||
async_fire_time_changed(hass, next_update)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
state = hass.states.get(ENTITY_ID)
|
||||
assert state.name == "fake-device-1"
|
||||
assert state.state != STATE_UNAVAILABLE
|
||||
|
||||
device().update_state.side_effect = DeviceTimeoutError
|
||||
device().push_state_update.side_effect = DeviceTimeoutError
|
||||
|
||||
# Second update to make an initial error (device is still available)
|
||||
next_update = mock_now + timedelta(minutes=5)
|
||||
with patch("homeassistant.util.dt.utcnow", return_value=next_update):
|
||||
async_fire_time_changed(hass, next_update)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
state = hass.states.get(ENTITY_ID)
|
||||
assert state is not None
|
||||
assert state.name == "fake-device-1"
|
||||
assert state.state != STATE_UNAVAILABLE
|
||||
|
||||
# Second attempt should make the device unavailable
|
||||
assert await hass.services.async_call(
|
||||
DOMAIN,
|
||||
SERVICE_SET_HVAC_MODE,
|
||||
{ATTR_ENTITY_ID: ENTITY_ID, ATTR_HVAC_MODE: HVAC_MODE_AUTO},
|
||||
blocking=True,
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
state = hass.states.get(ENTITY_ID)
|
||||
assert state is not None
|
||||
assert state.state == STATE_UNAVAILABLE
|
||||
|
||||
|
||||
async def test_send_command_device_unknown_error(hass, discovery, device, mock_now):
|
||||
"""Test for sending power on command to the device with a device timeout."""
|
||||
device().update_state.side_effect = [DEFAULT_MOCK, Exception]
|
||||
device().push_state_update.side_effect = Exception
|
||||
|
||||
await async_setup_gree(hass)
|
||||
|
||||
next_update = mock_now + timedelta(minutes=5)
|
||||
with patch("homeassistant.util.dt.utcnow", return_value=next_update):
|
||||
async_fire_time_changed(hass, next_update)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
# First update to make the device available
|
||||
state = hass.states.get(ENTITY_ID)
|
||||
assert state.name == "fake-device-1"
|
||||
assert state.state != STATE_UNAVAILABLE
|
||||
|
||||
assert await hass.services.async_call(
|
||||
DOMAIN,
|
||||
SERVICE_SET_HVAC_MODE,
|
||||
{ATTR_ENTITY_ID: ENTITY_ID, ATTR_HVAC_MODE: HVAC_MODE_AUTO},
|
||||
blocking=True,
|
||||
)
|
||||
|
||||
state = hass.states.get(ENTITY_ID)
|
||||
assert state is not None
|
||||
assert state.state == STATE_UNAVAILABLE
|
||||
|
||||
|
||||
async def test_send_power_on(hass, discovery, device, mock_now):
|
||||
"""Test for sending power on command to the device."""
|
||||
await async_setup_gree(hass)
|
||||
|
||||
next_update = mock_now + timedelta(minutes=5)
|
||||
with patch("homeassistant.util.dt.utcnow", return_value=next_update):
|
||||
async_fire_time_changed(hass, next_update)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert await hass.services.async_call(
|
||||
DOMAIN,
|
||||
SERVICE_SET_HVAC_MODE,
|
||||
{ATTR_ENTITY_ID: ENTITY_ID, ATTR_HVAC_MODE: HVAC_MODE_AUTO},
|
||||
blocking=True,
|
||||
)
|
||||
|
||||
state = hass.states.get(ENTITY_ID)
|
||||
assert state is not None
|
||||
assert state.state == HVAC_MODE_AUTO
|
||||
|
||||
|
||||
async def test_send_power_on_device_timeout(hass, discovery, device, mock_now):
|
||||
"""Test for sending power on command to the device with a device timeout."""
|
||||
device().push_state_update.side_effect = DeviceTimeoutError
|
||||
|
||||
await async_setup_gree(hass)
|
||||
|
||||
next_update = mock_now + timedelta(minutes=5)
|
||||
with patch("homeassistant.util.dt.utcnow", return_value=next_update):
|
||||
async_fire_time_changed(hass, next_update)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert await hass.services.async_call(
|
||||
DOMAIN,
|
||||
SERVICE_SET_HVAC_MODE,
|
||||
{ATTR_ENTITY_ID: ENTITY_ID, ATTR_HVAC_MODE: HVAC_MODE_AUTO},
|
||||
blocking=True,
|
||||
)
|
||||
|
||||
state = hass.states.get(ENTITY_ID)
|
||||
assert state is not None
|
||||
assert state.state == HVAC_MODE_AUTO
|
||||
|
||||
|
||||
async def test_send_target_temperature(hass, discovery, device, mock_now):
|
||||
"""Test for sending target temperature command to the device."""
|
||||
await async_setup_gree(hass)
|
||||
|
||||
next_update = mock_now + timedelta(minutes=5)
|
||||
with patch("homeassistant.util.dt.utcnow", return_value=next_update):
|
||||
async_fire_time_changed(hass, next_update)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert await hass.services.async_call(
|
||||
DOMAIN,
|
||||
SERVICE_SET_TEMPERATURE,
|
||||
{ATTR_ENTITY_ID: ENTITY_ID, ATTR_TEMPERATURE: 25.1},
|
||||
blocking=True,
|
||||
)
|
||||
|
||||
state = hass.states.get(ENTITY_ID)
|
||||
assert state is not None
|
||||
assert state.attributes.get(ATTR_TEMPERATURE) == 25
|
||||
|
||||
|
||||
async def test_send_target_temperature_device_timeout(
|
||||
hass, discovery, device, mock_now
|
||||
):
|
||||
"""Test for sending target temperature command to the device with a device timeout."""
|
||||
device().push_state_update.side_effect = DeviceTimeoutError
|
||||
|
||||
await async_setup_gree(hass)
|
||||
|
||||
next_update = mock_now + timedelta(minutes=5)
|
||||
with patch("homeassistant.util.dt.utcnow", return_value=next_update):
|
||||
async_fire_time_changed(hass, next_update)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert await hass.services.async_call(
|
||||
DOMAIN,
|
||||
SERVICE_SET_TEMPERATURE,
|
||||
{ATTR_ENTITY_ID: ENTITY_ID, ATTR_TEMPERATURE: 25.1},
|
||||
blocking=True,
|
||||
)
|
||||
|
||||
state = hass.states.get(ENTITY_ID)
|
||||
assert state is not None
|
||||
assert state.attributes.get(ATTR_TEMPERATURE) == 25
|
||||
|
||||
|
||||
async def test_update_target_temperature(hass, discovery, device, mock_now):
|
||||
"""Test for updating target temperature from the device."""
|
||||
device().target_temperature = 32
|
||||
|
||||
await async_setup_gree(hass)
|
||||
|
||||
next_update = mock_now + timedelta(minutes=5)
|
||||
with patch("homeassistant.util.dt.utcnow", return_value=next_update):
|
||||
async_fire_time_changed(hass, next_update)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
state = hass.states.get(ENTITY_ID)
|
||||
assert state is not None
|
||||
assert state.attributes.get(ATTR_TEMPERATURE) == 32
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"preset", (PRESET_AWAY, PRESET_ECO, PRESET_SLEEP, PRESET_BOOST, PRESET_NONE)
|
||||
)
|
||||
async def test_send_preset_mode(hass, discovery, device, mock_now, preset):
|
||||
"""Test for sending preset mode command to the device."""
|
||||
await async_setup_gree(hass)
|
||||
|
||||
next_update = mock_now + timedelta(minutes=5)
|
||||
with patch("homeassistant.util.dt.utcnow", return_value=next_update):
|
||||
async_fire_time_changed(hass, next_update)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert await hass.services.async_call(
|
||||
DOMAIN,
|
||||
SERVICE_SET_PRESET_MODE,
|
||||
{ATTR_ENTITY_ID: ENTITY_ID, ATTR_PRESET_MODE: preset},
|
||||
blocking=True,
|
||||
)
|
||||
|
||||
state = hass.states.get(ENTITY_ID)
|
||||
assert state is not None
|
||||
assert state.attributes.get(ATTR_PRESET_MODE) == preset
|
||||
|
||||
|
||||
async def test_send_invalid_preset_mode(hass, discovery, device, mock_now):
|
||||
"""Test for sending preset mode command to the device."""
|
||||
await async_setup_gree(hass)
|
||||
|
||||
next_update = mock_now + timedelta(minutes=5)
|
||||
with patch("homeassistant.util.dt.utcnow", return_value=next_update):
|
||||
async_fire_time_changed(hass, next_update)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
with pytest.raises(ValueError):
|
||||
await hass.services.async_call(
|
||||
DOMAIN,
|
||||
SERVICE_SET_PRESET_MODE,
|
||||
{ATTR_ENTITY_ID: ENTITY_ID, ATTR_PRESET_MODE: "invalid"},
|
||||
blocking=True,
|
||||
)
|
||||
|
||||
state = hass.states.get(ENTITY_ID)
|
||||
assert state is not None
|
||||
assert state.attributes.get(ATTR_PRESET_MODE) != "invalid"
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"preset", (PRESET_AWAY, PRESET_ECO, PRESET_SLEEP, PRESET_BOOST, PRESET_NONE)
|
||||
)
|
||||
async def test_send_preset_mode_device_timeout(
|
||||
hass, discovery, device, mock_now, preset
|
||||
):
|
||||
"""Test for sending preset mode command to the device with a device timeout."""
|
||||
device().push_state_update.side_effect = DeviceTimeoutError
|
||||
|
||||
await async_setup_gree(hass)
|
||||
|
||||
next_update = mock_now + timedelta(minutes=5)
|
||||
with patch("homeassistant.util.dt.utcnow", return_value=next_update):
|
||||
async_fire_time_changed(hass, next_update)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert await hass.services.async_call(
|
||||
DOMAIN,
|
||||
SERVICE_SET_PRESET_MODE,
|
||||
{ATTR_ENTITY_ID: ENTITY_ID, ATTR_PRESET_MODE: preset},
|
||||
blocking=True,
|
||||
)
|
||||
|
||||
state = hass.states.get(ENTITY_ID)
|
||||
assert state is not None
|
||||
assert state.attributes.get(ATTR_PRESET_MODE) == preset
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"preset", (PRESET_AWAY, PRESET_ECO, PRESET_SLEEP, PRESET_BOOST, PRESET_NONE)
|
||||
)
|
||||
async def test_update_preset_mode(hass, discovery, device, mock_now, preset):
|
||||
"""Test for updating preset mode from the device."""
|
||||
device().steady_heat = preset == PRESET_AWAY
|
||||
device().power_save = preset == PRESET_ECO
|
||||
device().sleep = preset == PRESET_SLEEP
|
||||
device().turbo = preset == PRESET_BOOST
|
||||
|
||||
await async_setup_gree(hass)
|
||||
|
||||
next_update = mock_now + timedelta(minutes=5)
|
||||
with patch("homeassistant.util.dt.utcnow", return_value=next_update):
|
||||
async_fire_time_changed(hass, next_update)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
state = hass.states.get(ENTITY_ID)
|
||||
assert state is not None
|
||||
assert state.attributes.get(ATTR_PRESET_MODE) == preset
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"hvac_mode",
|
||||
(
|
||||
HVAC_MODE_OFF,
|
||||
HVAC_MODE_AUTO,
|
||||
HVAC_MODE_COOL,
|
||||
HVAC_MODE_DRY,
|
||||
HVAC_MODE_FAN_ONLY,
|
||||
HVAC_MODE_HEAT,
|
||||
),
|
||||
)
|
||||
async def test_send_hvac_mode(hass, discovery, device, mock_now, hvac_mode):
|
||||
"""Test for sending hvac mode command to the device."""
|
||||
await async_setup_gree(hass)
|
||||
|
||||
next_update = mock_now + timedelta(minutes=5)
|
||||
with patch("homeassistant.util.dt.utcnow", return_value=next_update):
|
||||
async_fire_time_changed(hass, next_update)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert await hass.services.async_call(
|
||||
DOMAIN,
|
||||
SERVICE_SET_HVAC_MODE,
|
||||
{ATTR_ENTITY_ID: ENTITY_ID, ATTR_HVAC_MODE: hvac_mode},
|
||||
blocking=True,
|
||||
)
|
||||
|
||||
state = hass.states.get(ENTITY_ID)
|
||||
assert state is not None
|
||||
assert state.state == hvac_mode
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"hvac_mode",
|
||||
(HVAC_MODE_AUTO, HVAC_MODE_COOL, HVAC_MODE_DRY, HVAC_MODE_FAN_ONLY, HVAC_MODE_HEAT),
|
||||
)
|
||||
async def test_send_hvac_mode_device_timeout(
|
||||
hass, discovery, device, mock_now, hvac_mode
|
||||
):
|
||||
"""Test for sending hvac mode command to the device with a device timeout."""
|
||||
device().push_state_update.side_effect = DeviceTimeoutError
|
||||
|
||||
await async_setup_gree(hass)
|
||||
|
||||
next_update = mock_now + timedelta(minutes=5)
|
||||
with patch("homeassistant.util.dt.utcnow", return_value=next_update):
|
||||
async_fire_time_changed(hass, next_update)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert await hass.services.async_call(
|
||||
DOMAIN,
|
||||
SERVICE_SET_HVAC_MODE,
|
||||
{ATTR_ENTITY_ID: ENTITY_ID, ATTR_HVAC_MODE: hvac_mode},
|
||||
blocking=True,
|
||||
)
|
||||
|
||||
state = hass.states.get(ENTITY_ID)
|
||||
assert state is not None
|
||||
assert state.state == hvac_mode
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"hvac_mode",
|
||||
(
|
||||
HVAC_MODE_OFF,
|
||||
HVAC_MODE_AUTO,
|
||||
HVAC_MODE_COOL,
|
||||
HVAC_MODE_DRY,
|
||||
HVAC_MODE_FAN_ONLY,
|
||||
HVAC_MODE_HEAT,
|
||||
),
|
||||
)
|
||||
async def test_update_hvac_mode(hass, discovery, device, mock_now, hvac_mode):
|
||||
"""Test for updating hvac mode from the device."""
|
||||
device().power = hvac_mode != HVAC_MODE_OFF
|
||||
device().mode = HVAC_MODES_REVERSE.get(hvac_mode)
|
||||
|
||||
await async_setup_gree(hass)
|
||||
|
||||
next_update = mock_now + timedelta(minutes=5)
|
||||
with patch("homeassistant.util.dt.utcnow", return_value=next_update):
|
||||
async_fire_time_changed(hass, next_update)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
state = hass.states.get(ENTITY_ID)
|
||||
assert state is not None
|
||||
assert state.state == hvac_mode
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"fan_mode",
|
||||
(FAN_AUTO, FAN_LOW, FAN_MEDIUM_LOW, FAN_MEDIUM, FAN_MEDIUM_HIGH, FAN_HIGH),
|
||||
)
|
||||
async def test_send_fan_mode(hass, discovery, device, mock_now, fan_mode):
|
||||
"""Test for sending fan mode command to the device."""
|
||||
await async_setup_gree(hass)
|
||||
|
||||
next_update = mock_now + timedelta(minutes=5)
|
||||
with patch("homeassistant.util.dt.utcnow", return_value=next_update):
|
||||
async_fire_time_changed(hass, next_update)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert await hass.services.async_call(
|
||||
DOMAIN,
|
||||
SERVICE_SET_FAN_MODE,
|
||||
{ATTR_ENTITY_ID: ENTITY_ID, ATTR_FAN_MODE: fan_mode},
|
||||
blocking=True,
|
||||
)
|
||||
|
||||
state = hass.states.get(ENTITY_ID)
|
||||
assert state is not None
|
||||
assert state.attributes.get(ATTR_FAN_MODE) == fan_mode
|
||||
|
||||
|
||||
async def test_send_invalid_fan_mode(hass, discovery, device, mock_now):
|
||||
"""Test for sending fan mode command to the device."""
|
||||
await async_setup_gree(hass)
|
||||
|
||||
next_update = mock_now + timedelta(minutes=5)
|
||||
with patch("homeassistant.util.dt.utcnow", return_value=next_update):
|
||||
async_fire_time_changed(hass, next_update)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
with pytest.raises(ValueError):
|
||||
await hass.services.async_call(
|
||||
DOMAIN,
|
||||
SERVICE_SET_FAN_MODE,
|
||||
{ATTR_ENTITY_ID: ENTITY_ID, ATTR_FAN_MODE: "invalid"},
|
||||
blocking=True,
|
||||
)
|
||||
|
||||
state = hass.states.get(ENTITY_ID)
|
||||
assert state is not None
|
||||
assert state.attributes.get(ATTR_FAN_MODE) != "invalid"
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"fan_mode",
|
||||
(FAN_AUTO, FAN_LOW, FAN_MEDIUM_LOW, FAN_MEDIUM, FAN_MEDIUM_HIGH, FAN_HIGH),
|
||||
)
|
||||
async def test_send_fan_mode_device_timeout(
|
||||
hass, discovery, device, mock_now, fan_mode
|
||||
):
|
||||
"""Test for sending fan mode command to the device with a device timeout."""
|
||||
device().push_state_update.side_effect = DeviceTimeoutError
|
||||
|
||||
await async_setup_gree(hass)
|
||||
|
||||
next_update = mock_now + timedelta(minutes=5)
|
||||
with patch("homeassistant.util.dt.utcnow", return_value=next_update):
|
||||
async_fire_time_changed(hass, next_update)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert await hass.services.async_call(
|
||||
DOMAIN,
|
||||
SERVICE_SET_FAN_MODE,
|
||||
{ATTR_ENTITY_ID: ENTITY_ID, ATTR_FAN_MODE: fan_mode},
|
||||
blocking=True,
|
||||
)
|
||||
|
||||
state = hass.states.get(ENTITY_ID)
|
||||
assert state is not None
|
||||
assert state.attributes.get(ATTR_FAN_MODE) == fan_mode
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"fan_mode",
|
||||
(FAN_AUTO, FAN_LOW, FAN_MEDIUM_LOW, FAN_MEDIUM, FAN_MEDIUM_HIGH, FAN_HIGH),
|
||||
)
|
||||
async def test_update_fan_mode(hass, discovery, device, mock_now, fan_mode):
|
||||
"""Test for updating fan mode from the device."""
|
||||
device().fan_speed = FAN_MODES_REVERSE.get(fan_mode)
|
||||
|
||||
await async_setup_gree(hass)
|
||||
|
||||
next_update = mock_now + timedelta(minutes=5)
|
||||
with patch("homeassistant.util.dt.utcnow", return_value=next_update):
|
||||
async_fire_time_changed(hass, next_update)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
state = hass.states.get(ENTITY_ID)
|
||||
assert state is not None
|
||||
assert state.attributes.get(ATTR_FAN_MODE) == fan_mode
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"swing_mode", (SWING_OFF, SWING_BOTH, SWING_VERTICAL, SWING_HORIZONTAL)
|
||||
)
|
||||
async def test_send_swing_mode(hass, discovery, device, mock_now, swing_mode):
|
||||
"""Test for sending swing mode command to the device."""
|
||||
await async_setup_gree(hass)
|
||||
|
||||
next_update = mock_now + timedelta(minutes=5)
|
||||
with patch("homeassistant.util.dt.utcnow", return_value=next_update):
|
||||
async_fire_time_changed(hass, next_update)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert await hass.services.async_call(
|
||||
DOMAIN,
|
||||
SERVICE_SET_SWING_MODE,
|
||||
{ATTR_ENTITY_ID: ENTITY_ID, ATTR_SWING_MODE: swing_mode},
|
||||
blocking=True,
|
||||
)
|
||||
|
||||
state = hass.states.get(ENTITY_ID)
|
||||
assert state is not None
|
||||
assert state.attributes.get(ATTR_SWING_MODE) == swing_mode
|
||||
|
||||
|
||||
async def test_send_invalid_swing_mode(hass, discovery, device, mock_now):
|
||||
"""Test for sending swing mode command to the device."""
|
||||
await async_setup_gree(hass)
|
||||
|
||||
next_update = mock_now + timedelta(minutes=5)
|
||||
with patch("homeassistant.util.dt.utcnow", return_value=next_update):
|
||||
async_fire_time_changed(hass, next_update)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
with pytest.raises(ValueError):
|
||||
await hass.services.async_call(
|
||||
DOMAIN,
|
||||
SERVICE_SET_SWING_MODE,
|
||||
{ATTR_ENTITY_ID: ENTITY_ID, ATTR_SWING_MODE: "invalid"},
|
||||
blocking=True,
|
||||
)
|
||||
|
||||
state = hass.states.get(ENTITY_ID)
|
||||
assert state is not None
|
||||
assert state.attributes.get(ATTR_SWING_MODE) != "invalid"
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"swing_mode", (SWING_OFF, SWING_BOTH, SWING_VERTICAL, SWING_HORIZONTAL)
|
||||
)
|
||||
async def test_send_swing_mode_device_timeout(
|
||||
hass, discovery, device, mock_now, swing_mode
|
||||
):
|
||||
"""Test for sending swing mode command to the device with a device timeout."""
|
||||
device().push_state_update.side_effect = DeviceTimeoutError
|
||||
|
||||
await async_setup_gree(hass)
|
||||
|
||||
next_update = mock_now + timedelta(minutes=5)
|
||||
with patch("homeassistant.util.dt.utcnow", return_value=next_update):
|
||||
async_fire_time_changed(hass, next_update)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert await hass.services.async_call(
|
||||
DOMAIN,
|
||||
SERVICE_SET_SWING_MODE,
|
||||
{ATTR_ENTITY_ID: ENTITY_ID, ATTR_SWING_MODE: swing_mode},
|
||||
blocking=True,
|
||||
)
|
||||
|
||||
state = hass.states.get(ENTITY_ID)
|
||||
assert state is not None
|
||||
assert state.attributes.get(ATTR_SWING_MODE) == swing_mode
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"swing_mode", (SWING_OFF, SWING_BOTH, SWING_VERTICAL, SWING_HORIZONTAL)
|
||||
)
|
||||
async def test_update_swing_mode(hass, discovery, device, mock_now, swing_mode):
|
||||
"""Test for updating swing mode from the device."""
|
||||
device().horizontal_swing = (
|
||||
HorizontalSwing.FullSwing
|
||||
if swing_mode in (SWING_BOTH, SWING_HORIZONTAL)
|
||||
else HorizontalSwing.Default
|
||||
)
|
||||
device().vertical_swing = (
|
||||
VerticalSwing.FullSwing
|
||||
if swing_mode in (SWING_BOTH, SWING_VERTICAL)
|
||||
else VerticalSwing.Default
|
||||
)
|
||||
|
||||
await async_setup_gree(hass)
|
||||
|
||||
next_update = mock_now + timedelta(minutes=5)
|
||||
with patch("homeassistant.util.dt.utcnow", return_value=next_update):
|
||||
async_fire_time_changed(hass, next_update)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
state = hass.states.get(ENTITY_ID)
|
||||
assert state is not None
|
||||
assert state.attributes.get(ATTR_SWING_MODE) == swing_mode
|
||||
|
||||
|
||||
async def test_name(hass, discovery, device):
|
||||
"""Test for name property."""
|
||||
await async_setup_gree(hass)
|
||||
state = hass.states.get(ENTITY_ID)
|
||||
assert state.attributes[ATTR_FRIENDLY_NAME] == "fake-device-1"
|
||||
|
||||
|
||||
async def test_supported_features_with_turnon(hass, discovery, device):
|
||||
"""Test for supported_features property."""
|
||||
await async_setup_gree(hass)
|
||||
state = hass.states.get(ENTITY_ID)
|
||||
assert state.attributes[ATTR_SUPPORTED_FEATURES] == SUPPORTED_FEATURES
|
20
tests/components/gree/test_config_flow.py
Normal file
20
tests/components/gree/test_config_flow.py
Normal file
@ -0,0 +1,20 @@
|
||||
"""Tests for the Gree Integration."""
|
||||
from homeassistant import config_entries, data_entry_flow
|
||||
from homeassistant.components.gree.const import DOMAIN as GREE_DOMAIN
|
||||
|
||||
|
||||
async def test_creating_entry_sets_up_climate(hass, discovery, device, setup):
|
||||
"""Test setting up Gree creates the climate components."""
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
GREE_DOMAIN, context={"source": config_entries.SOURCE_USER}
|
||||
)
|
||||
|
||||
# Confirmation form
|
||||
assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
|
||||
|
||||
result = await hass.config_entries.flow.async_configure(result["flow_id"], {})
|
||||
assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY
|
||||
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert len(setup.mock_calls) == 1
|
38
tests/components/gree/test_init.py
Normal file
38
tests/components/gree/test_init.py
Normal file
@ -0,0 +1,38 @@
|
||||
"""Tests for the Gree Integration."""
|
||||
|
||||
from homeassistant.components.gree.const import DOMAIN as GREE_DOMAIN
|
||||
from homeassistant.config_entries import ENTRY_STATE_LOADED, ENTRY_STATE_NOT_LOADED
|
||||
from homeassistant.setup import async_setup_component
|
||||
|
||||
from tests.async_mock import patch
|
||||
from tests.common import MockConfigEntry
|
||||
|
||||
|
||||
async def test_setup_simple(hass):
|
||||
"""Test gree integration is setup."""
|
||||
await async_setup_component(hass, GREE_DOMAIN, {})
|
||||
await hass.async_block_till_done()
|
||||
|
||||
# No flows started
|
||||
assert len(hass.config_entries.flow.async_progress()) == 0
|
||||
|
||||
|
||||
async def test_unload_config_entry(hass):
|
||||
"""Test that the async_unload_entry works."""
|
||||
# As we have currently no configuration, we just to pass the domain here.
|
||||
entry = MockConfigEntry(domain=GREE_DOMAIN)
|
||||
entry.add_to_hass(hass)
|
||||
|
||||
with patch(
|
||||
"homeassistant.components.gree.climate.async_setup_entry",
|
||||
return_value=True,
|
||||
) as climate_setup:
|
||||
assert await async_setup_component(hass, GREE_DOMAIN, {})
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert len(climate_setup.mock_calls) == 1
|
||||
assert entry.state == ENTRY_STATE_LOADED
|
||||
|
||||
await hass.config_entries.async_unload(entry.entry_id)
|
||||
|
||||
assert entry.state == ENTRY_STATE_NOT_LOADED
|
Reference in New Issue
Block a user