mirror of
https://github.com/home-assistant/core.git
synced 2025-07-16 17:57:11 +00:00
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:
parent
63d42867e8
commit
4ce6d00a22
@ -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
|
||||
|
@ -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)
|
||||
|
@ -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)
|
||||
)
|
||||
|
||||
|
||||
|
@ -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
|
||||
|
||||
|
||||
|
@ -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"
|
||||
|
||||
|
@ -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"]
|
||||
}
|
@ -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)
|
||||
)
|
||||
|
||||
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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"
|
||||
|
@ -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
|
||||
|
@ -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,
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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)
|
||||
|
Loading…
x
Reference in New Issue
Block a user