diff --git a/homeassistant/components/gree/__init__.py b/homeassistant/components/gree/__init__.py index 96f2401e8d7..92b56a4804e 100644 --- a/homeassistant/components/gree/__init__.py +++ b/homeassistant/components/gree/__init__.py @@ -1,12 +1,14 @@ """The Gree Climate integration.""" +import asyncio 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 .bridge import CannotConnect, DeviceHelper -from .const import DOMAIN +from .bridge import CannotConnect, DeviceDataUpdateCoordinator, DeviceHelper +from .const import COORDINATOR, DOMAIN _LOGGER = logging.getLogger(__name__) @@ -40,23 +42,31 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): ) devices.append(device) - hass.data[DOMAIN]["devices"] = devices - hass.data[DOMAIN]["pending"] = devices + 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) ) + hass.async_create_task( + hass.config_entries.async_forward_entry_setup(entry, SWITCH_DOMAIN) + ) return True async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry): """Unload a config entry.""" - unload_ok = await hass.config_entries.async_forward_entry_unload( - entry, CLIMATE_DOMAIN + results = asyncio.gather( + 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: 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 diff --git a/homeassistant/components/gree/bridge.py b/homeassistant/components/gree/bridge.py index 44adaf970b8..3fbf4a21fb3 100644 --- a/homeassistant/components/gree/bridge.py +++ b/homeassistant/components/gree/bridge.py @@ -1,11 +1,71 @@ """Helper and wrapper classes for Gree module.""" +from datetime import timedelta +import logging from typing import List from greeclimate.device import Device, DeviceInfo from greeclimate.discovery import Discovery -from greeclimate.exceptions import DeviceNotBoundError +from greeclimate.exceptions import DeviceNotBoundError, DeviceTimeoutError 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: diff --git a/homeassistant/components/gree/climate.py b/homeassistant/components/gree/climate.py index 724903ef360..6a33e3341b0 100644 --- a/homeassistant/components/gree/climate.py +++ b/homeassistant/components/gree/climate.py @@ -1,5 +1,4 @@ """Support for interface with a Gree climate systems.""" -from datetime import timedelta import logging from typing import List @@ -10,7 +9,6 @@ from greeclimate.device import ( TemperatureUnits, VerticalSwing, ) -from greeclimate.exceptions import DeviceTimeoutError from homeassistant.components.climate import ClimateEntity from homeassistant.components.climate.const import ( @@ -45,12 +43,13 @@ from homeassistant.const import ( TEMP_FAHRENHEIT, ) from homeassistant.helpers.device_registry import CONNECTION_NETWORK_MAC +from homeassistant.helpers.update_coordinator import CoordinatorEntity from .const import ( + COORDINATOR, DOMAIN, FAN_MEDIUM_HIGH, FAN_MEDIUM_LOW, - MAX_ERRORS, MAX_TEMP, MIN_TEMP, TARGET_TEMPERATURE_STEP, @@ -58,9 +57,6 @@ from .const import ( _LOGGER = logging.getLogger(__name__) -SCAN_INTERVAL = timedelta(seconds=60) -PARALLEL_UPDATES = 0 - HVAC_MODES = { Mode.Auto: HVAC_MODE_AUTO, Mode.Cool: HVAC_MODE_COOL, @@ -101,85 +97,21 @@ 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(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.""" - def __init__(self, device): + def __init__(self, coordinator): """Initialize the Gree device.""" - self._device = device - self._name = device.device_info.name - self._mac = device.device_info.mac - self._available = False - self._error_count = 0 - - async def async_update(self): - """Update the state of the device.""" - try: - await self._device.update_state() - - if not self._available and self._error_count: - _LOGGER.warning( - "Device is available: %s (%s)", - self._name, - str(self._device.device_info), - ) - - self._available = True - self._error_count = 0 - except DeviceTimeoutError: - self._error_count += 1 - - # Under normal conditions GREE units timeout every once in a while - if self._available and self._error_count >= MAX_ERRORS: - self._available = False - _LOGGER.warning( - "Device is unavailable: %s (%s)", - self._name, - self._device.device_info, - ) - except Exception: # pylint: disable=broad-except - # Under normal conditions GREE units timeout every once in a while - if self._available: - self._available = False - _LOGGER.exception( - "Unknown exception caught during update by gree device: %s (%s)", - self._name, - self._device.device_info, - ) - - async def _push_state_update(self): - """Send state updates to the physical device.""" - try: - return await self._device.push_state_update() - except DeviceTimeoutError: - self._error_count += 1 - - # Under normal conditions GREE units timeout every once in a while - if self._available and self._error_count >= MAX_ERRORS: - self._available = False - _LOGGER.warning( - "Device timedout while sending state update: %s (%s)", - self._name, - self._device.device_info, - ) - except Exception: # pylint: disable=broad-except - # Under normal conditions GREE units timeout every once in a while - if self._available: - self._available = False - _LOGGER.exception( - "Unknown exception caught while sending state update to: %s (%s)", - self._name, - self._device.device_info, - ) - - @property - def available(self) -> bool: - """Return if the device is available.""" - return self._available + super().__init__(coordinator) + self._name = coordinator.device.device_info.name + self._mac = coordinator.device.device_info.mac @property def name(self) -> str: @@ -204,7 +136,7 @@ class GreeClimateEntity(ClimateEntity): @property def temperature_unit(self) -> str: """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 @property @@ -220,7 +152,7 @@ class GreeClimateEntity(ClimateEntity): @property def target_temperature(self) -> float: """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): """Set new target temperature.""" @@ -234,8 +166,9 @@ class GreeClimateEntity(ClimateEntity): self._name, ) - self._device.target_temperature = round(temperature) - await self._push_state_update() + self.coordinator.device.target_temperature = round(temperature) + await self.coordinator.push_state_update() + self.async_write_ha_state() @property def min_temp(self) -> float: @@ -255,10 +188,10 @@ class GreeClimateEntity(ClimateEntity): @property def hvac_mode(self) -> str: """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_MODES.get(self._device.mode) + return HVAC_MODES.get(self.coordinator.device.mode) async def async_set_hvac_mode(self, hvac_mode): """Set new target hvac mode.""" @@ -272,15 +205,17 @@ class GreeClimateEntity(ClimateEntity): ) if hvac_mode == HVAC_MODE_OFF: - self._device.power = False - await self._push_state_update() + self.coordinator.device.power = False + await self.coordinator.push_state_update() + self.async_write_ha_state() return - if not self._device.power: - self._device.power = True + if not self.coordinator.device.power: + self.coordinator.device.power = True - self._device.mode = HVAC_MODES_REVERSE.get(hvac_mode) - await self._push_state_update() + self.coordinator.device.mode = HVAC_MODES_REVERSE.get(hvac_mode) + await self.coordinator.push_state_update() + self.async_write_ha_state() @property def hvac_modes(self) -> List[str]: @@ -292,13 +227,13 @@ class GreeClimateEntity(ClimateEntity): @property def preset_mode(self) -> str: """Return the current preset mode for the device.""" - if self._device.steady_heat: + if self.coordinator.device.steady_heat: return PRESET_AWAY - if self._device.power_save: + if self.coordinator.device.power_save: return PRESET_ECO - if self._device.sleep: + if self.coordinator.device.sleep: return PRESET_SLEEP - if self._device.turbo: + if self.coordinator.device.turbo: return PRESET_BOOST return PRESET_NONE @@ -313,21 +248,22 @@ class GreeClimateEntity(ClimateEntity): self._name, ) - self._device.steady_heat = False - self._device.power_save = False - self._device.turbo = False - self._device.sleep = False + self.coordinator.device.steady_heat = False + self.coordinator.device.power_save = False + self.coordinator.device.turbo = False + self.coordinator.device.sleep = False if preset_mode == PRESET_AWAY: - self._device.steady_heat = True + self.coordinator.device.steady_heat = True elif preset_mode == PRESET_ECO: - self._device.power_save = True + self.coordinator.device.power_save = True elif preset_mode == PRESET_BOOST: - self._device.turbo = True + self.coordinator.device.turbo = True 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 def preset_modes(self) -> List[str]: @@ -337,7 +273,7 @@ class GreeClimateEntity(ClimateEntity): @property def fan_mode(self) -> str: """Return the current fan mode for the device.""" - speed = self._device.fan_speed + speed = self.coordinator.device.fan_speed return FAN_MODES.get(speed) async def async_set_fan_mode(self, fan_mode): @@ -345,8 +281,9 @@ class GreeClimateEntity(ClimateEntity): if fan_mode not in FAN_MODES_REVERSE: raise ValueError(f"Invalid fan mode: {fan_mode}") - self._device.fan_speed = FAN_MODES_REVERSE.get(fan_mode) - await self._push_state_update() + self.coordinator.device.fan_speed = FAN_MODES_REVERSE.get(fan_mode) + await self.coordinator.push_state_update() + self.async_write_ha_state() @property def fan_modes(self) -> List[str]: @@ -356,8 +293,8 @@ class GreeClimateEntity(ClimateEntity): @property def swing_mode(self) -> str: """Return the current swing mode for the device.""" - h_swing = self._device.horizontal_swing == HorizontalSwing.FullSwing - v_swing = self._device.vertical_swing == VerticalSwing.FullSwing + h_swing = self.coordinator.device.horizontal_swing == HorizontalSwing.FullSwing + v_swing = self.coordinator.device.vertical_swing == VerticalSwing.FullSwing if h_swing and v_swing: return SWING_BOTH @@ -378,14 +315,15 @@ class GreeClimateEntity(ClimateEntity): self._name, ) - self._device.horizontal_swing = HorizontalSwing.Center - self._device.vertical_swing = VerticalSwing.FixedMiddle + self.coordinator.device.horizontal_swing = HorizontalSwing.Center + self.coordinator.device.vertical_swing = VerticalSwing.FixedMiddle 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): - 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 def swing_modes(self) -> List[str]: diff --git a/homeassistant/components/gree/const.py b/homeassistant/components/gree/const.py index 95435bb3bd9..9c645062256 100644 --- a/homeassistant/components/gree/const.py +++ b/homeassistant/components/gree/const.py @@ -1,6 +1,7 @@ """Constants for the Gree Climate integration.""" DOMAIN = "gree" +COORDINATOR = "coordinator" FAN_MEDIUM_LOW = "medium low" FAN_MEDIUM_HIGH = "medium high" diff --git a/homeassistant/components/gree/strings.json b/homeassistant/components/gree/strings.json index ad8f0f41ae7..9f3518bcf8d 100644 --- a/homeassistant/components/gree/strings.json +++ b/homeassistant/components/gree/strings.json @@ -10,4 +10,4 @@ "no_devices_found": "[%key:common::config_flow::abort::no_devices_found%]" } } -} +} \ No newline at end of file diff --git a/homeassistant/components/gree/switch.py b/homeassistant/components/gree/switch.py new file mode 100644 index 00000000000..f4e9792a589 --- /dev/null +++ b/homeassistant/components/gree/switch.py @@ -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() diff --git a/tests/components/gree/test_climate.py b/tests/components/gree/test_climate.py index 9f3946e6ade..534168fa78e 100644 --- a/tests/components/gree/test_climate.py +++ b/tests/components/gree/test_climate.py @@ -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): """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) + # First update becomes unavailable next_update = mock_now + timedelta(minutes=5) with patch("homeassistant.util.dt.utcnow", return_value=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.state == STATE_UNAVAILABLE + # Second update restores the connection next_update = mock_now + timedelta(minutes=10) with patch("homeassistant.util.dt.utcnow", return_value=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) - next_update = mock_now + timedelta(minutes=5) - with patch("homeassistant.util.dt.utcnow", return_value=next_update): - async_fire_time_changed(hass, next_update) - await hass.async_block_till_done() - state = hass.states.get(ENTITY_ID) assert state.name == "fake-device-1" assert state.state != STATE_UNAVAILABLE @@ -221,21 +222,9 @@ async def test_send_command_device_timeout(hass, discovery, device, mock_now): assert state.name == "fake-device-1" assert state.state != STATE_UNAVAILABLE - device().update_state.side_effect = DeviceTimeoutError device().push_state_update.side_effect = DeviceTimeoutError - # Second update to make an initial error (device is still available) - next_update = mock_now + timedelta(minutes=5) - with patch("homeassistant.util.dt.utcnow", return_value=next_update): - async_fire_time_changed(hass, next_update) - await hass.async_block_till_done() - - state = hass.states.get(ENTITY_ID) - assert state is not None - assert state.name == "fake-device-1" - assert state.state != STATE_UNAVAILABLE - - # Second attempt should make the device unavailable + # Send failure should not raise exceptions or change device state assert await hass.services.async_call( DOMAIN, 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) assert state is not None - assert state.state == STATE_UNAVAILABLE - - -async def test_send_command_device_unknown_error(hass, discovery, device, mock_now): - """Test for sending power on command to the device with a device timeout.""" - device().update_state.side_effect = [DEFAULT_MOCK, Exception] - device().push_state_update.side_effect = Exception - - await async_setup_gree(hass) - - next_update = mock_now + timedelta(minutes=5) - with patch("homeassistant.util.dt.utcnow", return_value=next_update): - async_fire_time_changed(hass, next_update) - await hass.async_block_till_done() - - # First update to make the device available - state = hass.states.get(ENTITY_ID) - assert state.name == "fake-device-1" assert state.state != STATE_UNAVAILABLE - assert await hass.services.async_call( - DOMAIN, - SERVICE_SET_HVAC_MODE, - {ATTR_ENTITY_ID: ENTITY_ID, ATTR_HVAC_MODE: HVAC_MODE_AUTO}, - blocking=True, - ) - - state = hass.states.get(ENTITY_ID) - assert state is not None - assert state.state == STATE_UNAVAILABLE - async def test_send_power_on(hass, discovery, device, mock_now): """Test for sending power on command to the device.""" await async_setup_gree(hass) - next_update = mock_now + timedelta(minutes=5) - with patch("homeassistant.util.dt.utcnow", return_value=next_update): - async_fire_time_changed(hass, next_update) - await hass.async_block_till_done() - assert await hass.services.async_call( DOMAIN, SERVICE_SET_HVAC_MODE, @@ -305,11 +260,6 @@ async def test_send_power_on_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_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.""" await async_setup_gree(hass) - next_update = mock_now + timedelta(minutes=5) - with patch("homeassistant.util.dt.utcnow", return_value=next_update): - async_fire_time_changed(hass, next_update) - await hass.async_block_till_done() - assert await hass.services.async_call( DOMAIN, SERVICE_SET_TEMPERATURE, @@ -351,11 +296,6 @@ async def test_send_target_temperature_device_timeout( await async_setup_gree(hass) - next_update = mock_now + timedelta(minutes=5) - with patch("homeassistant.util.dt.utcnow", return_value=next_update): - async_fire_time_changed(hass, next_update) - await hass.async_block_till_done() - assert await hass.services.async_call( DOMAIN, SERVICE_SET_TEMPERATURE, @@ -374,11 +314,6 @@ async def test_update_target_temperature(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() - state = hass.states.get(ENTITY_ID) assert state is not None 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.""" await async_setup_gree(hass) - next_update = mock_now + timedelta(minutes=5) - with patch("homeassistant.util.dt.utcnow", return_value=next_update): - async_fire_time_changed(hass, next_update) - await hass.async_block_till_done() - assert await hass.services.async_call( DOMAIN, SERVICE_SET_PRESET_MODE, @@ -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.""" await async_setup_gree(hass) - next_update = mock_now + timedelta(minutes=5) - with patch("homeassistant.util.dt.utcnow", return_value=next_update): - async_fire_time_changed(hass, next_update) - await hass.async_block_till_done() - with pytest.raises(ValueError): await hass.services.async_call( DOMAIN, @@ -441,11 +366,6 @@ async def test_send_preset_mode_device_timeout( await async_setup_gree(hass) - next_update = mock_now + timedelta(minutes=5) - with patch("homeassistant.util.dt.utcnow", return_value=next_update): - async_fire_time_changed(hass, next_update) - await hass.async_block_till_done() - assert await hass.services.async_call( DOMAIN, SERVICE_SET_PRESET_MODE, @@ -470,11 +390,6 @@ async def test_update_preset_mode(hass, discovery, device, mock_now, preset): await async_setup_gree(hass) - next_update = mock_now + timedelta(minutes=5) - with patch("homeassistant.util.dt.utcnow", return_value=next_update): - async_fire_time_changed(hass, next_update) - await hass.async_block_till_done() - state = hass.states.get(ENTITY_ID) assert state is not None assert state.attributes.get(ATTR_PRESET_MODE) == preset @@ -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.""" await async_setup_gree(hass) - next_update = mock_now + timedelta(minutes=5) - with patch("homeassistant.util.dt.utcnow", return_value=next_update): - async_fire_time_changed(hass, next_update) - await hass.async_block_till_done() - assert await hass.services.async_call( DOMAIN, SERVICE_SET_HVAC_MODE, @@ -524,11 +434,6 @@ async def test_send_hvac_mode_device_timeout( await async_setup_gree(hass) - next_update = mock_now + timedelta(minutes=5) - with patch("homeassistant.util.dt.utcnow", return_value=next_update): - async_fire_time_changed(hass, next_update) - await hass.async_block_till_done() - assert await hass.services.async_call( DOMAIN, SERVICE_SET_HVAC_MODE, @@ -559,11 +464,6 @@ async def test_update_hvac_mode(hass, discovery, device, mock_now, hvac_mode): await async_setup_gree(hass) - next_update = mock_now + timedelta(minutes=5) - with patch("homeassistant.util.dt.utcnow", return_value=next_update): - async_fire_time_changed(hass, next_update) - await hass.async_block_till_done() - state = hass.states.get(ENTITY_ID) assert state is not None assert state.state == hvac_mode @@ -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.""" await async_setup_gree(hass) - next_update = mock_now + timedelta(minutes=5) - with patch("homeassistant.util.dt.utcnow", return_value=next_update): - async_fire_time_changed(hass, next_update) - await hass.async_block_till_done() - assert await hass.services.async_call( DOMAIN, SERVICE_SET_FAN_MODE, @@ -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.""" await async_setup_gree(hass) - next_update = mock_now + timedelta(minutes=5) - with patch("homeassistant.util.dt.utcnow", return_value=next_update): - async_fire_time_changed(hass, next_update) - await hass.async_block_till_done() - with pytest.raises(ValueError): await hass.services.async_call( DOMAIN, @@ -628,11 +518,6 @@ async def test_send_fan_mode_device_timeout( await async_setup_gree(hass) - next_update = mock_now + timedelta(minutes=5) - with patch("homeassistant.util.dt.utcnow", return_value=next_update): - async_fire_time_changed(hass, next_update) - await hass.async_block_till_done() - assert await hass.services.async_call( DOMAIN, SERVICE_SET_FAN_MODE, @@ -655,11 +540,6 @@ async def test_update_fan_mode(hass, discovery, device, mock_now, fan_mode): await async_setup_gree(hass) - next_update = mock_now + timedelta(minutes=5) - with patch("homeassistant.util.dt.utcnow", return_value=next_update): - async_fire_time_changed(hass, next_update) - await hass.async_block_till_done() - state = hass.states.get(ENTITY_ID) assert state is not None assert state.attributes.get(ATTR_FAN_MODE) == fan_mode @@ -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.""" await async_setup_gree(hass) - next_update = mock_now + timedelta(minutes=5) - with patch("homeassistant.util.dt.utcnow", return_value=next_update): - async_fire_time_changed(hass, next_update) - await hass.async_block_till_done() - assert await hass.services.async_call( DOMAIN, SERVICE_SET_SWING_MODE, @@ -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.""" await async_setup_gree(hass) - next_update = mock_now + timedelta(minutes=5) - with patch("homeassistant.util.dt.utcnow", return_value=next_update): - async_fire_time_changed(hass, next_update) - await hass.async_block_till_done() - with pytest.raises(ValueError): await hass.services.async_call( DOMAIN, @@ -722,11 +592,6 @@ async def test_send_swing_mode_device_timeout( await async_setup_gree(hass) - next_update = mock_now + timedelta(minutes=5) - with patch("homeassistant.util.dt.utcnow", return_value=next_update): - async_fire_time_changed(hass, next_update) - await hass.async_block_till_done() - assert await hass.services.async_call( DOMAIN, SERVICE_SET_SWING_MODE, @@ -757,11 +622,6 @@ async def test_update_swing_mode(hass, discovery, device, mock_now, swing_mode): await async_setup_gree(hass) - next_update = mock_now + timedelta(minutes=5) - with patch("homeassistant.util.dt.utcnow", return_value=next_update): - async_fire_time_changed(hass, next_update) - await hass.async_block_till_done() - state = hass.states.get(ENTITY_ID) assert state is not None assert state.attributes.get(ATTR_SWING_MODE) == swing_mode diff --git a/tests/components/gree/test_switch.py b/tests/components/gree/test_switch.py new file mode 100644 index 00000000000..89a8b224f1a --- /dev/null +++ b/tests/components/gree/test_switch.py @@ -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"