fix(coordinator): retry a command once after a dead-session reconnect
async_send_command's _do_put caught any exception, logged it, and returned -- no reconnect, no retry, no error the user could see. A command landing on a session Samsung's firmware closed between polls (the same 'known device behavior' _async_update_data already reconnects around) was silently lost, with nothing to do about it but a manual reload of the device (issue #294). Mirror the poll path's own recovery: on failure, close the dead session, pause, and retry the PUT once against a freshly reconnected one. If that also fails, raise a HomeAssistantError instead of just logging, so the user gets a visible error rather than a command that quietly did nothing. The retry runs under the same session lock the poll path uses, so a write landing mid-reconnect can't race a concurrent poll cycle rebuilding the same session.
This commit is contained in:
1 parent
252306838d
commit
77c2d7831e
8 files changed
+132
-7
No files matched your search
@@ -15,7 +15,7 @@ from typing import Any
|
||||
import cbor2
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
from homeassistant.exceptions import ServiceValidationError
|
||||
from homeassistant.exceptions import HomeAssistantError, ServiceValidationError
|
||||
from homeassistant.helpers import issue_registry as ir
|
||||
from homeassistant.helpers.device_registry import DeviceInfo
|
||||
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
|
||||
@@ -1026,18 +1026,42 @@ class LocalThingsCoordinator(DataUpdateCoordinator[dict[str, Any]]):
|
||||
)
|
||||
|
||||
def _do_put():
|
||||
if self._session is None:
|
||||
self._connect_session()
|
||||
sess = self._session
|
||||
if sess is None:
|
||||
raise RuntimeError("no session")
|
||||
code, _ = sess.post(path_segs, cbor2.dumps(body), timeout=self._POST_TIMEOUT_S)
|
||||
self._log.info("PUT %s → code %#04x", write_href, code)
|
||||
|
||||
try:
|
||||
await self.hass.async_add_executor_job(_do_put)
|
||||
except Exception as e:
|
||||
self._log.error("command failed for %s: %s", write_href, e)
|
||||
else:
|
||||
await self.async_request_refresh()
|
||||
# A session Samsung's firmware closed between polls (the same "known
|
||||
# device behavior" _async_update_data reconnects around) previously
|
||||
# sank the PUT here with nothing but a log line -- no retry, no
|
||||
# user-facing error, so the command was silently lost until the next
|
||||
# manual reload (issue #294). Mirror the poll path's own recovery:
|
||||
# one reconnect, one retry, and this time raise on final failure so
|
||||
# the user gets a red toast instead of nothing.
|
||||
#
|
||||
# Locked the same as the poll path so a write landing mid-reconnect
|
||||
# doesn't tear down a session _async_update_data is simultaneously
|
||||
# rebuilding (or vice versa).
|
||||
async with self._session_lock:
|
||||
try:
|
||||
await self.hass.async_add_executor_job(_do_put)
|
||||
except Exception as e:
|
||||
self._log.warning("command failed for %s, reconnecting: %s", write_href, e)
|
||||
await self.hass.async_add_executor_job(self._close_session)
|
||||
await asyncio.sleep(self._RECONNECT_PAUSE_S)
|
||||
try:
|
||||
await self.hass.async_add_executor_job(_do_put)
|
||||
except Exception as e2:
|
||||
self._log.error("command failed for %s after reconnect: %s", write_href, e2)
|
||||
raise HomeAssistantError(
|
||||
translation_domain=DOMAIN,
|
||||
translation_key="command_failed",
|
||||
translation_placeholders={"href": write_href, "error": str(e2)},
|
||||
) from e2
|
||||
await self.async_request_refresh()
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# Debug raw write (issue #54): a power-user escape hatch for the
|
||||
|
||||
@@ -1356,6 +1356,9 @@
|
||||
},
|
||||
"intensive_unavailable_for_cycle": {
|
||||
"message": "Intenzivní není u vybraného cyklu k dispozici."
|
||||
},
|
||||
"command_failed": {
|
||||
"message": "Příkaz pro {href} selhal i po opětovném připojení: {error}"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1356,6 +1356,9 @@
|
||||
},
|
||||
"intensive_unavailable_for_cycle": {
|
||||
"message": "Intensive isn't available on the selected cycle."
|
||||
},
|
||||
"command_failed": {
|
||||
"message": "The command to {href} failed even after reconnecting: {error}"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -119,6 +119,9 @@
|
||||
},
|
||||
"intensive_unavailable_for_cycle": {
|
||||
"message": "El modo intensivo no está disponible en el ciclo seleccionado."
|
||||
},
|
||||
"command_failed": {
|
||||
"message": "El comando para {href} falló incluso después de reconectar: {error}"
|
||||
}
|
||||
},
|
||||
"entity": {
|
||||
|
||||
@@ -1356,6 +1356,9 @@
|
||||
},
|
||||
"intensive_unavailable_for_cycle": {
|
||||
"message": "Intensivo non è disponibile per il ciclo selezionato."
|
||||
},
|
||||
"command_failed": {
|
||||
"message": "Il comando per {href} è fallito anche dopo la riconnessione: {error}"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1356,6 +1356,9 @@
|
||||
},
|
||||
"intensive_unavailable_for_cycle": {
|
||||
"message": "선택한 코스에서는 강력 세탁을 사용할 수 없습니다."
|
||||
},
|
||||
"command_failed": {
|
||||
"message": "재연결 후에도 {href} 명령이 실패했습니다: {error}"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1356,6 +1356,9 @@
|
||||
},
|
||||
"intensive_unavailable_for_cycle": {
|
||||
"message": "Intensief is niet beschikbaar voor het geselecteerde programma."
|
||||
},
|
||||
"command_failed": {
|
||||
"message": "Het commando naar {href} is ook na opnieuw verbinden mislukt: {error}"
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user