python-mobius 0.10.1__tar.gz → 0.10.2__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.1 → python_mobius-0.10.2}/CHANGELOG.md +5 -0
  2. {python_mobius-0.10.1 → python_mobius-0.10.2}/PKG-INFO +1 -1
  3. {python_mobius-0.10.1 → python_mobius-0.10.2}/pyproject.toml +1 -1
  4. {python_mobius-0.10.1 → python_mobius-0.10.2}/src/mobius/__init__.py +1 -1
  5. {python_mobius-0.10.1 → python_mobius-0.10.2}/src/mobius/device.py +38 -9
  6. {python_mobius-0.10.1 → python_mobius-0.10.2}/tests/test_networked_thread_devices.py +65 -0
  7. {python_mobius-0.10.1 → python_mobius-0.10.2}/.forgejo/scripts/extract_changelog_section.py +0 -0
  8. {python_mobius-0.10.1 → python_mobius-0.10.2}/.forgejo/workflows/release.yml +0 -0
  9. {python_mobius-0.10.1 → python_mobius-0.10.2}/.gitignore +0 -0
  10. {python_mobius-0.10.1 → python_mobius-0.10.2}/LICENSE +0 -0
  11. {python_mobius-0.10.1 → python_mobius-0.10.2}/README.md +0 -0
  12. {python_mobius-0.10.1 → python_mobius-0.10.2}/documentation/00-overview.md +0 -0
  13. {python_mobius-0.10.1 → python_mobius-0.10.2}/documentation/01-ble-transport.md +0 -0
  14. {python_mobius-0.10.1 → python_mobius-0.10.2}/documentation/02-framing-and-crc.md +0 -0
  15. {python_mobius-0.10.1 → python_mobius-0.10.2}/documentation/03-attributes-and-opcodes.md +0 -0
  16. {python_mobius-0.10.1 → python_mobius-0.10.2}/documentation/04-device-identity.md +0 -0
  17. {python_mobius-0.10.1 → python_mobius-0.10.2}/documentation/05-scenes.md +0 -0
  18. {python_mobius-0.10.1 → python_mobius-0.10.2}/documentation/06-light-schedule.md +0 -0
  19. {python_mobius-0.10.1 → python_mobius-0.10.2}/documentation/07-pump-schedule.md +0 -0
  20. {python_mobius-0.10.1 → python_mobius-0.10.2}/documentation/08-manufacturer-data.md +0 -0
  21. {python_mobius-0.10.1 → python_mobius-0.10.2}/documentation/09-thread-coap-relay.md +0 -0
  22. {python_mobius-0.10.1 → python_mobius-0.10.2}/documentation/10-known-gaps-and-open-questions.md +0 -0
  23. {python_mobius-0.10.1 → python_mobius-0.10.2}/documentation/11-device-status-attributes.md +0 -0
  24. {python_mobius-0.10.1 → python_mobius-0.10.2}/documentation/12-device-identity-and-address-stability.md +0 -0
  25. {python_mobius-0.10.1 → python_mobius-0.10.2}/documentation/13-attribute-dump.md +0 -0
  26. {python_mobius-0.10.1 → python_mobius-0.10.2}/documentation/14-attribute-reference.md +0 -0
  27. {python_mobius-0.10.1 → python_mobius-0.10.2}/documentation/15-mob-template-format.md +0 -0
  28. {python_mobius-0.10.1 → python_mobius-0.10.2}/documentation/16-python-library.md +0 -0
  29. {python_mobius-0.10.1 → python_mobius-0.10.2}/documentation/17-configuration-checksums.md +0 -0
  30. {python_mobius-0.10.1 → python_mobius-0.10.2}/src/mobius/checksums.py +0 -0
  31. {python_mobius-0.10.1 → python_mobius-0.10.2}/src/mobius/cli.py +0 -0
  32. {python_mobius-0.10.1 → python_mobius-0.10.2}/src/mobius/coap.py +0 -0
  33. {python_mobius-0.10.1 → python_mobius-0.10.2}/src/mobius/constants.py +0 -0
  34. {python_mobius-0.10.1 → python_mobius-0.10.2}/src/mobius/crc.py +0 -0
  35. {python_mobius-0.10.1 → python_mobius-0.10.2}/src/mobius/device_status.py +0 -0
  36. {python_mobius-0.10.1 → python_mobius-0.10.2}/src/mobius/discovery.py +0 -0
  37. {python_mobius-0.10.1 → python_mobius-0.10.2}/src/mobius/dump.py +0 -0
  38. {python_mobius-0.10.1 → python_mobius-0.10.2}/src/mobius/frame.py +0 -0
  39. {python_mobius-0.10.1 → python_mobius-0.10.2}/src/mobius/manufacturer.py +0 -0
  40. {python_mobius-0.10.1 → python_mobius-0.10.2}/src/mobius/mesh_address.py +0 -0
  41. {python_mobius-0.10.1 → python_mobius-0.10.2}/src/mobius/mob.py +0 -0
  42. {python_mobius-0.10.1 → python_mobius-0.10.2}/src/mobius/modifiers.py +0 -0
  43. {python_mobius-0.10.1 → python_mobius-0.10.2}/src/mobius/power.py +0 -0
  44. {python_mobius-0.10.1 → python_mobius-0.10.2}/src/mobius/pump_status.py +0 -0
  45. {python_mobius-0.10.1 → python_mobius-0.10.2}/src/mobius/relay.py +0 -0
  46. {python_mobius-0.10.1 → python_mobius-0.10.2}/src/mobius/scenes.py +0 -0
  47. {python_mobius-0.10.1 → python_mobius-0.10.2}/src/mobius/schedule.py +0 -0
  48. {python_mobius-0.10.1 → python_mobius-0.10.2}/src/mobius/timezone.py +0 -0
  49. {python_mobius-0.10.1 → python_mobius-0.10.2}/tests/test_attributes_batch.py +0 -0
  50. {python_mobius-0.10.1 → python_mobius-0.10.2}/tests/test_battery_settings.py +0 -0
  51. {python_mobius-0.10.1 → python_mobius-0.10.2}/tests/test_bluetooth_only.py +0 -0
  52. {python_mobius-0.10.1 → python_mobius-0.10.2}/tests/test_checksums.py +0 -0
  53. {python_mobius-0.10.1 → python_mobius-0.10.2}/tests/test_cli_group_targets.py +0 -0
  54. {python_mobius-0.10.1 → python_mobius-0.10.2}/tests/test_cli_mob_file.py +0 -0
  55. {python_mobius-0.10.1 → python_mobius-0.10.2}/tests/test_cli_schedule_file.py +0 -0
  56. {python_mobius-0.10.1 → python_mobius-0.10.2}/tests/test_coap.py +0 -0
  57. {python_mobius-0.10.1 → python_mobius-0.10.2}/tests/test_coap_indication_handling.py +0 -0
  58. {python_mobius-0.10.1 → python_mobius-0.10.2}/tests/test_configuration_cache.py +0 -0
  59. {python_mobius-0.10.1 → python_mobius-0.10.2}/tests/test_connection_cleanup.py +0 -0
  60. {python_mobius-0.10.1 → python_mobius-0.10.2}/tests/test_device_control.py +0 -0
  61. {python_mobius-0.10.1 → python_mobius-0.10.2}/tests/test_device_status.py +0 -0
  62. {python_mobius-0.10.1 → python_mobius-0.10.2}/tests/test_device_summary.py +0 -0
  63. {python_mobius-0.10.1 → python_mobius-0.10.2}/tests/test_device_time.py +0 -0
  64. {python_mobius-0.10.1 → python_mobius-0.10.2}/tests/test_discovery_company_ids.py +0 -0
  65. {python_mobius-0.10.1 → python_mobius-0.10.2}/tests/test_dump.py +0 -0
  66. {python_mobius-0.10.1 → python_mobius-0.10.2}/tests/test_extra_physical_values.py +0 -0
  67. {python_mobius-0.10.1 → python_mobius-0.10.2}/tests/test_fan_thresholds.py +0 -0
  68. {python_mobius-0.10.1 → python_mobius-0.10.2}/tests/test_firmware_versions.py +0 -0
  69. {python_mobius-0.10.1 → python_mobius-0.10.2}/tests/test_frame.py +0 -0
  70. {python_mobius-0.10.1 → python_mobius-0.10.2}/tests/test_frame_sniffer.py +0 -0
  71. {python_mobius-0.10.1 → python_mobius-0.10.2}/tests/test_full_poll_batch.py +0 -0
  72. {python_mobius-0.10.1 → python_mobius-0.10.2}/tests/test_group_bitmap.py +0 -0
  73. {python_mobius-0.10.1 → python_mobius-0.10.2}/tests/test_hyperdrive_device.py +0 -0
  74. {python_mobius-0.10.1 → python_mobius-0.10.2}/tests/test_insolation_detection.py +0 -0
  75. {python_mobius-0.10.1 → python_mobius-0.10.2}/tests/test_light_intensity_branching.py +0 -0
  76. {python_mobius-0.10.1 → python_mobius-0.10.2}/tests/test_light_poll_batch.py +0 -0
  77. {python_mobius-0.10.1 → python_mobius-0.10.2}/tests/test_light_schedule.py +0 -0
  78. {python_mobius-0.10.1 → python_mobius-0.10.2}/tests/test_manufacturer.py +0 -0
  79. {python_mobius-0.10.1 → python_mobius-0.10.2}/tests/test_manufacturer_lookup.py +0 -0
  80. {python_mobius-0.10.1 → python_mobius-0.10.2}/tests/test_mesh_address.py +0 -0
  81. {python_mobius-0.10.1 → python_mobius-0.10.2}/tests/test_mesh_discovery.py +0 -0
  82. {python_mobius-0.10.1 → python_mobius-0.10.2}/tests/test_mesh_discovery_direct_connect.py +0 -0
  83. {python_mobius-0.10.1 → python_mobius-0.10.2}/tests/test_metadata_batch.py +0 -0
  84. {python_mobius-0.10.1 → python_mobius-0.10.2}/tests/test_mob.py +0 -0
  85. {python_mobius-0.10.1 → python_mobius-0.10.2}/tests/test_modifiers.py +0 -0
  86. {python_mobius-0.10.1 → python_mobius-0.10.2}/tests/test_new_diagnostic_attributes.py +0 -0
  87. {python_mobius-0.10.1 → python_mobius-0.10.2}/tests/test_power.py +0 -0
  88. {python_mobius-0.10.1 → python_mobius-0.10.2}/tests/test_pump_param_ranges.py +0 -0
  89. {python_mobius-0.10.1 → python_mobius-0.10.2}/tests/test_pump_reverse.py +0 -0
  90. {python_mobius-0.10.1 → python_mobius-0.10.2}/tests/test_pump_schedule.py +0 -0
  91. {python_mobius-0.10.1 → python_mobius-0.10.2}/tests/test_pump_status.py +0 -0
  92. {python_mobius-0.10.1 → python_mobius-0.10.2}/tests/test_pump_supported_modes.py +0 -0
  93. {python_mobius-0.10.1 → python_mobius-0.10.2}/tests/test_pump_telemetry.py +0 -0
  94. {python_mobius-0.10.1 → python_mobius-0.10.2}/tests/test_relay.py +0 -0
  95. {python_mobius-0.10.1 → python_mobius-0.10.2}/tests/test_scenes.py +0 -0
  96. {python_mobius-0.10.1 → python_mobius-0.10.2}/tests/test_schedule_file_format.py +0 -0
  97. {python_mobius-0.10.1 → python_mobius-0.10.2}/tests/test_schedule_intensity_scalar.py +0 -0
  98. {python_mobius-0.10.1 → python_mobius-0.10.2}/tests/test_serial_decoding.py +0 -0
  99. {python_mobius-0.10.1 → python_mobius-0.10.2}/tests/test_serial_identity.py +0 -0
  100. {python_mobius-0.10.1 → python_mobius-0.10.2}/tests/test_shared_helpers.py +0 -0
  101. {python_mobius-0.10.1 → python_mobius-0.10.2}/tests/test_supported_attributes.py +0 -0
  102. {python_mobius-0.10.1 → python_mobius-0.10.2}/tests/test_time_zone.py +0 -0
  103. {python_mobius-0.10.1 → python_mobius-0.10.2}/tests/test_write_size.py +0 -0
@@ -1,5 +1,10 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.10.2
4
+
5
+ - Mesh peer discovery can raise its error (`raise_errors=True`) instead
6
+ of returning an empty list on failure.
7
+
3
8
  ## 0.10.1
4
9
 
5
10
  - Fixed lights not being read through a gateway: the poll's reply was too
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: python-mobius
3
- Version: 0.10.1
3
+ Version: 0.10.2
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.1"
7
+ version = "0.10.2"
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.1"
90
+ __version__ = "0.10.2"
91
91
 
92
92
  __all__ = [
93
93
  "__version__",
@@ -1842,7 +1842,7 @@ class MobiusDevice:
1842
1842
  return None
1843
1843
  return raw
1844
1844
 
1845
- async def discover_mesh_peers(self) -> list[MeshPeer]:
1845
+ async def discover_mesh_peers(self, raise_errors: bool = False) -> list[MeshPeer]:
1846
1846
  """
1847
1847
  Mesh peers from the ShortAddressArray/SerialNumberArray/
1848
1848
  DeviceModelArray (3700/3701/3702) parallel arrays, with addresses
@@ -1852,10 +1852,12 @@ class MobiusDevice:
1852
1852
  UnsupportedAttribute and this returns []. Whether the connected
1853
1853
  device lists itself is not known. Empty slots (serial
1854
1854
  "00000000000000") and short entries are skipped. Returns [] on any
1855
- failure.
1855
+ failure, or raises it with raise_errors=True.
1856
1856
  """
1857
1857
  own_address = await self.get_own_mesh_address()
1858
1858
  if own_address is None:
1859
+ if raise_errors:
1860
+ raise IOError("own mesh address (MeshLocalAddresses) not readable")
1859
1861
  return []
1860
1862
  prefix = mesh_local_prefix_from_own_address(own_address)
1861
1863
 
@@ -1864,6 +1866,8 @@ class MobiusDevice:
1864
1866
  models_raw = await self.get_attribute(C2Attribute.DeviceModelArray, index=0, count=ALL_ELEMENTS)
1865
1867
  shorts_raw = await self.get_attribute(C2Attribute.ShortAddressArray, index=0, count=ALL_ELEMENTS)
1866
1868
  except Exception:
1869
+ if raise_errors:
1870
+ raise
1867
1871
  return []
1868
1872
 
1869
1873
  peers: list[MeshPeer] = []
@@ -1882,7 +1886,7 @@ class MobiusDevice:
1882
1886
  ))
1883
1887
  return peers
1884
1888
 
1885
- async def discover_networked_thread_devices(self) -> list[MeshPeer]:
1889
+ async def discover_networked_thread_devices(self, raise_errors: bool = False) -> list[MeshPeer]:
1886
1890
  """
1887
1891
  Every device on this device's Thread mesh, including itself, from
1888
1892
  MLPrefix (2128) and NetworkedThreadDevices (1001). Supported more
@@ -1896,7 +1900,8 @@ class MobiusDevice:
1896
1900
  12-13 model id, i16 LE
1897
1901
  14-27 serial, ASCII
1898
1902
  Empty slots are all zeros and are skipped, as are entries of any
1899
- other length. Returns [] on any failure.
1903
+ other length. Returns [] on any failure, or raises it with
1904
+ raise_errors=True.
1900
1905
  """
1901
1906
  try:
1902
1907
  mlprefix_raw = await self.get_attribute(C2Attribute.MLPrefix)
@@ -1904,8 +1909,15 @@ class MobiusDevice:
1904
1909
  C2Attribute.NetworkedThreadDevices, index=0, count=ALL_ELEMENTS,
1905
1910
  )
1906
1911
  except Exception:
1912
+ if raise_errors:
1913
+ raise
1907
1914
  return []
1908
1915
  if not mlprefix_raw or len(mlprefix_raw[0]) != 24:
1916
+ if raise_errors:
1917
+ raise IOError(
1918
+ f"unexpected MLPrefix: {mlprefix_raw[0].hex() if mlprefix_raw else 'empty'} "
1919
+ f"(expected 24 bytes)"
1920
+ )
1909
1921
  return []
1910
1922
  prefix = mesh_local_prefix_from_mlprefix(mlprefix_raw[0])
1911
1923
 
@@ -1928,18 +1940,35 @@ class MobiusDevice:
1928
1940
  ))
1929
1941
  return peers
1930
1942
 
1931
- async def discover_mesh_peers_auto(self) -> list[MeshPeer]:
1943
+ async def discover_mesh_peers_auto(self, raise_errors: bool = False) -> list[MeshPeer]:
1932
1944
  """
1933
1945
  discover_networked_thread_devices(), falling back to
1934
1946
  discover_mesh_peers() when it finds nothing. The recommended entry
1935
1947
  point. mobius.discovery.discover_mesh_peers_via_direct_connect() is
1936
1948
  the remaining fallback, but it needs scan results and has to be
1937
1949
  called explicitly.
1950
+
1951
+ raise_errors=True: a failure raises instead of looking like an
1952
+ empty mesh. The fallback is then only tried when
1953
+ NetworkedThreadDevices can't be read (an MXM hub), and the error
1954
+ raised names both attempts.
1938
1955
  """
1939
- peers = await self.discover_networked_thread_devices()
1940
- if peers:
1941
- return peers
1942
- return await self.discover_mesh_peers()
1956
+ if not raise_errors:
1957
+ peers = await self.discover_networked_thread_devices()
1958
+ if peers:
1959
+ return peers
1960
+ return await self.discover_mesh_peers()
1961
+
1962
+ try:
1963
+ return await self.discover_networked_thread_devices(raise_errors=True)
1964
+ except Exception as networked_error:
1965
+ try:
1966
+ return await self.discover_mesh_peers(raise_errors=True)
1967
+ except Exception as arrays_error:
1968
+ raise IOError(
1969
+ f"NetworkedThreadDevices: {networked_error or type(networked_error).__name__}; "
1970
+ f"peer arrays: {arrays_error or type(arrays_error).__name__}"
1971
+ ) from networked_error
1943
1972
 
1944
1973
  async def get_firmware_versions(self, model: Optional[Model] = None) -> dict:
1945
1974
  """{label: version} for every firmware component (FirmwareVersion,
@@ -328,3 +328,68 @@ async def test_discover_networked_thread_devices_real_hardware_capture():
328
328
  assert p.model == Model.RadionXR15wG6Pro
329
329
  assert p.age == 8490
330
330
  assert p.short_address == 0x0400
331
+
332
+
333
+ @pytest.mark.asyncio
334
+ async def test_raise_errors_reports_a_failed_read_instead_of_an_empty_mesh():
335
+ device = MobiusDevice(_fake_ble_device())
336
+
337
+ async def fake_get_attribute(attr_id, index=0, count=1):
338
+ raise IOError("timed out")
339
+
340
+ device.get_attribute = fake_get_attribute
341
+
342
+ assert await device.discover_networked_thread_devices() == []
343
+ with pytest.raises(IOError, match="timed out"):
344
+ await device.discover_networked_thread_devices(raise_errors=True)
345
+
346
+
347
+ @pytest.mark.asyncio
348
+ async def test_raise_errors_reports_an_unexpected_mlprefix():
349
+ device = MobiusDevice(_fake_ble_device())
350
+
351
+ async def fake_get_attribute(attr_id, index=0, count=1):
352
+ if attr_id == C2Attribute.MLPrefix:
353
+ return [PREFIX] # 8 bytes instead of 24
354
+ return []
355
+
356
+ device.get_attribute = fake_get_attribute
357
+
358
+ with pytest.raises(IOError, match="unexpected MLPrefix"):
359
+ await device.discover_networked_thread_devices(raise_errors=True)
360
+
361
+
362
+ @pytest.mark.asyncio
363
+ async def test_auto_with_raise_errors_names_both_attempts_when_both_fail():
364
+ device = MobiusDevice(_fake_ble_device())
365
+
366
+ async def fake_get_attribute(attr_id, index=0, count=1):
367
+ raise IOError(f"no answer for {int(attr_id)}")
368
+
369
+ async def no_own_address():
370
+ return None
371
+
372
+ device.get_attribute = fake_get_attribute
373
+ device.get_own_mesh_address = no_own_address
374
+
375
+ assert await device.discover_mesh_peers_auto() == []
376
+ with pytest.raises(IOError, match=r"NetworkedThreadDevices: no answer for 2128; peer arrays: own mesh address"):
377
+ await device.discover_mesh_peers_auto(raise_errors=True)
378
+
379
+
380
+ @pytest.mark.asyncio
381
+ async def test_auto_with_raise_errors_returns_networked_devices():
382
+ device = MobiusDevice(_fake_ble_device())
383
+ suffix = bytes.fromhex("000000fffe00") + struct.pack("<H", 0x1234)
384
+
385
+ async def fake_get_attribute(attr_id, index=0, count=1):
386
+ if attr_id == C2Attribute.MLPrefix:
387
+ return [MLPREFIX_RAW]
388
+ if attr_id == C2Attribute.NetworkedThreadDevices:
389
+ return [_entry(suffix, 42, Model.VorTechMP40wG3QD, "00000000000001")]
390
+ raise AssertionError("the peer arrays must not be read")
391
+
392
+ device.get_attribute = fake_get_attribute
393
+
394
+ peers = await device.discover_mesh_peers_auto(raise_errors=True)
395
+ assert [p.serial for p in peers] == ["00000000000001"]
File without changes
File without changes