Skip to content

Enhancement: Implement retry logic in _send to handle sporadic Wi-Fi packet loss #1

@faanskit

Description

@faanskit

Describe the issue

I am experiencing frequent intermittent "unavailable" states with this integration in Home Assistant. After diagnosing my local network, I noticed that my heat pump has a slightly unstable Wi-Fi connection with occasional packet loss (dropping a ping here and there).

Since the integration communicates via UDP, any single dropped packet during an update cycle causes _send() to immediately raise a TimeoutError, which subsequently breaks the Home Assistant state update.

Analysis of client.py

In the current implementation of PoolComfortClient._send, the thread blocks on slot.event.wait(self.timeout). If the heat pump doesn't reply within the timeout window, a TimeoutError is raised immediately:

if not slot.event.wait(self.timeout):
    raise TimeoutError("no response from heat pump")

There is no mechanism to retry the transmission if a packet is lost in transit.

Proposed Solution

I suggest wrapping the inner logic of _send() in a retry loop (e.g., maximum 3 attempts) with a brief delay (time.sleep) between attempts. If a packet is dropped due to weak Wi-Fi signal, the client will automatically re-transmit the same packet (keeping the same sequence number).

This would greatly improve reliability for users with outdoor or distant Wi-Fi installations without impacting users with strong connections.

Code proposal from Gemini 3.5

def _send(self, packet: Packet) -> Packet:
        if self._sock is None:
            raise RuntimeError("client is not connected")
        
        key = (packet.sequence, packet.message_type)
        max_retries = 3
        retry_delay = 0.3
        
        for attempt in range(1, max_retries + 1):
            slot = _Pending(event=threading.Event())
            with self._pending_lock:
                self._pending[key] = slot
            
            try:
                with self._send_lock:
                    if self._sock is None:
                        raise RuntimeError("client is not connected")
                    self._sock.sendto(packet.build(), (self.host, CONTROL_PORT))
                    self._last_send = time.monotonic()
                
                # Vänta på svar från lästråden
                if slot.event.wait(self.timeout):
                    if slot.reply is None:
                        raise RuntimeError("client closed during send")
                    return slot.reply
                
                # Om vi hamnar här har slot.event.wait() nått timeout (returnerat False)
                if attempt < max_retries:
                    import logging
                    logger = logging.getLogger(__name__)
                    logger.warning(
                        "Pool communication timeout. Retrying packet (seq: %s, type: %s). Attempt %d/%d",
                        packet.sequence, packet.message_type, attempt, max_retries
                    )
                    time.sleep(retry_delay)
                else:
                    raise TimeoutError("no response from heat pump after maximum retries")
                    
            finally:
                with self._pending_lock:
                    self._pending.pop(key, None)

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions