commit d4169b56e7f62daf735763314414d22ee4bb47eb parent 9687404153db75656dcbec2e37ef3cc9ee5fe30a Author: Stéphan Kochen <git@stephank.nl> Date: Wed, 02 Sep 2026 08:28:01 +0200 Don't swallow errors
diff --git a/src/pyhunterbtt/session.py b/src/pyhunterbtt/session.py index 1275276abaefbe8850e1990490e59d25f22a1d05..a592a81cc7bb5fdb292fb4b3d38e6d5abd8d22ab 100644 --- a/src/pyhunterbtt/session.py +++ b/src/pyhunterbtt/session.py @@ -85,6 +85,12 @@ zone1: ZoneSchedule zone2: ZoneSchedule | None +def _operation_error(message: str, err: Exception) -> str: + """Add a useful backend error to a library-level operation error.""" + detail = str(err) or type(err).__name__ + return f"{message}: {detail}" + + class HunterBTTSession: """Authentication and whitelisted reads for one connected BLE session. @@ -115,24 +121,24 @@ try: client_identifier = bytes( await self._client.read_gatt_char(CLIENT_IDENTIFIER_UUID) ) - except (BleakError, OSError): + except (BleakError, OSError) as err: msg = "could not read the transient authentication identifier" - raise AuthenticationIdentifierError(msg) from None + raise AuthenticationIdentifierError(_operation_error(msg, err)) from err payload = encode_authentication(passcode, client_identifier) try: await self._client.write_gatt_char( AUTHENTICATION_UUID, payload, response=True ) - except (BleakError, OSError): + except (BleakError, OSError) as err: msg = "could not write session authentication" - raise AuthenticationWriteError(msg) from None + raise AuthenticationWriteError(_operation_error(msg, err)) from err self._authenticated = True try: return await self.async_read_status() - except (ProtocolError, StatusReadError): + except (ProtocolError, StatusReadError) as err: self._authenticated = False msg = "could not verify session authentication" - raise AuthenticationVerificationError(msg) from None + raise AuthenticationVerificationError(_operation_error(msg, err)) from err async def async_read_status(self) -> HunterBTTState: """Read status and conditionally read FF8A runtime while watering.""" @@ -141,9 +147,9 @@ msg = "authenticate the current BLE connection before reading status" raise NotAuthenticatedError(msg) try: state = await self._transport.async_read_snapshot() - except (BleakError, OSError): + except (BleakError, OSError) as err: msg = "could not read controller status" - raise StatusReadError(msg) from None + raise StatusReadError(_operation_error(msg, err)) from err controller = state.controller if controller is None or not any( zone.activity is not None and zone.activity is not ZoneActivity.IDLE @@ -152,9 +158,9 @@ ): return state try: payload = bytes(await self._client.read_gatt_char(RUNTIME_UUID)) - except (BleakError, OSError): + except (BleakError, OSError) as err: msg = "could not read active watering runtime" - raise StatusReadError(msg) from None + raise StatusReadError(_operation_error(msg, err)) from err state.update(RUNTIME_UUID, payload) return state @@ -186,9 +192,9 @@ ZONE2_TIMER_UUID, ZONE2_CYCLING_UUID, ZONE2_ADDITIONAL_START_TIMES_UUID, ) - except (BleakError, OSError): + except (BleakError, OSError) as err: msg = "could not read controller configuration" - raise ConfigurationReadError(msg) from None + raise ConfigurationReadError(_operation_error(msg, err)) from err return HunterBTTConfiguration( controller_command=decode_controller_command( values[CONTROLLER_COMMAND_UUID] @@ -220,9 +226,9 @@ ) config = decode_zone_config( bytes(await self._client.read_gatt_char(ZONE1_CONFIG_UUID)) ) - except (BleakError, OSError): + except (BleakError, OSError) as err: msg = "could not read manual-watering command state" - raise ManualWateringError(msg) from None + raise ManualWateringError(_operation_error(msg, err)) from err plan = plan_manual_start_zone1(command, config, duration_seconds) await self._async_write_manual_plan(plan.writes) @@ -235,9 +241,9 @@ try: command = decode_controller_command( bytes(await self._client.read_gatt_char(CONTROLLER_COMMAND_UUID)) ) - except (BleakError, OSError): + except (BleakError, OSError) as err: msg = "could not read manual-watering command state" - raise ManualWateringError(msg) from None + raise ManualWateringError(_operation_error(msg, err)) from err plan = plan_manual_stop_zone1(command) await self._async_write_manual_plan(plan.writes) @@ -279,6 +285,6 @@ for write in writes: await self._client.write_gatt_char( write.uuid, write.payload, response=True ) - except (BleakError, OSError): + except (BleakError, OSError) as err: msg = "could not write manual-watering command" - raise ManualWateringError(msg) from None + raise ManualWateringError(_operation_error(msg, err)) from err diff --git a/tests/test_transport.py b/tests/test_transport.py index 875d02069703f22a01e0145448d0f84008c4ce47..4546e8ebed8fa611d94ddb2080b9a9c32d0062bc 100644 --- a/tests/test_transport.py +++ b/tests/test_transport.py @@ -223,9 +223,14 @@ async def test_identifier_read_failure_is_typed_and_not_authenticated(self) -> None: self.client.read_errors[CLIENT_IDENTIFIER_UUID] = OSError("synthetic") - with self.assertRaises(AuthenticationIdentifierError): + with self.assertRaises(AuthenticationIdentifierError) as context: await self.session.async_authenticate("2468") + self.assertEqual( + str(context.exception), + "could not read the transient authentication identifier: synthetic", + ) + self.assertIsInstance(context.exception.__cause__, OSError) self.assertFalse(self.session.is_authenticated) self.assertEqual(self.client.writes, []) @@ -242,9 +247,15 @@ async def test_verification_failure_clears_authentication(self) -> None: self.client.read_errors[FIRMWARE_REVISION_UUID] = OSError("synthetic") - with self.assertRaises(AuthenticationVerificationError): + with self.assertRaises(AuthenticationVerificationError) as context: await self.session.async_authenticate("2468") + self.assertEqual( + str(context.exception), + "could not verify session authentication: " + "could not read controller status: synthetic", + ) + self.assertIsInstance(context.exception.__cause__, StatusReadError) self.assertFalse(self.session.is_authenticated) async def test_status_read_failure_is_typed(self) -> None: