Skip to content

Commit

Permalink
Update greeclimate to 2.0.0 (#121030)
Browse files Browse the repository at this point in the history
Co-authored-by: Joostlek <[email protected]>
  • Loading branch information
2 people authored and frenck committed Aug 5, 2024
1 parent fe82e7f commit 7b1bf82
Show file tree
Hide file tree
Showing 7 changed files with 188 additions and 103 deletions.
2 changes: 2 additions & 0 deletions homeassistant/components/gree/const.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,3 +18,5 @@
MAX_ERRORS = 2

TARGET_TEMPERATURE_STEP = 1

UPDATE_INTERVAL = 60
61 changes: 53 additions & 8 deletions homeassistant/components/gree/coordinator.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,29 +2,34 @@

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,
DISCOVERY_TIMEOUT,
DISPATCH_DEVICE_DISCOVERED,
DOMAIN,
MAX_ERRORS,
UPDATE_INTERVAL,
)

_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:
Expand All @@ -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: Any) -> 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) -> dict[str, 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."""
Expand Down
2 changes: 1 addition & 1 deletion homeassistant/components/gree/manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"]
}
2 changes: 1 addition & 1 deletion requirements_all.txt
Original file line number Diff line number Diff line change
Expand Up @@ -1007,7 +1007,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
Expand Down
2 changes: 1 addition & 1 deletion requirements_test_all.txt
Original file line number Diff line number Diff line change
Expand Up @@ -848,7 +848,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
Expand Down
35 changes: 33 additions & 2 deletions tests/components/gree/test_bridge.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,12 @@
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.climate import DOMAIN, HVACMode
from homeassistant.components.gree.const import (
COORDINATORS,
DOMAIN as GREE,
UPDATE_INTERVAL,
)
from homeassistant.core import HomeAssistant
import homeassistant.util.dt as dt_util

Expand Down Expand Up @@ -69,3 +73,30 @@ 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
) -> 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

callback = device().add_handler.call_args_list[0][0][1]

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))
async_fire_time_changed(hass)
await hass.async_block_till_done()

state = hass.states.get(ENTITY_ID_1)
assert state is not None
assert state.state != HVACMode.OFF
Loading

0 comments on commit 7b1bf82

Please sign in to comment.