Skip to content

Commit edd38fe

Browse files
authored
Merge pull request #39 from Shocker/implement-retries
2 parents bc1d975 + 62d48cf commit edd38fe

10 files changed

Lines changed: 149 additions & 56 deletions

File tree

custom_components/http_agent/config_flow.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
CONF_INTERVAL,
2020
CONF_METHOD,
2121
CONF_PAYLOAD,
22+
CONF_RETRIES,
2223
CONF_SENSOR_COLOR,
2324
CONF_SENSOR_DEVICE_CLASS,
2425
CONF_SENSOR_ICON,
@@ -37,6 +38,7 @@
3738
CONTENT_TYPES,
3839
DEFAULT_INTERVAL,
3940
DEFAULT_METHOD,
41+
DEFAULT_RETRIES,
4042
DEFAULT_TIMEOUT,
4143
DEFAULT_VERIFY_SSL,
4244
DOMAIN,
@@ -88,6 +90,9 @@ async def async_step_user(
8890
vol.Optional(CONF_TIMEOUT, default=DEFAULT_TIMEOUT): vol.All(
8991
vol.Coerce(int), vol.Range(min=1, max=300)
9092
),
93+
vol.Required(CONF_RETRIES, default=DEFAULT_RETRIES): vol.All(
94+
vol.Coerce(int), vol.Range(min=0, max=10)
95+
),
9196
vol.Optional(CONF_INTERVAL, default=DEFAULT_INTERVAL): vol.All(
9297
vol.Coerce(int), vol.Range(min=5, max=86400)
9398
),
@@ -430,6 +435,10 @@ async def async_step_basic(
430435
vol.Optional(
431436
CONF_TIMEOUT, default=self.data.get(CONF_TIMEOUT, DEFAULT_TIMEOUT)
432437
): vol.All(vol.Coerce(int), vol.Range(min=1, max=300)),
438+
vol.Required(
439+
CONF_RETRIES,
440+
default=self.data.get(CONF_RETRIES, DEFAULT_RETRIES),
441+
): vol.All(vol.Coerce(int), vol.Range(min=0, max=10)),
433442
vol.Optional(
434443
CONF_INTERVAL,
435444
default=self.data.get(CONF_INTERVAL, DEFAULT_INTERVAL),

custom_components/http_agent/const.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66

77
# Default values
88
DEFAULT_TIMEOUT = 10
9+
DEFAULT_RETRIES = 0
910
DEFAULT_INTERVAL = 60
1011
DEFAULT_METHOD = "GET"
1112
DEFAULT_VERIFY_SSL = True
@@ -14,6 +15,7 @@
1415
CONF_URL = "url"
1516
CONF_METHOD = "method"
1617
CONF_TIMEOUT = "timeout"
18+
CONF_RETRIES = "retries"
1719
CONF_INTERVAL = "interval"
1820
CONF_VERIFY_SSL = "verify_ssl"
1921
CONF_HEADERS = "headers"

custom_components/http_agent/coordinator.py

Lines changed: 124 additions & 56 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
CONF_INTERVAL,
2424
CONF_METHOD,
2525
CONF_PAYLOAD,
26+
CONF_RETRIES,
2627
CONF_SENSOR_COLOR,
2728
CONF_SENSOR_DEVICE_CLASS,
2829
CONF_SENSOR_ICON,
@@ -39,6 +40,7 @@
3940
CONF_URL,
4041
CONF_VERIFY_SSL,
4142
DEFAULT_INTERVAL,
43+
DEFAULT_RETRIES,
4244
DEFAULT_TIMEOUT,
4345
DEFAULT_VERIFY_SSL,
4446
DOMAIN,
@@ -87,6 +89,7 @@ def __init__(self, hass: HomeAssistant, entry_data: dict) -> None:
8789
self.url = entry_data[CONF_URL]
8890
self.method = entry_data[CONF_METHOD]
8991
self.timeout = entry_data.get(CONF_TIMEOUT, DEFAULT_TIMEOUT)
92+
self.retries = entry_data.get(CONF_RETRIES, DEFAULT_RETRIES)
9093
self.verify_ssl = entry_data.get(CONF_VERIFY_SSL, DEFAULT_VERIFY_SSL)
9194
self.headers = {h["key"]: h["value"] for h in entry_data.get(CONF_HEADERS, [])}
9295
self.payload = entry_data.get(CONF_PAYLOAD, "")
@@ -144,75 +147,140 @@ async def _async_update_data(self) -> dict[str, Any]:
144147
kwargs["data"] = rendered_payload
145148
else:
146149
kwargs["data"] = rendered_payload
150+
total_attempts = max(1, int(self.retries) + 1)
147151

148-
async with self.session.request(self.method, **kwargs) as response:
149-
response_text = await response.text()
152+
for attempt in range(1, total_attempts + 1):
153+
try:
154+
async with self.session.request(self.method, **kwargs) as response:
155+
response_text = await response.text()
156+
157+
# Retry on empty response or non-2xx status
158+
if (
159+
not response_text
160+
or response.status < 200
161+
or response.status >= 300
162+
):
163+
error_msg = f"HTTP {response.status}"
164+
if not response_text:
165+
error_msg = "Empty response"
166+
167+
_LOGGER.debug(
168+
"HTTP request attempt %s/%s failed for %s: %s",
169+
attempt,
170+
total_attempts,
171+
rendered_url,
172+
error_msg,
173+
)
174+
175+
if attempt == total_attempts:
176+
raise UpdateFailed(
177+
f"Failed to fetch data after {attempt} attempts: {error_msg}"
178+
)
179+
180+
# wait 1s before next attempt
181+
await asyncio.sleep(1)
182+
183+
continue
150184

151-
_LOGGER.debug(
152-
"HTTP request to %s returned status %s",
153-
rendered_url,
154-
response.status,
155-
)
185+
_LOGGER.debug(
186+
"HTTP request to %s returned status %s",
187+
rendered_url,
188+
response.status,
189+
)
156190

157-
# Create custom response object for templates
158-
http_response = HTTPResponse(
159-
text=response_text,
160-
status=response.status,
161-
headers=dict(response.headers),
162-
)
191+
# Create custom response object for templates
192+
http_response = HTTPResponse(
193+
text=response_text,
194+
status=response.status,
195+
headers=dict(response.headers),
196+
)
163197

164-
# Extract sensor data
165-
sensor_data = {}
166-
for sensor_config in self.sensors_config:
167-
sensor_name = sensor_config[CONF_SENSOR_NAME]
168-
sensor_type = sensor_config.get(CONF_SENSOR_TYPE, "sensor")
169-
170-
# Base sensor values
171-
sensor_values = {
172-
"type": sensor_type,
173-
"state": self._extract_value_auto(
174-
http_response, sensor_config.get(CONF_SENSOR_STATE, "")
175-
),
176-
"icon": self._extract_value_auto(
177-
http_response, sensor_config.get(CONF_SENSOR_ICON, "")
178-
),
179-
"color": self._extract_value_auto(
180-
http_response, sensor_config.get(CONF_SENSOR_COLOR, "")
181-
),
182-
"device_class": sensor_config.get(CONF_SENSOR_DEVICE_CLASS, ""),
183-
"unit": sensor_config.get(CONF_SENSOR_UNIT, ""),
184-
}
185-
186-
# Add device tracker specific data
187-
if sensor_type == "device_tracker":
188-
sensor_values.update(
189-
{
190-
"latitude": self._extract_value_auto(
198+
# Extract sensor data
199+
sensor_data = {}
200+
for sensor_config in self.sensors_config:
201+
sensor_name = sensor_config[CONF_SENSOR_NAME]
202+
sensor_type = sensor_config.get(CONF_SENSOR_TYPE, "sensor")
203+
204+
# Base sensor values
205+
sensor_values = {
206+
"type": sensor_type,
207+
"state": self._extract_value_auto(
191208
http_response,
192-
sensor_config.get(CONF_TRACKER_LATITUDE, ""),
209+
sensor_config.get(CONF_SENSOR_STATE, ""),
193210
),
194-
"longitude": self._extract_value_auto(
211+
"icon": self._extract_value_auto(
195212
http_response,
196-
sensor_config.get(CONF_TRACKER_LONGITUDE, ""),
213+
sensor_config.get(CONF_SENSOR_ICON, ""),
197214
),
198-
"location_name": self._extract_value_auto(
215+
"color": self._extract_value_auto(
199216
http_response,
200-
sensor_config.get(CONF_TRACKER_LOCATION_NAME, ""),
217+
sensor_config.get(CONF_SENSOR_COLOR, ""),
201218
),
202-
"source_type": sensor_config.get(
203-
CONF_TRACKER_SOURCE_TYPE, "gps"
219+
"device_class": sensor_config.get(
220+
CONF_SENSOR_DEVICE_CLASS, ""
204221
),
222+
"unit": sensor_config.get(CONF_SENSOR_UNIT, ""),
205223
}
206-
)
207-
208-
sensor_data[sensor_name] = sensor_values
209224

210-
return sensor_data
211-
212-
except asyncio.TimeoutError as err:
213-
raise UpdateFailed(f"Timeout while fetching data: {err}") from err
214-
except aiohttp.ClientError as err:
215-
raise UpdateFailed(f"Error fetching data: {err}") from err
225+
# Add device tracker specific data
226+
if sensor_type == "device_tracker":
227+
sensor_values.update(
228+
{
229+
"latitude": self._extract_value_auto(
230+
http_response,
231+
sensor_config.get(
232+
CONF_TRACKER_LATITUDE, ""
233+
),
234+
),
235+
"longitude": self._extract_value_auto(
236+
http_response,
237+
sensor_config.get(
238+
CONF_TRACKER_LONGITUDE, ""
239+
),
240+
),
241+
"location_name": self._extract_value_auto(
242+
http_response,
243+
sensor_config.get(
244+
CONF_TRACKER_LOCATION_NAME, ""
245+
),
246+
),
247+
"source_type": sensor_config.get(
248+
CONF_TRACKER_SOURCE_TYPE, "gps"
249+
),
250+
}
251+
)
252+
253+
sensor_data[sensor_name] = sensor_values
254+
255+
return sensor_data
256+
257+
except (asyncio.TimeoutError, aiohttp.ClientError) as err:
258+
err_detail = str(err) or type(err).__name__
259+
_LOGGER.debug(
260+
"HTTP request attempt %s/%s failed for %s: %s",
261+
attempt,
262+
total_attempts,
263+
rendered_url,
264+
err_detail,
265+
)
266+
if attempt == total_attempts:
267+
if isinstance(err, asyncio.TimeoutError):
268+
raise UpdateFailed(
269+
f"Timeout while fetching data after {attempt} attempts"
270+
) from err
271+
raise UpdateFailed(
272+
f"Error fetching data after {attempt} attempts: {err_detail}"
273+
) from err
274+
275+
# wait 1s before next attempt if this was not a timeout
276+
if not isinstance(err, asyncio.TimeoutError):
277+
await asyncio.sleep(1)
278+
279+
# Prevent linter errors. Should never reach here - loop always returns or raises
280+
raise UpdateFailed("Failed to fetch data")
281+
282+
except UpdateFailed:
283+
raise
216284
except Exception as err:
217285
raise UpdateFailed(f"Unexpected error: {err}") from err
218286

custom_components/http_agent/strings.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
"method": "HTTP Method",
1010
"timeout": "Timeout (seconds)",
1111
"interval": "Update Interval (seconds)",
12+
"retries": "Retries on error",
1213
"verify_ssl": "Verify SSL Certificate"
1314
}
1415
},
@@ -101,6 +102,7 @@
101102
"method": "HTTP Method",
102103
"timeout": "Timeout (seconds)",
103104
"interval": "Update Interval (seconds)",
105+
"retries": "Retries on error",
104106
"verify_ssl": "Verify SSL Certificate"
105107
}
106108
},

custom_components/http_agent/translations/da.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
"method": "HTTP-metode",
1010
"timeout": "Timeout (sekunder)",
1111
"interval": "Opdateringsinterval (sekunder)",
12+
"retries": "Genforsøg ved fejl",
1213
"verify_ssl": "Verificer SSL-certifikat"
1314
}
1415
},
@@ -101,6 +102,7 @@
101102
"method": "HTTP-metode",
102103
"timeout": "Timeout (sekunder)",
103104
"interval": "Opdateringsinterval (sekunder)",
105+
"retries": "Genforsøg ved fejl",
104106
"verify_ssl": "Verificer SSL-certifikat"
105107
}
106108
},

custom_components/http_agent/translations/de.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
"method": "HTTP-Methode",
1010
"timeout": "Zeitüberschreitung (Sekunden)",
1111
"interval": "Aktualisierungsintervall (Sekunden)",
12+
"retries": "Wiederholungsversuche bei Fehlern",
1213
"verify_ssl": "SSL-Zertifikat verifizieren"
1314
}
1415
},
@@ -101,6 +102,7 @@
101102
"method": "HTTP-Methode",
102103
"timeout": "Zeitüberschreitung (Sekunden)",
103104
"interval": "Aktualisierungsintervall (Sekunden)",
105+
"retries": "Wiederholungsversuche bei Fehlern",
104106
"verify_ssl": "SSL-Zertifikat verifizieren"
105107
}
106108
},

custom_components/http_agent/translations/en.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
"method": "HTTP Method",
1010
"timeout": "Timeout (seconds)",
1111
"interval": "Update Interval (seconds)",
12+
"retries": "Retries on error",
1213
"verify_ssl": "Verify SSL Certificate"
1314
}
1415
},
@@ -101,6 +102,7 @@
101102
"method": "HTTP Method",
102103
"timeout": "Timeout (seconds)",
103104
"interval": "Update Interval (seconds)",
105+
"retries": "Retries on error",
104106
"verify_ssl": "Verify SSL Certificate"
105107
}
106108
},

custom_components/http_agent/translations/fi.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
"method": "HTTP-metodi",
1010
"timeout": "Aikakatkaisu (sekuntia)",
1111
"interval": "Päivitysväli (sekuntia)",
12+
"retries": "Uudelleenyritykset virheen sattuessa",
1213
"verify_ssl": "Vahvista SSL-varmenne"
1314
}
1415
},
@@ -101,6 +102,7 @@
101102
"method": "HTTP-metodi",
102103
"timeout": "Aikakatkaisu (sekuntia)",
103104
"interval": "Päivitysväli (sekuntia)",
105+
"retries": "Uudelleenyritykset virheen sattuessa",
104106
"verify_ssl": "Vahvista SSL-varmenne"
105107
}
106108
},

custom_components/http_agent/translations/no.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
"method": "HTTP-metode",
1010
"timeout": "Tidsavbrudd (sekunder)",
1111
"interval": "Oppdateringsintervall (sekunder)",
12+
"retries": "Forsøk på nytt ved feil",
1213
"verify_ssl": "Verifiser SSL-sertifikat"
1314
}
1415
},
@@ -101,6 +102,7 @@
101102
"method": "HTTP-metode",
102103
"timeout": "Tidsavbrudd (sekunder)",
103104
"interval": "Oppdateringsintervall (sekunder)",
105+
"retries": "Forsøk på nytt ved feil",
104106
"verify_ssl": "Verifiser SSL-sertifikat"
105107
}
106108
},

custom_components/http_agent/translations/sv.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
"method": "HTTP-metod",
1010
"timeout": "Timeout (sekunder)",
1111
"interval": "Uppdateringsintervall (sekunder)",
12+
"retries": "Försök igen vid fel",
1213
"verify_ssl": "Verifiera SSL-certifikat"
1314
}
1415
},
@@ -101,6 +102,7 @@
101102
"method": "HTTP-metod",
102103
"timeout": "Timeout (sekunder)",
103104
"interval": "Uppdateringsintervall (sekunder)",
105+
"retries": "Försök igen vid fel",
104106
"verify_ssl": "Verifiera SSL-certifikat"
105107
}
106108
},

0 commit comments

Comments
 (0)