python-mobius 0.8.4__py3-none-any.whl → 0.10.0__py3-none-any.whl
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.
- mobius/__init__.py +29 -20
- mobius/checksums.py +119 -0
- mobius/cli.py +617 -884
- mobius/coap.py +60 -152
- mobius/constants.py +269 -277
- mobius/crc.py +3 -5
- mobius/device.py +1766 -3194
- mobius/device_status.py +173 -493
- mobius/discovery.py +97 -181
- mobius/dump.py +65 -189
- mobius/frame.py +74 -124
- mobius/manufacturer.py +33 -36
- mobius/mesh_address.py +34 -74
- mobius/mob.py +58 -114
- mobius/modifiers.py +38 -73
- mobius/power.py +18 -44
- mobius/pump_status.py +12 -23
- mobius/relay.py +102 -373
- mobius/scenes.py +55 -38
- mobius/schedule.py +175 -240
- mobius/timezone.py +63 -0
- python_mobius-0.10.0.dist-info/METADATA +161 -0
- python_mobius-0.10.0.dist-info/RECORD +26 -0
- {python_mobius-0.8.4.dist-info → python_mobius-0.10.0.dist-info}/WHEEL +1 -1
- python_mobius-0.8.4.dist-info/METADATA +0 -208
- python_mobius-0.8.4.dist-info/RECORD +0 -24
- {python_mobius-0.8.4.dist-info → python_mobius-0.10.0.dist-info}/entry_points.txt +0 -0
- {python_mobius-0.8.4.dist-info → python_mobius-0.10.0.dist-info}/licenses/LICENSE +0 -0
mobius/__init__.py
CHANGED
|
@@ -1,16 +1,18 @@
|
|
|
1
1
|
"""
|
|
2
2
|
python-mobius: a reverse-engineered Python client for the BLE protocol used
|
|
3
3
|
by EcoTech Marine (VorTech, Radion), AquaIllumination (Prime, Hydra),
|
|
4
|
-
Neptune Systems
|
|
4
|
+
Neptune Systems and NYOS "Mobius" aquarium devices.
|
|
5
5
|
|
|
6
6
|
Not affiliated with or endorsed by any of those companies. This is an
|
|
7
|
-
independent
|
|
8
|
-
|
|
9
|
-
publicly-distributed Mobius Android app. See documentation/ for the full
|
|
10
|
-
protocol writeup and confirmation evidence for every field.
|
|
7
|
+
independent implementation of the wire protocol for interoperability. See
|
|
8
|
+
documentation/ for the protocol description.
|
|
11
9
|
"""
|
|
12
10
|
|
|
13
11
|
from .crc import crc16, CRC16_TABLE
|
|
12
|
+
from .checksums import (
|
|
13
|
+
ConfigurationCache, ConfigurationChecksums, configured_scenes_checksum, schedule_checksum,
|
|
14
|
+
supported_attributes_crc,
|
|
15
|
+
)
|
|
14
16
|
from .frame import (
|
|
15
17
|
build_frame, parse_frame, ParsedFrame,
|
|
16
18
|
encode_get_attribute, encode_get_attributes_batch, encode_set_attribute, decode_attribute_response, AttributeValue,
|
|
@@ -22,9 +24,10 @@ from .frame import (
|
|
|
22
24
|
)
|
|
23
25
|
from .constants import (
|
|
24
26
|
PrimitiveType, PRIMITIVE_SIZE, LIGHT_PRIMITIVES,
|
|
25
|
-
PUMP_PRIMITIVES_VERIFIED, PUMP_PRIMITIVES_EXPERIMENTAL,
|
|
26
|
-
|
|
27
|
-
|
|
27
|
+
PUMP_PRIMITIVES_VERIFIED, PUMP_PRIMITIVES_EXPERIMENTAL, PUMP_PRIMITIVES, enum_or_none,
|
|
28
|
+
primitive_type_from_name, support_tier, PumpParamRange, pump_param_range, pump_param_errors,
|
|
29
|
+
Model, MODEL_MANUFACTURER, manufacturer_for_model, BLUETOOTH_ONLY_MODELS, is_bluetooth_only,
|
|
30
|
+
ErrorState, C2Attribute, OperationMode, PhysicalValueID, VisualID,
|
|
28
31
|
SceneID, OperationState, FsciStatus,
|
|
29
32
|
PumpMode, PumpOverrideMode, RampType, PumpParam, PUMP_PARAM_SIZE, PUMP_MODE_PARAMS,
|
|
30
33
|
supported_pump_modes,
|
|
@@ -37,11 +40,11 @@ from .schedule import (
|
|
|
37
40
|
LightPrimitive, SchedulePoint, interpolate_light_schedule,
|
|
38
41
|
PumpPrimitiveValue, PumpSchedulePoint, get_active_pump_block,
|
|
39
42
|
light_schedule_to_dict, light_schedule_from_dict,
|
|
40
|
-
pump_schedule_to_dict, pump_schedule_from_dict,
|
|
43
|
+
pump_schedule_to_dict, pump_schedule_from_dict, pump_params_to_dict,
|
|
41
44
|
MAX_PUMP_SCHEDULE_POINT_SIZE,
|
|
42
45
|
)
|
|
43
46
|
from .scenes import Scene, ActiveScene, decode_configured_scenes, decode_active_scene
|
|
44
|
-
from .manufacturer import MOBIUS_COMPANY_ID_ECOTECH, MOBIUS_COMPANY_ID_AQUAILLUMINATION, MOBIUS_COMPANY_IDS, MobiusAdvertisement, parse_manufacturer_data
|
|
47
|
+
from .manufacturer import MOBIUS_COMPANY_ID_ECOTECH, MOBIUS_COMPANY_ID_AQUAILLUMINATION, MOBIUS_COMPANY_IDS, MobiusAdvertisement, parse_manufacturer_data, parse_advertisement
|
|
45
48
|
from .modifiers import (
|
|
46
49
|
AcclimationInfo, is_night_segment,
|
|
47
50
|
julian_day_from_date, lunar_days_into_phase, lunar_percent_reduction,
|
|
@@ -58,19 +61,21 @@ from .coap import (
|
|
|
58
61
|
from .mesh_address import (
|
|
59
62
|
mesh_local_prefix_from_own_address, mesh_local_prefix_from_mlprefix,
|
|
60
63
|
is_valid_mesh_address, build_rloc_address, extract_short_address,
|
|
61
|
-
is_short_address_derived,
|
|
64
|
+
is_short_address_derived, format_mesh_address,
|
|
62
65
|
)
|
|
63
66
|
from .relay import RelayedMobiusDevice
|
|
64
67
|
from .device import (
|
|
65
68
|
MobiusDevice, MobiusPump, LightIntensityResult, LightPollResult, FullPollResult,
|
|
69
|
+
schedule_capacity, min_schedule_capacity, tank_group,
|
|
66
70
|
SERVICE_GENERAL, CHAR_RX_DATA, CHAR_RX_FINAL, CHAR_TX_DATA, CHAR_TX_FINAL,
|
|
67
71
|
)
|
|
68
72
|
from .discovery import (
|
|
69
73
|
scan_for_mobius_devices, scan_for_mobius_devices_with_info, group_by_pan_id,
|
|
70
74
|
dedupe_by_serial, find_device_by_serial, discover_mesh_peers_via_direct_connect,
|
|
71
|
-
discover_tank,
|
|
75
|
+
discover_tank, find_bluetooth_only_tank_members,
|
|
72
76
|
)
|
|
73
77
|
from .frame import SupportedAttribute
|
|
78
|
+
from .timezone import posix_tz_string, TIME_ZONE_FIELD_SIZE
|
|
74
79
|
from .dump import (
|
|
75
80
|
dump_attributes, format_attribute_dump_json, parse_attribute_dump_json,
|
|
76
81
|
format_attribute_dump_text, parse_attribute_dump_text, parse_attribute_dump,
|
|
@@ -82,12 +87,13 @@ from .mob import (
|
|
|
82
87
|
MAX_PUMP_PRIMITIVE_SIZE,
|
|
83
88
|
)
|
|
84
89
|
|
|
85
|
-
__version__ = "0.
|
|
90
|
+
__version__ = "0.10.0"
|
|
86
91
|
|
|
87
92
|
__all__ = [
|
|
88
93
|
"__version__",
|
|
89
94
|
# crc
|
|
90
|
-
"crc16", "CRC16_TABLE",
|
|
95
|
+
"crc16", "CRC16_TABLE", "supported_attributes_crc", "configured_scenes_checksum",
|
|
96
|
+
"schedule_checksum", "ConfigurationChecksums", "ConfigurationCache",
|
|
91
97
|
# frame
|
|
92
98
|
"build_frame", "parse_frame", "ParsedFrame",
|
|
93
99
|
"encode_get_attribute", "encode_get_attributes_batch", "encode_set_attribute", "decode_attribute_response", "AttributeValue",
|
|
@@ -98,9 +104,10 @@ __all__ = [
|
|
|
98
104
|
"OPCODE_GET_ATTR_ELEMENTS", "OPCODE_GET_ATTR_ELEMENTS_EXT", "OPCODE_GET_OPCODES",
|
|
99
105
|
# constants
|
|
100
106
|
"PrimitiveType", "PRIMITIVE_SIZE", "LIGHT_PRIMITIVES",
|
|
101
|
-
"PUMP_PRIMITIVES_VERIFIED", "PUMP_PRIMITIVES_EXPERIMENTAL",
|
|
102
|
-
"
|
|
103
|
-
"
|
|
107
|
+
"PUMP_PRIMITIVES_VERIFIED", "PUMP_PRIMITIVES_EXPERIMENTAL", "PUMP_PRIMITIVES", "enum_or_none",
|
|
108
|
+
"primitive_type_from_name", "support_tier", "PumpParamRange", "pump_param_range", "pump_param_errors",
|
|
109
|
+
"Model", "MODEL_MANUFACTURER", "BLUETOOTH_ONLY_MODELS", "is_bluetooth_only", "manufacturer_for_model",
|
|
110
|
+
"ErrorState", "C2Attribute", "OperationMode", "PhysicalValueID", "VisualID",
|
|
104
111
|
"SceneID", "OperationState", "FsciStatus",
|
|
105
112
|
"PumpMode", "PumpOverrideMode", "RampType", "PumpParam", "PUMP_PARAM_SIZE", "PUMP_MODE_PARAMS",
|
|
106
113
|
"supported_pump_modes",
|
|
@@ -112,12 +119,12 @@ __all__ = [
|
|
|
112
119
|
"LightPrimitive", "SchedulePoint", "interpolate_light_schedule",
|
|
113
120
|
"PumpPrimitiveValue", "PumpSchedulePoint", "get_active_pump_block",
|
|
114
121
|
"light_schedule_to_dict", "light_schedule_from_dict",
|
|
115
|
-
"pump_schedule_to_dict", "pump_schedule_from_dict",
|
|
122
|
+
"pump_schedule_to_dict", "pump_schedule_from_dict", "pump_params_to_dict",
|
|
116
123
|
"MAX_PUMP_SCHEDULE_POINT_SIZE",
|
|
117
124
|
# scenes
|
|
118
125
|
"Scene", "ActiveScene", "decode_configured_scenes", "decode_active_scene",
|
|
119
126
|
# manufacturer
|
|
120
|
-
"MOBIUS_COMPANY_ID_ECOTECH", "MOBIUS_COMPANY_ID_AQUAILLUMINATION", "MOBIUS_COMPANY_IDS", "MobiusAdvertisement", "parse_manufacturer_data",
|
|
127
|
+
"MOBIUS_COMPANY_ID_ECOTECH", "MOBIUS_COMPANY_ID_AQUAILLUMINATION", "MOBIUS_COMPANY_IDS", "MobiusAdvertisement", "parse_manufacturer_data", "parse_advertisement",
|
|
121
128
|
# modifiers
|
|
122
129
|
"AcclimationInfo", "is_night_segment",
|
|
123
130
|
"julian_day_from_date", "lunar_days_into_phase", "lunar_percent_reduction",
|
|
@@ -133,14 +140,16 @@ __all__ = [
|
|
|
133
140
|
"next_coap_token",
|
|
134
141
|
"mesh_local_prefix_from_own_address", "mesh_local_prefix_from_mlprefix", "is_valid_mesh_address",
|
|
135
142
|
"build_rloc_address", "extract_short_address", "is_short_address_derived",
|
|
143
|
+
"format_mesh_address", "posix_tz_string", "TIME_ZONE_FIELD_SIZE",
|
|
136
144
|
"RelayedMobiusDevice",
|
|
137
145
|
# device
|
|
138
146
|
"MobiusDevice", "MobiusPump", "LightIntensityResult", "LightPollResult", "FullPollResult",
|
|
147
|
+
"schedule_capacity", "min_schedule_capacity", "tank_group",
|
|
139
148
|
"SERVICE_GENERAL", "CHAR_RX_DATA", "CHAR_RX_FINAL", "CHAR_TX_DATA", "CHAR_TX_FINAL",
|
|
140
149
|
# discovery
|
|
141
150
|
"scan_for_mobius_devices", "scan_for_mobius_devices_with_info", "group_by_pan_id",
|
|
142
151
|
"dedupe_by_serial", "find_device_by_serial", "discover_mesh_peers_via_direct_connect",
|
|
143
|
-
"discover_tank",
|
|
152
|
+
"find_bluetooth_only_tank_members", "discover_tank",
|
|
144
153
|
"SupportedAttribute", "dump_attributes", "enrich_attribute_dump", "generic_interpretations",
|
|
145
154
|
"format_attribute_dump_json", "parse_attribute_dump_json",
|
|
146
155
|
"format_attribute_dump_text", "parse_attribute_dump_text", "parse_attribute_dump",
|
mobius/checksums.py
ADDED
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
"""Checksums a device reports about its own configuration, and how to
|
|
2
|
+
compute them. Reading a 2-byte checksum instead of the data it covers lets
|
|
3
|
+
a client skip re-reading data that hasn't changed.
|
|
4
|
+
|
|
5
|
+
| Attribute | ID | Covers |
|
|
6
|
+
|---|---|---|
|
|
7
|
+
| `SupportedAttributesCRC` | 14 | The supported attribute table (see supported_attributes_crc()) |
|
|
8
|
+
| `ConfiguredScenesChecksum` | 408 | ConfiguredScenes (400) |
|
|
9
|
+
| `Schedule1Checksum` | 501 | Schedule1 (500) |
|
|
10
|
+
| `Schedule2Checksum` | 504 | Schedule2 (503) |
|
|
11
|
+
|
|
12
|
+
All four are CRC16 with seed 0xFFFF (see the functions below for what each
|
|
13
|
+
covers), confirmed on a VorTech MP40 and a Radion XR15 G6. The app
|
|
14
|
+
computes SupportedAttributesCRC itself and compares it with the device's
|
|
15
|
+
value to decide whether to read the attribute table again; the other
|
|
16
|
+
three it doesn't read.
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
from __future__ import annotations
|
|
20
|
+
|
|
21
|
+
import struct
|
|
22
|
+
from dataclasses import dataclass, field
|
|
23
|
+
from typing import Iterable, Optional
|
|
24
|
+
|
|
25
|
+
from .crc import crc16
|
|
26
|
+
from .frame import AttributeValue, SupportedAttribute
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def supported_attributes_crc(supported: Iterable[SupportedAttribute]) -> int:
|
|
30
|
+
"""
|
|
31
|
+
The app's checksum of a supported attribute table, for comparison with
|
|
32
|
+
SupportedAttributesCRC (14): crc16 over, for each attribute in the
|
|
33
|
+
order the device lists them, the id (u16 LE), the number of indexes
|
|
34
|
+
(u8) and each index (u8). Counts and indexes above 255 are truncated
|
|
35
|
+
to their low byte, as the app does.
|
|
36
|
+
"""
|
|
37
|
+
data = bytearray()
|
|
38
|
+
for attribute in supported:
|
|
39
|
+
data += struct.pack("<H", attribute.attr_id)
|
|
40
|
+
data.append(len(attribute.indexes) & 0xFF)
|
|
41
|
+
data.extend(index & 0xFF for index in attribute.indexes)
|
|
42
|
+
return crc16(bytes(data))
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def configured_scenes_checksum(slots: Iterable[bytes]) -> int:
|
|
46
|
+
"""ConfiguredScenesChecksum (408) of ConfiguredScenes (400) slots as
|
|
47
|
+
read: crc16 over the used slots (scene id != 0) in slot order."""
|
|
48
|
+
return crc16(b"".join(slot for slot in slots if len(slot) >= 2 and struct.unpack_from("<H", slot)[0] != 0))
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def schedule_checksum(points: Iterable[bytes]) -> int:
|
|
52
|
+
"""Schedule1Checksum (501) / Schedule2Checksum (504) of Schedule1 (500) /
|
|
53
|
+
Schedule2 (503) points as read: crc16 over the active points (flags
|
|
54
|
+
bit 0 set) in order. Schedule1Intensity/Schedule2Intensity are not
|
|
55
|
+
included."""
|
|
56
|
+
return crc16(b"".join(point for point in points if len(point) >= 3 and point[2] & 0x01))
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
@dataclass(frozen=True)
|
|
60
|
+
class ConfigurationChecksums:
|
|
61
|
+
"""The checksums a device reports; None when not supported or not
|
|
62
|
+
readable."""
|
|
63
|
+
supported_attributes: Optional[int] = None
|
|
64
|
+
configured_scenes: Optional[int] = None
|
|
65
|
+
schedule1: Optional[int] = None
|
|
66
|
+
schedule2: Optional[int] = None
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
@dataclass
|
|
70
|
+
class ConfigurationCache:
|
|
71
|
+
"""
|
|
72
|
+
Data of one device kept between get_full_poll_batch(cache=...) calls
|
|
73
|
+
so a poll reads the 2-byte checksums instead of the data they cover.
|
|
74
|
+
|
|
75
|
+
entries: {data attribute id: (checksum, response blocks)} for
|
|
76
|
+
ConfiguredScenes and the polled schedule, each with the checksum the
|
|
77
|
+
device reported in the same request. A poll whose checksum differs
|
|
78
|
+
(or can't be read) reads the data again. The device's own value is
|
|
79
|
+
the key, so no local computation is involved, and a change by any
|
|
80
|
+
client, this one included, shows up as a changed checksum.
|
|
81
|
+
supported_attributes_crc: SupportedAttributesCRC from the last poll.
|
|
82
|
+
When it changes (a firmware update), the poll result says so
|
|
83
|
+
(FullPollResult.attribute_table_changed) and the static attributes
|
|
84
|
+
are read again on the next poll. `entries` stay valid: each is still
|
|
85
|
+
checked against its own checksum.
|
|
86
|
+
static / static_read_at / static_refresh_interval: attributes that
|
|
87
|
+
rarely change (identity, firmware and hardware, color channels,
|
|
88
|
+
calibration, advanced settings, pump flow range and battery settings;
|
|
89
|
+
STATIC_POLL_ATTRIBUTES in device.py), read at most every
|
|
90
|
+
`static_refresh_interval` seconds and kept here in between. None (the
|
|
91
|
+
default) reads them on every poll. A client that writes one of them
|
|
92
|
+
calls invalidate_static() so the next poll reads them again.
|
|
93
|
+
|
|
94
|
+
One per device.
|
|
95
|
+
"""
|
|
96
|
+
entries: dict[int, tuple[int, list[AttributeValue]]] = field(default_factory=dict)
|
|
97
|
+
supported_attributes_crc: Optional[int] = None
|
|
98
|
+
static: dict[int, list[AttributeValue]] = field(default_factory=dict)
|
|
99
|
+
static_read_at: Optional[float] = None
|
|
100
|
+
static_refresh_interval: Optional[float] = None
|
|
101
|
+
|
|
102
|
+
def static_due(self, now: float) -> bool:
|
|
103
|
+
"""Whether the static attributes are to be read by a poll at
|
|
104
|
+
`now` (time.monotonic())."""
|
|
105
|
+
return (
|
|
106
|
+
self.static_refresh_interval is None or self.static_read_at is None
|
|
107
|
+
or now - self.static_read_at >= self.static_refresh_interval
|
|
108
|
+
)
|
|
109
|
+
|
|
110
|
+
def invalidate_static(self) -> None:
|
|
111
|
+
"""Makes the next poll read the static attributes."""
|
|
112
|
+
self.static.clear()
|
|
113
|
+
self.static_read_at = None
|
|
114
|
+
|
|
115
|
+
def clear(self) -> None:
|
|
116
|
+
"""Forgets everything: the next poll reads all data."""
|
|
117
|
+
self.entries.clear()
|
|
118
|
+
self.supported_attributes_crc = None
|
|
119
|
+
self.invalidate_static()
|