Improve the discovery process for Gree (#45449)

* Add support for async device discovery

* FIx missing dispatcher cleanup breaking integration reload

* Update homeassistant/components/gree/climate.py

Co-authored-by: Erik Montnemery <erik@montnemery.com>

* Update homeassistant/components/gree/switch.py

Co-authored-by: Erik Montnemery <erik@montnemery.com>

* Update homeassistant/components/gree/bridge.py

Co-authored-by: Erik Montnemery <erik@montnemery.com>

* Working on feedback

* Improving load/unload tests

* Update homeassistant/components/gree/__init__.py

Co-authored-by: Erik Montnemery <erik@montnemery.com>

* Working on more feedback

* Add tests covering async discovery scenarios

* Remove unnecessary shutdown

* Update homeassistant/components/gree/__init__.py

Co-authored-by: Erik Montnemery <erik@montnemery.com>

* Code refactor from reviews

Co-authored-by: Erik Montnemery <erik@montnemery.com>
This commit is contained in:
Clifford Roche 2021-04-13 05:54:03 -04:00 committed by GitHub
parent 63d42867e8
commit 4ce6d00a22
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
15 changed files with 357 additions and 180 deletions

View File

@ -1,14 +1,23 @@
"""The Gree Climate integration.""" """The Gree Climate integration."""
import asyncio import asyncio
from datetime import timedelta
import logging import logging
from homeassistant.components.climate import DOMAIN as CLIMATE_DOMAIN from homeassistant.components.climate import DOMAIN as CLIMATE_DOMAIN
from homeassistant.components.switch import DOMAIN as SWITCH_DOMAIN from homeassistant.components.switch import DOMAIN as SWITCH_DOMAIN
from homeassistant.config_entries import ConfigEntry from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant
from homeassistant.helpers.event import async_track_time_interval
from .bridge import CannotConnect, DeviceDataUpdateCoordinator, DeviceHelper from .bridge import DiscoveryService
from .const import COORDINATOR, DOMAIN from .const import (
COORDINATORS,
DATA_DISCOVERY_INTERVAL,
DATA_DISCOVERY_SERVICE,
DISCOVERY_SCAN_INTERVAL,
DISPATCHERS,
DOMAIN,
)
_LOGGER = logging.getLogger(__name__) _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): async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry):
"""Set up Gree Climate from a config entry.""" """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 hass.data[DOMAIN].setdefault(DISPATCHERS, [])
# 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)
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.async_create_task(
hass.config_entries.async_forward_entry_setup(entry, CLIMATE_DOMAIN) 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) 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 return True
async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry): async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry):
"""Unload a config entry.""" """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( results = asyncio.gather(
hass.config_entries.async_forward_entry_unload(entry, CLIMATE_DOMAIN), hass.config_entries.async_forward_entry_unload(entry, CLIMATE_DOMAIN),
hass.config_entries.async_forward_entry_unload(entry, SWITCH_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) unload_ok = all(await results)
if unload_ok: if unload_ok:
hass.data[DOMAIN].pop("devices", None) hass.data[DOMAIN].pop(COORDINATORS, None)
hass.data[DOMAIN].pop(CLIMATE_DOMAIN, None) hass.data[DOMAIN].pop(DISPATCHERS, None)
hass.data[DOMAIN].pop(SWITCH_DOMAIN, None)
return unload_ok return unload_ok

View File

@ -5,14 +5,20 @@ from datetime import timedelta
import logging import logging
from greeclimate.device import Device, DeviceInfo from greeclimate.device import Device, DeviceInfo
from greeclimate.discovery import Discovery from greeclimate.discovery import Discovery, Listener
from greeclimate.exceptions import DeviceNotBoundError, DeviceTimeoutError from greeclimate.exceptions import DeviceNotBoundError, DeviceTimeoutError
from homeassistant import exceptions
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant
from homeassistant.helpers.dispatcher import async_dispatcher_send
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed 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__) _LOGGER = logging.getLogger(__name__)
@ -36,6 +42,8 @@ class DeviceDataUpdateCoordinator(DataUpdateCoordinator):
"""Update the state of the device.""" """Update the state of the device."""
try: try:
await self.device.update_state() await self.device.update_state()
except DeviceNotBoundError as error:
raise UpdateFailed(f"Device {self.name} is unavailable") from error
except DeviceTimeoutError as error: except DeviceTimeoutError as error:
self._error_count += 1 self._error_count += 1
@ -46,16 +54,7 @@ class DeviceDataUpdateCoordinator(DataUpdateCoordinator):
self.name, self.name,
self.device.device_info, self.device.device_info,
) )
raise UpdateFailed(error) from error raise UpdateFailed(f"Device {self.name} is unavailable") 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
async def push_state_update(self): async def push_state_update(self):
"""Send state updates to the physical device.""" """Send state updates to the physical device."""
@ -69,28 +68,38 @@ class DeviceDataUpdateCoordinator(DataUpdateCoordinator):
) )
class DeviceHelper: class DiscoveryService(Listener):
"""Device search and bind wrapper for Gree platform.""" """Discovery event handler for gree devices."""
@staticmethod def __init__(self, hass) -> None:
async def try_bind_device(device_info: DeviceInfo) -> Device: """Initialize discovery service."""
"""Try and bing with a discovered device. 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) device = Device(device_info)
try: try:
await device.bind() await device.bind()
except DeviceNotBoundError as exception: except DeviceNotBoundError:
raise CannotConnect from exception _LOGGER.error("Unable to bind to gree device: %s", device_info)
return device except DeviceTimeoutError:
_LOGGER.error("Timeout trying to bind to gree device: %s", device_info)
@staticmethod _LOGGER.info(
async def find_devices() -> list[DeviceInfo]: "Adding Gree device %s at %s:%i",
"""Gather a list of device infos from the local network.""" device.device_info.name,
return await Discovery.search_devices() device.device_info.ip,
device.device_info.port,
)
coordo = DeviceDataUpdateCoordinator(self.hass, device)
self.hass.data[DOMAIN][COORDINATORS].append(coordo)
await coordo.async_refresh()
async_dispatcher_send(self.hass, DISPATCH_DEVICE_DISCOVERED, coordo)
class CannotConnect(exceptions.HomeAssistantError):
"""Error to indicate we cannot connect."""

View File

@ -43,11 +43,15 @@ from homeassistant.const import (
TEMP_CELSIUS, TEMP_CELSIUS,
TEMP_FAHRENHEIT, TEMP_FAHRENHEIT,
) )
from homeassistant.core import callback
from homeassistant.helpers.device_registry import CONNECTION_NETWORK_MAC 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 homeassistant.helpers.update_coordinator import CoordinatorEntity
from .const import ( from .const import (
COORDINATOR, COORDINATORS,
DISPATCH_DEVICE_DISCOVERED,
DISPATCHERS,
DOMAIN, DOMAIN,
FAN_MEDIUM_HIGH, FAN_MEDIUM_HIGH,
FAN_MEDIUM_LOW, FAN_MEDIUM_LOW,
@ -97,11 +101,17 @@ SUPPORTED_FEATURES = (
async def async_setup_entry(hass, config_entry, async_add_entities): async def async_setup_entry(hass, config_entry, async_add_entities):
"""Set up the Gree HVAC device from a config entry.""" """Set up the Gree HVAC device from a config entry."""
async_add_entities(
[ @callback
GreeClimateEntity(coordinator) def init_device(coordinator):
for coordinator in hass.data[DOMAIN][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)
) )

View File

@ -1,14 +1,16 @@
"""Config flow for Gree.""" """Config flow for Gree."""
from greeclimate.discovery import Discovery
from homeassistant import config_entries from homeassistant import config_entries
from homeassistant.helpers import config_entry_flow from homeassistant.helpers import config_entry_flow
from .bridge import DeviceHelper from .const import DISCOVERY_TIMEOUT, DOMAIN
from .const import DOMAIN
async def _async_has_devices(hass) -> bool: async def _async_has_devices(hass) -> bool:
"""Return if there are devices that can be discovered.""" """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 return len(devices) > 0

View File

@ -1,5 +1,15 @@
"""Constants for the Gree Climate integration.""" """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" DOMAIN = "gree"
COORDINATOR = "coordinator" COORDINATOR = "coordinator"

View File

@ -3,6 +3,6 @@
"name": "Gree Climate", "name": "Gree Climate",
"config_flow": true, "config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/gree", "documentation": "https://www.home-assistant.io/integrations/gree",
"requirements": ["greeclimate==0.10.3"], "requirements": ["greeclimate==0.11.4"],
"codeowners": ["@cmroche"] "codeowners": ["@cmroche"]
} }

View File

@ -2,19 +2,27 @@
from __future__ import annotations from __future__ import annotations
from homeassistant.components.switch import DEVICE_CLASS_SWITCH, SwitchEntity 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.device_registry import CONNECTION_NETWORK_MAC
from homeassistant.helpers.dispatcher import async_dispatcher_connect
from homeassistant.helpers.update_coordinator import CoordinatorEntity 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): async def async_setup_entry(hass, config_entry, async_add_entities):
"""Set up the Gree HVAC device from a config entry.""" """Set up the Gree HVAC device from a config entry."""
async_add_entities(
[ @callback
GreeSwitchEntity(coordinator) def init_device(coordinator):
for coordinator in hass.data[DOMAIN][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)
) )

View File

@ -699,7 +699,7 @@ gpiozero==1.5.1
gps3==0.33.3 gps3==0.33.3
# homeassistant.components.gree # homeassistant.components.gree
greeclimate==0.10.3 greeclimate==0.11.4
# homeassistant.components.greeneye_monitor # homeassistant.components.greeneye_monitor
greeneye_monitor==2.1 greeneye_monitor==2.1

View File

@ -384,7 +384,7 @@ google-nest-sdm==0.2.12
googlemaps==2.5.1 googlemaps==2.5.1
# homeassistant.components.gree # homeassistant.components.gree
greeclimate==0.10.3 greeclimate==0.11.4
# homeassistant.components.profiler # homeassistant.components.profiler
guppy3==3.1.0 guppy3==3.1.0

View File

@ -1,6 +1,43 @@
"""Common helpers for gree test cases.""" """Common helpers for gree test cases."""
import asyncio
import logging
from unittest.mock import AsyncMock, Mock 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( def build_device_info_mock(
name="fake-device-1", ipAddress="1.1.1.1", mac="aabbcc112233" name="fake-device-1", ipAddress="1.1.1.1", mac="aabbcc112233"

View File

@ -1,36 +1,24 @@
"""Pytest module configuration.""" """Pytest module configuration."""
from unittest.mock import AsyncMock, patch from unittest.mock import patch
import pytest 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(): def discovery_fixture():
"""Patch the discovery service.""" """Patch the discovery object."""
with patch( with patch("homeassistant.components.gree.bridge.Discovery") as mock:
"homeassistant.components.gree.bridge.Discovery.search_devices", mock.return_value = FakeDiscovery()
new_callable=AsyncMock,
return_value=[build_device_info_mock()],
) as mock:
yield mock yield mock
@pytest.fixture(name="device") @pytest.fixture(autouse=True, name="device")
def device_fixture(): def device_fixture():
"""Path the device search and bind.""" """Patch the device search and bind."""
with patch( with patch(
"homeassistant.components.gree.bridge.Device", "homeassistant.components.gree.bridge.Device",
return_value=build_device_mock(), return_value=build_device_mock(),
) as mock: ) as mock:
yield 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

View File

@ -97,7 +97,7 @@ async def test_discovery_setup(hass, discovery, device):
name="fake-device-2", ipAddress="2.2.2.2", mac="bbccdd223344" 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] device.side_effect = [MockDevice1, MockDevice2]
await async_setup_gree(hass) 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 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.""" """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) MockDevice1.bind = AsyncMock(side_effect=DeviceNotBoundError)
MockDevice2 = build_device_mock(name="fake-device-2") MockDevice2 = build_device_mock(
MockDevice2.bind = AsyncMock(side_effect=DeviceNotBoundError) 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] device.side_effect = [MockDevice1, MockDevice2]
await async_setup_gree(hass) await async_setup_gree(hass)
await hass.async_block_till_done() 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.""" """Testing update hvac connection failure exception."""
device().update_state.side_effect = [ device().update_state.side_effect = [
DEFAULT_MOCK, 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 # Send failure should not raise exceptions or change device state
assert await hass.services.async_call( assert await hass.services.async_call(
DOMAIN, DOMAIN,
SERVICE_SET_HVAC_MODE, SERVICE_TURN_ON,
{ATTR_ENTITY_ID: ENTITY_ID, ATTR_HVAC_MODE: HVAC_MODE_AUTO}, {ATTR_ENTITY_ID: ENTITY_ID},
blocking=True, blocking=True,
) )
await hass.async_block_till_done()
state = hass.states.get(ENTITY_ID) state = hass.states.get(ENTITY_ID)
assert state is not None 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.""" """Test for sending power on command to the device."""
await async_setup_gree(hass) 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( assert await hass.services.async_call(
DOMAIN, DOMAIN,
SERVICE_TURN_OFF, 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) 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( assert await hass.services.async_call(
DOMAIN, DOMAIN,
SERVICE_TURN_OFF, SERVICE_TURN_OFF,

View File

@ -1,20 +1,60 @@
"""Tests for the Gree Integration.""" """Tests for the Gree Integration."""
from unittest.mock import patch
from homeassistant import config_entries, data_entry_flow from homeassistant import config_entries, data_entry_flow
from homeassistant.components.gree.const import DOMAIN as GREE_DOMAIN 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.""" """Test setting up Gree creates the climate components."""
result = await hass.config_entries.flow.async_init( with patch(
GREE_DOMAIN, context={"source": config_entries.SOURCE_USER} "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 # Confirmation form
assert result["type"] == data_entry_flow.RESULT_TYPE_FORM assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
result = await hass.config_entries.flow.async_configure(result["flow_id"], {}) result = await hass.config_entries.flow.async_configure(result["flow_id"], {})
assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY 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

View File

@ -1,5 +1,4 @@
"""Tests for the Gree Integration.""" """Tests for the Gree Integration."""
from unittest.mock import patch from unittest.mock import patch
from homeassistant.components.gree.const import DOMAIN as GREE_DOMAIN 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 from tests.common import MockConfigEntry
async def test_setup_simple(hass, discovery, device): async def test_setup_simple(hass):
"""Test gree integration is setup.""" """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 = MockConfigEntry(domain=GREE_DOMAIN)
entry.add_to_hass(hass) entry.add_to_hass(hass)
with patch( with patch(
"homeassistant.components.gree.climate.async_setup_entry", "homeassistant.components.gree.climate.async_setup_entry",
return_value=True, 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, {}) assert await async_setup_component(hass, GREE_DOMAIN, {})
await hass.async_block_till_done() await hass.async_block_till_done()
assert len(climate_setup.mock_calls) == 1 assert len(climate_setup.mock_calls) == 1
assert len(switch_setup.mock_calls) == 1
assert entry.state == ENTRY_STATE_LOADED 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.config_entries.async_unload(entry.entry_id)
await hass.async_block_till_done()
assert entry.state == ENTRY_STATE_NOT_LOADED assert entry.state == ENTRY_STATE_NOT_LOADED

View File

@ -26,7 +26,7 @@ async def async_setup_gree(hass):
await hass.async_block_till_done() 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.""" """Test for sending power on command to the device."""
await async_setup_gree(hass) await async_setup_gree(hass)
@ -42,7 +42,7 @@ async def test_send_panel_light_on(hass, discovery, device):
assert state.state == STATE_ON 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.""" """Test for sending power on command to the device with a device timeout."""
device().push_state_update.side_effect = DeviceTimeoutError 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 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.""" """Test for sending power on command to the device."""
await async_setup_gree(hass) await async_setup_gree(hass)
@ -76,7 +76,7 @@ async def test_send_panel_light_off(hass, discovery, device):
assert state.state == STATE_OFF 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.""" """Test for sending power on command to the device."""
await async_setup_gree(hass) await async_setup_gree(hass)
@ -117,7 +117,7 @@ async def test_send_panel_light_toggle(hass, discovery, device):
assert state.state == STATE_ON 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.""" """Test for name property."""
await async_setup_gree(hass) await async_setup_gree(hass)
state = hass.states.get(ENTITY_ID) state = hass.states.get(ENTITY_ID)