diff --git a/homeassistant/components/gree/__init__.py b/homeassistant/components/gree/__init__.py index 92b56a4804e..b215d4eb911 100644 --- a/homeassistant/components/gree/__init__.py +++ b/homeassistant/components/gree/__init__.py @@ -1,14 +1,23 @@ """The Gree Climate integration.""" import asyncio +from datetime import timedelta import logging from homeassistant.components.climate import DOMAIN as CLIMATE_DOMAIN from homeassistant.components.switch import DOMAIN as SWITCH_DOMAIN from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant +from homeassistant.helpers.event import async_track_time_interval -from .bridge import CannotConnect, DeviceDataUpdateCoordinator, DeviceHelper -from .const import COORDINATOR, DOMAIN +from .bridge import DiscoveryService +from .const import ( + COORDINATORS, + DATA_DISCOVERY_INTERVAL, + DATA_DISCOVERY_SERVICE, + DISCOVERY_SCAN_INTERVAL, + DISPATCHERS, + DOMAIN, +) _LOGGER = logging.getLogger(__name__) @@ -21,31 +30,11 @@ async def async_setup(hass: HomeAssistant, config: dict): async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): """Set up Gree Climate from a config entry.""" - devices = [] + gree_discovery = DiscoveryService(hass) + hass.data[DATA_DISCOVERY_SERVICE] = gree_discovery - # 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") + hass.data[DOMAIN].setdefault(DISPATCHERS, []) - 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) - - coordinators = [DeviceDataUpdateCoordinator(hass, d) for d in devices] - await asyncio.gather(*[x.async_refresh() for x in coordinators]) - - hass.data[DOMAIN][COORDINATOR] = coordinators hass.async_create_task( hass.config_entries.async_forward_entry_setup(entry, CLIMATE_DOMAIN) ) @@ -53,11 +42,31 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): hass.config_entries.async_forward_entry_setup(entry, SWITCH_DOMAIN) ) + async def _async_scan_update(_=None): + await gree_discovery.discovery.scan() + + _LOGGER.debug("Scanning network for Gree devices") + await _async_scan_update() + + hass.data[DOMAIN][DATA_DISCOVERY_INTERVAL] = async_track_time_interval( + hass, _async_scan_update, timedelta(seconds=DISCOVERY_SCAN_INTERVAL) + ) + return True async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry): """Unload a config entry.""" + if hass.data[DOMAIN].get(DISPATCHERS) is not None: + for cleanup in hass.data[DOMAIN][DISPATCHERS]: + cleanup() + + if hass.data[DOMAIN].get(DATA_DISCOVERY_INTERVAL) is not None: + hass.data[DOMAIN].pop(DATA_DISCOVERY_INTERVAL)() + + if hass.data.get(DATA_DISCOVERY_SERVICE) is not None: + hass.data.pop(DATA_DISCOVERY_SERVICE) + results = asyncio.gather( hass.config_entries.async_forward_entry_unload(entry, CLIMATE_DOMAIN), hass.config_entries.async_forward_entry_unload(entry, SWITCH_DOMAIN), @@ -65,8 +74,7 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry): unload_ok = all(await results) if unload_ok: - hass.data[DOMAIN].pop("devices", None) - hass.data[DOMAIN].pop(CLIMATE_DOMAIN, None) - hass.data[DOMAIN].pop(SWITCH_DOMAIN, None) + hass.data[DOMAIN].pop(COORDINATORS, None) + hass.data[DOMAIN].pop(DISPATCHERS, None) return unload_ok diff --git a/homeassistant/components/gree/bridge.py b/homeassistant/components/gree/bridge.py index af523f385aa..87f02ab82c4 100644 --- a/homeassistant/components/gree/bridge.py +++ b/homeassistant/components/gree/bridge.py @@ -5,14 +5,20 @@ from datetime import timedelta import logging from greeclimate.device import Device, DeviceInfo -from greeclimate.discovery import Discovery +from greeclimate.discovery import Discovery, Listener from greeclimate.exceptions import DeviceNotBoundError, DeviceTimeoutError -from homeassistant import exceptions from homeassistant.core import HomeAssistant +from homeassistant.helpers.dispatcher import async_dispatcher_send from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed -from .const import DOMAIN, MAX_ERRORS +from .const import ( + COORDINATORS, + DISCOVERY_TIMEOUT, + DISPATCH_DEVICE_DISCOVERED, + DOMAIN, + MAX_ERRORS, +) _LOGGER = logging.getLogger(__name__) @@ -36,6 +42,8 @@ class DeviceDataUpdateCoordinator(DataUpdateCoordinator): """Update the state of the device.""" try: await self.device.update_state() + except DeviceNotBoundError as error: + raise UpdateFailed(f"Device {self.name} is unavailable") from error except DeviceTimeoutError as error: self._error_count += 1 @@ -46,16 +54,7 @@ class DeviceDataUpdateCoordinator(DataUpdateCoordinator): self.name, self.device.device_info, ) - raise UpdateFailed(error) from error - else: - if not self.last_update_success and self._error_count: - _LOGGER.warning( - "Device is available: %s (%s)", - self.name, - str(self.device.device_info), - ) - - self._error_count = 0 + raise UpdateFailed(f"Device {self.name} is unavailable") from error async def push_state_update(self): """Send state updates to the physical device.""" @@ -69,28 +68,38 @@ class DeviceDataUpdateCoordinator(DataUpdateCoordinator): ) -class DeviceHelper: - """Device search and bind wrapper for Gree platform.""" +class DiscoveryService(Listener): + """Discovery event handler for gree devices.""" - @staticmethod - async def try_bind_device(device_info: DeviceInfo) -> Device: - """Try and bing with a discovered device. + def __init__(self, hass) -> None: + """Initialize discovery service.""" + super().__init__() + self.hass = hass + + self.discovery = Discovery(DISCOVERY_TIMEOUT) + self.discovery.add_listener(self) + + hass.data[DOMAIN].setdefault(COORDINATORS, []) + + async def device_found(self, device_info: DeviceInfo) -> None: + """Handle new device found on the network.""" - 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 + except DeviceNotBoundError: + _LOGGER.error("Unable to bind to gree device: %s", device_info) + except DeviceTimeoutError: + _LOGGER.error("Timeout trying to bind to gree device: %s", device_info) - @staticmethod - async def find_devices() -> list[DeviceInfo]: - """Gather a list of device infos from the local network.""" - return await Discovery.search_devices() + _LOGGER.info( + "Adding Gree device %s at %s:%i", + device.device_info.name, + device.device_info.ip, + device.device_info.port, + ) + coordo = DeviceDataUpdateCoordinator(self.hass, device) + self.hass.data[DOMAIN][COORDINATORS].append(coordo) + await coordo.async_refresh() - -class CannotConnect(exceptions.HomeAssistantError): - """Error to indicate we cannot connect.""" + async_dispatcher_send(self.hass, DISPATCH_DEVICE_DISCOVERED, coordo) diff --git a/homeassistant/components/gree/climate.py b/homeassistant/components/gree/climate.py index a5ef39be071..e468195ff92 100644 --- a/homeassistant/components/gree/climate.py +++ b/homeassistant/components/gree/climate.py @@ -43,11 +43,15 @@ from homeassistant.const import ( TEMP_CELSIUS, TEMP_FAHRENHEIT, ) +from homeassistant.core import callback from homeassistant.helpers.device_registry import CONNECTION_NETWORK_MAC +from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.update_coordinator import CoordinatorEntity from .const import ( - COORDINATOR, + COORDINATORS, + DISPATCH_DEVICE_DISCOVERED, + DISPATCHERS, DOMAIN, FAN_MEDIUM_HIGH, FAN_MEDIUM_LOW, @@ -97,11 +101,17 @@ SUPPORTED_FEATURES = ( 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(coordinator) - for coordinator in hass.data[DOMAIN][COORDINATOR] - ] + + @callback + def init_device(coordinator): + """Register the device.""" + async_add_entities([GreeClimateEntity(coordinator)]) + + for coordinator in hass.data[DOMAIN][COORDINATORS]: + init_device(coordinator) + + hass.data[DOMAIN][DISPATCHERS].append( + async_dispatcher_connect(hass, DISPATCH_DEVICE_DISCOVERED, init_device) ) diff --git a/homeassistant/components/gree/config_flow.py b/homeassistant/components/gree/config_flow.py index 76ea2159e2f..cc61eabe12c 100644 --- a/homeassistant/components/gree/config_flow.py +++ b/homeassistant/components/gree/config_flow.py @@ -1,14 +1,16 @@ """Config flow for Gree.""" +from greeclimate.discovery import Discovery + from homeassistant import config_entries from homeassistant.helpers import config_entry_flow -from .bridge import DeviceHelper -from .const import DOMAIN +from .const import DISCOVERY_TIMEOUT, DOMAIN async def _async_has_devices(hass) -> bool: """Return if there are devices that can be discovered.""" - devices = await DeviceHelper.find_devices() + gree_discovery = Discovery(DISCOVERY_TIMEOUT) + devices = await gree_discovery.scan(wait_for=DISCOVERY_TIMEOUT) return len(devices) > 0 diff --git a/homeassistant/components/gree/const.py b/homeassistant/components/gree/const.py index 9c645062256..2d9a48496b2 100644 --- a/homeassistant/components/gree/const.py +++ b/homeassistant/components/gree/const.py @@ -1,5 +1,15 @@ """Constants for the Gree Climate integration.""" +COORDINATORS = "coordinators" + +DATA_DISCOVERY_SERVICE = "gree_discovery" +DATA_DISCOVERY_INTERVAL = "gree_discovery_interval" + +DISCOVERY_SCAN_INTERVAL = 300 +DISCOVERY_TIMEOUT = 8 +DISPATCH_DEVICE_DISCOVERED = "gree_device_discovered" +DISPATCHERS = "dispatchers" + DOMAIN = "gree" COORDINATOR = "coordinator" diff --git a/homeassistant/components/gree/manifest.json b/homeassistant/components/gree/manifest.json index 0d2bed3ff28..c163fc152fd 100644 --- a/homeassistant/components/gree/manifest.json +++ b/homeassistant/components/gree/manifest.json @@ -3,6 +3,6 @@ "name": "Gree Climate", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/gree", - "requirements": ["greeclimate==0.10.3"], + "requirements": ["greeclimate==0.11.4"], "codeowners": ["@cmroche"] } \ No newline at end of file diff --git a/homeassistant/components/gree/switch.py b/homeassistant/components/gree/switch.py index 12c94ddec61..7f659d7e64b 100644 --- a/homeassistant/components/gree/switch.py +++ b/homeassistant/components/gree/switch.py @@ -2,19 +2,27 @@ from __future__ import annotations from homeassistant.components.switch import DEVICE_CLASS_SWITCH, SwitchEntity +from homeassistant.core import callback from homeassistant.helpers.device_registry import CONNECTION_NETWORK_MAC +from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.update_coordinator import CoordinatorEntity -from .const import COORDINATOR, DOMAIN +from .const import COORDINATORS, DISPATCH_DEVICE_DISCOVERED, DISPATCHERS, DOMAIN async def async_setup_entry(hass, config_entry, async_add_entities): """Set up the Gree HVAC device from a config entry.""" - async_add_entities( - [ - GreeSwitchEntity(coordinator) - for coordinator in hass.data[DOMAIN][COORDINATOR] - ] + + @callback + def init_device(coordinator): + """Register the device.""" + async_add_entities([GreeSwitchEntity(coordinator)]) + + for coordinator in hass.data[DOMAIN][COORDINATORS]: + init_device(coordinator) + + hass.data[DOMAIN][DISPATCHERS].append( + async_dispatcher_connect(hass, DISPATCH_DEVICE_DISCOVERED, init_device) ) diff --git a/requirements_all.txt b/requirements_all.txt index 4de94af64a1..d256c4b4a8e 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -699,7 +699,7 @@ gpiozero==1.5.1 gps3==0.33.3 # homeassistant.components.gree -greeclimate==0.10.3 +greeclimate==0.11.4 # homeassistant.components.greeneye_monitor greeneye_monitor==2.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 458a1a4ab5e..0c68adf2cdb 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -384,7 +384,7 @@ google-nest-sdm==0.2.12 googlemaps==2.5.1 # homeassistant.components.gree -greeclimate==0.10.3 +greeclimate==0.11.4 # homeassistant.components.profiler guppy3==3.1.0 diff --git a/tests/components/gree/common.py b/tests/components/gree/common.py index d9fcfba39ce..2c9c295da1c 100644 --- a/tests/components/gree/common.py +++ b/tests/components/gree/common.py @@ -1,6 +1,43 @@ """Common helpers for gree test cases.""" +import asyncio +import logging from unittest.mock import AsyncMock, Mock +from greeclimate.discovery import Listener + +from homeassistant.components.gree.const import DISCOVERY_TIMEOUT + +_LOGGER = logging.getLogger(__name__) + + +class FakeDiscovery: + """Mock class replacing Gree device discovery.""" + + def __init__(self, timeout: int = DISCOVERY_TIMEOUT) -> None: + """Initialize the class.""" + self.mock_devices = [build_device_mock()] + self.timeout = timeout + self._listeners = [] + self.scan_count = 0 + + def add_listener(self, listener: Listener) -> None: + """Add an event listener.""" + self._listeners.append(listener) + + async def scan(self, wait_for: int = 0): + """Search for devices, return mocked data.""" + self.scan_count += 1 + _LOGGER.info("CALLED SCAN %d TIMES", self.scan_count) + + infos = [x.device_info for x in self.mock_devices] + for listener in self._listeners: + [await listener.device_found(x) for x in infos] + + if wait_for: + await asyncio.sleep(wait_for) + + return infos + def build_device_info_mock( name="fake-device-1", ipAddress="1.1.1.1", mac="aabbcc112233" diff --git a/tests/components/gree/conftest.py b/tests/components/gree/conftest.py index bc9a6451dce..6aabb95a1bb 100644 --- a/tests/components/gree/conftest.py +++ b/tests/components/gree/conftest.py @@ -1,36 +1,24 @@ """Pytest module configuration.""" -from unittest.mock import AsyncMock, patch +from unittest.mock import patch import pytest -from .common import build_device_info_mock, build_device_mock +from .common import FakeDiscovery, build_device_mock -@pytest.fixture(name="discovery") +@pytest.fixture(autouse=True, 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: + """Patch the discovery object.""" + with patch("homeassistant.components.gree.bridge.Discovery") as mock: + mock.return_value = FakeDiscovery() yield mock -@pytest.fixture(name="device") +@pytest.fixture(autouse=True, name="device") def device_fixture(): - """Path the device search and bind.""" + """Patch 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 index d85976c2410..62dd7ca545f 100644 --- a/tests/components/gree/test_climate.py +++ b/tests/components/gree/test_climate.py @@ -97,7 +97,7 @@ async def test_discovery_setup(hass, discovery, device): name="fake-device-2", ipAddress="2.2.2.2", mac="bbccdd223344" ) - discovery.return_value = [MockDevice1.device_info, MockDevice2.device_info] + discovery.return_value.mock_devices = [MockDevice1, MockDevice2] device.side_effect = [MockDevice1, MockDevice2] await async_setup_gree(hass) @@ -106,24 +106,127 @@ async def test_discovery_setup(hass, discovery, device): assert len(hass.states.async_all(DOMAIN)) == 2 -async def test_discovery_setup_connection_error(hass, discovery, device): +async def test_discovery_setup_connection_error(hass, discovery, device, mock_now): """Test gree integration is setup.""" - MockDevice1 = build_device_mock(name="fake-device-1") + MockDevice1 = build_device_mock( + name="fake-device-1", ipAddress="1.1.1.1", mac="aabbcc112233" + ) + MockDevice1.bind = AsyncMock(side_effect=DeviceNotBoundError) + MockDevice1.update_state = AsyncMock(side_effect=DeviceNotBoundError) + + discovery.return_value.mock_devices = [MockDevice1] + device.return_value = MockDevice1 + + await async_setup_gree(hass) + await hass.async_block_till_done() + + assert len(hass.states.async_all(DOMAIN)) == 1 + state = hass.states.get(ENTITY_ID) + assert state.name == "fake-device-1" + assert state.state == STATE_UNAVAILABLE + + +async def test_discovery_after_setup(hass, discovery, device, mock_now): + """Test gree devices don't change after multiple discoveries.""" + MockDevice1 = build_device_mock( + name="fake-device-1", ipAddress="1.1.1.1", mac="aabbcc112233" + ) MockDevice1.bind = AsyncMock(side_effect=DeviceNotBoundError) - MockDevice2 = build_device_mock(name="fake-device-2") - MockDevice2.bind = AsyncMock(side_effect=DeviceNotBoundError) + MockDevice2 = build_device_mock( + name="fake-device-2", ipAddress="2.2.2.2", mac="bbccdd223344" + ) + MockDevice2.bind = AsyncMock(side_effect=DeviceTimeoutError) + discovery.return_value.mock_devices = [MockDevice1, MockDevice2] 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) + assert discovery.return_value.scan_count == 1 + assert len(hass.states.async_all(DOMAIN)) == 2 + + # rediscover the same devices shouldn't change anything + discovery.return_value.mock_devices = [MockDevice1, MockDevice2] + device.side_effect = [MockDevice1, MockDevice2] + + next_update = mock_now + timedelta(minutes=6) + with patch("homeassistant.util.dt.utcnow", return_value=next_update): + async_fire_time_changed(hass, next_update) + await hass.async_block_till_done() + + assert discovery.return_value.scan_count == 2 + assert len(hass.states.async_all(DOMAIN)) == 2 -async def test_update_connection_failure(hass, discovery, device, mock_now): +async def test_discovery_add_device_after_setup(hass, discovery, device, mock_now): + """Test gree devices can be added after initial setup.""" + MockDevice1 = build_device_mock( + name="fake-device-1", ipAddress="1.1.1.1", mac="aabbcc112233" + ) + MockDevice1.bind = AsyncMock(side_effect=DeviceNotBoundError) + + MockDevice2 = build_device_mock( + name="fake-device-2", ipAddress="2.2.2.2", mac="bbccdd223344" + ) + MockDevice2.bind = AsyncMock(side_effect=DeviceTimeoutError) + + discovery.return_value.mock_devices = [MockDevice1] + device.side_effect = [MockDevice1] + + await async_setup_gree(hass) + await hass.async_block_till_done() + + assert discovery.return_value.scan_count == 1 + assert len(hass.states.async_all(DOMAIN)) == 1 + + # rediscover the same devices shouldn't change anything + discovery.return_value.mock_devices = [MockDevice2] + device.side_effect = [MockDevice2] + + next_update = mock_now + timedelta(minutes=6) + with patch("homeassistant.util.dt.utcnow", return_value=next_update): + async_fire_time_changed(hass, next_update) + await hass.async_block_till_done() + + assert discovery.return_value.scan_count == 2 + assert len(hass.states.async_all(DOMAIN)) == 2 + + +async def test_discovery_device_bind_after_setup(hass, discovery, device, mock_now): + """Test gree devices can be added after a late device bind.""" + MockDevice1 = build_device_mock( + name="fake-device-1", ipAddress="1.1.1.1", mac="aabbcc112233" + ) + MockDevice1.bind = AsyncMock(side_effect=DeviceNotBoundError) + MockDevice1.update_state = AsyncMock(side_effect=DeviceNotBoundError) + + discovery.return_value.mock_devices = [MockDevice1] + device.return_value = MockDevice1 + + await async_setup_gree(hass) + await hass.async_block_till_done() + + assert len(hass.states.async_all(DOMAIN)) == 1 + state = hass.states.get(ENTITY_ID) + assert state.name == "fake-device-1" + assert state.state == STATE_UNAVAILABLE + + # Now the device becomes available + MockDevice1.bind.side_effect = None + MockDevice1.update_state.side_effect = None + + 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.state != STATE_UNAVAILABLE + + +async def test_update_connection_failure(hass, device, mock_now): """Testing update hvac connection failure exception.""" device().update_state.side_effect = [ DEFAULT_MOCK, @@ -229,11 +332,10 @@ async def test_send_command_device_timeout(hass, discovery, device, mock_now): # Send failure should not raise exceptions or change device state assert await hass.services.async_call( DOMAIN, - SERVICE_SET_HVAC_MODE, - {ATTR_ENTITY_ID: ENTITY_ID, ATTR_HVAC_MODE: HVAC_MODE_AUTO}, + SERVICE_TURN_ON, + {ATTR_ENTITY_ID: ENTITY_ID}, blocking=True, ) - await hass.async_block_till_done() state = hass.states.get(ENTITY_ID) assert state is not None @@ -244,45 +346,6 @@ 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) - assert await hass.services.async_call( - DOMAIN, - SERVICE_TURN_ON, - {ATTR_ENTITY_ID: ENTITY_ID}, - blocking=True, - ) - - state = hass.states.get(ENTITY_ID) - assert state is not None - assert state.state != HVAC_MODE_OFF - - -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) - - assert await hass.services.async_call( - DOMAIN, - SERVICE_TURN_ON, - {ATTR_ENTITY_ID: ENTITY_ID}, - blocking=True, - ) - - state = hass.states.get(ENTITY_ID) - assert state is not None - assert state.state != HVAC_MODE_OFF - - -async def test_send_power_off(hass, discovery, device, mock_now): - """Test for sending power off 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_TURN_OFF, @@ -301,11 +364,6 @@ async def test_send_power_off_device_timeout(hass, discovery, device, mock_now): 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_TURN_OFF, diff --git a/tests/components/gree/test_config_flow.py b/tests/components/gree/test_config_flow.py index bb5f59b573d..a3e881d6daf 100644 --- a/tests/components/gree/test_config_flow.py +++ b/tests/components/gree/test_config_flow.py @@ -1,20 +1,60 @@ """Tests for the Gree Integration.""" +from unittest.mock import patch + from homeassistant import config_entries, data_entry_flow from homeassistant.components.gree.const import DOMAIN as GREE_DOMAIN +from .common import FakeDiscovery -async def test_creating_entry_sets_up_climate(hass, discovery, device, setup): + +async def test_creating_entry_sets_up_climate(hass): """Test setting up Gree creates the climate components.""" - result = await hass.config_entries.flow.async_init( - GREE_DOMAIN, context={"source": config_entries.SOURCE_USER} - ) + with patch( + "homeassistant.components.gree.climate.async_setup_entry", return_value=True + ) as setup, patch( + "homeassistant.components.gree.bridge.Discovery", return_value=FakeDiscovery() + ), patch( + "homeassistant.components.gree.config_flow.Discovery", + return_value=FakeDiscovery(), + ): + 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 + # 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 + 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() + await hass.async_block_till_done() - assert len(setup.mock_calls) == 1 + assert len(setup.mock_calls) == 1 + + +async def test_creating_entry_has_no_devices(hass): + """Test setting up Gree creates the climate components.""" + with patch( + "homeassistant.components.gree.climate.async_setup_entry", return_value=True + ) as setup, patch( + "homeassistant.components.gree.bridge.Discovery", return_value=FakeDiscovery() + ) as discovery, patch( + "homeassistant.components.gree.config_flow.Discovery", + return_value=FakeDiscovery(), + ) as discovery2: + discovery.return_value.mock_devices = [] + discovery2.return_value.mock_devices = [] + + 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_ABORT + + await hass.async_block_till_done() + + assert len(setup.mock_calls) == 0 diff --git a/tests/components/gree/test_init.py b/tests/components/gree/test_init.py index bf999ee9e6f..7443ae1e94c 100644 --- a/tests/components/gree/test_init.py +++ b/tests/components/gree/test_init.py @@ -1,5 +1,4 @@ """Tests for the Gree Integration.""" - from unittest.mock import patch from homeassistant.components.gree.const import DOMAIN as GREE_DOMAIN @@ -9,31 +8,39 @@ from homeassistant.setup import async_setup_component from tests.common import MockConfigEntry -async def test_setup_simple(hass, discovery, device): +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, discovery, device): - """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: + ) as climate_setup, patch( + "homeassistant.components.gree.switch.async_setup_entry", + return_value=True, + ) as switch_setup: assert await async_setup_component(hass, GREE_DOMAIN, {}) await hass.async_block_till_done() assert len(climate_setup.mock_calls) == 1 + assert len(switch_setup.mock_calls) == 1 assert entry.state == ENTRY_STATE_LOADED + # 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) + + assert await async_setup_component(hass, GREE_DOMAIN, {}) + await hass.async_block_till_done() + await hass.config_entries.async_unload(entry.entry_id) + await hass.async_block_till_done() assert entry.state == ENTRY_STATE_NOT_LOADED diff --git a/tests/components/gree/test_switch.py b/tests/components/gree/test_switch.py index 89a8b224f1a..39ad536880c 100644 --- a/tests/components/gree/test_switch.py +++ b/tests/components/gree/test_switch.py @@ -26,7 +26,7 @@ async def async_setup_gree(hass): await hass.async_block_till_done() -async def test_send_panel_light_on(hass, discovery, device): +async def test_send_panel_light_on(hass): """Test for sending power on command to the device.""" await async_setup_gree(hass) @@ -42,7 +42,7 @@ async def test_send_panel_light_on(hass, discovery, device): assert state.state == STATE_ON -async def test_send_panel_light_on_device_timeout(hass, discovery, device): +async def test_send_panel_light_on_device_timeout(hass, device): """Test for sending power on command to the device with a device timeout.""" device().push_state_update.side_effect = DeviceTimeoutError @@ -60,7 +60,7 @@ async def test_send_panel_light_on_device_timeout(hass, discovery, device): assert state.state == STATE_ON -async def test_send_panel_light_off(hass, discovery, device): +async def test_send_panel_light_off(hass): """Test for sending power on command to the device.""" await async_setup_gree(hass) @@ -76,7 +76,7 @@ async def test_send_panel_light_off(hass, discovery, device): assert state.state == STATE_OFF -async def test_send_panel_light_toggle(hass, discovery, device): +async def test_send_panel_light_toggle(hass): """Test for sending power on command to the device.""" await async_setup_gree(hass) @@ -117,7 +117,7 @@ async def test_send_panel_light_toggle(hass, discovery, device): assert state.state == STATE_ON -async def test_panel_light_name(hass, discovery, device): +async def test_panel_light_name(hass): """Test for name property.""" await async_setup_gree(hass) state = hass.states.get(ENTITY_ID)