python-mobius 0.11.0__tar.gz → 0.12.0__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.11.0 → python_mobius-0.12.0}/CHANGELOG.md +43 -0
- {python_mobius-0.11.0 → python_mobius-0.12.0}/PKG-INFO +1 -1
- {python_mobius-0.11.0 → python_mobius-0.12.0}/documentation/16-python-library.md +8 -6
- {python_mobius-0.11.0 → python_mobius-0.12.0}/pyproject.toml +1 -1
- {python_mobius-0.11.0 → python_mobius-0.12.0}/src/mobius/__init__.py +5 -5
- {python_mobius-0.11.0 → python_mobius-0.12.0}/src/mobius/cli.py +85 -83
- {python_mobius-0.11.0 → python_mobius-0.12.0}/src/mobius/device.py +559 -539
- {python_mobius-0.11.0 → python_mobius-0.12.0}/src/mobius/device_status.py +19 -7
- {python_mobius-0.11.0 → python_mobius-0.12.0}/src/mobius/discovery.py +44 -29
- {python_mobius-0.11.0 → python_mobius-0.12.0}/src/mobius/dump.py +12 -17
- {python_mobius-0.11.0 → python_mobius-0.12.0}/src/mobius/modifiers.py +0 -2
- {python_mobius-0.11.0 → python_mobius-0.12.0}/src/mobius/relay.py +14 -48
- {python_mobius-0.11.0 → python_mobius-0.12.0}/src/mobius/scenes.py +4 -3
- {python_mobius-0.11.0 → python_mobius-0.12.0}/src/mobius/schedule.py +31 -19
- {python_mobius-0.11.0 → python_mobius-0.12.0}/src/mobius/timezone.py +2 -6
- python_mobius-0.12.0/tests/fake_reads.py +20 -0
- {python_mobius-0.11.0 → python_mobius-0.12.0}/tests/test_attributes_batch.py +4 -3
- {python_mobius-0.11.0 → python_mobius-0.12.0}/tests/test_bluetooth_only.py +1 -1
- {python_mobius-0.11.0 → python_mobius-0.12.0}/tests/test_checksums.py +1 -1
- python_mobius-0.12.0/tests/test_cli_diagnostics.py +31 -0
- {python_mobius-0.11.0 → python_mobius-0.12.0}/tests/test_cli_group_targets.py +11 -12
- {python_mobius-0.11.0 → python_mobius-0.12.0}/tests/test_cli_mob_file.py +3 -3
- {python_mobius-0.11.0 → python_mobius-0.12.0}/tests/test_coap_indication_handling.py +5 -18
- {python_mobius-0.11.0 → python_mobius-0.12.0}/tests/test_connection_cleanup.py +6 -4
- {python_mobius-0.11.0 → python_mobius-0.12.0}/tests/test_device_control.py +3 -2
- {python_mobius-0.11.0 → python_mobius-0.12.0}/tests/test_device_summary.py +64 -16
- {python_mobius-0.11.0 → python_mobius-0.12.0}/tests/test_discovery_company_ids.py +15 -0
- {python_mobius-0.11.0 → python_mobius-0.12.0}/tests/test_dump.py +16 -3
- python_mobius-0.12.0/tests/test_enum_decoding.py +37 -0
- {python_mobius-0.11.0 → python_mobius-0.12.0}/tests/test_firmware_updates.py +2 -2
- {python_mobius-0.11.0 → python_mobius-0.12.0}/tests/test_firmware_versions.py +75 -38
- {python_mobius-0.11.0 → python_mobius-0.12.0}/tests/test_full_poll_batch.py +105 -26
- python_mobius-0.12.0/tests/test_get_attribute.py +50 -0
- {python_mobius-0.11.0 → python_mobius-0.12.0}/tests/test_hyperdrive_device.py +17 -15
- {python_mobius-0.11.0 → python_mobius-0.12.0}/tests/test_insolation_detection.py +7 -5
- {python_mobius-0.11.0 → python_mobius-0.12.0}/tests/test_light_intensity_branching.py +13 -11
- {python_mobius-0.11.0 → python_mobius-0.12.0}/tests/test_light_poll_batch.py +5 -3
- {python_mobius-0.11.0 → python_mobius-0.12.0}/tests/test_mesh_discovery.py +51 -1
- {python_mobius-0.11.0 → python_mobius-0.12.0}/tests/test_metadata_batch.py +38 -49
- python_mobius-0.12.0/tests/test_model_decoding.py +39 -0
- {python_mobius-0.11.0 → python_mobius-0.12.0}/tests/test_networked_thread_devices.py +1 -1
- {python_mobius-0.11.0 → python_mobius-0.12.0}/tests/test_new_diagnostic_attributes.py +10 -15
- {python_mobius-0.11.0 → python_mobius-0.12.0}/tests/test_pump_telemetry.py +40 -7
- python_mobius-0.12.0/tests/test_received_frame_logging.py +33 -0
- {python_mobius-0.11.0 → python_mobius-0.12.0}/tests/test_relay.py +6 -9
- {python_mobius-0.11.0 → python_mobius-0.12.0}/tests/test_schedule_intensity_scalar.py +8 -6
- {python_mobius-0.11.0 → python_mobius-0.12.0}/tests/test_serial_identity.py +12 -8
- python_mobius-0.12.0/tests/test_unknown_light_channels.py +33 -0
- python_mobius-0.11.0/tests/test_frame_sniffer.py +0 -88
- {python_mobius-0.11.0 → python_mobius-0.12.0}/.forgejo/scripts/extract_changelog_section.py +0 -0
- {python_mobius-0.11.0 → python_mobius-0.12.0}/.forgejo/workflows/release.yml +0 -0
- {python_mobius-0.11.0 → python_mobius-0.12.0}/.gitignore +0 -0
- {python_mobius-0.11.0 → python_mobius-0.12.0}/LICENSE +0 -0
- {python_mobius-0.11.0 → python_mobius-0.12.0}/README.md +0 -0
- {python_mobius-0.11.0 → python_mobius-0.12.0}/documentation/00-overview.md +0 -0
- {python_mobius-0.11.0 → python_mobius-0.12.0}/documentation/01-ble-transport.md +0 -0
- {python_mobius-0.11.0 → python_mobius-0.12.0}/documentation/02-framing-and-crc.md +0 -0
- {python_mobius-0.11.0 → python_mobius-0.12.0}/documentation/03-attributes-and-opcodes.md +0 -0
- {python_mobius-0.11.0 → python_mobius-0.12.0}/documentation/04-device-identity.md +0 -0
- {python_mobius-0.11.0 → python_mobius-0.12.0}/documentation/05-scenes.md +0 -0
- {python_mobius-0.11.0 → python_mobius-0.12.0}/documentation/06-light-schedule.md +0 -0
- {python_mobius-0.11.0 → python_mobius-0.12.0}/documentation/07-pump-schedule.md +0 -0
- {python_mobius-0.11.0 → python_mobius-0.12.0}/documentation/08-manufacturer-data.md +0 -0
- {python_mobius-0.11.0 → python_mobius-0.12.0}/documentation/09-thread-coap-relay.md +0 -0
- {python_mobius-0.11.0 → python_mobius-0.12.0}/documentation/10-known-gaps-and-open-questions.md +0 -0
- {python_mobius-0.11.0 → python_mobius-0.12.0}/documentation/11-device-status-attributes.md +0 -0
- {python_mobius-0.11.0 → python_mobius-0.12.0}/documentation/12-device-identity-and-address-stability.md +0 -0
- {python_mobius-0.11.0 → python_mobius-0.12.0}/documentation/13-attribute-dump.md +0 -0
- {python_mobius-0.11.0 → python_mobius-0.12.0}/documentation/14-attribute-reference.md +0 -0
- {python_mobius-0.11.0 → python_mobius-0.12.0}/documentation/15-mob-template-format.md +0 -0
- {python_mobius-0.11.0 → python_mobius-0.12.0}/documentation/17-configuration-checksums.md +0 -0
- {python_mobius-0.11.0 → python_mobius-0.12.0}/src/mobius/checksums.py +0 -0
- {python_mobius-0.11.0 → python_mobius-0.12.0}/src/mobius/coap.py +0 -0
- {python_mobius-0.11.0 → python_mobius-0.12.0}/src/mobius/constants.py +0 -0
- {python_mobius-0.11.0 → python_mobius-0.12.0}/src/mobius/crc.py +0 -0
- {python_mobius-0.11.0 → python_mobius-0.12.0}/src/mobius/firmware.py +0 -0
- {python_mobius-0.11.0 → python_mobius-0.12.0}/src/mobius/frame.py +0 -0
- {python_mobius-0.11.0 → python_mobius-0.12.0}/src/mobius/manufacturer.py +0 -0
- {python_mobius-0.11.0 → python_mobius-0.12.0}/src/mobius/mesh_address.py +0 -0
- {python_mobius-0.11.0 → python_mobius-0.12.0}/src/mobius/mob.py +0 -0
- {python_mobius-0.11.0 → python_mobius-0.12.0}/src/mobius/power.py +0 -0
- {python_mobius-0.11.0 → python_mobius-0.12.0}/src/mobius/pump_status.py +0 -0
- {python_mobius-0.11.0 → python_mobius-0.12.0}/tests/test_battery_settings.py +0 -0
- {python_mobius-0.11.0 → python_mobius-0.12.0}/tests/test_cli_schedule_file.py +0 -0
- {python_mobius-0.11.0 → python_mobius-0.12.0}/tests/test_coap.py +0 -0
- {python_mobius-0.11.0 → python_mobius-0.12.0}/tests/test_configuration_cache.py +0 -0
- {python_mobius-0.11.0 → python_mobius-0.12.0}/tests/test_device_status.py +0 -0
- {python_mobius-0.11.0 → python_mobius-0.12.0}/tests/test_device_time.py +0 -0
- {python_mobius-0.11.0 → python_mobius-0.12.0}/tests/test_extra_physical_values.py +0 -0
- {python_mobius-0.11.0 → python_mobius-0.12.0}/tests/test_fan_thresholds.py +0 -0
- {python_mobius-0.11.0 → python_mobius-0.12.0}/tests/test_frame.py +0 -0
- {python_mobius-0.11.0 → python_mobius-0.12.0}/tests/test_group_bitmap.py +0 -0
- {python_mobius-0.11.0 → python_mobius-0.12.0}/tests/test_light_schedule.py +0 -0
- {python_mobius-0.11.0 → python_mobius-0.12.0}/tests/test_manufacturer.py +0 -0
- {python_mobius-0.11.0 → python_mobius-0.12.0}/tests/test_manufacturer_lookup.py +0 -0
- {python_mobius-0.11.0 → python_mobius-0.12.0}/tests/test_mesh_address.py +0 -0
- {python_mobius-0.11.0 → python_mobius-0.12.0}/tests/test_mesh_discovery_direct_connect.py +0 -0
- {python_mobius-0.11.0 → python_mobius-0.12.0}/tests/test_mob.py +0 -0
- {python_mobius-0.11.0 → python_mobius-0.12.0}/tests/test_modifiers.py +0 -0
- {python_mobius-0.11.0 → python_mobius-0.12.0}/tests/test_power.py +0 -0
- {python_mobius-0.11.0 → python_mobius-0.12.0}/tests/test_pump_param_ranges.py +0 -0
- {python_mobius-0.11.0 → python_mobius-0.12.0}/tests/test_pump_reverse.py +0 -0
- {python_mobius-0.11.0 → python_mobius-0.12.0}/tests/test_pump_schedule.py +0 -0
- {python_mobius-0.11.0 → python_mobius-0.12.0}/tests/test_pump_status.py +0 -0
- {python_mobius-0.11.0 → python_mobius-0.12.0}/tests/test_pump_supported_modes.py +0 -0
- {python_mobius-0.11.0 → python_mobius-0.12.0}/tests/test_scenes.py +0 -0
- {python_mobius-0.11.0 → python_mobius-0.12.0}/tests/test_schedule_file_format.py +0 -0
- {python_mobius-0.11.0 → python_mobius-0.12.0}/tests/test_serial_decoding.py +0 -0
- {python_mobius-0.11.0 → python_mobius-0.12.0}/tests/test_shared_helpers.py +0 -0
- {python_mobius-0.11.0 → python_mobius-0.12.0}/tests/test_supported_attributes.py +0 -0
- {python_mobius-0.11.0 → python_mobius-0.12.0}/tests/test_time_zone.py +0 -0
- {python_mobius-0.11.0 → python_mobius-0.12.0}/tests/test_write_size.py +0 -0
|
@@ -1,5 +1,48 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.12.0
|
|
4
|
+
|
|
5
|
+
- Fixed `get_attribute()` keeping only the first block of a split reply
|
|
6
|
+
(schedules, scenes and mesh peer lists could come back incomplete).
|
|
7
|
+
- `get_attribute_raw_all()` is now `get_attribute_blocks()`;
|
|
8
|
+
`get_attribute_raw()` and `get_attribute_all()` are removed.
|
|
9
|
+
- Unknown PrimitiveType and OperationState values decode to `None` instead
|
|
10
|
+
of raising; `find_device_by_serial()` stops scanning once it's seen.
|
|
11
|
+
- Removed the `MobiusPump` alias, `wait_for_coap_response()` and the unused
|
|
12
|
+
`which` argument of `get_lunar_enabled()`/`get_insolation_enabled()`.
|
|
13
|
+
- Relay debugging uses debug logging (`mobius.relay`, `mobius.device`)
|
|
14
|
+
instead of `debug=True` prints and the `on_any_frame` hook.
|
|
15
|
+
- A `MeshPeer` can be built from a serial and address alone.
|
|
16
|
+
- When batching fails, the same attributes are read one at a time and
|
|
17
|
+
decoded the same way (no more double reads); an unreachable device raises.
|
|
18
|
+
- `get_current_light_intensities()` uses the light poll's calculation;
|
|
19
|
+
decoders of single-value attributes take one value.
|
|
20
|
+
- `get_full_poll_batch()` works without `primitive` (it's read first) and
|
|
21
|
+
for any device: only lights and pumps get their type-specific data.
|
|
22
|
+
- The full poll includes Vectra settings and the main firmware version
|
|
23
|
+
(`main_firmware_version()`).
|
|
24
|
+
- `mobius-scan --dump-diagnostics` prints the diagnostic attributes.
|
|
25
|
+
- Model ids read from a device are unsigned, as in advertisements (the
|
|
26
|
+
development boards, 61441-61445, decoded as unknown).
|
|
27
|
+
- Mesh peer lists are decoded by `decode_networked_thread_devices()` /
|
|
28
|
+
`decode_mesh_peer_arrays()`, shared with `mobius-scan`; `discover_tank()`
|
|
29
|
+
leaves out its own entry when its serial can't be read.
|
|
30
|
+
- `decode_device_info()` takes the decoded PrimitiveType
|
|
31
|
+
(`decode_primitive_type()`) instead of its raw bytes.
|
|
32
|
+
- `get_pump_telemetry()` is one batched read (one at a time as a fallback),
|
|
33
|
+
decoded like the full poll's (`decode_pump_telemetry_from_batch()`).
|
|
34
|
+
- The poll no longer requests ErrorState twice.
|
|
35
|
+
- `get_device_summary()` is the full poll plus one batch (it read each
|
|
36
|
+
attribute separately, some twice); a pump's summary has no
|
|
37
|
+
`schedule_error`. `LightPollResult` includes the acclimation ramp.
|
|
38
|
+
- One attribute at a time, a schedule or scene list that can't be read no
|
|
39
|
+
longer fails the whole poll.
|
|
40
|
+
- Light channel ids that aren't a `VisualID` are kept in
|
|
41
|
+
`LightPrimitive.unknown_channels` (they all became `VisualID.Unknown`, and
|
|
42
|
+
were written back as channel 0).
|
|
43
|
+
- `decode_active_scene()` accepts `None`; attribute dumps decode ErrorState
|
|
44
|
+
as the device does (16-bit).
|
|
45
|
+
|
|
3
46
|
## 0.11.0
|
|
4
47
|
|
|
5
48
|
- Added firmware update checks (`firmware.py`): the images the app checks
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: python-mobius
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.12.0
|
|
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
|
|
@@ -43,8 +43,8 @@ async with MobiusDevice(serial="00000000000002") as device:
|
|
|
43
43
|
|
|
44
44
|
## Reading and writing
|
|
45
45
|
|
|
46
|
-
Low level: `get_attribute()
|
|
47
|
-
`
|
|
46
|
+
Low level: `get_attribute()` (values of every block),
|
|
47
|
+
`get_attribute_blocks()` (blocks with their indexes),
|
|
48
48
|
`get_attributes_batch()`, `get_supported_attributes()`, `set_attribute()`,
|
|
49
49
|
`set_attribute_multi()`. Every read that fails raises; the higher-level
|
|
50
50
|
getters return `None` or an empty result for unsupported attributes.
|
|
@@ -83,12 +83,13 @@ fails (`used_batch` on the result says which path ran):
|
|
|
83
83
|
|---|---|
|
|
84
84
|
| `get_metadata_batch()` | Advanced features, calibration, firmware, hardware, channels, error state, clock |
|
|
85
85
|
| `get_light_poll_batch()` | A light's schedule and every input of the intensity computation |
|
|
86
|
-
| `get_full_poll_batch(
|
|
86
|
+
| `get_full_poll_batch()` | Identity, metadata, the light or pump state (including battery settings, operation mode and Vectra settings), and scenes |
|
|
87
87
|
|
|
88
88
|
Pass `supported_attribute_ids` (read once with
|
|
89
89
|
`get_supported_attributes()`) to skip listing attributes on every poll.
|
|
90
|
-
`get_full_poll_batch()`
|
|
91
|
-
`
|
|
90
|
+
`get_full_poll_batch()` reads the `PrimitiveType` first when it isn't
|
|
91
|
+
passed; pass the result's `primitive` and `model` to later polls and one
|
|
92
|
+
request per poll is enough.
|
|
92
93
|
|
|
93
94
|
Pass a `ConfigurationCache` (one per device, kept between polls) as
|
|
94
95
|
`get_full_poll_batch(cache=...)` to read the scenes and the schedule only
|
|
@@ -157,9 +158,10 @@ With `--by-serial SERIAL` (connects to one device):
|
|
|
157
158
|
| Option | Does |
|
|
158
159
|
|---|---|
|
|
159
160
|
| `--dump-schedule`, `--dump-scenes` | Print the schedule and the scene slots |
|
|
161
|
+
| `--dump-diagnostics` | Print the diagnostic attributes (clock sync, Thread, BLE, FSCI, ...) |
|
|
160
162
|
| `--dump-mesh-peers` | Mesh discovery, step by step |
|
|
161
163
|
| `--relay-target SERIAL` | Reach another device through this one; the options below then act on it. A Bluetooth-only device is connected directly instead ([09](./09-thread-coap-relay.md#bluetooth-only-devices)) |
|
|
162
|
-
| `--debug-relay` |
|
|
164
|
+
| `--debug-relay` | Log every layer of each relayed request and every received frame |
|
|
163
165
|
| `--set-time-to-now`, `--reboot` | Commands |
|
|
164
166
|
| `--set-local-control-enabled`, `--set-auto-dim-timeout`, `--set-max-fan-speed`, `--set-fan-shutdown-enabled` | Advanced features |
|
|
165
167
|
| `--set-battery-backup-speed`, `--set-boosted-battery-power`, `--set-boosted-battery-on-time`, `--set-boosted-battery-off-time` | Pump battery settings |
|
|
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "python-mobius"
|
|
7
|
-
version = "0.
|
|
7
|
+
version = "0.12.0"
|
|
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" }
|
|
@@ -64,14 +64,14 @@ from .mesh_address import (
|
|
|
64
64
|
is_short_address_derived, format_mesh_address,
|
|
65
65
|
)
|
|
66
66
|
from .relay import RelayedMobiusDevice
|
|
67
|
-
from .device import firmware_label
|
|
67
|
+
from .device import firmware_label, main_firmware_version
|
|
68
68
|
from .firmware import (
|
|
69
69
|
FIRMWARE_CLOUD_HOST, ImageId, FirmwareImage, installable_images, firmware_images,
|
|
70
70
|
firmware_check_url, AvailableFirmware, FirmwareCheck, parse_firmware_check,
|
|
71
71
|
ChangelogEntry, Changelog, parse_changelog, version_tuple,
|
|
72
72
|
)
|
|
73
73
|
from .device import (
|
|
74
|
-
MobiusDevice,
|
|
74
|
+
MobiusDevice, LightIntensityResult, LightPollResult, FullPollResult,
|
|
75
75
|
schedule_capacity, min_schedule_capacity, tank_group,
|
|
76
76
|
SERVICE_GENERAL, CHAR_RX_DATA, CHAR_RX_FINAL, CHAR_TX_DATA, CHAR_TX_FINAL,
|
|
77
77
|
)
|
|
@@ -93,7 +93,7 @@ from .mob import (
|
|
|
93
93
|
MAX_PUMP_PRIMITIVE_SIZE,
|
|
94
94
|
)
|
|
95
95
|
|
|
96
|
-
__version__ = "0.
|
|
96
|
+
__version__ = "0.12.0"
|
|
97
97
|
|
|
98
98
|
__all__ = [
|
|
99
99
|
"__version__",
|
|
@@ -120,7 +120,7 @@ __all__ = [
|
|
|
120
120
|
"get_pump_reverse",
|
|
121
121
|
"FirmwareType", "FIRMWARE_TYPE_LABELS_ETM", "HardwareInfo",
|
|
122
122
|
# firmware
|
|
123
|
-
"firmware_label", "FIRMWARE_CLOUD_HOST", "ImageId", "FirmwareImage", "installable_images", "firmware_images",
|
|
123
|
+
"firmware_label", "main_firmware_version", "FIRMWARE_CLOUD_HOST", "ImageId", "FirmwareImage", "installable_images", "firmware_images",
|
|
124
124
|
"firmware_check_url", "AvailableFirmware", "FirmwareCheck", "parse_firmware_check",
|
|
125
125
|
"ChangelogEntry", "Changelog", "parse_changelog", "version_tuple",
|
|
126
126
|
"Color", "COLOR_LABELS", "RadioType", "RADIO_TYPE_LABELS",
|
|
@@ -153,7 +153,7 @@ __all__ = [
|
|
|
153
153
|
"format_mesh_address", "posix_tz_string", "TIME_ZONE_FIELD_SIZE",
|
|
154
154
|
"RelayedMobiusDevice",
|
|
155
155
|
# device
|
|
156
|
-
"MobiusDevice", "
|
|
156
|
+
"MobiusDevice", "LightIntensityResult", "LightPollResult", "FullPollResult",
|
|
157
157
|
"schedule_capacity", "min_schedule_capacity", "tank_group",
|
|
158
158
|
"SERVICE_GENERAL", "CHAR_RX_DATA", "CHAR_RX_FINAL", "CHAR_TX_DATA", "CHAR_TX_FINAL",
|
|
159
159
|
# discovery
|
|
@@ -13,19 +13,22 @@ import argparse
|
|
|
13
13
|
import asyncio
|
|
14
14
|
import contextlib
|
|
15
15
|
import json
|
|
16
|
+
import logging
|
|
16
17
|
import struct
|
|
17
18
|
from typing import Optional
|
|
18
19
|
|
|
19
20
|
from .checksums import configured_scenes_checksum, schedule_checksum, supported_attributes_crc
|
|
20
|
-
from .device import
|
|
21
|
+
from .device import (
|
|
22
|
+
ALL_ELEMENTS, MobiusDevice, min_schedule_capacity, decode_mesh_peer_arrays, decode_networked_thread_devices,
|
|
23
|
+
)
|
|
21
24
|
from .relay import RelayedMobiusDevice
|
|
22
25
|
from .device_status import MeshPeer
|
|
23
26
|
from .mesh_address import (
|
|
24
27
|
is_valid_mesh_address, mesh_local_prefix_from_own_address,
|
|
25
|
-
mesh_local_prefix_from_mlprefix,
|
|
28
|
+
mesh_local_prefix_from_mlprefix,
|
|
26
29
|
)
|
|
27
30
|
from .constants import (
|
|
28
|
-
PrimitiveType, C2Attribute,
|
|
31
|
+
PrimitiveType, C2Attribute,
|
|
29
32
|
primitive_type_from_name, support_tier,
|
|
30
33
|
)
|
|
31
34
|
from .discovery import (
|
|
@@ -82,13 +85,11 @@ async def _dump_schedule(device: MobiusDevice, support: str) -> None:
|
|
|
82
85
|
and for lights the lunar/insolation/intensity/acclimation state."""
|
|
83
86
|
print(" --- full schedule ---")
|
|
84
87
|
if support == "light":
|
|
85
|
-
|
|
86
|
-
for point in sorted(
|
|
88
|
+
poll = await device.get_light_poll_batch(which=1)
|
|
89
|
+
for point in sorted(poll.schedule_points, key=lambda p: p.time_minutes):
|
|
87
90
|
print(f" {_format_time(point.time_minutes)}{_format_flags(point)}: "
|
|
88
91
|
f"{_format_channels(point.light.channels)}")
|
|
89
|
-
|
|
90
|
-
lunar_enabled = await device.get_lunar_enabled()
|
|
91
|
-
print(f" lunar_enabled: {lunar_enabled}")
|
|
92
|
+
print(f" lunar_enabled: {poll.lunar_enabled}")
|
|
92
93
|
|
|
93
94
|
# Firmware-computed lunar state, to compare with the calculated
|
|
94
95
|
# "current_intensities"/"light_diagnostics" of the summary.
|
|
@@ -98,12 +99,9 @@ async def _dump_schedule(device: MobiusDevice, support: str) -> None:
|
|
|
98
99
|
except Exception as e:
|
|
99
100
|
print(f" lunar_phase_info: read failed: {e}")
|
|
100
101
|
|
|
101
|
-
insolation_active
|
|
102
|
-
schedule_intensity
|
|
103
|
-
acclimation
|
|
104
|
-
print(f" insolation_active: {insolation_active}")
|
|
105
|
-
print(f" schedule_intensity: {schedule_intensity}")
|
|
106
|
-
print(f" acclimation: {acclimation}")
|
|
102
|
+
print(f" insolation_active: {poll.intensities.diagnostics['insolation_active']}")
|
|
103
|
+
print(f" schedule_intensity: {poll.schedule_intensity}")
|
|
104
|
+
print(f" acclimation: {poll.acclimation}")
|
|
107
105
|
|
|
108
106
|
elif support.startswith("pump"):
|
|
109
107
|
points = await device.get_pump_schedule(which=1)
|
|
@@ -120,6 +118,18 @@ def _support_from_primitive_type(primitive_type_name: str | None) -> str:
|
|
|
120
118
|
return support_tier(primitive_type_from_name(primitive_type_name))
|
|
121
119
|
|
|
122
120
|
|
|
121
|
+
async def _target_identity(target, serial: str, fallback_support: str) -> tuple[str, Optional[int]]:
|
|
122
|
+
"""(support tier, group_mask) read from `target` itself; when its device
|
|
123
|
+
info can't be read, (`fallback_support`, None) with a message."""
|
|
124
|
+
try:
|
|
125
|
+
info = await target.get_device_info()
|
|
126
|
+
except Exception as e:
|
|
127
|
+
print(f" Could not read {serial!r}'s own device info ({e}) -- using support={fallback_support!r} "
|
|
128
|
+
f"and leaving out the rest of its schedule group.")
|
|
129
|
+
return fallback_support, None
|
|
130
|
+
return _support_from_primitive_type(info.get("primitive_type")), info.get("group_mask")
|
|
131
|
+
|
|
132
|
+
|
|
123
133
|
async def _dump_schedule_to_file(device: MobiusDevice, support: str, serial: str, path: str) -> int:
|
|
124
134
|
"""
|
|
125
135
|
Writes Schedule1 and Schedule2 to `path` as JSON
|
|
@@ -200,8 +210,7 @@ async def _dump_schedule_to_mob_file(device: MobiusDevice, support: str, path: s
|
|
|
200
210
|
points = await device.get_light_schedule(which=1)
|
|
201
211
|
mob = light_schedule_to_mob(points, template_name)
|
|
202
212
|
elif support.startswith("pump"):
|
|
203
|
-
|
|
204
|
-
primitive_type = PrimitiveType[info["primitive_type"]]
|
|
213
|
+
primitive_type = await device.identify_device_type()
|
|
205
214
|
points = await device.get_pump_schedule(which=1)
|
|
206
215
|
mob = pump_schedule_to_mob(points, primitive_type, template_name)
|
|
207
216
|
else:
|
|
@@ -235,7 +244,7 @@ async def _load_schedule_from_mob_file(device: MobiusDevice, support: str, path:
|
|
|
235
244
|
|
|
236
245
|
async def _resolve_group_targets(
|
|
237
246
|
gateway: MobiusDevice, target_serial: str, target_device, target_group_mask,
|
|
238
|
-
support: str,
|
|
247
|
+
support: str, pan_id: Optional[int] = None,
|
|
239
248
|
stack: Optional[contextlib.AsyncExitStack] = None, args=None,
|
|
240
249
|
) -> list[tuple[str, object]]:
|
|
241
250
|
"""
|
|
@@ -267,7 +276,7 @@ async def _resolve_group_targets(
|
|
|
267
276
|
for peer in (tank.peers if tank is not None else []):
|
|
268
277
|
if peer.serial == target_serial:
|
|
269
278
|
continue
|
|
270
|
-
relayed = RelayedMobiusDevice(gateway, peer
|
|
279
|
+
relayed = RelayedMobiusDevice(gateway, peer)
|
|
271
280
|
try:
|
|
272
281
|
peer_info = await relayed.get_device_info()
|
|
273
282
|
except Exception as e:
|
|
@@ -378,7 +387,7 @@ async def _check_checksums(target) -> None:
|
|
|
378
387
|
if data_attr not in supported_ids:
|
|
379
388
|
print(f" {label}: not supported by this device")
|
|
380
389
|
continue
|
|
381
|
-
values = await target.
|
|
390
|
+
values = await target.get_attribute(data_attr, 0, ALL_ELEMENTS)
|
|
382
391
|
if data_attr == C2Attribute.ConfiguredScenes:
|
|
383
392
|
computed = configured_scenes_checksum(values)
|
|
384
393
|
used = sum(1 for v in values if len(v) >= 2 and struct.unpack_from("<H", v)[0] != 0)
|
|
@@ -410,7 +419,7 @@ async def _debug_mesh_discovery(device) -> list:
|
|
|
410
419
|
print(f" MLPrefix OK. Derived mesh-local prefix: {prefix.hex()}")
|
|
411
420
|
try:
|
|
412
421
|
entries_raw = await device.get_attribute(
|
|
413
|
-
C2Attribute.NetworkedThreadDevices, index=0, count=
|
|
422
|
+
C2Attribute.NetworkedThreadDevices, index=0, count=ALL_ELEMENTS,
|
|
414
423
|
)
|
|
415
424
|
except Exception as e:
|
|
416
425
|
print(f" NetworkedThreadDevices FAILED: {e!r}")
|
|
@@ -418,25 +427,10 @@ async def _debug_mesh_discovery(device) -> list:
|
|
|
418
427
|
print(f" NetworkedThreadDevices returned {len(entries_raw)} element(s): "
|
|
419
428
|
f"{[e.hex() for e in entries_raw]}")
|
|
420
429
|
|
|
421
|
-
peers = []
|
|
422
430
|
for entry in entries_raw:
|
|
423
431
|
if len(entry) != 28:
|
|
424
|
-
print(f" Skipping malformed entry (expected 28 bytes, got "
|
|
425
|
-
|
|
426
|
-
continue
|
|
427
|
-
try:
|
|
428
|
-
serial = entry[14:28].decode("ascii").rstrip("\x00")
|
|
429
|
-
except UnicodeDecodeError:
|
|
430
|
-
continue
|
|
431
|
-
if not serial or serial == "00000000000000":
|
|
432
|
-
continue
|
|
433
|
-
address = prefix + entry[0:8]
|
|
434
|
-
model_raw = struct.unpack("<h", entry[12:14])[0]
|
|
435
|
-
peers.append(MeshPeer(
|
|
436
|
-
serial=serial, model_raw=model_raw, model=enum_or_none(Model, model_raw),
|
|
437
|
-
short_address=extract_short_address(address), address=address,
|
|
438
|
-
age=struct.unpack("<I", entry[8:12])[0],
|
|
439
|
-
))
|
|
432
|
+
print(f" Skipping malformed entry (expected 28 bytes, got {len(entry)}): {entry.hex()}")
|
|
433
|
+
peers = decode_networked_thread_devices(prefix, entries_raw)
|
|
440
434
|
if peers:
|
|
441
435
|
print(f" Parsed {len(peers)} peer(s) via NetworkedThreadDevices -- "
|
|
442
436
|
f"skipping the MXM-hub steps below.")
|
|
@@ -474,7 +468,7 @@ async def _debug_mesh_discovery(device) -> list:
|
|
|
474
468
|
for attr in (C2Attribute.SerialNumberArray, C2Attribute.DeviceModelArray, C2Attribute.ShortAddressArray):
|
|
475
469
|
print(f" Step 2: {attr.name} ({attr.value})...")
|
|
476
470
|
try:
|
|
477
|
-
values = await device.get_attribute(attr, index=0, count=
|
|
471
|
+
values = await device.get_attribute(attr, index=0, count=ALL_ELEMENTS)
|
|
478
472
|
except Exception as e:
|
|
479
473
|
print(f" FAILED: {e!r}")
|
|
480
474
|
values = []
|
|
@@ -482,24 +476,10 @@ async def _debug_mesh_discovery(device) -> list:
|
|
|
482
476
|
print(f" Returned {len(values)} element(s): {[v.hex() for v in values]}")
|
|
483
477
|
arrays[attr] = values
|
|
484
478
|
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
arrays[C2Attribute.SerialNumberArray], arrays[C2Attribute.DeviceModelArray],
|
|
479
|
+
return decode_mesh_peer_arrays(
|
|
480
|
+
prefix, arrays[C2Attribute.SerialNumberArray], arrays[C2Attribute.DeviceModelArray],
|
|
488
481
|
arrays[C2Attribute.ShortAddressArray],
|
|
489
|
-
)
|
|
490
|
-
try:
|
|
491
|
-
serial = serial_bytes.decode("ascii").rstrip("\x00")
|
|
492
|
-
except (UnicodeDecodeError, AttributeError):
|
|
493
|
-
continue
|
|
494
|
-
if not serial or serial == "00000000000000" or len(model_bytes) < 2 or len(short_bytes) < 2:
|
|
495
|
-
continue
|
|
496
|
-
model_raw = struct.unpack("<h", model_bytes[:2])[0]
|
|
497
|
-
short_address = struct.unpack("<H", short_bytes[:2])[0]
|
|
498
|
-
peers.append(MeshPeer(
|
|
499
|
-
serial=serial, model_raw=model_raw, model=enum_or_none(Model, model_raw),
|
|
500
|
-
short_address=short_address, address=build_rloc_address(prefix, short_address),
|
|
501
|
-
))
|
|
502
|
-
return peers
|
|
482
|
+
)
|
|
503
483
|
|
|
504
484
|
|
|
505
485
|
def _print_mesh_peers_or_hint(peers: list) -> None:
|
|
@@ -511,6 +491,28 @@ def _print_mesh_peers_or_hint(peers: list) -> None:
|
|
|
511
491
|
_print_peers(peers)
|
|
512
492
|
|
|
513
493
|
|
|
494
|
+
# Diagnostic getters printed by --dump-diagnostics, in order.
|
|
495
|
+
_DIAGNOSTIC_GETTERS = (
|
|
496
|
+
"get_fsci_diagnostic_info", "get_clock_sync_info", "get_thread_provisioning_info",
|
|
497
|
+
"get_thread_network_state_info", "get_ble_connection_info", "get_scene_diagnostic_info",
|
|
498
|
+
"get_calibration_extra_info", "get_legacy_device_info", "get_wifi_remnant_info",
|
|
499
|
+
"get_engineering_diagnostic_info", "get_channel_intensities", "get_insolation_tables",
|
|
500
|
+
"get_device_local_date",
|
|
501
|
+
)
|
|
502
|
+
|
|
503
|
+
|
|
504
|
+
async def _dump_diagnostics(device: MobiusDevice) -> None:
|
|
505
|
+
"""Prints every diagnostic getter's result (each field is None when the
|
|
506
|
+
device doesn't support it)."""
|
|
507
|
+
print(" --- diagnostics ---")
|
|
508
|
+
for name in _DIAGNOSTIC_GETTERS:
|
|
509
|
+
try:
|
|
510
|
+
value = await getattr(device, name)()
|
|
511
|
+
except Exception as e:
|
|
512
|
+
value = f"read failed: {e}"
|
|
513
|
+
print(f" {name.removeprefix('get_')}: {value}")
|
|
514
|
+
|
|
515
|
+
|
|
514
516
|
async def _print_summary(mdevice: MobiusDevice, args) -> dict:
|
|
515
517
|
"""Prints get_device_summary() plus the optional schedule/scene dumps."""
|
|
516
518
|
summary = await mdevice.get_device_summary()
|
|
@@ -520,6 +522,8 @@ async def _print_summary(mdevice: MobiusDevice, args) -> dict:
|
|
|
520
522
|
await _dump_schedule(mdevice, summary.get("support", ""))
|
|
521
523
|
if args.dump_scenes:
|
|
522
524
|
await _dump_scenes(mdevice, primitive_type_from_name(summary.get("primitive_type")))
|
|
525
|
+
if args.dump_diagnostics:
|
|
526
|
+
await _dump_diagnostics(mdevice)
|
|
523
527
|
return summary
|
|
524
528
|
|
|
525
529
|
|
|
@@ -538,11 +542,8 @@ async def _learn_peer_directly(args, found_target) -> Optional[MeshPeer]:
|
|
|
538
542
|
return None
|
|
539
543
|
print(f" Learned address directly: {target_address.hex()}")
|
|
540
544
|
return MeshPeer(
|
|
541
|
-
serial=args.relay_target,
|
|
542
|
-
model_raw=target_info.model_raw if target_info else
|
|
543
|
-
model=target_info.model if target_info else None,
|
|
544
|
-
short_address=extract_short_address(target_address),
|
|
545
|
-
address=target_address,
|
|
545
|
+
serial=args.relay_target, address=target_address,
|
|
546
|
+
model_raw=target_info.model_raw if target_info else None,
|
|
546
547
|
)
|
|
547
548
|
|
|
548
549
|
|
|
@@ -556,20 +557,14 @@ async def _write_to_group(mdevice, args, target, label: str, serial: str, summar
|
|
|
556
557
|
exceeds the smallest capacity in the group. `write(device, support,
|
|
557
558
|
serial)` performs the write for one device and returns a summary line.
|
|
558
559
|
"""
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
target_support = _support_from_primitive_type(target_info.get("primitive_type"))
|
|
564
|
-
except Exception as e:
|
|
565
|
-
print(f" Could not read {serial!r}'s own device info ({e}) -- "
|
|
566
|
-
f"writing only to it, not the rest of its group.")
|
|
567
|
-
target_group_mask = None
|
|
568
|
-
target_support = summary.get("support", "") if not args.relay_target else ""
|
|
560
|
+
# Read from the actual target: the summary describes the gateway.
|
|
561
|
+
target_support, target_group_mask = await _target_identity(
|
|
562
|
+
target, serial, summary.get("support", "") if not args.relay_target else "",
|
|
563
|
+
)
|
|
569
564
|
|
|
570
565
|
print(f"\n=== resolving schedule-group targets for {label} ===")
|
|
571
566
|
group_targets = await _resolve_group_targets(
|
|
572
|
-
mdevice, serial, target, target_group_mask, target_support,
|
|
567
|
+
mdevice, serial, target, target_group_mask, target_support,
|
|
573
568
|
pan_id=pan_id, stack=stack, args=args,
|
|
574
569
|
)
|
|
575
570
|
if len(group_targets) > 1:
|
|
@@ -653,7 +648,7 @@ async def _run_by_serial(args) -> None:
|
|
|
653
648
|
f"brief direct connection to {args.relay_target!r} to learn its address...")
|
|
654
649
|
target_peer = await _learn_peer_directly(args, found_target)
|
|
655
650
|
if target_peer is not None:
|
|
656
|
-
relayed = RelayedMobiusDevice(mdevice, target_peer
|
|
651
|
+
relayed = RelayedMobiusDevice(mdevice, target_peer)
|
|
657
652
|
try:
|
|
658
653
|
for k, v in (await relayed.get_device_summary()).items():
|
|
659
654
|
print(f" {k}: {v}")
|
|
@@ -678,12 +673,10 @@ async def _run_by_serial(args) -> None:
|
|
|
678
673
|
return f"{args.relay_target!r} (via relay through {args.by_serial!r})", relayed, args.relay_target
|
|
679
674
|
|
|
680
675
|
async def target_support(target, serial: str) -> str:
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
f"falling back to {args.by_serial!r}'s own support.")
|
|
686
|
-
return summary.get("support", "") if not args.relay_target else ""
|
|
676
|
+
support, _group_mask = await _target_identity(
|
|
677
|
+
target, serial, summary.get("support", "") if not args.relay_target else "",
|
|
678
|
+
)
|
|
679
|
+
return support
|
|
687
680
|
|
|
688
681
|
if args.reboot and (picked := pick_target("reboot()")):
|
|
689
682
|
label, target, _serial = picked
|
|
@@ -971,6 +964,11 @@ def main() -> None:
|
|
|
971
964
|
help="Also print every configured scene on each connected device -- "
|
|
972
965
|
"id, name, timeout and light/pump payload of every slot, including "
|
|
973
966
|
"empty ones.")
|
|
967
|
+
parser.add_argument("--dump-diagnostics", action="store_true",
|
|
968
|
+
help="Also print each connected device's diagnostic attributes: FSCI, "
|
|
969
|
+
"clock sync, Thread provisioning and network state, BLE connection, "
|
|
970
|
+
"scene, calibration, legacy, Wi-Fi and engineering diagnostics, "
|
|
971
|
+
"channel intensities, insolation tables and the device's local date.")
|
|
974
972
|
parser.add_argument("--dump-mesh-peers", action="store_true",
|
|
975
973
|
help="Only usable with --by-serial. After connecting, calls "
|
|
976
974
|
"discover_mesh_peers_auto() on it and prints every other device "
|
|
@@ -988,11 +986,10 @@ def main() -> None:
|
|
|
988
986
|
"get_device_summary() read to it through the gateway and prints "
|
|
989
987
|
"the result. The other --by-serial actions apply to this target.")
|
|
990
988
|
parser.add_argument("--debug-relay", action="store_true",
|
|
991
|
-
help="
|
|
992
|
-
"
|
|
993
|
-
"
|
|
994
|
-
"
|
|
995
|
-
"indication).")
|
|
989
|
+
help="Logs the raw bytes of every layer of each relayed "
|
|
990
|
+
"request/response (inner frame, outer frame, CoAP response) and "
|
|
991
|
+
"every frame the gateway receives (debug logging for "
|
|
992
|
+
"mobius.relay and mobius.device).")
|
|
996
993
|
parser.add_argument("--set-time-to-now", action="store_true",
|
|
997
994
|
help="Only usable with --by-serial. A WRITE -- use on hardware you're "
|
|
998
995
|
"prepared to have its clock changed on. Calls set_time_to_now() "
|
|
@@ -1106,6 +1103,11 @@ def main() -> None:
|
|
|
1106
1103
|
print(f"\t{element['raw_hex']}{suffix}")
|
|
1107
1104
|
return
|
|
1108
1105
|
|
|
1106
|
+
if args.debug_relay:
|
|
1107
|
+
logging.basicConfig(format=" [%(name)s] %(message)s")
|
|
1108
|
+
for name in ("mobius.relay", "mobius.device"):
|
|
1109
|
+
logging.getLogger(name).setLevel(logging.DEBUG)
|
|
1110
|
+
|
|
1109
1111
|
asyncio.run(_run(args))
|
|
1110
1112
|
|
|
1111
1113
|
|