|
23 | 23 | CONF_INTERVAL, |
24 | 24 | CONF_METHOD, |
25 | 25 | CONF_PAYLOAD, |
| 26 | + CONF_RETRIES, |
26 | 27 | CONF_SENSOR_COLOR, |
27 | 28 | CONF_SENSOR_DEVICE_CLASS, |
28 | 29 | CONF_SENSOR_ICON, |
|
39 | 40 | CONF_URL, |
40 | 41 | CONF_VERIFY_SSL, |
41 | 42 | DEFAULT_INTERVAL, |
| 43 | + DEFAULT_RETRIES, |
42 | 44 | DEFAULT_TIMEOUT, |
43 | 45 | DEFAULT_VERIFY_SSL, |
44 | 46 | DOMAIN, |
@@ -87,6 +89,7 @@ def __init__(self, hass: HomeAssistant, entry_data: dict) -> None: |
87 | 89 | self.url = entry_data[CONF_URL] |
88 | 90 | self.method = entry_data[CONF_METHOD] |
89 | 91 | self.timeout = entry_data.get(CONF_TIMEOUT, DEFAULT_TIMEOUT) |
| 92 | + self.retries = entry_data.get(CONF_RETRIES, DEFAULT_RETRIES) |
90 | 93 | self.verify_ssl = entry_data.get(CONF_VERIFY_SSL, DEFAULT_VERIFY_SSL) |
91 | 94 | self.headers = {h["key"]: h["value"] for h in entry_data.get(CONF_HEADERS, [])} |
92 | 95 | self.payload = entry_data.get(CONF_PAYLOAD, "") |
@@ -144,75 +147,140 @@ async def _async_update_data(self) -> dict[str, Any]: |
144 | 147 | kwargs["data"] = rendered_payload |
145 | 148 | else: |
146 | 149 | kwargs["data"] = rendered_payload |
| 150 | + total_attempts = max(1, int(self.retries) + 1) |
147 | 151 |
|
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 |
150 | 184 |
|
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 | + ) |
156 | 190 |
|
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 | + ) |
163 | 197 |
|
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( |
191 | 208 | http_response, |
192 | | - sensor_config.get(CONF_TRACKER_LATITUDE, ""), |
| 209 | + sensor_config.get(CONF_SENSOR_STATE, ""), |
193 | 210 | ), |
194 | | - "longitude": self._extract_value_auto( |
| 211 | + "icon": self._extract_value_auto( |
195 | 212 | http_response, |
196 | | - sensor_config.get(CONF_TRACKER_LONGITUDE, ""), |
| 213 | + sensor_config.get(CONF_SENSOR_ICON, ""), |
197 | 214 | ), |
198 | | - "location_name": self._extract_value_auto( |
| 215 | + "color": self._extract_value_auto( |
199 | 216 | http_response, |
200 | | - sensor_config.get(CONF_TRACKER_LOCATION_NAME, ""), |
| 217 | + sensor_config.get(CONF_SENSOR_COLOR, ""), |
201 | 218 | ), |
202 | | - "source_type": sensor_config.get( |
203 | | - CONF_TRACKER_SOURCE_TYPE, "gps" |
| 219 | + "device_class": sensor_config.get( |
| 220 | + CONF_SENSOR_DEVICE_CLASS, "" |
204 | 221 | ), |
| 222 | + "unit": sensor_config.get(CONF_SENSOR_UNIT, ""), |
205 | 223 | } |
206 | | - ) |
207 | | - |
208 | | - sensor_data[sensor_name] = sensor_values |
209 | 224 |
|
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 |
216 | 284 | except Exception as err: |
217 | 285 | raise UpdateFailed(f"Unexpected error: {err}") from err |
218 | 286 |
|
|
0 commit comments