diff --git a/CODEOWNERS b/CODEOWNERS index 04f5b7c5a86..d67c8666865 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -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 diff --git a/homeassistant/components/gree/__init__.py b/homeassistant/components/gree/__init__.py new file mode 100644 index 00000000000..96f2401e8d7 --- /dev/null +++ b/homeassistant/components/gree/__init__.py @@ -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 diff --git a/homeassistant/components/gree/bridge.py b/homeassistant/components/gree/bridge.py new file mode 100644 index 00000000000..6c02126e678 --- /dev/null +++ b/homeassistant/components/gree/bridge.py @@ -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.""" diff --git a/homeassistant/components/gree/climate.py b/homeassistant/components/gree/climate.py new file mode 100644 index 00000000000..724903ef360 --- /dev/null +++ b/homeassistant/components/gree/climate.py @@ -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 diff --git a/homeassistant/components/gree/config_flow.py b/homeassistant/components/gree/config_flow.py new file mode 100644 index 00000000000..76ea2159e2f --- /dev/null +++ b/homeassistant/components/gree/config_flow.py @@ -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 +) diff --git a/homeassistant/components/gree/const.py b/homeassistant/components/gree/const.py new file mode 100644 index 00000000000..95435bb3bd9 --- /dev/null +++ b/homeassistant/components/gree/const.py @@ -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 diff --git a/homeassistant/components/gree/manifest.json b/homeassistant/components/gree/manifest.json new file mode 100644 index 00000000000..ea04eb12f51 --- /dev/null +++ b/homeassistant/components/gree/manifest.json @@ -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"] +} diff --git a/homeassistant/components/gree/strings.json b/homeassistant/components/gree/strings.json new file mode 100644 index 00000000000..878ebc3bbd7 --- /dev/null +++ b/homeassistant/components/gree/strings.json @@ -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%]" + } + } +} diff --git a/homeassistant/components/gree/translations/en.json b/homeassistant/components/gree/translations/en.json new file mode 100644 index 00000000000..996292cde3f --- /dev/null +++ b/homeassistant/components/gree/translations/en.json @@ -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" +} \ No newline at end of file diff --git a/homeassistant/generated/config_flows.py b/homeassistant/generated/config_flows.py index 9c416f2fb40..db9dd15ab5f 100644 --- a/homeassistant/generated/config_flows.py +++ b/homeassistant/generated/config_flows.py @@ -72,6 +72,7 @@ FLOWS = [ "goalzero", "gogogate2", "gpslogger", + "gree", "griddy", "guardian", "hangouts", diff --git a/requirements_all.txt b/requirements_all.txt index 8b8e6cd6ab1..f76fc35be51 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -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 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 80978d6496a..b4e39eb766b 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -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 diff --git a/tests/components/gree/__init__.py b/tests/components/gree/__init__.py new file mode 100644 index 00000000000..2e2884381a3 --- /dev/null +++ b/tests/components/gree/__init__.py @@ -0,0 +1 @@ +"""Tests for the Gree Climate integration.""" diff --git a/tests/components/gree/common.py b/tests/components/gree/common.py new file mode 100644 index 00000000000..8894730c8ca --- /dev/null +++ b/tests/components/gree/common.py @@ -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 diff --git a/tests/components/gree/conftest.py b/tests/components/gree/conftest.py new file mode 100644 index 00000000000..b102f0f36fe --- /dev/null +++ b/tests/components/gree/conftest.py @@ -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 diff --git a/tests/components/gree/test_climate.py b/tests/components/gree/test_climate.py new file mode 100644 index 00000000000..9f3946e6ade --- /dev/null +++ b/tests/components/gree/test_climate.py @@ -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 diff --git a/tests/components/gree/test_config_flow.py b/tests/components/gree/test_config_flow.py new file mode 100644 index 00000000000..bb5f59b573d --- /dev/null +++ b/tests/components/gree/test_config_flow.py @@ -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 diff --git a/tests/components/gree/test_init.py b/tests/components/gree/test_init.py new file mode 100644 index 00000000000..1ea0727b220 --- /dev/null +++ b/tests/components/gree/test_init.py @@ -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