python-mobius 0.10.2__tar.gz → 0.10.3__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (103) hide show
  1. {python_mobius-0.10.2 → python_mobius-0.10.3}/CHANGELOG.md +4 -0
  2. {python_mobius-0.10.2 → python_mobius-0.10.3}/PKG-INFO +1 -1
  3. {python_mobius-0.10.2 → python_mobius-0.10.3}/pyproject.toml +1 -1
  4. {python_mobius-0.10.2 → python_mobius-0.10.3}/src/mobius/__init__.py +1 -1
  5. {python_mobius-0.10.2 → python_mobius-0.10.3}/src/mobius/device.py +29 -0
  6. {python_mobius-0.10.2 → python_mobius-0.10.3}/tests/test_relay.py +14 -0
  7. {python_mobius-0.10.2 → python_mobius-0.10.3}/.forgejo/scripts/extract_changelog_section.py +0 -0
  8. {python_mobius-0.10.2 → python_mobius-0.10.3}/.forgejo/workflows/release.yml +0 -0
  9. {python_mobius-0.10.2 → python_mobius-0.10.3}/.gitignore +0 -0
  10. {python_mobius-0.10.2 → python_mobius-0.10.3}/LICENSE +0 -0
  11. {python_mobius-0.10.2 → python_mobius-0.10.3}/README.md +0 -0
  12. {python_mobius-0.10.2 → python_mobius-0.10.3}/documentation/00-overview.md +0 -0
  13. {python_mobius-0.10.2 → python_mobius-0.10.3}/documentation/01-ble-transport.md +0 -0
  14. {python_mobius-0.10.2 → python_mobius-0.10.3}/documentation/02-framing-and-crc.md +0 -0
  15. {python_mobius-0.10.2 → python_mobius-0.10.3}/documentation/03-attributes-and-opcodes.md +0 -0
  16. {python_mobius-0.10.2 → python_mobius-0.10.3}/documentation/04-device-identity.md +0 -0
  17. {python_mobius-0.10.2 → python_mobius-0.10.3}/documentation/05-scenes.md +0 -0
  18. {python_mobius-0.10.2 → python_mobius-0.10.3}/documentation/06-light-schedule.md +0 -0
  19. {python_mobius-0.10.2 → python_mobius-0.10.3}/documentation/07-pump-schedule.md +0 -0
  20. {python_mobius-0.10.2 → python_mobius-0.10.3}/documentation/08-manufacturer-data.md +0 -0
  21. {python_mobius-0.10.2 → python_mobius-0.10.3}/documentation/09-thread-coap-relay.md +0 -0
  22. {python_mobius-0.10.2 → python_mobius-0.10.3}/documentation/10-known-gaps-and-open-questions.md +0 -0
  23. {python_mobius-0.10.2 → python_mobius-0.10.3}/documentation/11-device-status-attributes.md +0 -0
  24. {python_mobius-0.10.2 → python_mobius-0.10.3}/documentation/12-device-identity-and-address-stability.md +0 -0
  25. {python_mobius-0.10.2 → python_mobius-0.10.3}/documentation/13-attribute-dump.md +0 -0
  26. {python_mobius-0.10.2 → python_mobius-0.10.3}/documentation/14-attribute-reference.md +0 -0
  27. {python_mobius-0.10.2 → python_mobius-0.10.3}/documentation/15-mob-template-format.md +0 -0
  28. {python_mobius-0.10.2 → python_mobius-0.10.3}/documentation/16-python-library.md +0 -0
  29. {python_mobius-0.10.2 → python_mobius-0.10.3}/documentation/17-configuration-checksums.md +0 -0
  30. {python_mobius-0.10.2 → python_mobius-0.10.3}/src/mobius/checksums.py +0 -0
  31. {python_mobius-0.10.2 → python_mobius-0.10.3}/src/mobius/cli.py +0 -0
  32. {python_mobius-0.10.2 → python_mobius-0.10.3}/src/mobius/coap.py +0 -0
  33. {python_mobius-0.10.2 → python_mobius-0.10.3}/src/mobius/constants.py +0 -0
  34. {python_mobius-0.10.2 → python_mobius-0.10.3}/src/mobius/crc.py +0 -0
  35. {python_mobius-0.10.2 → python_mobius-0.10.3}/src/mobius/device_status.py +0 -0
  36. {python_mobius-0.10.2 → python_mobius-0.10.3}/src/mobius/discovery.py +0 -0
  37. {python_mobius-0.10.2 → python_mobius-0.10.3}/src/mobius/dump.py +0 -0
  38. {python_mobius-0.10.2 → python_mobius-0.10.3}/src/mobius/frame.py +0 -0
  39. {python_mobius-0.10.2 → python_mobius-0.10.3}/src/mobius/manufacturer.py +0 -0
  40. {python_mobius-0.10.2 → python_mobius-0.10.3}/src/mobius/mesh_address.py +0 -0
  41. {python_mobius-0.10.2 → python_mobius-0.10.3}/src/mobius/mob.py +0 -0
  42. {python_mobius-0.10.2 → python_mobius-0.10.3}/src/mobius/modifiers.py +0 -0
  43. {python_mobius-0.10.2 → python_mobius-0.10.3}/src/mobius/power.py +0 -0
  44. {python_mobius-0.10.2 → python_mobius-0.10.3}/src/mobius/pump_status.py +0 -0
  45. {python_mobius-0.10.2 → python_mobius-0.10.3}/src/mobius/relay.py +0 -0
  46. {python_mobius-0.10.2 → python_mobius-0.10.3}/src/mobius/scenes.py +0 -0
  47. {python_mobius-0.10.2 → python_mobius-0.10.3}/src/mobius/schedule.py +0 -0
  48. {python_mobius-0.10.2 → python_mobius-0.10.3}/src/mobius/timezone.py +0 -0
  49. {python_mobius-0.10.2 → python_mobius-0.10.3}/tests/test_attributes_batch.py +0 -0
  50. {python_mobius-0.10.2 → python_mobius-0.10.3}/tests/test_battery_settings.py +0 -0
  51. {python_mobius-0.10.2 → python_mobius-0.10.3}/tests/test_bluetooth_only.py +0 -0
  52. {python_mobius-0.10.2 → python_mobius-0.10.3}/tests/test_checksums.py +0 -0
  53. {python_mobius-0.10.2 → python_mobius-0.10.3}/tests/test_cli_group_targets.py +0 -0
  54. {python_mobius-0.10.2 → python_mobius-0.10.3}/tests/test_cli_mob_file.py +0 -0
  55. {python_mobius-0.10.2 → python_mobius-0.10.3}/tests/test_cli_schedule_file.py +0 -0
  56. {python_mobius-0.10.2 → python_mobius-0.10.3}/tests/test_coap.py +0 -0
  57. {python_mobius-0.10.2 → python_mobius-0.10.3}/tests/test_coap_indication_handling.py +0 -0
  58. {python_mobius-0.10.2 → python_mobius-0.10.3}/tests/test_configuration_cache.py +0 -0
  59. {python_mobius-0.10.2 → python_mobius-0.10.3}/tests/test_connection_cleanup.py +0 -0
  60. {python_mobius-0.10.2 → python_mobius-0.10.3}/tests/test_device_control.py +0 -0
  61. {python_mobius-0.10.2 → python_mobius-0.10.3}/tests/test_device_status.py +0 -0
  62. {python_mobius-0.10.2 → python_mobius-0.10.3}/tests/test_device_summary.py +0 -0
  63. {python_mobius-0.10.2 → python_mobius-0.10.3}/tests/test_device_time.py +0 -0
  64. {python_mobius-0.10.2 → python_mobius-0.10.3}/tests/test_discovery_company_ids.py +0 -0
  65. {python_mobius-0.10.2 → python_mobius-0.10.3}/tests/test_dump.py +0 -0
  66. {python_mobius-0.10.2 → python_mobius-0.10.3}/tests/test_extra_physical_values.py +0 -0
  67. {python_mobius-0.10.2 → python_mobius-0.10.3}/tests/test_fan_thresholds.py +0 -0
  68. {python_mobius-0.10.2 → python_mobius-0.10.3}/tests/test_firmware_versions.py +0 -0
  69. {python_mobius-0.10.2 → python_mobius-0.10.3}/tests/test_frame.py +0 -0
  70. {python_mobius-0.10.2 → python_mobius-0.10.3}/tests/test_frame_sniffer.py +0 -0
  71. {python_mobius-0.10.2 → python_mobius-0.10.3}/tests/test_full_poll_batch.py +0 -0
  72. {python_mobius-0.10.2 → python_mobius-0.10.3}/tests/test_group_bitmap.py +0 -0
  73. {python_mobius-0.10.2 → python_mobius-0.10.3}/tests/test_hyperdrive_device.py +0 -0
  74. {python_mobius-0.10.2 → python_mobius-0.10.3}/tests/test_insolation_detection.py +0 -0
  75. {python_mobius-0.10.2 → python_mobius-0.10.3}/tests/test_light_intensity_branching.py +0 -0
  76. {python_mobius-0.10.2 → python_mobius-0.10.3}/tests/test_light_poll_batch.py +0 -0
  77. {python_mobius-0.10.2 → python_mobius-0.10.3}/tests/test_light_schedule.py +0 -0
  78. {python_mobius-0.10.2 → python_mobius-0.10.3}/tests/test_manufacturer.py +0 -0
  79. {python_mobius-0.10.2 → python_mobius-0.10.3}/tests/test_manufacturer_lookup.py +0 -0
  80. {python_mobius-0.10.2 → python_mobius-0.10.3}/tests/test_mesh_address.py +0 -0
  81. {python_mobius-0.10.2 → python_mobius-0.10.3}/tests/test_mesh_discovery.py +0 -0
  82. {python_mobius-0.10.2 → python_mobius-0.10.3}/tests/test_mesh_discovery_direct_connect.py +0 -0
  83. {python_mobius-0.10.2 → python_mobius-0.10.3}/tests/test_metadata_batch.py +0 -0
  84. {python_mobius-0.10.2 → python_mobius-0.10.3}/tests/test_mob.py +0 -0
  85. {python_mobius-0.10.2 → python_mobius-0.10.3}/tests/test_modifiers.py +0 -0
  86. {python_mobius-0.10.2 → python_mobius-0.10.3}/tests/test_networked_thread_devices.py +0 -0
  87. {python_mobius-0.10.2 → python_mobius-0.10.3}/tests/test_new_diagnostic_attributes.py +0 -0
  88. {python_mobius-0.10.2 → python_mobius-0.10.3}/tests/test_power.py +0 -0
  89. {python_mobius-0.10.2 → python_mobius-0.10.3}/tests/test_pump_param_ranges.py +0 -0
  90. {python_mobius-0.10.2 → python_mobius-0.10.3}/tests/test_pump_reverse.py +0 -0
  91. {python_mobius-0.10.2 → python_mobius-0.10.3}/tests/test_pump_schedule.py +0 -0
  92. {python_mobius-0.10.2 → python_mobius-0.10.3}/tests/test_pump_status.py +0 -0
  93. {python_mobius-0.10.2 → python_mobius-0.10.3}/tests/test_pump_supported_modes.py +0 -0
  94. {python_mobius-0.10.2 → python_mobius-0.10.3}/tests/test_pump_telemetry.py +0 -0
  95. {python_mobius-0.10.2 → python_mobius-0.10.3}/tests/test_scenes.py +0 -0
  96. {python_mobius-0.10.2 → python_mobius-0.10.3}/tests/test_schedule_file_format.py +0 -0
  97. {python_mobius-0.10.2 → python_mobius-0.10.3}/tests/test_schedule_intensity_scalar.py +0 -0
  98. {python_mobius-0.10.2 → python_mobius-0.10.3}/tests/test_serial_decoding.py +0 -0
  99. {python_mobius-0.10.2 → python_mobius-0.10.3}/tests/test_serial_identity.py +0 -0
  100. {python_mobius-0.10.2 → python_mobius-0.10.3}/tests/test_shared_helpers.py +0 -0
  101. {python_mobius-0.10.2 → python_mobius-0.10.3}/tests/test_supported_attributes.py +0 -0
  102. {python_mobius-0.10.2 → python_mobius-0.10.3}/tests/test_time_zone.py +0 -0
  103. {python_mobius-0.10.2 → python_mobius-0.10.3}/tests/test_write_size.py +0 -0
@@ -1,5 +1,9 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.10.3
4
+
5
+ - Frames that fail to parse or fail their CRC are logged at debug level.
6
+
3
7
  ## 0.10.2
4
8
 
5
9
  - Mesh peer discovery can raise its error (`raise_errors=True`) instead
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: python-mobius
3
- Version: 0.10.2
3
+ Version: 0.10.3
4
4
  Summary: Reverse-engineered Python client for the Mobius BLE protocol (EcoTech Marine VorTech/Radion, AquaIllumination, Neptune Systems, NYOS)
5
5
  Project-URL: Homepage, https://code.r3pek.org/r3pek/python-mobius
6
6
  Project-URL: Documentation, https://code.r3pek.org/r3pek/python-mobius/src/branch/main/documentation
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "python-mobius"
7
- version = "0.10.2"
7
+ version = "0.10.3"
8
8
  description = "Reverse-engineered Python client for the Mobius BLE protocol (EcoTech Marine VorTech/Radion, AquaIllumination, Neptune Systems, NYOS)"
9
9
  readme = "README.md"
10
10
  license = { text = "GPL-2.0-only" }
@@ -87,7 +87,7 @@ from .mob import (
87
87
  MAX_PUMP_PRIMITIVE_SIZE,
88
88
  )
89
89
 
90
- __version__ = "0.10.2"
90
+ __version__ = "0.10.3"
91
91
 
92
92
  __all__ = [
93
93
  "__version__",
@@ -12,6 +12,7 @@ from __future__ import annotations
12
12
  import asyncio
13
13
  import datetime
14
14
  import struct
15
+ import logging
15
16
  import time
16
17
  from dataclasses import dataclass, field
17
18
  from typing import Optional, Callable, Any
@@ -79,6 +80,8 @@ CHAR_TX_DATA = "01ff0103-ba5e-f4ee-5ca1-eb1e5e4b1ce0" # write: app -> device,
79
80
  CHAR_TX_FINAL = "01ff0104-ba5e-f4ee-5ca1-eb1e5e4b1ce0" # write: app -> device, last fragment
80
81
 
81
82
  # count=0xFFFF requests every element of a list attribute.
83
+ _LOGGER = logging.getLogger(__name__)
84
+
82
85
  ALL_ELEMENTS = 0xFFFF
83
86
 
84
87
  # Write fragment size when the ATT MTU is unknown or the minimum (23).
@@ -87,6 +90,23 @@ DEFAULT_WRITE_SIZE = 20
87
90
  MTU_WRITE_OVERHEAD = 5
88
91
 
89
92
 
93
+ def _log_bad_frame(raw: bytes, frame, trace: list[tuple[str, int]]) -> None:
94
+ """Debug details of a received frame that doesn't parse or fails its
95
+ CRC: what was received against what its header declares, and the
96
+ notifications it was assembled from (D = RX_DATA, F = RX_FINAL)."""
97
+ if not _LOGGER.isEnabledFor(logging.DEBUG):
98
+ return
99
+ declared = struct.unpack_from("<H", raw, 7)[0] + 11 if len(raw) >= 9 else None
100
+ pieces = " ".join(f"{kind}{length}" for kind, length in trace)
101
+ _LOGGER.debug(
102
+ "Received a frame that %s: %d bytes, header declares %s; op_group=%s op_code=%s msg_id=%s; "
103
+ "%d notifications [%s]; first bytes %s; last bytes %s",
104
+ "doesn't parse" if frame is None else "fails its CRC", len(raw), declared,
105
+ f"{frame.op_group:#04x}" if frame else "?", frame.op_code if frame else "?",
106
+ frame.message_id if frame else "?", len(trace), pieces, raw[:24].hex(), raw[-24:].hex(),
107
+ )
108
+
109
+
90
110
  def write_size_for_mtu(att_mtu: Optional[int]) -> int:
91
111
  """Write fragment size for a negotiated ATT MTU: `att_mtu` minus 5, as
92
112
  the app does, or DEFAULT_WRITE_SIZE when it is unknown or not above the
@@ -917,6 +937,9 @@ class MobiusDevice:
917
937
  self.adapter = adapter
918
938
  self._client: Optional[BleakClient] = None
919
939
  self._rx_buffer = bytearray()
940
+ # ("D"/"F", length) of each notification of the frame being
941
+ # received, logged when it fails its CRC.
942
+ self._rx_trace: list[tuple[str, int]] = []
920
943
  # Pending direct requests, keyed by FSCI message ID.
921
944
  self._pending: dict[int, asyncio.Future] = {}
922
945
  # Pending relayed requests, keyed by CoAP token (see mobius.relay).
@@ -1014,12 +1037,18 @@ class MobiusDevice:
1014
1037
 
1015
1038
  def _on_rx_data(self, _handle, data: bytearray):
1016
1039
  self._rx_buffer.extend(data)
1040
+ self._rx_trace.append(("D", len(data)))
1017
1041
 
1018
1042
  def _on_rx_final(self, _handle, data: bytearray):
1019
1043
  self._rx_buffer.extend(data)
1044
+ self._rx_trace.append(("F", len(data)))
1020
1045
  raw = bytes(self._rx_buffer)
1046
+ trace = self._rx_trace
1021
1047
  self._rx_buffer.clear()
1048
+ self._rx_trace = []
1022
1049
  frame = parse_frame(raw)
1050
+ if frame is None or not frame.crc_ok:
1051
+ _log_bad_frame(raw, frame, trace)
1023
1052
  if frame is None:
1024
1053
  return
1025
1054
  if self.on_any_frame is not None:
@@ -819,3 +819,17 @@ class TestRelayedMultiBlockResponse:
819
819
  versions = await relayed.get_firmware_versions(model=Model.RadionXR15wG6Pro)
820
820
  assert versions["Product OS"] == "1.0"
821
821
  assert versions["Radio Firmware"] == "1.5.103"
822
+
823
+
824
+ def test_a_frame_failing_its_crc_is_logged_with_its_notifications(caplog):
825
+ caplog.set_level(logging.DEBUG, logger="mobius.device")
826
+ device = MobiusDevice(BLEDevice("AA:BB:CC:DD:EE:FF", "MOBIUS", {}))
827
+ frame = bytearray(build_frame(OPGROUP_C2CI_REQUEST, 1, b"\x00" * 30, 7))
828
+ frame[-1] ^= 0xFF # corrupt the CRC
829
+
830
+ device._on_rx_data(None, frame[:20])
831
+ device._on_rx_final(None, frame[20:])
832
+
833
+ assert "fails its CRC" in caplog.text
834
+ assert "2 notifications [D20 F" in caplog.text
835
+ assert device._rx_trace == []
File without changes
File without changes