python-mobius 0.10.1__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.
- {python_mobius-0.10.1 → python_mobius-0.10.3}/CHANGELOG.md +9 -0
- {python_mobius-0.10.1 → python_mobius-0.10.3}/PKG-INFO +1 -1
- {python_mobius-0.10.1 → python_mobius-0.10.3}/pyproject.toml +1 -1
- {python_mobius-0.10.1 → python_mobius-0.10.3}/src/mobius/__init__.py +1 -1
- {python_mobius-0.10.1 → python_mobius-0.10.3}/src/mobius/device.py +67 -9
- {python_mobius-0.10.1 → python_mobius-0.10.3}/tests/test_networked_thread_devices.py +65 -0
- {python_mobius-0.10.1 → python_mobius-0.10.3}/tests/test_relay.py +14 -0
- {python_mobius-0.10.1 → python_mobius-0.10.3}/.forgejo/scripts/extract_changelog_section.py +0 -0
- {python_mobius-0.10.1 → python_mobius-0.10.3}/.forgejo/workflows/release.yml +0 -0
- {python_mobius-0.10.1 → python_mobius-0.10.3}/.gitignore +0 -0
- {python_mobius-0.10.1 → python_mobius-0.10.3}/LICENSE +0 -0
- {python_mobius-0.10.1 → python_mobius-0.10.3}/README.md +0 -0
- {python_mobius-0.10.1 → python_mobius-0.10.3}/documentation/00-overview.md +0 -0
- {python_mobius-0.10.1 → python_mobius-0.10.3}/documentation/01-ble-transport.md +0 -0
- {python_mobius-0.10.1 → python_mobius-0.10.3}/documentation/02-framing-and-crc.md +0 -0
- {python_mobius-0.10.1 → python_mobius-0.10.3}/documentation/03-attributes-and-opcodes.md +0 -0
- {python_mobius-0.10.1 → python_mobius-0.10.3}/documentation/04-device-identity.md +0 -0
- {python_mobius-0.10.1 → python_mobius-0.10.3}/documentation/05-scenes.md +0 -0
- {python_mobius-0.10.1 → python_mobius-0.10.3}/documentation/06-light-schedule.md +0 -0
- {python_mobius-0.10.1 → python_mobius-0.10.3}/documentation/07-pump-schedule.md +0 -0
- {python_mobius-0.10.1 → python_mobius-0.10.3}/documentation/08-manufacturer-data.md +0 -0
- {python_mobius-0.10.1 → python_mobius-0.10.3}/documentation/09-thread-coap-relay.md +0 -0
- {python_mobius-0.10.1 → python_mobius-0.10.3}/documentation/10-known-gaps-and-open-questions.md +0 -0
- {python_mobius-0.10.1 → python_mobius-0.10.3}/documentation/11-device-status-attributes.md +0 -0
- {python_mobius-0.10.1 → python_mobius-0.10.3}/documentation/12-device-identity-and-address-stability.md +0 -0
- {python_mobius-0.10.1 → python_mobius-0.10.3}/documentation/13-attribute-dump.md +0 -0
- {python_mobius-0.10.1 → python_mobius-0.10.3}/documentation/14-attribute-reference.md +0 -0
- {python_mobius-0.10.1 → python_mobius-0.10.3}/documentation/15-mob-template-format.md +0 -0
- {python_mobius-0.10.1 → python_mobius-0.10.3}/documentation/16-python-library.md +0 -0
- {python_mobius-0.10.1 → python_mobius-0.10.3}/documentation/17-configuration-checksums.md +0 -0
- {python_mobius-0.10.1 → python_mobius-0.10.3}/src/mobius/checksums.py +0 -0
- {python_mobius-0.10.1 → python_mobius-0.10.3}/src/mobius/cli.py +0 -0
- {python_mobius-0.10.1 → python_mobius-0.10.3}/src/mobius/coap.py +0 -0
- {python_mobius-0.10.1 → python_mobius-0.10.3}/src/mobius/constants.py +0 -0
- {python_mobius-0.10.1 → python_mobius-0.10.3}/src/mobius/crc.py +0 -0
- {python_mobius-0.10.1 → python_mobius-0.10.3}/src/mobius/device_status.py +0 -0
- {python_mobius-0.10.1 → python_mobius-0.10.3}/src/mobius/discovery.py +0 -0
- {python_mobius-0.10.1 → python_mobius-0.10.3}/src/mobius/dump.py +0 -0
- {python_mobius-0.10.1 → python_mobius-0.10.3}/src/mobius/frame.py +0 -0
- {python_mobius-0.10.1 → python_mobius-0.10.3}/src/mobius/manufacturer.py +0 -0
- {python_mobius-0.10.1 → python_mobius-0.10.3}/src/mobius/mesh_address.py +0 -0
- {python_mobius-0.10.1 → python_mobius-0.10.3}/src/mobius/mob.py +0 -0
- {python_mobius-0.10.1 → python_mobius-0.10.3}/src/mobius/modifiers.py +0 -0
- {python_mobius-0.10.1 → python_mobius-0.10.3}/src/mobius/power.py +0 -0
- {python_mobius-0.10.1 → python_mobius-0.10.3}/src/mobius/pump_status.py +0 -0
- {python_mobius-0.10.1 → python_mobius-0.10.3}/src/mobius/relay.py +0 -0
- {python_mobius-0.10.1 → python_mobius-0.10.3}/src/mobius/scenes.py +0 -0
- {python_mobius-0.10.1 → python_mobius-0.10.3}/src/mobius/schedule.py +0 -0
- {python_mobius-0.10.1 → python_mobius-0.10.3}/src/mobius/timezone.py +0 -0
- {python_mobius-0.10.1 → python_mobius-0.10.3}/tests/test_attributes_batch.py +0 -0
- {python_mobius-0.10.1 → python_mobius-0.10.3}/tests/test_battery_settings.py +0 -0
- {python_mobius-0.10.1 → python_mobius-0.10.3}/tests/test_bluetooth_only.py +0 -0
- {python_mobius-0.10.1 → python_mobius-0.10.3}/tests/test_checksums.py +0 -0
- {python_mobius-0.10.1 → python_mobius-0.10.3}/tests/test_cli_group_targets.py +0 -0
- {python_mobius-0.10.1 → python_mobius-0.10.3}/tests/test_cli_mob_file.py +0 -0
- {python_mobius-0.10.1 → python_mobius-0.10.3}/tests/test_cli_schedule_file.py +0 -0
- {python_mobius-0.10.1 → python_mobius-0.10.3}/tests/test_coap.py +0 -0
- {python_mobius-0.10.1 → python_mobius-0.10.3}/tests/test_coap_indication_handling.py +0 -0
- {python_mobius-0.10.1 → python_mobius-0.10.3}/tests/test_configuration_cache.py +0 -0
- {python_mobius-0.10.1 → python_mobius-0.10.3}/tests/test_connection_cleanup.py +0 -0
- {python_mobius-0.10.1 → python_mobius-0.10.3}/tests/test_device_control.py +0 -0
- {python_mobius-0.10.1 → python_mobius-0.10.3}/tests/test_device_status.py +0 -0
- {python_mobius-0.10.1 → python_mobius-0.10.3}/tests/test_device_summary.py +0 -0
- {python_mobius-0.10.1 → python_mobius-0.10.3}/tests/test_device_time.py +0 -0
- {python_mobius-0.10.1 → python_mobius-0.10.3}/tests/test_discovery_company_ids.py +0 -0
- {python_mobius-0.10.1 → python_mobius-0.10.3}/tests/test_dump.py +0 -0
- {python_mobius-0.10.1 → python_mobius-0.10.3}/tests/test_extra_physical_values.py +0 -0
- {python_mobius-0.10.1 → python_mobius-0.10.3}/tests/test_fan_thresholds.py +0 -0
- {python_mobius-0.10.1 → python_mobius-0.10.3}/tests/test_firmware_versions.py +0 -0
- {python_mobius-0.10.1 → python_mobius-0.10.3}/tests/test_frame.py +0 -0
- {python_mobius-0.10.1 → python_mobius-0.10.3}/tests/test_frame_sniffer.py +0 -0
- {python_mobius-0.10.1 → python_mobius-0.10.3}/tests/test_full_poll_batch.py +0 -0
- {python_mobius-0.10.1 → python_mobius-0.10.3}/tests/test_group_bitmap.py +0 -0
- {python_mobius-0.10.1 → python_mobius-0.10.3}/tests/test_hyperdrive_device.py +0 -0
- {python_mobius-0.10.1 → python_mobius-0.10.3}/tests/test_insolation_detection.py +0 -0
- {python_mobius-0.10.1 → python_mobius-0.10.3}/tests/test_light_intensity_branching.py +0 -0
- {python_mobius-0.10.1 → python_mobius-0.10.3}/tests/test_light_poll_batch.py +0 -0
- {python_mobius-0.10.1 → python_mobius-0.10.3}/tests/test_light_schedule.py +0 -0
- {python_mobius-0.10.1 → python_mobius-0.10.3}/tests/test_manufacturer.py +0 -0
- {python_mobius-0.10.1 → python_mobius-0.10.3}/tests/test_manufacturer_lookup.py +0 -0
- {python_mobius-0.10.1 → python_mobius-0.10.3}/tests/test_mesh_address.py +0 -0
- {python_mobius-0.10.1 → python_mobius-0.10.3}/tests/test_mesh_discovery.py +0 -0
- {python_mobius-0.10.1 → python_mobius-0.10.3}/tests/test_mesh_discovery_direct_connect.py +0 -0
- {python_mobius-0.10.1 → python_mobius-0.10.3}/tests/test_metadata_batch.py +0 -0
- {python_mobius-0.10.1 → python_mobius-0.10.3}/tests/test_mob.py +0 -0
- {python_mobius-0.10.1 → python_mobius-0.10.3}/tests/test_modifiers.py +0 -0
- {python_mobius-0.10.1 → python_mobius-0.10.3}/tests/test_new_diagnostic_attributes.py +0 -0
- {python_mobius-0.10.1 → python_mobius-0.10.3}/tests/test_power.py +0 -0
- {python_mobius-0.10.1 → python_mobius-0.10.3}/tests/test_pump_param_ranges.py +0 -0
- {python_mobius-0.10.1 → python_mobius-0.10.3}/tests/test_pump_reverse.py +0 -0
- {python_mobius-0.10.1 → python_mobius-0.10.3}/tests/test_pump_schedule.py +0 -0
- {python_mobius-0.10.1 → python_mobius-0.10.3}/tests/test_pump_status.py +0 -0
- {python_mobius-0.10.1 → python_mobius-0.10.3}/tests/test_pump_supported_modes.py +0 -0
- {python_mobius-0.10.1 → python_mobius-0.10.3}/tests/test_pump_telemetry.py +0 -0
- {python_mobius-0.10.1 → python_mobius-0.10.3}/tests/test_scenes.py +0 -0
- {python_mobius-0.10.1 → python_mobius-0.10.3}/tests/test_schedule_file_format.py +0 -0
- {python_mobius-0.10.1 → python_mobius-0.10.3}/tests/test_schedule_intensity_scalar.py +0 -0
- {python_mobius-0.10.1 → python_mobius-0.10.3}/tests/test_serial_decoding.py +0 -0
- {python_mobius-0.10.1 → python_mobius-0.10.3}/tests/test_serial_identity.py +0 -0
- {python_mobius-0.10.1 → python_mobius-0.10.3}/tests/test_shared_helpers.py +0 -0
- {python_mobius-0.10.1 → python_mobius-0.10.3}/tests/test_supported_attributes.py +0 -0
- {python_mobius-0.10.1 → python_mobius-0.10.3}/tests/test_time_zone.py +0 -0
- {python_mobius-0.10.1 → python_mobius-0.10.3}/tests/test_write_size.py +0 -0
|
@@ -1,5 +1,14 @@
|
|
|
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
|
+
|
|
7
|
+
## 0.10.2
|
|
8
|
+
|
|
9
|
+
- Mesh peer discovery can raise its error (`raise_errors=True`) instead
|
|
10
|
+
of returning an empty list on failure.
|
|
11
|
+
|
|
3
12
|
## 0.10.1
|
|
4
13
|
|
|
5
14
|
- 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.
|
|
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.
|
|
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" }
|
|
@@ -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:
|
|
@@ -1842,7 +1871,7 @@ class MobiusDevice:
|
|
|
1842
1871
|
return None
|
|
1843
1872
|
return raw
|
|
1844
1873
|
|
|
1845
|
-
async def discover_mesh_peers(self) -> list[MeshPeer]:
|
|
1874
|
+
async def discover_mesh_peers(self, raise_errors: bool = False) -> list[MeshPeer]:
|
|
1846
1875
|
"""
|
|
1847
1876
|
Mesh peers from the ShortAddressArray/SerialNumberArray/
|
|
1848
1877
|
DeviceModelArray (3700/3701/3702) parallel arrays, with addresses
|
|
@@ -1852,10 +1881,12 @@ class MobiusDevice:
|
|
|
1852
1881
|
UnsupportedAttribute and this returns []. Whether the connected
|
|
1853
1882
|
device lists itself is not known. Empty slots (serial
|
|
1854
1883
|
"00000000000000") and short entries are skipped. Returns [] on any
|
|
1855
|
-
failure.
|
|
1884
|
+
failure, or raises it with raise_errors=True.
|
|
1856
1885
|
"""
|
|
1857
1886
|
own_address = await self.get_own_mesh_address()
|
|
1858
1887
|
if own_address is None:
|
|
1888
|
+
if raise_errors:
|
|
1889
|
+
raise IOError("own mesh address (MeshLocalAddresses) not readable")
|
|
1859
1890
|
return []
|
|
1860
1891
|
prefix = mesh_local_prefix_from_own_address(own_address)
|
|
1861
1892
|
|
|
@@ -1864,6 +1895,8 @@ class MobiusDevice:
|
|
|
1864
1895
|
models_raw = await self.get_attribute(C2Attribute.DeviceModelArray, index=0, count=ALL_ELEMENTS)
|
|
1865
1896
|
shorts_raw = await self.get_attribute(C2Attribute.ShortAddressArray, index=0, count=ALL_ELEMENTS)
|
|
1866
1897
|
except Exception:
|
|
1898
|
+
if raise_errors:
|
|
1899
|
+
raise
|
|
1867
1900
|
return []
|
|
1868
1901
|
|
|
1869
1902
|
peers: list[MeshPeer] = []
|
|
@@ -1882,7 +1915,7 @@ class MobiusDevice:
|
|
|
1882
1915
|
))
|
|
1883
1916
|
return peers
|
|
1884
1917
|
|
|
1885
|
-
async def discover_networked_thread_devices(self) -> list[MeshPeer]:
|
|
1918
|
+
async def discover_networked_thread_devices(self, raise_errors: bool = False) -> list[MeshPeer]:
|
|
1886
1919
|
"""
|
|
1887
1920
|
Every device on this device's Thread mesh, including itself, from
|
|
1888
1921
|
MLPrefix (2128) and NetworkedThreadDevices (1001). Supported more
|
|
@@ -1896,7 +1929,8 @@ class MobiusDevice:
|
|
|
1896
1929
|
12-13 model id, i16 LE
|
|
1897
1930
|
14-27 serial, ASCII
|
|
1898
1931
|
Empty slots are all zeros and are skipped, as are entries of any
|
|
1899
|
-
other length. Returns [] on any failure
|
|
1932
|
+
other length. Returns [] on any failure, or raises it with
|
|
1933
|
+
raise_errors=True.
|
|
1900
1934
|
"""
|
|
1901
1935
|
try:
|
|
1902
1936
|
mlprefix_raw = await self.get_attribute(C2Attribute.MLPrefix)
|
|
@@ -1904,8 +1938,15 @@ class MobiusDevice:
|
|
|
1904
1938
|
C2Attribute.NetworkedThreadDevices, index=0, count=ALL_ELEMENTS,
|
|
1905
1939
|
)
|
|
1906
1940
|
except Exception:
|
|
1941
|
+
if raise_errors:
|
|
1942
|
+
raise
|
|
1907
1943
|
return []
|
|
1908
1944
|
if not mlprefix_raw or len(mlprefix_raw[0]) != 24:
|
|
1945
|
+
if raise_errors:
|
|
1946
|
+
raise IOError(
|
|
1947
|
+
f"unexpected MLPrefix: {mlprefix_raw[0].hex() if mlprefix_raw else 'empty'} "
|
|
1948
|
+
f"(expected 24 bytes)"
|
|
1949
|
+
)
|
|
1909
1950
|
return []
|
|
1910
1951
|
prefix = mesh_local_prefix_from_mlprefix(mlprefix_raw[0])
|
|
1911
1952
|
|
|
@@ -1928,18 +1969,35 @@ class MobiusDevice:
|
|
|
1928
1969
|
))
|
|
1929
1970
|
return peers
|
|
1930
1971
|
|
|
1931
|
-
async def discover_mesh_peers_auto(self) -> list[MeshPeer]:
|
|
1972
|
+
async def discover_mesh_peers_auto(self, raise_errors: bool = False) -> list[MeshPeer]:
|
|
1932
1973
|
"""
|
|
1933
1974
|
discover_networked_thread_devices(), falling back to
|
|
1934
1975
|
discover_mesh_peers() when it finds nothing. The recommended entry
|
|
1935
1976
|
point. mobius.discovery.discover_mesh_peers_via_direct_connect() is
|
|
1936
1977
|
the remaining fallback, but it needs scan results and has to be
|
|
1937
1978
|
called explicitly.
|
|
1979
|
+
|
|
1980
|
+
raise_errors=True: a failure raises instead of looking like an
|
|
1981
|
+
empty mesh. The fallback is then only tried when
|
|
1982
|
+
NetworkedThreadDevices can't be read (an MXM hub), and the error
|
|
1983
|
+
raised names both attempts.
|
|
1938
1984
|
"""
|
|
1939
|
-
|
|
1940
|
-
|
|
1941
|
-
|
|
1942
|
-
|
|
1985
|
+
if not raise_errors:
|
|
1986
|
+
peers = await self.discover_networked_thread_devices()
|
|
1987
|
+
if peers:
|
|
1988
|
+
return peers
|
|
1989
|
+
return await self.discover_mesh_peers()
|
|
1990
|
+
|
|
1991
|
+
try:
|
|
1992
|
+
return await self.discover_networked_thread_devices(raise_errors=True)
|
|
1993
|
+
except Exception as networked_error:
|
|
1994
|
+
try:
|
|
1995
|
+
return await self.discover_mesh_peers(raise_errors=True)
|
|
1996
|
+
except Exception as arrays_error:
|
|
1997
|
+
raise IOError(
|
|
1998
|
+
f"NetworkedThreadDevices: {networked_error or type(networked_error).__name__}; "
|
|
1999
|
+
f"peer arrays: {arrays_error or type(arrays_error).__name__}"
|
|
2000
|
+
) from networked_error
|
|
1943
2001
|
|
|
1944
2002
|
async def get_firmware_versions(self, model: Optional[Model] = None) -> dict:
|
|
1945
2003
|
"""{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"]
|
|
@@ -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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{python_mobius-0.10.1 → python_mobius-0.10.3}/documentation/10-known-gaps-and-open-questions.md
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|