Procházet zdrojové kódy

Include the device error code in connection failure logs (#5525)

* Include the device error code in connection failure logs

When a connection to a device fails, the error returned by tinytuya is
discarded and only "Failed to refresh device state for <name>." is
logged, so every cause looks the same in the log.

This is most confusing for error 914. tinytuya returns it for any
failure to negotiate a session, and its message ("Check device key or
version") only names two of the possible causes. A device that has
stopped accepting local connections and needs to be power cycled
reports the same 914, with a correct key and protocol version. That
case is a recurring source of reports (#5347, #5136, and jasonacox/
tinytuya#581), and the log currently gives no way to tell it apart
from a genuine misconfiguration.

Log the error code and the device's message alongside the existing
text, and for 914 spell out the causes it can stand for.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* Update error hints for device connection issues

* Apply ruff format to the 914 error hint

The single-line hint string no longer needs to be wrapped, which
ruff format --check (run in CI) was flagging.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* Remove parentheses from error hint for code 914

These were only needed for the multi-line version to group the
separate strings into a single statement

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-authored-by: Jason Rumney <make-all@users.noreply.github.com>
Jordan Rodgers před 2 dny
rodič
revize
87547e74d1
2 změnil soubory, kde provedl 78 přidání a 2 odebrání
  1. 24 2
      custom_components/tuya_local/device.py
  2. 54 0
      tests/test_device.py

+ 24 - 2
custom_components/tuya_local/device.py

@@ -34,6 +34,14 @@ from .helpers.log import log_json
 
 _LOGGER = logging.getLogger(__name__)
 
+# Extra context for tinytuya error codes whose message does not fully describe
+# the possible causes.  Error 914 in particular is reported for any failure to
+# negotiate a session, which includes a device that is refusing connections
+# until it is power cycled, not just a misconfigured key or protocol version.
+_ERROR_HINTS = {
+    "914": "  If previously running OK, likely the device needs to be power cycled.",
+}
+
 
 def _collect_possible_matches(cached_state, product_ids):
     """Collect possible matches from generator into an array."""
@@ -658,12 +666,14 @@ class TuyaLocalDevice(object):
         )
 
         last_err_code = None
+        last_err_msg = None
         for i in range(connections):
             try:
                 if not self._hass.is_stopping:
                     retval = await self._hass.async_add_executor_job(func)
                     if isinstance(retval, dict) and "Error" in retval:
                         last_err_code = retval.get("Err")
+                        last_err_msg = retval.get("Error")
                         if last_err_code == "900":
                             # Some devices (e.g. IR/RF remotes) never return
                             # status data; error 900 is their normal response
@@ -699,12 +709,24 @@ class TuyaLocalDevice(object):
                         self._api_protocol_working = False
                         for entity in self._children:
                             entity.async_schedule_update_ha_state()
+                    if last_err_code:
+                        log_format = "%s Device reported error %s: %s%s"
+                        log_args = (
+                            error_message,
+                            last_err_code,
+                            last_err_msg,
+                            _ERROR_HINTS.get(last_err_code, ""),
+                        )
+                    else:
+                        log_format = "%s"
+                        log_args = (error_message,)
+
                     if self._api_working_protocol_failures == 1 and not (
                         last_err_code == "914" and self._protocol_configured == "auto"
                     ):
-                        _LOGGER.error(error_message)
+                        _LOGGER.error(log_format, *log_args)
                     else:
-                        _LOGGER.debug(error_message)
+                        _LOGGER.debug(log_format, *log_args)
 
                 if not self._api_protocol_working:
                     await self._rotate_api_protocol_version()

+ 54 - 0
tests/test_device.py

@@ -1,4 +1,5 @@
 import asyncio
+import logging
 from time import time
 
 import pytest
@@ -676,3 +677,56 @@ def test_should_poll(subject):
     # Test initial polling
     subject._cached_state = {}
     assert subject.should_poll
+
+
+@pytest.mark.asyncio
+async def test_refresh_error_reports_device_error_code(subject, mock_api, caplog):
+    """The error code and message returned by the device are logged."""
+    subject._api_protocol_working = False
+    subject._protocol_configured = "3.3"
+    mock_api().status.return_value = {
+        "Error": "Check device key or version",
+        "Err": "914",
+        "Payload": None,
+    }
+
+    with caplog.at_level(logging.ERROR):
+        await subject.async_refresh()
+
+    assert "914" in caplog.text
+    assert "Check device key or version" in caplog.text
+    # 914 is ambiguous, so the possible causes are spelled out
+    assert "power cycled" in caplog.text
+
+
+@pytest.mark.asyncio
+async def test_refresh_error_without_error_code_is_unchanged(subject, mock_api, caplog):
+    """A failure that is not a device error logs the bare message."""
+    subject._api_protocol_working = False
+    subject._protocol_configured = "3.3"
+    mock_api().status.side_effect = Exception("connection refused")
+
+    with caplog.at_level(logging.ERROR):
+        await subject.async_refresh()
+
+    assert "Failed to refresh device state for Some name." in caplog.text
+    assert "Device reported error" not in caplog.text
+
+
+@pytest.mark.asyncio
+async def test_refresh_error_914_stays_quiet_when_rotating_protocols(
+    subject, mock_api, caplog
+):
+    """914 is expected while auto-detecting, so it must not log at error."""
+    subject._api_protocol_working = False
+    subject._protocol_configured = "auto"
+    mock_api().status.return_value = {
+        "Error": "Check device key or version",
+        "Err": "914",
+        "Payload": None,
+    }
+
+    with caplog.at_level(logging.ERROR):
+        await subject.async_refresh()
+
+    assert caplog.text == ""