From c039057c1f21917325c96e4d257094a6806d5d33 Mon Sep 17 00:00:00 2001 From: Clifford Roche <1007595+cmroche@users.noreply.github.com> Date: Sat, 13 Jul 2024 17:49:11 -0400 Subject: [PATCH 1/7] Update to greeclimate 2.0.0 --- homeassistant/components/gree/const.py | 2 + homeassistant/components/gree/coordinator.py | 59 +++++- homeassistant/components/gree/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/gree/test_bridge.py | 38 +++- tests/components/gree/test_climate.py | 204 +++++++++++-------- 7 files changed, 208 insertions(+), 101 deletions(-) diff --git a/homeassistant/components/gree/const.py b/homeassistant/components/gree/const.py index 46479210921411..f926eb1c53ea3c 100644 --- a/homeassistant/components/gree/const.py +++ b/homeassistant/components/gree/const.py @@ -18,3 +18,5 @@ MAX_ERRORS = 2 TARGET_TEMPERATURE_STEP = 1 + +UPDATE_INTERVAL = 60 diff --git a/homeassistant/components/gree/coordinator.py b/homeassistant/components/gree/coordinator.py index 1bccf3bbc484e5..3685111f2cd9d9 100644 --- a/homeassistant/components/gree/coordinator.py +++ b/homeassistant/components/gree/coordinator.py @@ -2,16 +2,20 @@ from __future__ import annotations -from datetime import timedelta +from datetime import datetime, timedelta import logging +from typing import Any from greeclimate.device import Device, DeviceInfo from greeclimate.discovery import Discovery, Listener from greeclimate.exceptions import DeviceNotBoundError, DeviceTimeoutError +from greeclimate.network import Response from homeassistant.core import HomeAssistant from homeassistant.helpers.dispatcher import async_dispatcher_send +from homeassistant.helpers.json import json_dumps from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed +from homeassistant.util.dt import utcnow from .const import ( COORDINATORS, @@ -19,6 +23,7 @@ DISPATCH_DEVICE_DISCOVERED, DOMAIN, MAX_ERRORS, + UPDATE_INTERVAL, ) _LOGGER = logging.getLogger(__name__) @@ -34,28 +39,68 @@ def __init__(self, hass: HomeAssistant, device: Device) -> None: hass, _LOGGER, name=f"{DOMAIN}-{device.device_info.name}", - update_interval=timedelta(seconds=60), + update_interval=timedelta(seconds=UPDATE_INTERVAL), + always_update=False, ) self.device = device + self.device.add_handler(Response.DATA, self.device_state_updated) + self.device.add_handler(Response.RESULT, self.device_state_updated) + + self._error_count: int = 0 + self._last_response_time: datetime = utcnow() + self._last_error_time: datetime | None = None + + def device_state_updated(self, *args) -> None: + """Handle device state updates.""" + _LOGGER.debug("Device state updated: %s", json_dumps(args)) self._error_count = 0 + self._last_response_time = utcnow() + self.async_set_updated_data(self.device.raw_properties) - async def _async_update_data(self): + async def _async_update_data(self) -> Any: """Update the state of the device.""" + _LOGGER.debug( + "Updating device state: %s, error count: %d", self.name, self._error_count + ) try: await self.device.update_state() except DeviceNotBoundError as error: - raise UpdateFailed(f"Device {self.name} is unavailable") from error + raise UpdateFailed( + f"Device {self.name} is unavailable, device is not bound." + ) from error 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)", + "Device %s is unavailable: %s", self.name, self.device.device_info + ) + raise UpdateFailed( + f"Device {self.name} is unavailable, could not send update request" + ) from error + else: + # raise update failed if time for more than MAX_ERRORS has passed since last update + now = utcnow() + elapsed_success = now - self._last_response_time + if self.update_interval and elapsed_success >= self.update_interval: + if not self._last_error_time or ( + (now - self.update_interval) >= self._last_error_time + ): + self._last_error_time = now + self._error_count += 1 + + _LOGGER.warning( + "Device %s is unresponsive for %s seconds", self.name, - self.device.device_info, + elapsed_success, + ) + if self.last_update_success and self._error_count >= MAX_ERRORS: + raise UpdateFailed( + f"Device {self.name} is unresponsive for too long and now unavailable" ) - raise UpdateFailed(f"Device {self.name} is unavailable") from error + + return self.device.raw_properties async def push_state_update(self): """Send state updates to the physical device.""" diff --git a/homeassistant/components/gree/manifest.json b/homeassistant/components/gree/manifest.json index a7c884c4042dee..ca1c4b5b7542ba 100644 --- a/homeassistant/components/gree/manifest.json +++ b/homeassistant/components/gree/manifest.json @@ -7,5 +7,5 @@ "documentation": "https://www.home-assistant.io/integrations/gree", "iot_class": "local_polling", "loggers": ["greeclimate"], - "requirements": ["greeclimate==1.4.6"] + "requirements": ["greeclimate==2.0.0"] } diff --git a/requirements_all.txt b/requirements_all.txt index abb050562d20b8..79a7fc9a4ff448 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1010,7 +1010,7 @@ gpiozero==1.6.2 gps3==0.33.3 # homeassistant.components.gree -greeclimate==1.4.6 +greeclimate==2.0.0 # homeassistant.components.greeneye_monitor greeneye_monitor==3.0.3 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 9ce1e717a9c09b..49cf70a51f0377 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -836,7 +836,7 @@ govee-local-api==1.5.1 gps3==0.33.3 # homeassistant.components.gree -greeclimate==1.4.6 +greeclimate==2.0.0 # homeassistant.components.greeneye_monitor greeneye_monitor==3.0.3 diff --git a/tests/components/gree/test_bridge.py b/tests/components/gree/test_bridge.py index 37b0b0dc15eaf1..2891ca9f5229f2 100644 --- a/tests/components/gree/test_bridge.py +++ b/tests/components/gree/test_bridge.py @@ -1,12 +1,18 @@ """Tests for gree component.""" from datetime import timedelta +from unittest.mock import patch from freezegun.api import FrozenDateTimeFactory import pytest from homeassistant.components.climate import DOMAIN -from homeassistant.components.gree.const import COORDINATORS, DOMAIN as GREE +from homeassistant.components.gree.const import ( + COORDINATORS, + DOMAIN as GREE, + UPDATE_INTERVAL, +) +from homeassistant.components.gree.coordinator import DeviceDataUpdateCoordinator from homeassistant.core import HomeAssistant import homeassistant.util.dt as dt_util @@ -69,3 +75,33 @@ async def test_discovery_after_setup( device_infos = [x.device.device_info for x in hass.data[GREE][COORDINATORS]] assert device_infos[0].ip == "1.1.1.2" assert device_infos[1].ip == "2.2.2.1" + + +async def test_coordinator_updates( + hass: HomeAssistant, freezer: FrozenDateTimeFactory, discovery, device, mock_now +) -> None: + """Test gree devices update their state.""" + await async_setup_gree(hass) + await hass.async_block_till_done() + + assert len(hass.states.async_all(DOMAIN)) == 1 + + coordinator = hass.data[GREE][COORDINATORS][0] + + def update_device_state(): + """Update the device state.""" + coordinator.device_state_updated(["test"]) + + device().update_state.side_effect = update_device_state + + next_update = mock_now + timedelta(seconds=UPDATE_INTERVAL) + freezer.move_to(next_update) + + with patch.object( + DeviceDataUpdateCoordinator, "async_set_updated_data" + ) as mock_set_updated_data: + async_fire_time_changed(hass, next_update) + await hass.async_block_till_done() + mock_set_updated_data.assert_called_once_with(device().raw_properties) + + assert coordinator.last_update_success is not None diff --git a/tests/components/gree/test_climate.py b/tests/components/gree/test_climate.py index e6f24ade1aaf8c..5374cc9627ae70 100644 --- a/tests/components/gree/test_climate.py +++ b/tests/components/gree/test_climate.py @@ -48,7 +48,12 @@ HVAC_MODES_REVERSE, GreeClimateEntity, ) -from homeassistant.components.gree.const import FAN_MEDIUM_HIGH, FAN_MEDIUM_LOW +from homeassistant.components.gree.const import ( + DISCOVERY_SCAN_INTERVAL, + FAN_MEDIUM_HIGH, + FAN_MEDIUM_LOW, + UPDATE_INTERVAL, +) from homeassistant.const import ( ATTR_ENTITY_ID, ATTR_TEMPERATURE, @@ -61,7 +66,6 @@ from homeassistant.core import HomeAssistant from homeassistant.exceptions import ServiceValidationError from homeassistant.helpers import entity_registry as er -import homeassistant.util.dt as dt_util from .common import async_setup_gree, build_device_mock @@ -70,12 +74,6 @@ ENTITY_ID = f"{DOMAIN}.fake_device_1" -@pytest.fixture -def mock_now(): - """Fixture for dtutil.now.""" - return dt_util.utcnow() - - async def test_discovery_called_once(hass: HomeAssistant, discovery, device) -> None: """Test discovery is only ever called once.""" await async_setup_gree(hass) @@ -104,7 +102,7 @@ async def test_discovery_setup(hass: HomeAssistant, discovery, device) -> None: async def test_discovery_setup_connection_error( - hass: HomeAssistant, discovery, device, mock_now + hass: HomeAssistant, discovery, device ) -> None: """Test gree integration is setup.""" MockDevice1 = build_device_mock( @@ -126,7 +124,7 @@ async def test_discovery_setup_connection_error( async def test_discovery_after_setup( - hass: HomeAssistant, freezer: FrozenDateTimeFactory, discovery, device, mock_now + hass: HomeAssistant, freezer: FrozenDateTimeFactory, discovery, device ) -> None: """Test gree devices don't change after multiple discoveries.""" MockDevice1 = build_device_mock( @@ -142,8 +140,9 @@ async def test_discovery_after_setup( discovery.return_value.mock_devices = [MockDevice1, MockDevice2] device.side_effect = [MockDevice1, MockDevice2] - await async_setup_gree(hass) - await hass.async_block_till_done() + await hass.config.async_set_time_zone("UTC") + freezer.move_to("2021-01-09 12:00:00+00:00") + await async_setup_gree(hass) # Update 1 assert discovery.return_value.scan_count == 1 assert len(hass.states.async_all(DOMAIN)) == 2 @@ -152,9 +151,8 @@ async def test_discovery_after_setup( discovery.return_value.mock_devices = [MockDevice1, MockDevice2] device.side_effect = [MockDevice1, MockDevice2] - next_update = mock_now + timedelta(minutes=6) - freezer.move_to(next_update) - async_fire_time_changed(hass, next_update) + freezer.tick(timedelta(seconds=DISCOVERY_SCAN_INTERVAL) + timedelta(minutes=1)) + async_fire_time_changed(hass) await hass.async_block_till_done() assert discovery.return_value.scan_count == 2 @@ -162,7 +160,7 @@ async def test_discovery_after_setup( async def test_discovery_add_device_after_setup( - hass: HomeAssistant, freezer: FrozenDateTimeFactory, discovery, device, mock_now + hass: HomeAssistant, freezer: FrozenDateTimeFactory, discovery, device ) -> None: """Test gree devices can be added after initial setup.""" MockDevice1 = build_device_mock( @@ -178,6 +176,10 @@ async def test_discovery_add_device_after_setup( discovery.return_value.mock_devices = [MockDevice1] device.side_effect = [MockDevice1] + await hass.config.async_set_time_zone("UTC") + freezer.move_to("2021-01-09 12:00:00+00:00") + await async_setup_gree(hass) # Update 1 + await async_setup_gree(hass) await hass.async_block_till_done() @@ -188,9 +190,8 @@ async def test_discovery_add_device_after_setup( discovery.return_value.mock_devices = [MockDevice2] device.side_effect = [MockDevice2] - next_update = mock_now + timedelta(minutes=6) - freezer.move_to(next_update) - async_fire_time_changed(hass, next_update) + freezer.tick(timedelta(seconds=DISCOVERY_SCAN_INTERVAL) + timedelta(minutes=1)) + async_fire_time_changed(hass) await hass.async_block_till_done() assert discovery.return_value.scan_count == 2 @@ -198,7 +199,7 @@ async def test_discovery_add_device_after_setup( async def test_discovery_device_bind_after_setup( - hass: HomeAssistant, freezer: FrozenDateTimeFactory, discovery, device, mock_now + hass: HomeAssistant, freezer: FrozenDateTimeFactory, discovery, device ) -> None: """Test gree devices can be added after a late device bind.""" MockDevice1 = build_device_mock( @@ -210,8 +211,9 @@ async def test_discovery_device_bind_after_setup( discovery.return_value.mock_devices = [MockDevice1] device.return_value = MockDevice1 - await async_setup_gree(hass) - await hass.async_block_till_done() + await hass.config.async_set_time_zone("UTC") + freezer.move_to("2021-01-09 12:00:00+00:00") + await async_setup_gree(hass) # Update 1 assert len(hass.states.async_all(DOMAIN)) == 1 state = hass.states.get(ENTITY_ID) @@ -222,9 +224,8 @@ async def test_discovery_device_bind_after_setup( MockDevice1.bind.side_effect = None MockDevice1.update_state.side_effect = None - next_update = mock_now + timedelta(minutes=5) - freezer.move_to(next_update) - async_fire_time_changed(hass, next_update) + freezer.tick(timedelta(seconds=DISCOVERY_SCAN_INTERVAL) + timedelta(minutes=1)) + async_fire_time_changed(hass) await hass.async_block_till_done() state = hass.states.get(ENTITY_ID) @@ -232,7 +233,7 @@ async def test_discovery_device_bind_after_setup( async def test_update_connection_failure( - hass: HomeAssistant, freezer: FrozenDateTimeFactory, device, mock_now + hass: HomeAssistant, freezer: FrozenDateTimeFactory, discovery, device ) -> None: """Testing update hvac connection failure exception.""" device().update_state.side_effect = [ @@ -241,36 +242,34 @@ async def test_update_connection_failure( DeviceTimeoutError, ] - await async_setup_gree(hass) + await hass.config.async_set_time_zone("UTC") + freezer.move_to("2021-01-09 12:00:00+00:00") + await async_setup_gree(hass) # Update 1 + + async def run_update(): + freezer.tick(timedelta(seconds=UPDATE_INTERVAL) + timedelta(seconds=1)) + async_fire_time_changed(hass) - next_update = mock_now + timedelta(minutes=5) - freezer.move_to(next_update) - async_fire_time_changed(hass, next_update) await hass.async_block_till_done() - # First update to make the device available + # Update 2 + await run_update() state = hass.states.get(ENTITY_ID) assert state.name == "fake-device-1" assert state.state != STATE_UNAVAILABLE - next_update = mock_now + timedelta(minutes=10) - freezer.move_to(next_update) - async_fire_time_changed(hass, next_update) - await hass.async_block_till_done() - - next_update = mock_now + timedelta(minutes=15) - freezer.move_to(next_update) - async_fire_time_changed(hass, next_update) - await hass.async_block_till_done() + # Update 3 + await run_update() - # Then two more update failures to make the device unavailable + # Update 4 + await run_update() state = hass.states.get(ENTITY_ID) assert state.name == "fake-device-1" assert state.state == STATE_UNAVAILABLE -async def test_update_connection_failure_recovery( - hass: HomeAssistant, freezer: FrozenDateTimeFactory, discovery, device, mock_now +async def test_update_connection_send_failure_recovery( + hass: HomeAssistant, freezer: FrozenDateTimeFactory, discovery, device ) -> None: """Testing update hvac connection failure recovery.""" device().update_state.side_effect = [ @@ -279,44 +278,44 @@ async def test_update_connection_failure_recovery( DEFAULT_MOCK, ] - await async_setup_gree(hass) + await hass.config.async_set_time_zone("UTC") + freezer.move_to("2021-01-09 12:00:00+00:00") + + await async_setup_gree(hass) # Update 1 + + async def run_update(): + freezer.tick(timedelta(seconds=UPDATE_INTERVAL) + timedelta(seconds=1)) + async_fire_time_changed(hass) - # First update becomes unavailable - next_update = mock_now + timedelta(minutes=5) - freezer.move_to(next_update) - async_fire_time_changed(hass, next_update) await hass.async_block_till_done() + await run_update() # Update 2 state = hass.states.get(ENTITY_ID) assert state.name == "fake-device-1" assert state.state == STATE_UNAVAILABLE - # Second update restores the connection - next_update = mock_now + timedelta(minutes=10) - freezer.move_to(next_update) - async_fire_time_changed(hass, next_update) - await hass.async_block_till_done() - + await run_update() # Update 3 state = hass.states.get(ENTITY_ID) assert state.name == "fake-device-1" assert state.state != STATE_UNAVAILABLE async def test_update_unhandled_exception( - hass: HomeAssistant, freezer: FrozenDateTimeFactory, discovery, device, mock_now + hass: HomeAssistant, freezer: FrozenDateTimeFactory, discovery, device ) -> None: """Testing update hvac connection unhandled response exception.""" device().update_state.side_effect = [DEFAULT_MOCK, Exception] + await hass.config.async_set_time_zone("UTC") + freezer.move_to("2021-01-09 12:00:00+00:00") await async_setup_gree(hass) state = hass.states.get(ENTITY_ID) assert state.name == "fake-device-1" assert state.state != STATE_UNAVAILABLE - next_update = mock_now + timedelta(minutes=10) - freezer.move_to(next_update) - async_fire_time_changed(hass, next_update) + freezer.tick(timedelta(seconds=UPDATE_INTERVAL) + timedelta(seconds=1)) + async_fire_time_changed(hass) await hass.async_block_till_done() state = hass.states.get(ENTITY_ID) @@ -325,15 +324,15 @@ async def test_update_unhandled_exception( async def test_send_command_device_timeout( - hass: HomeAssistant, freezer: FrozenDateTimeFactory, discovery, device, mock_now + hass: HomeAssistant, freezer: FrozenDateTimeFactory, discovery, device ) -> None: """Test for sending power on command to the device with a device timeout.""" + await hass.config.async_set_time_zone("UTC") + freezer.move_to("2021-01-09 12:00:00+00:00") await async_setup_gree(hass) - # First update to make the device available - next_update = mock_now + timedelta(minutes=5) - freezer.move_to(next_update) - async_fire_time_changed(hass, next_update) + freezer.tick(timedelta(seconds=UPDATE_INTERVAL) + timedelta(seconds=1)) + async_fire_time_changed(hass) await hass.async_block_till_done() state = hass.states.get(ENTITY_ID) @@ -355,7 +354,42 @@ async def test_send_command_device_timeout( assert state.state != STATE_UNAVAILABLE -async def test_send_power_on(hass: HomeAssistant, discovery, device, mock_now) -> None: +async def test_unresponsive_device( + hass: HomeAssistant, freezer: FrozenDateTimeFactory, discovery, device +) -> None: + """Test for unresponsive device.""" + await hass.config.async_set_time_zone("UTC") + freezer.move_to("2021-01-09 12:00:00+00:00") + await async_setup_gree(hass) + + async def run_update(): + freezer.tick(timedelta(seconds=UPDATE_INTERVAL) + timedelta(seconds=1)) + async_fire_time_changed(hass) + await hass.async_block_till_done() + + # Update 2 + await run_update() + state = hass.states.get(ENTITY_ID) + assert state.name == "fake-device-1" + assert state.state != STATE_UNAVAILABLE + + # Update 3, 4, 5 + await run_update() + await run_update() + await run_update() + state = hass.states.get(ENTITY_ID) + assert state.name == "fake-device-1" + assert state.state == STATE_UNAVAILABLE + + # Receiving update from device will reset the state to available again + device().device_state_updated("test") + await run_update() + state = hass.states.get(ENTITY_ID) + assert state.name == "fake-device-1" + assert state.state != STATE_UNAVAILABLE + + +async def test_send_power_on(hass: HomeAssistant, discovery, device) -> None: """Test for sending power on command to the device.""" await async_setup_gree(hass) @@ -372,7 +406,7 @@ async def test_send_power_on(hass: HomeAssistant, discovery, device, mock_now) - async def test_send_power_off_device_timeout( - hass: HomeAssistant, discovery, device, mock_now + hass: HomeAssistant, discovery, device ) -> None: """Test for sending power off command to the device with a device timeout.""" device().push_state_update.side_effect = DeviceTimeoutError @@ -543,9 +577,7 @@ async def test_update_target_temperature( @pytest.mark.parametrize( "preset", [PRESET_AWAY, PRESET_ECO, PRESET_SLEEP, PRESET_BOOST, PRESET_NONE] ) -async def test_send_preset_mode( - hass: HomeAssistant, discovery, device, mock_now, preset -) -> None: +async def test_send_preset_mode(hass: HomeAssistant, discovery, device, preset) -> None: """Test for sending preset mode command to the device.""" await async_setup_gree(hass) @@ -561,9 +593,7 @@ async def test_send_preset_mode( assert state.attributes.get(ATTR_PRESET_MODE) == preset -async def test_send_invalid_preset_mode( - hass: HomeAssistant, discovery, device, mock_now -) -> None: +async def test_send_invalid_preset_mode(hass: HomeAssistant, discovery, device) -> None: """Test for sending preset mode command to the device.""" await async_setup_gree(hass) @@ -584,7 +614,7 @@ async def test_send_invalid_preset_mode( "preset", [PRESET_AWAY, PRESET_ECO, PRESET_SLEEP, PRESET_BOOST, PRESET_NONE] ) async def test_send_preset_mode_device_timeout( - hass: HomeAssistant, discovery, device, mock_now, preset + hass: HomeAssistant, discovery, device, preset ) -> None: """Test for sending preset mode command to the device with a device timeout.""" device().push_state_update.side_effect = DeviceTimeoutError @@ -607,7 +637,7 @@ async def test_send_preset_mode_device_timeout( "preset", [PRESET_AWAY, PRESET_ECO, PRESET_SLEEP, PRESET_BOOST, PRESET_NONE] ) async def test_update_preset_mode( - hass: HomeAssistant, discovery, device, mock_now, preset + hass: HomeAssistant, discovery, device, preset ) -> None: """Test for updating preset mode from the device.""" device().steady_heat = preset == PRESET_AWAY @@ -634,7 +664,7 @@ async def test_update_preset_mode( ], ) async def test_send_hvac_mode( - hass: HomeAssistant, discovery, device, mock_now, hvac_mode + hass: HomeAssistant, discovery, device, hvac_mode ) -> None: """Test for sending hvac mode command to the device.""" await async_setup_gree(hass) @@ -656,7 +686,7 @@ async def test_send_hvac_mode( [HVACMode.AUTO, HVACMode.COOL, HVACMode.DRY, HVACMode.FAN_ONLY, HVACMode.HEAT], ) async def test_send_hvac_mode_device_timeout( - hass: HomeAssistant, discovery, device, mock_now, hvac_mode + hass: HomeAssistant, discovery, device, hvac_mode ) -> None: """Test for sending hvac mode command to the device with a device timeout.""" device().push_state_update.side_effect = DeviceTimeoutError @@ -687,7 +717,7 @@ async def test_send_hvac_mode_device_timeout( ], ) async def test_update_hvac_mode( - hass: HomeAssistant, discovery, device, mock_now, hvac_mode + hass: HomeAssistant, discovery, device, hvac_mode ) -> None: """Test for updating hvac mode from the device.""" device().power = hvac_mode != HVACMode.OFF @@ -704,9 +734,7 @@ async def test_update_hvac_mode( "fan_mode", [FAN_AUTO, FAN_LOW, FAN_MEDIUM_LOW, FAN_MEDIUM, FAN_MEDIUM_HIGH, FAN_HIGH], ) -async def test_send_fan_mode( - hass: HomeAssistant, discovery, device, mock_now, fan_mode -) -> None: +async def test_send_fan_mode(hass: HomeAssistant, discovery, device, fan_mode) -> None: """Test for sending fan mode command to the device.""" await async_setup_gree(hass) @@ -722,9 +750,7 @@ async def test_send_fan_mode( assert state.attributes.get(ATTR_FAN_MODE) == fan_mode -async def test_send_invalid_fan_mode( - hass: HomeAssistant, discovery, device, mock_now -) -> None: +async def test_send_invalid_fan_mode(hass: HomeAssistant, discovery, device) -> None: """Test for sending fan mode command to the device.""" await async_setup_gree(hass) @@ -746,7 +772,7 @@ async def test_send_invalid_fan_mode( [FAN_AUTO, FAN_LOW, FAN_MEDIUM_LOW, FAN_MEDIUM, FAN_MEDIUM_HIGH, FAN_HIGH], ) async def test_send_fan_mode_device_timeout( - hass: HomeAssistant, discovery, device, mock_now, fan_mode + hass: HomeAssistant, discovery, device, fan_mode ) -> None: """Test for sending fan mode command to the device with a device timeout.""" device().push_state_update.side_effect = DeviceTimeoutError @@ -770,7 +796,7 @@ async def test_send_fan_mode_device_timeout( [FAN_AUTO, FAN_LOW, FAN_MEDIUM_LOW, FAN_MEDIUM, FAN_MEDIUM_HIGH, FAN_HIGH], ) async def test_update_fan_mode( - hass: HomeAssistant, discovery, device, mock_now, fan_mode + hass: HomeAssistant, discovery, device, fan_mode ) -> None: """Test for updating fan mode from the device.""" device().fan_speed = FAN_MODES_REVERSE.get(fan_mode) @@ -786,7 +812,7 @@ async def test_update_fan_mode( "swing_mode", [SWING_OFF, SWING_BOTH, SWING_VERTICAL, SWING_HORIZONTAL] ) async def test_send_swing_mode( - hass: HomeAssistant, discovery, device, mock_now, swing_mode + hass: HomeAssistant, discovery, device, swing_mode ) -> None: """Test for sending swing mode command to the device.""" await async_setup_gree(hass) @@ -803,9 +829,7 @@ async def test_send_swing_mode( assert state.attributes.get(ATTR_SWING_MODE) == swing_mode -async def test_send_invalid_swing_mode( - hass: HomeAssistant, discovery, device, mock_now -) -> None: +async def test_send_invalid_swing_mode(hass: HomeAssistant, discovery, device) -> None: """Test for sending swing mode command to the device.""" await async_setup_gree(hass) @@ -826,7 +850,7 @@ async def test_send_invalid_swing_mode( "swing_mode", [SWING_OFF, SWING_BOTH, SWING_VERTICAL, SWING_HORIZONTAL] ) async def test_send_swing_mode_device_timeout( - hass: HomeAssistant, discovery, device, mock_now, swing_mode + hass: HomeAssistant, discovery, device, swing_mode ) -> None: """Test for sending swing mode command to the device with a device timeout.""" device().push_state_update.side_effect = DeviceTimeoutError @@ -849,7 +873,7 @@ async def test_send_swing_mode_device_timeout( "swing_mode", [SWING_OFF, SWING_BOTH, SWING_VERTICAL, SWING_HORIZONTAL] ) async def test_update_swing_mode( - hass: HomeAssistant, discovery, device, mock_now, swing_mode + hass: HomeAssistant, discovery, device, swing_mode ) -> None: """Test for updating swing mode from the device.""" device().horizontal_swing = ( From ee0c62b1dfea2e04e3c32cefe50b422202922e21 Mon Sep 17 00:00:00 2001 From: Clifford Roche <1007595+cmroche@users.noreply.github.com> Date: Sun, 4 Aug 2024 14:36:01 -0400 Subject: [PATCH 2/7] Improve test_coordinator_update --- tests/components/gree/test_bridge.py | 36 +++++++++++++--------------- 1 file changed, 17 insertions(+), 19 deletions(-) diff --git a/tests/components/gree/test_bridge.py b/tests/components/gree/test_bridge.py index 2891ca9f5229f2..fce5b31526aac4 100644 --- a/tests/components/gree/test_bridge.py +++ b/tests/components/gree/test_bridge.py @@ -1,18 +1,16 @@ """Tests for gree component.""" from datetime import timedelta -from unittest.mock import patch from freezegun.api import FrozenDateTimeFactory import pytest -from homeassistant.components.climate import DOMAIN +from homeassistant.components.climate import DOMAIN, HVACMode from homeassistant.components.gree.const import ( COORDINATORS, DOMAIN as GREE, UPDATE_INTERVAL, ) -from homeassistant.components.gree.coordinator import DeviceDataUpdateCoordinator from homeassistant.core import HomeAssistant import homeassistant.util.dt as dt_util @@ -78,30 +76,30 @@ async def test_discovery_after_setup( async def test_coordinator_updates( - hass: HomeAssistant, freezer: FrozenDateTimeFactory, discovery, device, mock_now + hass: HomeAssistant, freezer: FrozenDateTimeFactory, discovery, device ) -> None: """Test gree devices update their state.""" + await hass.config.async_set_time_zone("UTC") + freezer.move_to(dt_util.utcnow()) + await async_setup_gree(hass) await hass.async_block_till_done() assert len(hass.states.async_all(DOMAIN)) == 1 - coordinator = hass.data[GREE][COORDINATORS][0] + callback = device().add_handler.call_args_list[0][0][1] - def update_device_state(): - """Update the device state.""" - coordinator.device_state_updated(["test"]) + async def fake_update_state(*args): + """Fake update state.""" + device().power = True + callback() - device().update_state.side_effect = update_device_state + device().update_state.side_effect = fake_update_state - next_update = mock_now + timedelta(seconds=UPDATE_INTERVAL) - freezer.move_to(next_update) - - with patch.object( - DeviceDataUpdateCoordinator, "async_set_updated_data" - ) as mock_set_updated_data: - async_fire_time_changed(hass, next_update) - await hass.async_block_till_done() - mock_set_updated_data.assert_called_once_with(device().raw_properties) + freezer.tick(timedelta(seconds=UPDATE_INTERVAL) + timedelta(seconds=1)) + async_fire_time_changed(hass) + await hass.async_block_till_done() - assert coordinator.last_update_success is not None + state = hass.states.get(ENTITY_ID_1) + assert state is not None + assert state.state != HVACMode.OFF From a537084e883b3d5950c9db2c110911efa6586459 Mon Sep 17 00:00:00 2001 From: Clifford Roche <1007595+cmroche@users.noreply.github.com> Date: Sun, 4 Aug 2024 15:30:13 -0400 Subject: [PATCH 3/7] Improve test_coordinator_update --- homeassistant/components/gree/coordinator.py | 2 +- tests/components/gree/test_bridge.py | 8 +++----- 2 files changed, 4 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/gree/coordinator.py b/homeassistant/components/gree/coordinator.py index 3685111f2cd9d9..e0376718f1e99a 100644 --- a/homeassistant/components/gree/coordinator.py +++ b/homeassistant/components/gree/coordinator.py @@ -57,7 +57,7 @@ def device_state_updated(self, *args) -> None: self._last_response_time = utcnow() self.async_set_updated_data(self.device.raw_properties) - async def _async_update_data(self) -> Any: + async def _async_update_data(self) -> dict[str, Any]: """Update the state of the device.""" _LOGGER.debug( "Updating device state: %s, error count: %d", self.name, self._error_count diff --git a/tests/components/gree/test_bridge.py b/tests/components/gree/test_bridge.py index fce5b31526aac4..b125e9408e9f95 100644 --- a/tests/components/gree/test_bridge.py +++ b/tests/components/gree/test_bridge.py @@ -75,13 +75,11 @@ async def test_discovery_after_setup( assert device_infos[1].ip == "2.2.2.1" +@pytest.mark.freeze_time("2023-10-21") async def test_coordinator_updates( hass: HomeAssistant, freezer: FrozenDateTimeFactory, discovery, device ) -> None: """Test gree devices update their state.""" - await hass.config.async_set_time_zone("UTC") - freezer.move_to(dt_util.utcnow()) - await async_setup_gree(hass) await hass.async_block_till_done() @@ -89,14 +87,14 @@ async def test_coordinator_updates( callback = device().add_handler.call_args_list[0][0][1] - async def fake_update_state(*args): + async def fake_update_state(*args) -> None: """Fake update state.""" device().power = True callback() device().update_state.side_effect = fake_update_state - freezer.tick(timedelta(seconds=UPDATE_INTERVAL) + timedelta(seconds=1)) + freezer.tick(timedelta(seconds=UPDATE_INTERVAL)) async_fire_time_changed(hass) await hass.async_block_till_done() From 12ff69749ad3593b0bee5a416313ad15afc64080 Mon Sep 17 00:00:00 2001 From: Clifford Roche <1007595+cmroche@users.noreply.github.com> Date: Sun, 4 Aug 2024 15:34:03 -0400 Subject: [PATCH 4/7] Improve test_coordinator_update --- tests/components/gree/test_bridge.py | 1 - 1 file changed, 1 deletion(-) diff --git a/tests/components/gree/test_bridge.py b/tests/components/gree/test_bridge.py index b125e9408e9f95..32372bebf37f55 100644 --- a/tests/components/gree/test_bridge.py +++ b/tests/components/gree/test_bridge.py @@ -75,7 +75,6 @@ async def test_discovery_after_setup( assert device_infos[1].ip == "2.2.2.1" -@pytest.mark.freeze_time("2023-10-21") async def test_coordinator_updates( hass: HomeAssistant, freezer: FrozenDateTimeFactory, discovery, device ) -> None: From e549c0a1e054fa32e995484f53122e8afda21d8b Mon Sep 17 00:00:00 2001 From: Clifford Roche <1007595+cmroche@users.noreply.github.com> Date: Sun, 4 Aug 2024 15:37:25 -0400 Subject: [PATCH 5/7] Improve device tests --- tests/components/gree/test_climate.py | 17 ----------------- 1 file changed, 17 deletions(-) diff --git a/tests/components/gree/test_climate.py b/tests/components/gree/test_climate.py index 5374cc9627ae70..5e9784008b0864 100644 --- a/tests/components/gree/test_climate.py +++ b/tests/components/gree/test_climate.py @@ -140,8 +140,6 @@ async def test_discovery_after_setup( discovery.return_value.mock_devices = [MockDevice1, MockDevice2] device.side_effect = [MockDevice1, MockDevice2] - await hass.config.async_set_time_zone("UTC") - freezer.move_to("2021-01-09 12:00:00+00:00") await async_setup_gree(hass) # Update 1 assert discovery.return_value.scan_count == 1 @@ -176,8 +174,6 @@ async def test_discovery_add_device_after_setup( discovery.return_value.mock_devices = [MockDevice1] device.side_effect = [MockDevice1] - await hass.config.async_set_time_zone("UTC") - freezer.move_to("2021-01-09 12:00:00+00:00") await async_setup_gree(hass) # Update 1 await async_setup_gree(hass) @@ -211,8 +207,6 @@ async def test_discovery_device_bind_after_setup( discovery.return_value.mock_devices = [MockDevice1] device.return_value = MockDevice1 - await hass.config.async_set_time_zone("UTC") - freezer.move_to("2021-01-09 12:00:00+00:00") await async_setup_gree(hass) # Update 1 assert len(hass.states.async_all(DOMAIN)) == 1 @@ -242,8 +236,6 @@ async def test_update_connection_failure( DeviceTimeoutError, ] - await hass.config.async_set_time_zone("UTC") - freezer.move_to("2021-01-09 12:00:00+00:00") await async_setup_gree(hass) # Update 1 async def run_update(): @@ -278,9 +270,6 @@ async def test_update_connection_send_failure_recovery( DEFAULT_MOCK, ] - await hass.config.async_set_time_zone("UTC") - freezer.move_to("2021-01-09 12:00:00+00:00") - await async_setup_gree(hass) # Update 1 async def run_update(): @@ -306,8 +295,6 @@ async def test_update_unhandled_exception( """Testing update hvac connection unhandled response exception.""" device().update_state.side_effect = [DEFAULT_MOCK, Exception] - await hass.config.async_set_time_zone("UTC") - freezer.move_to("2021-01-09 12:00:00+00:00") await async_setup_gree(hass) state = hass.states.get(ENTITY_ID) @@ -327,8 +314,6 @@ async def test_send_command_device_timeout( hass: HomeAssistant, freezer: FrozenDateTimeFactory, discovery, device ) -> None: """Test for sending power on command to the device with a device timeout.""" - await hass.config.async_set_time_zone("UTC") - freezer.move_to("2021-01-09 12:00:00+00:00") await async_setup_gree(hass) freezer.tick(timedelta(seconds=UPDATE_INTERVAL) + timedelta(seconds=1)) @@ -358,8 +343,6 @@ async def test_unresponsive_device( hass: HomeAssistant, freezer: FrozenDateTimeFactory, discovery, device ) -> None: """Test for unresponsive device.""" - await hass.config.async_set_time_zone("UTC") - freezer.move_to("2021-01-09 12:00:00+00:00") await async_setup_gree(hass) async def run_update(): From 869e612923620e31115568d55548cf76f57f624f Mon Sep 17 00:00:00 2001 From: Clifford Roche <1007595+cmroche@users.noreply.github.com> Date: Sun, 4 Aug 2024 15:38:53 -0400 Subject: [PATCH 6/7] Improve device tests --- tests/components/gree/test_climate.py | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/tests/components/gree/test_climate.py b/tests/components/gree/test_climate.py index 5e9784008b0864..1bf49bbca266d5 100644 --- a/tests/components/gree/test_climate.py +++ b/tests/components/gree/test_climate.py @@ -149,7 +149,7 @@ async def test_discovery_after_setup( discovery.return_value.mock_devices = [MockDevice1, MockDevice2] device.side_effect = [MockDevice1, MockDevice2] - freezer.tick(timedelta(seconds=DISCOVERY_SCAN_INTERVAL) + timedelta(minutes=1)) + freezer.tick(timedelta(seconds=DISCOVERY_SCAN_INTERVAL)) async_fire_time_changed(hass) await hass.async_block_till_done() @@ -186,7 +186,7 @@ async def test_discovery_add_device_after_setup( discovery.return_value.mock_devices = [MockDevice2] device.side_effect = [MockDevice2] - freezer.tick(timedelta(seconds=DISCOVERY_SCAN_INTERVAL) + timedelta(minutes=1)) + freezer.tick(timedelta(seconds=DISCOVERY_SCAN_INTERVAL)) async_fire_time_changed(hass) await hass.async_block_till_done() @@ -218,7 +218,7 @@ async def test_discovery_device_bind_after_setup( MockDevice1.bind.side_effect = None MockDevice1.update_state.side_effect = None - freezer.tick(timedelta(seconds=DISCOVERY_SCAN_INTERVAL) + timedelta(minutes=1)) + freezer.tick(timedelta(seconds=DISCOVERY_SCAN_INTERVAL)) async_fire_time_changed(hass) await hass.async_block_till_done() @@ -239,7 +239,7 @@ async def test_update_connection_failure( await async_setup_gree(hass) # Update 1 async def run_update(): - freezer.tick(timedelta(seconds=UPDATE_INTERVAL) + timedelta(seconds=1)) + freezer.tick(timedelta(seconds=UPDATE_INTERVAL)) async_fire_time_changed(hass) await hass.async_block_till_done() @@ -273,7 +273,7 @@ async def test_update_connection_send_failure_recovery( await async_setup_gree(hass) # Update 1 async def run_update(): - freezer.tick(timedelta(seconds=UPDATE_INTERVAL) + timedelta(seconds=1)) + freezer.tick(timedelta(seconds=UPDATE_INTERVAL)) async_fire_time_changed(hass) await hass.async_block_till_done() @@ -301,7 +301,7 @@ async def test_update_unhandled_exception( assert state.name == "fake-device-1" assert state.state != STATE_UNAVAILABLE - freezer.tick(timedelta(seconds=UPDATE_INTERVAL) + timedelta(seconds=1)) + freezer.tick(timedelta(seconds=UPDATE_INTERVAL)) async_fire_time_changed(hass) await hass.async_block_till_done() @@ -316,7 +316,7 @@ async def test_send_command_device_timeout( """Test for sending power on command to the device with a device timeout.""" await async_setup_gree(hass) - freezer.tick(timedelta(seconds=UPDATE_INTERVAL) + timedelta(seconds=1)) + freezer.tick(timedelta(seconds=UPDATE_INTERVAL)) async_fire_time_changed(hass) await hass.async_block_till_done() @@ -346,7 +346,7 @@ async def test_unresponsive_device( await async_setup_gree(hass) async def run_update(): - freezer.tick(timedelta(seconds=UPDATE_INTERVAL) + timedelta(seconds=1)) + freezer.tick(timedelta(seconds=UPDATE_INTERVAL)) async_fire_time_changed(hass) await hass.async_block_till_done() From dbdb5225b07e788c67ed2df4b5ac10a0f341f6a5 Mon Sep 17 00:00:00 2001 From: Joostlek Date: Mon, 5 Aug 2024 10:55:29 +0200 Subject: [PATCH 7/7] Fix typing --- homeassistant/components/gree/coordinator.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/gree/coordinator.py b/homeassistant/components/gree/coordinator.py index e0376718f1e99a..ae8b22706ef43c 100644 --- a/homeassistant/components/gree/coordinator.py +++ b/homeassistant/components/gree/coordinator.py @@ -29,7 +29,7 @@ _LOGGER = logging.getLogger(__name__) -class DeviceDataUpdateCoordinator(DataUpdateCoordinator): +class DeviceDataUpdateCoordinator(DataUpdateCoordinator[dict[str, Any]]): """Manages polling for state changes from the device.""" def __init__(self, hass: HomeAssistant, device: Device) -> None: @@ -50,7 +50,7 @@ def __init__(self, hass: HomeAssistant, device: Device) -> None: self._last_response_time: datetime = utcnow() self._last_error_time: datetime | None = None - def device_state_updated(self, *args) -> None: + def device_state_updated(self, *args: Any) -> None: """Handle device state updates.""" _LOGGER.debug("Device state updated: %s", json_dumps(args)) self._error_count = 0