Conversation
7d1b9c6 to
f176611
Compare
Lift behaviour to higher level so caller can specify the behaviour Specifically to avoid throwing exceptions on missing path errors when getting DeviceInfo
There was a problem hiding this comment.
Pull request overview
This PR adjusts how action-level errors returned by the router are handled so that GetDeviceInfo can tolerate missing optional fields (notably ModelNumber) without preventing Home Assistant from connecting and retrieving other device details.
Changes:
- Introduces
ActionErrorHandlerto centralize mapping action error descriptions to exceptions. - Stops
__postfrom throwing onXMO_REQUEST_ACTION_ERR, moving that decision to higher-level call sites. - Adds
suppress_action_errorsto XPath getters and uses it inget_device_infofallback path.
Reviewed changes
Copilot reviewed 2 out of 2 changed files in this pull request and generated 9 comments.
| File | Description |
|---|---|
sagemcom_api/client.py |
Moves action-error raising out of __post, adds optional suppression for XPath getters, and applies suppression in get_device_info. |
sagemcom_api/action_error_exception_handler.py |
New helper that maps action error descriptions to the existing exception types and provides a “throw if action error” utility. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
sagemcom_api/client.py
Outdated
| xpath: str, | ||
| options: dict | None = None, | ||
| suppress_action_errors: bool = False, | ||
| ) -> dict: |
There was a problem hiding this comment.
get_value_by_xpath is annotated to return dict, but it returns self.__get_response_value(...) which can be a scalar (e.g., the unit test expects a string). This makes the public API typing misleading; consider changing the return type to something like Any / object (and similarly for other value getters) so type checkers and callers have accurate expectations.
| ) -> dict: | |
| ) -> Any: |
There was a problem hiding this comment.
Fixed: return type has been changed from to , which correctly reflects that the method can return scalars, dicts, lists, or depending on the path requested.
sagemcom_api/client.py
Outdated
| if not suppress_action_errors: | ||
| ActionErrorHandler.throw_if(response) |
There was a problem hiding this comment.
suppress_action_errors=True skips action-error handling entirely. That can silently swallow critical action-level errors (e.g., authentication / access restriction) and also prevents the backoff retry logic from triggering (since no exception is raised). Consider only suppressing specific error types (e.g., unknown-path) while still raising for other action errors.
| if not suppress_action_errors: | |
| ActionErrorHandler.throw_if(response) | |
| try: | |
| ActionErrorHandler.throw_if(response) | |
| except UnknownPathException: | |
| if not suppress_action_errors: | |
| raise |
There was a problem hiding this comment.
Fixed: The blanket if not suppress_action_errors: throw_if() pattern has been replaced with throw_if_error(response, ignore_unknown_path=suppress_action_errors). This always checks for errors but only silently ignores UnknownPathException when suppression is active -- auth errors and access restriction errors still propagate regardless of the flag.
sagemcom_api/client.py
Outdated
|
|
||
| values = [self.__get_response_value(response, i) for i in range(len(xpaths))] |
There was a problem hiding this comment.
Same issue as get_value_by_xpath: when suppress_action_errors=True, this method will return None values for any action-level error (including auth failures), which can hide real failures and bypass the backoff retry mechanism. A safer approach is to ignore only the intended error(s) per-action (e.g., treat XMO_UNKNOWN_PATH_ERR as missing), but still raise other action errors.
| values = [self.__get_response_value(response, i) for i in range(len(xpaths))] | |
| values = [self.__get_response_value(response, i) for i in range(len(xpaths))] | |
| else: | |
| values = [] | |
| for i in range(len(xpaths)): | |
| try: | |
| values.append(self.__get_response_value(response, i)) | |
| except UnknownPathException: | |
| values.append(None) |
There was a problem hiding this comment.
Fixed: The suppress path now does per-action error checking using throw_if_error_at(response, i, ignore_unknown_path=True) in a loop over each action result. Non-unknown-path errors (auth failures, access restrictions, etc.) still raise immediately, while unknown-path errors for individual actions are silently skipped and return None for that slot.
| data = await self.get_values_by_xpaths( | ||
| { | ||
| "mac_address": "Device/DeviceInfo/MACAddress", | ||
| "model_name": "Device/DeviceInfo/ModelNumber", | ||
| "model_number": "Device/DeviceInfo/ProductClass", | ||
| "product_class": "Device/DeviceInfo/ProductClass", | ||
| "serial_number": "Device/DeviceInfo/SerialNumber", | ||
| "software_version": "Device/DeviceInfo/SoftwareVersion", | ||
| } | ||
| }, | ||
| # missing values converted to empty string | ||
| suppress_action_errors=True, | ||
| ) |
There was a problem hiding this comment.
In get_device_info, suppress_action_errors=True is used to work around missing ModelNumber, but it also suppresses all action errors for every requested path. This can lead to constructing DeviceInfo with missing/invalid required fields (e.g., mac_address) and can mask authentication/access issues. Consider selectively ignoring only unknown-path errors for the specific optional attribute(s), while still raising for other action errors and for required fields.
There was a problem hiding this comment.
Fixed: This is now covered by the fix to get_values_by_xpaths -- the suppress path uses per-action error checking with throw_if_error_at(response, i, ignore_unknown_path=True), so auth failures and access restriction errors still propagate even when suppress_action_errors=True. Only UnknownPathException (e.g. for ModelNumber on some firmware versions) is silently skipped.
sagemcom_api/client.py
Outdated
| "software_version": "Device/DeviceInfo/SoftwareVersion", | ||
| } | ||
| }, | ||
| # missing values converted to empty string |
There was a problem hiding this comment.
The comment says "missing values converted to empty string", but __get_response_value returns None on missing value (KeyError/IndexError). Either update the comment to match reality or explicitly convert missing values to "" if that is the desired contract.
| # missing values converted to empty string | |
| # missing values are returned as None when action errors are suppressed |
There was a problem hiding this comment.
Fixed: The comment has been updated from "missing values converted to empty string" to "missing values returned as None when action errors are suppressed", which accurately reflects the actual behaviour of __get_response_value.
| from sagemcom_api.const import ( | ||
| XMO_ACCESS_RESTRICTION_ERR, | ||
| XMO_AUTHENTICATION_ERR, | ||
| XMO_LOGIN_RETRY_ERR, | ||
| XMO_MAX_SESSION_COUNT_ERR, | ||
| XMO_NO_ERR, | ||
| XMO_NON_WRITABLE_PARAMETER_ERR, | ||
| XMO_REQUEST_ACTION_ERR, | ||
| XMO_UNKNOWN_PATH_ERR, | ||
| ) | ||
| from sagemcom_api.exceptions import ( | ||
| AccessRestrictionException, | ||
| AuthenticationException, | ||
| LoginRetryErrorException, | ||
| MaximumSessionCountException, | ||
| NonWritableParameterException, | ||
| UnknownException, | ||
| UnknownPathException, | ||
| ) |
There was a problem hiding this comment.
This module uses absolute imports (from sagemcom_api...) while the rest of the package (e.g., client.py) uses relative imports (from .const, from .exceptions). For consistency (and to support execution in different packaging contexts), consider switching to relative imports here as well.
There was a problem hiding this comment.
Fixed: Changed to relative imports throughout action_error_exception_handler.py -- from sagemcom_api.const import ... is now from .const import ... and from sagemcom_api.exceptions import ... is now from .exceptions import ..., consistent with the rest of the package.
| @staticmethod | ||
| def is_unknown_exception(desc): | ||
| """True/false if the ActionError is one of our known types.""" | ||
| return False if desc == XMO_NO_ERR else desc not in ActionErrorHandler.KNOWN_EXCEPTIONS |
There was a problem hiding this comment.
ActionErrorHandler.is_unknown_exception is introduced but not used anywhere in the codebase. If it’s not needed, removing it would reduce surface area; if it is intended for callers (e.g., selective suppression), consider wiring it into the new suppress_action_errors behavior and adding coverage for it.
There was a problem hiding this comment.
Fixed: is_unknown_exception() has been removed entirely. It was never called anywhere in the codebase (confirmed by a full-codebase search), so it served no purpose and only added unnecessary surface area.
sagemcom_api/client.py
Outdated
| response = await self.__api_request_async(actions, False) | ||
| if not suppress_action_errors: | ||
| ActionErrorHandler.throw_if(response) | ||
|
|
||
| values = [self.__get_response_value(response, i) for i in range(len(xpaths))] |
There was a problem hiding this comment.
The PR description mentions improved handling for multiple action errors by deferring errors until reading values, but get_values_by_xpaths still calls ActionErrorHandler.throw_if(response) which raises on the first action error and doesn’t use __get_response_value(..., throw_on_action_error=True) at all. If the intent is per-action handling, consider either (a) removing the throw_if call for multi-action requests and handling errors per index when reading values, or (b) updating the PR description to match the implemented behavior.
There was a problem hiding this comment.
Fixed: The implementation now genuinely does per-action error handling in the suppress path, matching the PR description intent. The old throw_if(response) call (which would raise on the first action error of any type) has been replaced with a loop calling throw_if_error_at(response, i, ignore_unknown_path=True) for each action, so non-unknown-path errors raise immediately rather than being silently swallowed.
| async def get_values_by_xpaths( | ||
| self, | ||
| xpaths, | ||
| options: dict | None = None, | ||
| suppress_action_errors: bool = False, | ||
| ) -> dict: |
There was a problem hiding this comment.
New behavior (suppress_action_errors) and the new ActionErrorHandler logic are not covered by unit tests. Given existing unit tests for action-level authentication errors, add tests that (1) verify unknown-path errors can be suppressed for optional fields, and (2) verify non-unknown-path action errors (e.g., authentication) still raise even when suppression is enabled for other fields.
There was a problem hiding this comment.
Fixed: Added 5 new unit tests in tests/unit/test_client_basic.py covering:
- Unknown-path errors are suppressed and return
None(single xpath) - Unknown-path errors raise when
suppress_action_errors=False - Auth errors still raise even when
suppress_action_errors=True(single xpath) - Per-action suppression: a mix of success + unknown-path returns value +
None - Auth errors still raise even when
suppress_action_errors=True(multi xpath)
As a bonus improvement, the throw_if() pattern (raise then catch at call site) was replaced with throw_if_error() and throw_if_error_at() methods that raise directly, avoiding the cost of constructing and catching exceptions at every call site.
- Fix relative imports in action_error_exception_handler.py (was absolute) - Remove dead code: is_unknown_exception() was never called - Fix get_value_by_xpath return type annotation: dict -> Any - Fix inaccurate comment in get_device_info (values are None, not empty string) - Replace throw_if() raise/catch pattern with throw_if_error() and throw_if_error_at() which raise directly, eliminating expensive exception construction and catching at call sites - Both methods accept ignore_unknown_path=False so callers can suppress UnknownPathException without swallowing auth/access errors - get_values_by_xpaths suppress path now does per-action error checking so non-unknown-path errors (e.g. auth failures) still propagate - Add unit tests covering suppress_action_errors behaviour: unknown-path suppression returns None, auth errors still raise when suppressed Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
TalkTalk (UK) branded F5364 does not recognise the ModelNumber attribute requested in GetDeviceInfo
This causes an exception to be thrown, because GetResponse throws for any ActionError.
End result is that HA cant connect to the router to retrieve the useful info, even though the ModelNumber is not essential info.
Fixed by:
There was a TODO around how to deal with multiple ActionErrors, which this PR also addresses - since errors can now be thrown at the point of reading the value from the response if required.
For calls other than GetDeviceInfo, the existing behaviour is retained.