Skip to content

Commit

Permalink
Update to greeclimate 2.0.0
Browse files Browse the repository at this point in the history
  • Loading branch information
cmroche committed Jul 13, 2024
1 parent c044417 commit c039057
Show file tree
Hide file tree
Showing 7 changed files with 208 additions and 101 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
59 changes: 52 additions & 7 deletions homeassistant/components/gree/coordinator.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,23 +2,28 @@

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__)
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) -> 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."""
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 @@ -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
Expand Down
2 changes: 1 addition & 1 deletion requirements_test_all.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
38 changes: 37 additions & 1 deletion tests/components/gree/test_bridge.py
Original file line number Diff line number Diff line change
@@ -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

Expand Down Expand Up @@ -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
Loading

0 comments on commit c039057

Please sign in to comment.