Add support for Gree device light panels (#42979)

This commit is contained in:
Clifford Roche 2020-12-28 16:32:04 -05:00 committed by GitHub
parent a22d9e54db
commit ee97023053
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 342 additions and 271 deletions

View File

@ -1,12 +1,14 @@
"""The Gree Climate integration.""" """The Gree Climate integration."""
import asyncio
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.config_entries import ConfigEntry from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant
from .bridge import CannotConnect, DeviceHelper from .bridge import CannotConnect, DeviceDataUpdateCoordinator, DeviceHelper
from .const import DOMAIN from .const import COORDINATOR, DOMAIN
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
@ -40,23 +42,31 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry):
) )
devices.append(device) devices.append(device)
hass.data[DOMAIN]["devices"] = devices coordinators = [DeviceDataUpdateCoordinator(hass, d) for d in devices]
hass.data[DOMAIN]["pending"] = 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)
) )
hass.async_create_task(
hass.config_entries.async_forward_entry_setup(entry, SWITCH_DOMAIN)
)
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."""
unload_ok = await hass.config_entries.async_forward_entry_unload( results = asyncio.gather(
entry, CLIMATE_DOMAIN hass.config_entries.async_forward_entry_unload(entry, CLIMATE_DOMAIN),
hass.config_entries.async_forward_entry_unload(entry, SWITCH_DOMAIN),
) )
unload_ok = all(await results)
if unload_ok: if unload_ok:
hass.data[DOMAIN].pop("devices", None) hass.data[DOMAIN].pop("devices", None)
hass.data[DOMAIN].pop("pending", None) hass.data[DOMAIN].pop(CLIMATE_DOMAIN, None)
hass.data[DOMAIN].pop(SWITCH_DOMAIN, None)
return unload_ok return unload_ok

View File

@ -1,11 +1,71 @@
"""Helper and wrapper classes for Gree module.""" """Helper and wrapper classes for Gree module."""
from datetime import timedelta
import logging
from typing import List from typing import List
from greeclimate.device import Device, DeviceInfo from greeclimate.device import Device, DeviceInfo
from greeclimate.discovery import Discovery from greeclimate.discovery import Discovery
from greeclimate.exceptions import DeviceNotBoundError from greeclimate.exceptions import DeviceNotBoundError, DeviceTimeoutError
from homeassistant import exceptions from homeassistant import exceptions
from homeassistant.core import HomeAssistant
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
from .const import DOMAIN, MAX_ERRORS
_LOGGER = logging.getLogger(__name__)
class DeviceDataUpdateCoordinator(DataUpdateCoordinator):
"""Manages polling for state changes from the device."""
def __init__(self, hass: HomeAssistant, device: Device):
"""Initialize the data update coordinator."""
DataUpdateCoordinator.__init__(
self,
hass,
_LOGGER,
name=f"{DOMAIN}-{device.device_info.name}",
update_interval=timedelta(seconds=60),
)
self.device = device
self._error_count = 0
async def _async_update_data(self):
"""Update the state of the device."""
try:
await self.device.update_state()
except DeviceTimeoutError as error:
self._error_count += 1
# Under normal conditions GREE units timeout every once in a while
if self.last_update_success and self._error_count >= MAX_ERRORS:
_LOGGER.warning(
"Device is unavailable: %s (%s)",
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
async def push_state_update(self):
"""Send state updates to the physical device."""
try:
return await self.device.push_state_update()
except DeviceTimeoutError:
_LOGGER.warning(
"Timeout send state update to: %s (%s)",
self.name,
self.device.device_info,
)
class DeviceHelper: class DeviceHelper:

View File

@ -1,5 +1,4 @@
"""Support for interface with a Gree climate systems.""" """Support for interface with a Gree climate systems."""
from datetime import timedelta
import logging import logging
from typing import List from typing import List
@ -10,7 +9,6 @@ from greeclimate.device import (
TemperatureUnits, TemperatureUnits,
VerticalSwing, VerticalSwing,
) )
from greeclimate.exceptions import DeviceTimeoutError
from homeassistant.components.climate import ClimateEntity from homeassistant.components.climate import ClimateEntity
from homeassistant.components.climate.const import ( from homeassistant.components.climate.const import (
@ -45,12 +43,13 @@ from homeassistant.const import (
TEMP_FAHRENHEIT, TEMP_FAHRENHEIT,
) )
from homeassistant.helpers.device_registry import CONNECTION_NETWORK_MAC from homeassistant.helpers.device_registry import CONNECTION_NETWORK_MAC
from homeassistant.helpers.update_coordinator import CoordinatorEntity
from .const import ( from .const import (
COORDINATOR,
DOMAIN, DOMAIN,
FAN_MEDIUM_HIGH, FAN_MEDIUM_HIGH,
FAN_MEDIUM_LOW, FAN_MEDIUM_LOW,
MAX_ERRORS,
MAX_TEMP, MAX_TEMP,
MIN_TEMP, MIN_TEMP,
TARGET_TEMPERATURE_STEP, TARGET_TEMPERATURE_STEP,
@ -58,9 +57,6 @@ from .const import (
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
SCAN_INTERVAL = timedelta(seconds=60)
PARALLEL_UPDATES = 0
HVAC_MODES = { HVAC_MODES = {
Mode.Auto: HVAC_MODE_AUTO, Mode.Auto: HVAC_MODE_AUTO,
Mode.Cool: HVAC_MODE_COOL, Mode.Cool: HVAC_MODE_COOL,
@ -101,85 +97,21 @@ 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( async_add_entities(
GreeClimateEntity(device) for device in hass.data[DOMAIN].pop("pending") [
GreeClimateEntity(coordinator)
for coordinator in hass.data[DOMAIN][COORDINATOR]
]
) )
class GreeClimateEntity(ClimateEntity): class GreeClimateEntity(CoordinatorEntity, ClimateEntity):
"""Representation of a Gree HVAC device.""" """Representation of a Gree HVAC device."""
def __init__(self, device): def __init__(self, coordinator):
"""Initialize the Gree device.""" """Initialize the Gree device."""
self._device = device super().__init__(coordinator)
self._name = device.device_info.name self._name = coordinator.device.device_info.name
self._mac = device.device_info.mac self._mac = coordinator.device.device_info.mac
self._available = False
self._error_count = 0
async def async_update(self):
"""Update the state of the device."""
try:
await self._device.update_state()
if not self._available and self._error_count:
_LOGGER.warning(
"Device is available: %s (%s)",
self._name,
str(self._device.device_info),
)
self._available = True
self._error_count = 0
except DeviceTimeoutError:
self._error_count += 1
# Under normal conditions GREE units timeout every once in a while
if self._available and self._error_count >= MAX_ERRORS:
self._available = False
_LOGGER.warning(
"Device is unavailable: %s (%s)",
self._name,
self._device.device_info,
)
except Exception: # pylint: disable=broad-except
# Under normal conditions GREE units timeout every once in a while
if self._available:
self._available = False
_LOGGER.exception(
"Unknown exception caught during update by gree device: %s (%s)",
self._name,
self._device.device_info,
)
async def _push_state_update(self):
"""Send state updates to the physical device."""
try:
return await self._device.push_state_update()
except DeviceTimeoutError:
self._error_count += 1
# Under normal conditions GREE units timeout every once in a while
if self._available and self._error_count >= MAX_ERRORS:
self._available = False
_LOGGER.warning(
"Device timedout while sending state update: %s (%s)",
self._name,
self._device.device_info,
)
except Exception: # pylint: disable=broad-except
# Under normal conditions GREE units timeout every once in a while
if self._available:
self._available = False
_LOGGER.exception(
"Unknown exception caught while sending state update to: %s (%s)",
self._name,
self._device.device_info,
)
@property
def available(self) -> bool:
"""Return if the device is available."""
return self._available
@property @property
def name(self) -> str: def name(self) -> str:
@ -204,7 +136,7 @@ class GreeClimateEntity(ClimateEntity):
@property @property
def temperature_unit(self) -> str: def temperature_unit(self) -> str:
"""Return the temperature units for the device.""" """Return the temperature units for the device."""
units = self._device.temperature_units units = self.coordinator.device.temperature_units
return TEMP_CELSIUS if units == TemperatureUnits.C else TEMP_FAHRENHEIT return TEMP_CELSIUS if units == TemperatureUnits.C else TEMP_FAHRENHEIT
@property @property
@ -220,7 +152,7 @@ class GreeClimateEntity(ClimateEntity):
@property @property
def target_temperature(self) -> float: def target_temperature(self) -> float:
"""Return the target temperature for the device.""" """Return the target temperature for the device."""
return self._device.target_temperature return self.coordinator.device.target_temperature
async def async_set_temperature(self, **kwargs): async def async_set_temperature(self, **kwargs):
"""Set new target temperature.""" """Set new target temperature."""
@ -234,8 +166,9 @@ class GreeClimateEntity(ClimateEntity):
self._name, self._name,
) )
self._device.target_temperature = round(temperature) self.coordinator.device.target_temperature = round(temperature)
await self._push_state_update() await self.coordinator.push_state_update()
self.async_write_ha_state()
@property @property
def min_temp(self) -> float: def min_temp(self) -> float:
@ -255,10 +188,10 @@ class GreeClimateEntity(ClimateEntity):
@property @property
def hvac_mode(self) -> str: def hvac_mode(self) -> str:
"""Return the current HVAC mode for the device.""" """Return the current HVAC mode for the device."""
if not self._device.power: if not self.coordinator.device.power:
return HVAC_MODE_OFF return HVAC_MODE_OFF
return HVAC_MODES.get(self._device.mode) return HVAC_MODES.get(self.coordinator.device.mode)
async def async_set_hvac_mode(self, hvac_mode): async def async_set_hvac_mode(self, hvac_mode):
"""Set new target hvac mode.""" """Set new target hvac mode."""
@ -272,15 +205,17 @@ class GreeClimateEntity(ClimateEntity):
) )
if hvac_mode == HVAC_MODE_OFF: if hvac_mode == HVAC_MODE_OFF:
self._device.power = False self.coordinator.device.power = False
await self._push_state_update() await self.coordinator.push_state_update()
self.async_write_ha_state()
return return
if not self._device.power: if not self.coordinator.device.power:
self._device.power = True self.coordinator.device.power = True
self._device.mode = HVAC_MODES_REVERSE.get(hvac_mode) self.coordinator.device.mode = HVAC_MODES_REVERSE.get(hvac_mode)
await self._push_state_update() await self.coordinator.push_state_update()
self.async_write_ha_state()
@property @property
def hvac_modes(self) -> List[str]: def hvac_modes(self) -> List[str]:
@ -292,13 +227,13 @@ class GreeClimateEntity(ClimateEntity):
@property @property
def preset_mode(self) -> str: def preset_mode(self) -> str:
"""Return the current preset mode for the device.""" """Return the current preset mode for the device."""
if self._device.steady_heat: if self.coordinator.device.steady_heat:
return PRESET_AWAY return PRESET_AWAY
if self._device.power_save: if self.coordinator.device.power_save:
return PRESET_ECO return PRESET_ECO
if self._device.sleep: if self.coordinator.device.sleep:
return PRESET_SLEEP return PRESET_SLEEP
if self._device.turbo: if self.coordinator.device.turbo:
return PRESET_BOOST return PRESET_BOOST
return PRESET_NONE return PRESET_NONE
@ -313,21 +248,22 @@ class GreeClimateEntity(ClimateEntity):
self._name, self._name,
) )
self._device.steady_heat = False self.coordinator.device.steady_heat = False
self._device.power_save = False self.coordinator.device.power_save = False
self._device.turbo = False self.coordinator.device.turbo = False
self._device.sleep = False self.coordinator.device.sleep = False
if preset_mode == PRESET_AWAY: if preset_mode == PRESET_AWAY:
self._device.steady_heat = True self.coordinator.device.steady_heat = True
elif preset_mode == PRESET_ECO: elif preset_mode == PRESET_ECO:
self._device.power_save = True self.coordinator.device.power_save = True
elif preset_mode == PRESET_BOOST: elif preset_mode == PRESET_BOOST:
self._device.turbo = True self.coordinator.device.turbo = True
elif preset_mode == PRESET_SLEEP: elif preset_mode == PRESET_SLEEP:
self._device.sleep = True self.coordinator.device.sleep = True
await self._push_state_update() await self.coordinator.push_state_update()
self.async_write_ha_state()
@property @property
def preset_modes(self) -> List[str]: def preset_modes(self) -> List[str]:
@ -337,7 +273,7 @@ class GreeClimateEntity(ClimateEntity):
@property @property
def fan_mode(self) -> str: def fan_mode(self) -> str:
"""Return the current fan mode for the device.""" """Return the current fan mode for the device."""
speed = self._device.fan_speed speed = self.coordinator.device.fan_speed
return FAN_MODES.get(speed) return FAN_MODES.get(speed)
async def async_set_fan_mode(self, fan_mode): async def async_set_fan_mode(self, fan_mode):
@ -345,8 +281,9 @@ class GreeClimateEntity(ClimateEntity):
if fan_mode not in FAN_MODES_REVERSE: if fan_mode not in FAN_MODES_REVERSE:
raise ValueError(f"Invalid fan mode: {fan_mode}") raise ValueError(f"Invalid fan mode: {fan_mode}")
self._device.fan_speed = FAN_MODES_REVERSE.get(fan_mode) self.coordinator.device.fan_speed = FAN_MODES_REVERSE.get(fan_mode)
await self._push_state_update() await self.coordinator.push_state_update()
self.async_write_ha_state()
@property @property
def fan_modes(self) -> List[str]: def fan_modes(self) -> List[str]:
@ -356,8 +293,8 @@ class GreeClimateEntity(ClimateEntity):
@property @property
def swing_mode(self) -> str: def swing_mode(self) -> str:
"""Return the current swing mode for the device.""" """Return the current swing mode for the device."""
h_swing = self._device.horizontal_swing == HorizontalSwing.FullSwing h_swing = self.coordinator.device.horizontal_swing == HorizontalSwing.FullSwing
v_swing = self._device.vertical_swing == VerticalSwing.FullSwing v_swing = self.coordinator.device.vertical_swing == VerticalSwing.FullSwing
if h_swing and v_swing: if h_swing and v_swing:
return SWING_BOTH return SWING_BOTH
@ -378,14 +315,15 @@ class GreeClimateEntity(ClimateEntity):
self._name, self._name,
) )
self._device.horizontal_swing = HorizontalSwing.Center self.coordinator.device.horizontal_swing = HorizontalSwing.Center
self._device.vertical_swing = VerticalSwing.FixedMiddle self.coordinator.device.vertical_swing = VerticalSwing.FixedMiddle
if swing_mode in (SWING_BOTH, SWING_HORIZONTAL): if swing_mode in (SWING_BOTH, SWING_HORIZONTAL):
self._device.horizontal_swing = HorizontalSwing.FullSwing self.coordinator.device.horizontal_swing = HorizontalSwing.FullSwing
if swing_mode in (SWING_BOTH, SWING_VERTICAL): if swing_mode in (SWING_BOTH, SWING_VERTICAL):
self._device.vertical_swing = VerticalSwing.FullSwing self.coordinator.device.vertical_swing = VerticalSwing.FullSwing
await self._push_state_update() await self.coordinator.push_state_update()
self.async_write_ha_state()
@property @property
def swing_modes(self) -> List[str]: def swing_modes(self) -> List[str]:

View File

@ -1,6 +1,7 @@
"""Constants for the Gree Climate integration.""" """Constants for the Gree Climate integration."""
DOMAIN = "gree" DOMAIN = "gree"
COORDINATOR = "coordinator"
FAN_MEDIUM_LOW = "medium low" FAN_MEDIUM_LOW = "medium low"
FAN_MEDIUM_HIGH = "medium high" FAN_MEDIUM_HIGH = "medium high"

View File

@ -10,4 +10,4 @@
"no_devices_found": "[%key:common::config_flow::abort::no_devices_found%]" "no_devices_found": "[%key:common::config_flow::abort::no_devices_found%]"
} }
} }
} }

View File

@ -0,0 +1,78 @@
"""Support for interface with a Gree climate systems."""
import logging
from typing import Optional
from homeassistant.components.switch import DEVICE_CLASS_SWITCH, SwitchEntity
from homeassistant.helpers.device_registry import CONNECTION_NETWORK_MAC
from homeassistant.helpers.update_coordinator import CoordinatorEntity
from .const import COORDINATOR, DOMAIN
_LOGGER = logging.getLogger(__name__)
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]
]
)
class GreeSwitchEntity(CoordinatorEntity, SwitchEntity):
"""Representation of a Gree HVAC device."""
def __init__(self, coordinator):
"""Initialize the Gree device."""
super().__init__(coordinator)
self._name = coordinator.device.device_info.name + " Panel Light"
self._mac = coordinator.device.device_info.mac
@property
def name(self) -> str:
"""Return the name of the device."""
return self._name
@property
def unique_id(self) -> str:
"""Return a unique id for the device."""
return f"{self._mac}-panel-light"
@property
def icon(self) -> Optional[str]:
"""Return the icon for the device."""
return "mdi:lightbulb"
@property
def device_info(self):
"""Return device specific attributes."""
return {
"name": self._name,
"identifiers": {(DOMAIN, self._mac)},
"manufacturer": "Gree",
"connections": {(CONNECTION_NETWORK_MAC, self._mac)},
}
@property
def device_class(self):
"""Return the class of this device, from component DEVICE_CLASSES."""
return DEVICE_CLASS_SWITCH
@property
def is_on(self) -> bool:
"""Return if the light is turned on."""
return self.coordinator.device.light
async def async_turn_on(self, **kwargs):
"""Turn the entity on."""
self.coordinator.device.light = True
await self.coordinator.push_state_update()
self.async_write_ha_state()
async def async_turn_off(self, **kwargs):
"""Turn the entity off."""
self.coordinator.device.light = False
await self.coordinator.push_state_update()
self.async_write_ha_state()

View File

@ -159,10 +159,15 @@ async def test_update_connection_failure(hass, discovery, device, mock_now):
async def test_update_connection_failure_recovery(hass, discovery, device, mock_now): async def test_update_connection_failure_recovery(hass, discovery, device, mock_now):
"""Testing update hvac connection failure recovery.""" """Testing update hvac connection failure recovery."""
device().update_state.side_effect = [DeviceTimeoutError, DEFAULT_MOCK] device().update_state.side_effect = [
DeviceTimeoutError,
DeviceTimeoutError,
DEFAULT_MOCK,
]
await async_setup_gree(hass) await async_setup_gree(hass)
# First update becomes unavailable
next_update = mock_now + timedelta(minutes=5) next_update = mock_now + timedelta(minutes=5)
with patch("homeassistant.util.dt.utcnow", return_value=next_update): with patch("homeassistant.util.dt.utcnow", return_value=next_update):
async_fire_time_changed(hass, next_update) async_fire_time_changed(hass, next_update)
@ -172,6 +177,7 @@ async def test_update_connection_failure_recovery(hass, discovery, device, mock_
assert state.name == "fake-device-1" assert state.name == "fake-device-1"
assert state.state == STATE_UNAVAILABLE assert state.state == STATE_UNAVAILABLE
# Second update restores the connection
next_update = mock_now + timedelta(minutes=10) next_update = mock_now + timedelta(minutes=10)
with patch("homeassistant.util.dt.utcnow", return_value=next_update): with patch("homeassistant.util.dt.utcnow", return_value=next_update):
async_fire_time_changed(hass, next_update) async_fire_time_changed(hass, next_update)
@ -188,11 +194,6 @@ async def test_update_unhandled_exception(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()
state = hass.states.get(ENTITY_ID) state = hass.states.get(ENTITY_ID)
assert state.name == "fake-device-1" assert state.name == "fake-device-1"
assert state.state != STATE_UNAVAILABLE assert state.state != STATE_UNAVAILABLE
@ -221,21 +222,9 @@ async def test_send_command_device_timeout(hass, discovery, device, mock_now):
assert state.name == "fake-device-1" assert state.name == "fake-device-1"
assert state.state != STATE_UNAVAILABLE assert state.state != STATE_UNAVAILABLE
device().update_state.side_effect = DeviceTimeoutError
device().push_state_update.side_effect = DeviceTimeoutError device().push_state_update.side_effect = DeviceTimeoutError
# Second update to make an initial error (device is still available) # Send failure should not raise exceptions or change device state
next_update = mock_now + timedelta(minutes=5)
with patch("homeassistant.util.dt.utcnow", return_value=next_update):
async_fire_time_changed(hass, next_update)
await hass.async_block_till_done()
state = hass.states.get(ENTITY_ID)
assert state is not None
assert state.name == "fake-device-1"
assert state.state != STATE_UNAVAILABLE
# Second attempt should make the device unavailable
assert await hass.services.async_call( assert await hass.services.async_call(
DOMAIN, DOMAIN,
SERVICE_SET_HVAC_MODE, SERVICE_SET_HVAC_MODE,
@ -246,47 +235,13 @@ async def test_send_command_device_timeout(hass, discovery, device, mock_now):
state = hass.states.get(ENTITY_ID) state = hass.states.get(ENTITY_ID)
assert state is not None assert state is not None
assert state.state == STATE_UNAVAILABLE
async def test_send_command_device_unknown_error(hass, discovery, device, mock_now):
"""Test for sending power on command to the device with a device timeout."""
device().update_state.side_effect = [DEFAULT_MOCK, Exception]
device().push_state_update.side_effect = Exception
await async_setup_gree(hass)
next_update = mock_now + timedelta(minutes=5)
with patch("homeassistant.util.dt.utcnow", return_value=next_update):
async_fire_time_changed(hass, next_update)
await hass.async_block_till_done()
# First update to make the device available
state = hass.states.get(ENTITY_ID)
assert state.name == "fake-device-1"
assert state.state != STATE_UNAVAILABLE assert state.state != STATE_UNAVAILABLE
assert await hass.services.async_call(
DOMAIN,
SERVICE_SET_HVAC_MODE,
{ATTR_ENTITY_ID: ENTITY_ID, ATTR_HVAC_MODE: HVAC_MODE_AUTO},
blocking=True,
)
state = hass.states.get(ENTITY_ID)
assert state is not None
assert state.state == STATE_UNAVAILABLE
async def test_send_power_on(hass, discovery, device, mock_now): 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)
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_SET_HVAC_MODE, SERVICE_SET_HVAC_MODE,
@ -305,11 +260,6 @@ async def test_send_power_on_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_SET_HVAC_MODE, SERVICE_SET_HVAC_MODE,
@ -326,11 +276,6 @@ async def test_send_target_temperature(hass, discovery, device, mock_now):
"""Test for sending target temperature command to the device.""" """Test for sending target temperature command to the device."""
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_SET_TEMPERATURE, SERVICE_SET_TEMPERATURE,
@ -351,11 +296,6 @@ async def test_send_target_temperature_device_timeout(
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_SET_TEMPERATURE, SERVICE_SET_TEMPERATURE,
@ -374,11 +314,6 @@ async def test_update_target_temperature(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()
state = hass.states.get(ENTITY_ID) state = hass.states.get(ENTITY_ID)
assert state is not None assert state is not None
assert state.attributes.get(ATTR_TEMPERATURE) == 32 assert state.attributes.get(ATTR_TEMPERATURE) == 32
@ -391,11 +326,6 @@ async def test_send_preset_mode(hass, discovery, device, mock_now, preset):
"""Test for sending preset mode command to the device.""" """Test for sending preset mode command to the device."""
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_SET_PRESET_MODE, SERVICE_SET_PRESET_MODE,
@ -412,11 +342,6 @@ async def test_send_invalid_preset_mode(hass, discovery, device, mock_now):
"""Test for sending preset mode command to the device.""" """Test for sending preset mode command to the device."""
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()
with pytest.raises(ValueError): with pytest.raises(ValueError):
await hass.services.async_call( await hass.services.async_call(
DOMAIN, DOMAIN,
@ -441,11 +366,6 @@ async def test_send_preset_mode_device_timeout(
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_SET_PRESET_MODE, SERVICE_SET_PRESET_MODE,
@ -470,11 +390,6 @@ async def test_update_preset_mode(hass, discovery, device, mock_now, preset):
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()
state = hass.states.get(ENTITY_ID) state = hass.states.get(ENTITY_ID)
assert state is not None assert state is not None
assert state.attributes.get(ATTR_PRESET_MODE) == preset assert state.attributes.get(ATTR_PRESET_MODE) == preset
@ -495,11 +410,6 @@ async def test_send_hvac_mode(hass, discovery, device, mock_now, hvac_mode):
"""Test for sending hvac mode command to the device.""" """Test for sending hvac mode command to the device."""
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_SET_HVAC_MODE, SERVICE_SET_HVAC_MODE,
@ -524,11 +434,6 @@ async def test_send_hvac_mode_device_timeout(
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_SET_HVAC_MODE, SERVICE_SET_HVAC_MODE,
@ -559,11 +464,6 @@ async def test_update_hvac_mode(hass, discovery, device, mock_now, hvac_mode):
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()
state = hass.states.get(ENTITY_ID) state = hass.states.get(ENTITY_ID)
assert state is not None assert state is not None
assert state.state == hvac_mode assert state.state == hvac_mode
@ -577,11 +477,6 @@ async def test_send_fan_mode(hass, discovery, device, mock_now, fan_mode):
"""Test for sending fan mode command to the device.""" """Test for sending fan mode command to the device."""
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_SET_FAN_MODE, SERVICE_SET_FAN_MODE,
@ -598,11 +493,6 @@ async def test_send_invalid_fan_mode(hass, discovery, device, mock_now):
"""Test for sending fan mode command to the device.""" """Test for sending fan mode command to the device."""
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()
with pytest.raises(ValueError): with pytest.raises(ValueError):
await hass.services.async_call( await hass.services.async_call(
DOMAIN, DOMAIN,
@ -628,11 +518,6 @@ async def test_send_fan_mode_device_timeout(
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_SET_FAN_MODE, SERVICE_SET_FAN_MODE,
@ -655,11 +540,6 @@ async def test_update_fan_mode(hass, discovery, device, mock_now, fan_mode):
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()
state = hass.states.get(ENTITY_ID) state = hass.states.get(ENTITY_ID)
assert state is not None assert state is not None
assert state.attributes.get(ATTR_FAN_MODE) == fan_mode assert state.attributes.get(ATTR_FAN_MODE) == fan_mode
@ -672,11 +552,6 @@ async def test_send_swing_mode(hass, discovery, device, mock_now, swing_mode):
"""Test for sending swing mode command to the device.""" """Test for sending swing mode command to the device."""
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_SET_SWING_MODE, SERVICE_SET_SWING_MODE,
@ -693,11 +568,6 @@ async def test_send_invalid_swing_mode(hass, discovery, device, mock_now):
"""Test for sending swing mode command to the device.""" """Test for sending swing mode command to the device."""
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()
with pytest.raises(ValueError): with pytest.raises(ValueError):
await hass.services.async_call( await hass.services.async_call(
DOMAIN, DOMAIN,
@ -722,11 +592,6 @@ async def test_send_swing_mode_device_timeout(
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_SET_SWING_MODE, SERVICE_SET_SWING_MODE,
@ -757,11 +622,6 @@ async def test_update_swing_mode(hass, discovery, device, mock_now, swing_mode):
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()
state = hass.states.get(ENTITY_ID) state = hass.states.get(ENTITY_ID)
assert state is not None assert state is not None
assert state.attributes.get(ATTR_SWING_MODE) == swing_mode assert state.attributes.get(ATTR_SWING_MODE) == swing_mode

View File

@ -0,0 +1,124 @@
"""Tests for gree component."""
from greeclimate.exceptions import DeviceTimeoutError
from homeassistant.components.gree.const import DOMAIN as GREE_DOMAIN
from homeassistant.components.switch import DOMAIN
from homeassistant.const import (
ATTR_ENTITY_ID,
ATTR_FRIENDLY_NAME,
SERVICE_TOGGLE,
SERVICE_TURN_OFF,
SERVICE_TURN_ON,
STATE_OFF,
STATE_ON,
)
from homeassistant.setup import async_setup_component
from tests.common import MockConfigEntry
ENTITY_ID = f"{DOMAIN}.fake_device_1_panel_light"
async def async_setup_gree(hass):
"""Set up the gree switch platform."""
MockConfigEntry(domain=GREE_DOMAIN).add_to_hass(hass)
await async_setup_component(hass, GREE_DOMAIN, {GREE_DOMAIN: {DOMAIN: {}}})
await hass.async_block_till_done()
async def test_send_panel_light_on(hass, discovery, device):
"""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 == STATE_ON
async def test_send_panel_light_on_device_timeout(hass, discovery, device):
"""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 == STATE_ON
async def test_send_panel_light_off(hass, discovery, device):
"""Test for sending power on command to the device."""
await async_setup_gree(hass)
assert await hass.services.async_call(
DOMAIN,
SERVICE_TURN_OFF,
{ATTR_ENTITY_ID: ENTITY_ID},
blocking=True,
)
state = hass.states.get(ENTITY_ID)
assert state is not None
assert state.state == STATE_OFF
async def test_send_panel_light_toggle(hass, discovery, device):
"""Test for sending power on command to the device."""
await async_setup_gree(hass)
# Turn the service on first
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 == STATE_ON
# Toggle it off
assert await hass.services.async_call(
DOMAIN,
SERVICE_TOGGLE,
{ATTR_ENTITY_ID: ENTITY_ID},
blocking=True,
)
state = hass.states.get(ENTITY_ID)
assert state is not None
assert state.state == STATE_OFF
# Toggle is back on
assert await hass.services.async_call(
DOMAIN,
SERVICE_TOGGLE,
{ATTR_ENTITY_ID: ENTITY_ID},
blocking=True,
)
state = hass.states.get(ENTITY_ID)
assert state is not None
assert state.state == STATE_ON
async def test_panel_light_name(hass, discovery, device):
"""Test for name property."""
await async_setup_gree(hass)
state = hass.states.get(ENTITY_ID)
assert state.attributes[ATTR_FRIENDLY_NAME] == "fake-device-1 Panel Light"