Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 7 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,16 @@

# HomeAssistant-GreeClimateComponent

Custom Gree integration for Home Assistant written in Python 3. Controls ACs supporting the Gree UDP protocol.
Custom Gree integration for Home Assistant written in Python 3.

This integration connects directly to your HVAC devices via their IP address on the local network, unlike the official mobile app, which establishes a direct connection only during initial setup and subsequently operates through Gree’s servers.

> [!NOTE]
> This integration only supports the Gree UDP protocol. If you have a newer firmware/device that only communicates using the new MQTT protocol, this integration will not work.
> Use [this](https://github.com/davo22/homeassistant-gree-cloud) for a different feature set and support for Gree Cloud
**This integration only supports the Gree UDP protocol. If you have a newer firmware/device that only communicates using the new MQTT protocol, this integration will not work.**

> [!IMPORTANT]
> Due to the many issues being created revolving "TimeOut"/"Cannot connect" errors, I will be closing these. Feel free to make a PR fixing your TimeOut/Cannot connect error.
>
> More information on the "why" can be found here: https://github.com/RobHofmann/HomeAssistant-GreeClimateComponent/issues/405#issuecomment-4300110823

For a comprehensive list of tested devices, see [Supported Devices](supported-devices.md).

Expand Down
4 changes: 2 additions & 2 deletions custom_components/gree_custom/aiogree/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -515,7 +515,7 @@ async def gree_set_status(

async def gree_get_device_info(
transport: GreeTransport, cipher: CipherBase | None = None
) -> dict[str, str | dict | None]:
) -> dict[str, str | None]:
"""Tries to retrive the device info."""

data: dict = await get_result_pack(
Expand All @@ -526,7 +526,7 @@ async def gree_get_device_info(

_LOGGER.debug("Got device info: %s", data)

info: dict[str, str | dict | None] = {}
info: dict[str, str | None] = {}
info["raw"] = data
info["firmware_version"], info["firmware_code"] = extract_version(data)
info["mac"] = data.get("mac", "")
Expand Down
8 changes: 4 additions & 4 deletions custom_components/gree_custom/aiogree/device.py
Original file line number Diff line number Diff line change
Expand Up @@ -151,7 +151,7 @@ async def bind_device(self) -> bool:

return True

async def fetch_device_info(self, cipher: CipherBase | None = None):
async def fetch_device_info(self, cipher: CipherBase = None):
"""Updates the device info fields."""
try:
self._raw_info = await gree_get_device_info(
Expand Down Expand Up @@ -324,7 +324,7 @@ def _set_device_status(self, props: dict[GreeProp, int]) -> None:
def _bool_from_raw_state(self, prop: GreeProp, default: int = 0) -> bool:
prop_value: int | None = self._get_prop_raw(prop, default)

return bool(prop_value)
return None if prop_value is None else bool(prop_value)

def _remove_unsupported_props(self):
"""Remove unsupported properties from the list to update."""
Expand Down Expand Up @@ -510,9 +510,9 @@ def is_bound(self) -> bool:
return self._is_bound

@property
def has_hvac_error(self) -> bool:
def has_hvac_error(self) -> bool | None:
"""Return if there is an error with the device."""
return self._bool_from_raw_state(GreeProp.FAULT)
return self._bool_from_raw_state(GreeProp.FAULT, None)

@property
def beeper(self) -> bool:
Expand Down
93 changes: 55 additions & 38 deletions custom_components/gree_custom/aiogree/transport.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,27 @@ def __init__(
self.max_retries = max_retries
self.timeout = timeout

self._stream: asyncio_dgram.DatagramClient | None = None
self._lock: asyncio.Lock = asyncio.Lock()

async def _get_stream(self) -> asyncio_dgram.DatagramClient:
"""Create stream once and reuse it while possible."""
if self._stream is None:
_LOGGER.debug("Creating stream for %s", self.ip_addr)
self._stream = await asyncio_dgram.connect((self.ip_addr, self.port))

return self._stream

async def _reset_stream(self) -> None:
"""Safely reset UDP stream if it gets into a bad state."""
_LOGGER.debug("Reseting stream for %s", self.ip_addr)
if self._stream is not None:
try:
self._stream.close()
except Exception:
_LOGGER.exception("Could not close stream")
self._stream = None

async def udp_request(
self,
data: bytes,
Expand All @@ -31,49 +52,45 @@ async def udp_request(

last_error: Exception | None = None

for attempt in range(self.max_retries):
stream: asyncio_dgram.DatagramClient | None = None

try:
stream = await asyncio_dgram.connect((self.ip_addr, self.port))

await stream.send(data)

recv_task = asyncio.create_task(stream.recv())
async with self._lock: # prevents concurrent recv/send corruption
for attempt in range(self.max_retries):
stream: asyncio_dgram.DatagramClient | None = None

try:
received_data, _ = await asyncio.wait_for(recv_task, self.timeout)
except TimeoutError:
recv_task.cancel()
raise
stream = await self._get_stream()

await stream.send(data)

received_data, _ = await asyncio.wait_for(
stream.recv(), timeout=self.timeout
)

except TimeoutError as err:
last_error = err
_LOGGER.warning(
"Error communicating with %s. Attempt %d/%d",
self.ip_addr,
attempt + 1,
self.max_retries,
)
await self._reset_stream()

except Exception as err: # noqa: BLE001
last_error = err
_LOGGER.warning(
"Error communicating with %s. Attempt %d/%d",
self.ip_addr,
attempt + 1,
self.max_retries,
)
await self._reset_stream()

else:
return received_data

except Exception as err1: # noqa: BLE001
_LOGGER.warning(
"Error communicating with %s. Attempt %d/%d",
self.ip_addr,
attempt + 1,
self.max_retries,
)
last_error = err1

finally:
if stream:
try:
stream.close()
except Exception as err2: # noqa: BLE001
_LOGGER.warning(
"Error communicating with %s. Attempt %d/%d",
self.ip_addr,
attempt + 1,
self.max_retries,
)
last_error = err2

# Apply backoff before retrying
if attempt < self.max_retries - 1:
await asyncio.sleep(0.5 + attempt * 0.3) # 0.5s, 0.8s, 1.1s, ...
# Apply backoff before retrying
if attempt < self.max_retries - 1:
await asyncio.sleep(0.5 + attempt * 0.3) # 0.5s, 0.8s, 1.1s, ...

raise GreeConnectionError(
f"Failed to communicate with device '{self.ip_addr}:{self.port}' after {self.max_retries} attempts"
Expand Down
4 changes: 2 additions & 2 deletions custom_components/gree_custom/const.py
Original file line number Diff line number Diff line change
Expand Up @@ -40,12 +40,12 @@
DEFAULT_DISABLE_AVAILABLE_CHECK = False
DEFAULT_RESTORE_STATES = True
MIN_SCAN_INTERVAL = 5
DEFAULT_SCAN_INTERVAL = 30
DEFAULT_SCAN_INTERVAL = 60

DEFAULT_DEVICE_UID = 0
DEFAULT_DEVICE_PORT = 7000
DEFAULT_CONNECTION_MAX_ATTEMPTS = 3
DEFAULT_CONNECTION_TIMEOUT = 5
DEFAULT_CONNECTION_TIMEOUT = 10
DEFAULT_DISCOVERY_TIMEOUT = 5

MAX_UNICAST_SCAN_HOSTS = 65536
Expand Down
2 changes: 1 addition & 1 deletion manual-configuration.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -83,7 +83,7 @@ gree_custom:
external_temperature_sensor: "None" # Sets a given temperature sensor as the sensor for the AC | str (Entity ID) | default = "None"
external_humidity_sensor: "None" # Sets a given humidity sensor as the sensor for the AC | str (Entity ID) | default = "None"
restore_states: true # Wether to restore the last HA state to device when HA starts | bool | default = true
scan_interval: 30 # Device polling rate | int > 5 | default = 30
scan_interval: 60 # Device polling rate | int > 5 | default = 60


# Example for multiple AC units:
Expand Down
2 changes: 1 addition & 1 deletion supported-devices.md
Original file line number Diff line number Diff line change
Expand Up @@ -82,4 +82,4 @@ This helps other users find compatible devices and improves the integration's do
- **Encryption Version 1**: Older devices, typically uses ECB encryption
- **Encryption Version 2**: Newer devices, typically uses GCM encryption
- Most devices require encryption version 2, but some older models use encryption version 1
- If you're unsure, try encryption version 2 first, then fall back to encryption version 1 if connection fails
- If you're unsure, try encryption version 2 first, then fall back to encryption version 1 if connection fails
Loading