Skip to content

Commit 6a8c93f

Browse files
authored
Merge pull request #4 from rderewianko/fix/read-timeout-retries
Retry HTTP timeouts and keep last state on transient kettle unresponsiveness
2 parents 9a0baec + f09a74c commit 6a8c93f

2 files changed

Lines changed: 48 additions & 9 deletions

File tree

custom_components/fellow/__init__.py

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
1111

1212
from .const import DOMAIN
13-
from .kettle import StaggEKGClient
13+
from .kettle import KettleTimeoutError, StaggEKGClient
1414

1515
_LOGGER = logging.getLogger(__name__)
1616

@@ -82,5 +82,13 @@ async def _async_update_data(self):
8282
return {
8383
"state": state,
8484
}
85+
except KettleTimeoutError as err:
86+
# The kettle stops answering HTTP for a few seconds when it
87+
# transitions to Off. Keep the last known state instead of
88+
# surfacing an error every time that happens.
89+
if self.data is not None:
90+
_LOGGER.debug("Kettle unresponsive, keeping last state: %s", err)
91+
return self.data
92+
raise UpdateFailed(f"Kettle unresponsive: {err}")
8593
except Exception as err:
8694
raise UpdateFailed(f"Error communicating with API: {err}")

custom_components/fellow/kettle.py

Lines changed: 39 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,15 @@
1919
GUIDE_PRESETS_F = (180, 195, 200, 205, 212)
2020

2121

22+
class KettleTimeoutError(Exception):
23+
"""Raised when the kettle's HTTP server doesn't respond in time.
24+
25+
The kettle briefly stops answering HTTP requests when it transitions
26+
to Off (and occasionally at other points), so callers can treat this
27+
as a transient condition rather than a hard failure.
28+
"""
29+
30+
2231
@dataclass
2332
class KettleState:
2433
"""Represents the current state of the kettle"""
@@ -89,17 +98,39 @@ def __init__(self, host: str = "10.1.1.177", port: int = 80):
8998
self.session = requests.Session()
9099
self.session.headers.update({'User-Agent': 'StaggEKG-HA/1.0'})
91100

92-
def _send_command(self, cmd: str) -> str:
93-
"""Send a CLI command to the kettle"""
101+
def _send_command(self, cmd: str, retries: int = 2) -> str:
102+
"""Send a CLI command to the kettle, retrying on timeout.
103+
104+
The kettle's HTTP server goes unresponsive for several seconds
105+
during mode transitions (especially when turning off), so a single
106+
read timeout is not a reliable signal that anything is wrong.
107+
Retries swallow that window; if the kettle is still unresponsive
108+
after the final attempt, raise KettleTimeoutError so callers can
109+
distinguish it from other request failures.
110+
"""
94111
url = f"{self.base_url}/cli"
95112
params = {"cmd": cmd}
96113

97-
try:
98-
response = self.session.get(url, params=params, timeout=10)
99-
response.raise_for_status()
100-
return response.text
101-
except requests.exceptions.RequestException as e:
102-
raise Exception(f"Failed to send command '{cmd}': {e}")
114+
last_timeout: Optional[Exception] = None
115+
for attempt in range(retries + 1):
116+
try:
117+
response = self.session.get(url, params=params, timeout=10)
118+
response.raise_for_status()
119+
return response.text
120+
except requests.exceptions.Timeout as e:
121+
last_timeout = e
122+
if attempt < retries:
123+
_LOGGER.debug(
124+
"Kettle timeout on '%s' (attempt %d/%d), retrying",
125+
cmd, attempt + 1, retries + 1,
126+
)
127+
time.sleep(0.5)
128+
except requests.exceptions.RequestException as e:
129+
raise Exception(f"Failed to send command '{cmd}': {e}")
130+
131+
raise KettleTimeoutError(
132+
f"Kettle did not respond to '{cmd}' after {retries + 1} attempts: {last_timeout}"
133+
)
103134

104135
def get_state(self) -> KettleState:
105136
"""Get the current state of the kettle"""

0 commit comments

Comments
 (0)