python-mobius 0.1.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 +73 -0
- mobius/cli.py +68 -0
- mobius/constants.py +393 -0
- mobius/crc.py +49 -0
- mobius/device.py +429 -0
- mobius/discovery.py +64 -0
- mobius/frame.py +170 -0
- mobius/manufacturer.py +53 -0
- mobius/schedule.py +212 -0
- python_mobius-0.1.0.dist-info/METADATA +149 -0
- python_mobius-0.1.0.dist-info/RECORD +14 -0
- python_mobius-0.1.0.dist-info/WHEEL +4 -0
- python_mobius-0.1.0.dist-info/entry_points.txt +2 -0
- python_mobius-0.1.0.dist-info/licenses/LICENSE +338 -0
mobius/manufacturer.py
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Manufacturer-data parsing (panId/model/serial from BLE advertisements, no
|
|
3
|
+
GATT connection required) -- ported from Peripheral.parseManufactureData.
|
|
4
|
+
|
|
5
|
+
Verified byte-for-byte against real devices: decoded model and serial from
|
|
6
|
+
the advertisement match GATT-read values exactly. Company ID confirmed as
|
|
7
|
+
0x0202 via bleak. Only the "version 2, length 25" branch (Java's bArr
|
|
8
|
+
length; i.e. bleak's payload length 23, since bleak strips the 2-byte
|
|
9
|
+
company-ID prefix Java's parser expects) is implemented, since that's the
|
|
10
|
+
only one actually observed on real hardware -- the decompile has other
|
|
11
|
+
branches for other manufacturer-data format versions/lengths that are not
|
|
12
|
+
implemented here. See documentation/08-manufacturer-data.md.
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
from __future__ import annotations
|
|
16
|
+
|
|
17
|
+
import struct
|
|
18
|
+
from dataclasses import dataclass
|
|
19
|
+
from typing import Optional
|
|
20
|
+
|
|
21
|
+
from .constants import Model
|
|
22
|
+
|
|
23
|
+
MOBIUS_COMPANY_ID = 0x0202
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
@dataclass
|
|
27
|
+
class MobiusAdvertisement:
|
|
28
|
+
model_raw: int
|
|
29
|
+
model: Optional[Model]
|
|
30
|
+
pan_id: int
|
|
31
|
+
device_specific_data: int
|
|
32
|
+
serial: str
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def parse_manufacturer_data(payload: bytes) -> Optional[MobiusAdvertisement]:
|
|
36
|
+
"""
|
|
37
|
+
`payload` is bleak's AdvertisementData.manufacturer_data[MOBIUS_COMPANY_ID]
|
|
38
|
+
(i.e. WITHOUT the 2-byte company ID prefix -- bleak already strips it).
|
|
39
|
+
Only handles the 23-byte payload / Java-bArr-length-25 "version 2" format
|
|
40
|
+
actually observed on real hardware; returns None for anything else
|
|
41
|
+
rather than guessing at other branches.
|
|
42
|
+
"""
|
|
43
|
+
if len(payload) != 23:
|
|
44
|
+
return None
|
|
45
|
+
model_val = struct.unpack_from("<H", payload, 0)[0]
|
|
46
|
+
try:
|
|
47
|
+
model = Model(model_val)
|
|
48
|
+
except ValueError:
|
|
49
|
+
model = None
|
|
50
|
+
device_specific = struct.unpack_from("<I", payload, 3)[0]
|
|
51
|
+
pan_id = struct.unpack_from("<H", payload, 7)[0]
|
|
52
|
+
serial = payload[9:23].decode("ascii", errors="replace")
|
|
53
|
+
return MobiusAdvertisement(model_val, model, pan_id, device_specific, serial)
|
mobius/schedule.py
ADDED
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Schedule point parsing for both lights and pumps -- they share the exact
|
|
3
|
+
same Schedule1/Schedule2 attribute and Point wire framing
|
|
4
|
+
(time + flags + primitiveData), but interpret primitiveData differently and
|
|
5
|
+
combine points differently:
|
|
6
|
+
|
|
7
|
+
- Lights (LightPrimitive): continuous curve. The app fetches the whole
|
|
8
|
+
schedule and linearly interpolates between the two bracketing points for
|
|
9
|
+
"now" -- there's no live "current intensity" read from the device at
|
|
10
|
+
all. See interpolate_light_schedule().
|
|
11
|
+
|
|
12
|
+
- Pumps (PumpPrimitive): discrete blocks. Point.getEnd() in the decompile
|
|
13
|
+
literally returns "the next point's start time" -- each point's mode
|
|
14
|
+
and parameters are active verbatim from its own start time until the
|
|
15
|
+
next point starts. See get_active_pump_block().
|
|
16
|
+
|
|
17
|
+
See documentation/06-light-schedule.md and documentation/07-pump-schedule.md.
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
from __future__ import annotations
|
|
21
|
+
|
|
22
|
+
import struct
|
|
23
|
+
from dataclasses import dataclass
|
|
24
|
+
from typing import Optional
|
|
25
|
+
|
|
26
|
+
from .constants import (
|
|
27
|
+
VisualID, PumpMode, RampType, PumpParam,
|
|
28
|
+
PUMP_PARAM_SIZE, PUMP_MODE_PARAMS, PUMP_TIME_OFFSET_PARAMS, PUMP_TIME_OFFSET,
|
|
29
|
+
)
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
# --------------------------------------------------------------------------
|
|
33
|
+
# Lights
|
|
34
|
+
# --------------------------------------------------------------------------
|
|
35
|
+
|
|
36
|
+
@dataclass
|
|
37
|
+
class LightPrimitive:
|
|
38
|
+
"""A set of (channel -> intensity 0-1000 permille) pairs. Wire format:
|
|
39
|
+
repeating [VisualID(u8), intensity(u16 LE)] triplets."""
|
|
40
|
+
channels: dict # VisualID -> int (0-1000)
|
|
41
|
+
|
|
42
|
+
@classmethod
|
|
43
|
+
def parse(cls, data: bytes) -> "LightPrimitive":
|
|
44
|
+
channels = {}
|
|
45
|
+
for i in range(0, len(data) - 2, 3):
|
|
46
|
+
raw_id = data[i]
|
|
47
|
+
try:
|
|
48
|
+
vid = VisualID(raw_id)
|
|
49
|
+
except ValueError:
|
|
50
|
+
vid = VisualID.Unknown
|
|
51
|
+
intensity = struct.unpack_from("<H", data, i + 1)[0]
|
|
52
|
+
channels[vid] = intensity
|
|
53
|
+
return cls(channels)
|
|
54
|
+
|
|
55
|
+
def get(self, vid: VisualID) -> int:
|
|
56
|
+
return self.channels.get(vid, 0)
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
@dataclass
|
|
60
|
+
class SchedulePoint:
|
|
61
|
+
"""Wire format: time(u16 LE, minutes-since-midnight) + flags(u8) + primitiveData."""
|
|
62
|
+
time_minutes: int
|
|
63
|
+
flags: int
|
|
64
|
+
light: LightPrimitive
|
|
65
|
+
|
|
66
|
+
FLAG_ACTIVE = 1
|
|
67
|
+
FLAG_NIGHT = 2
|
|
68
|
+
FLAG_SUNRISE = 6
|
|
69
|
+
FLAG_SUNSET = 10
|
|
70
|
+
|
|
71
|
+
@classmethod
|
|
72
|
+
def parse(cls, data: bytes) -> Optional["SchedulePoint"]:
|
|
73
|
+
if len(data) < 3:
|
|
74
|
+
return None
|
|
75
|
+
time_minutes = struct.unpack_from("<H", data, 0)[0]
|
|
76
|
+
flags = data[2]
|
|
77
|
+
if (flags & cls.FLAG_ACTIVE) != cls.FLAG_ACTIVE:
|
|
78
|
+
return None
|
|
79
|
+
return cls(time_minutes, flags, LightPrimitive.parse(data[3:]))
|
|
80
|
+
|
|
81
|
+
def has(self, flag: int) -> bool:
|
|
82
|
+
return (self.flags & flag) == flag
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def interpolate_light_schedule(points: list[SchedulePoint], minute_of_day: int) -> dict:
|
|
86
|
+
"""
|
|
87
|
+
Port of PointSchedule.getIntensitiesAtTime(). Given the full set of
|
|
88
|
+
schedule points and a target time (0-1439, minutes since midnight),
|
|
89
|
+
returns {VisualID: intensity_permille} by linearly interpolating between
|
|
90
|
+
the two points that bracket that time, wrapping around midnight.
|
|
91
|
+
"""
|
|
92
|
+
if not points:
|
|
93
|
+
return {}
|
|
94
|
+
pts = sorted(points, key=lambda p: p.time_minutes)
|
|
95
|
+
n = len(pts)
|
|
96
|
+
minute_of_day %= 1440
|
|
97
|
+
|
|
98
|
+
for i in range(n):
|
|
99
|
+
p1 = pts[i]
|
|
100
|
+
p2 = pts[(i + 1) % n]
|
|
101
|
+
t1 = p1.time_minutes
|
|
102
|
+
t2 = p2.time_minutes
|
|
103
|
+
if (i + 1) % n == 0:
|
|
104
|
+
# wrapping from the last point back to the first: treat the
|
|
105
|
+
# first point's time as being on "the next day"
|
|
106
|
+
t2 += 1440
|
|
107
|
+
|
|
108
|
+
m = minute_of_day if minute_of_day >= t1 else minute_of_day + 1440
|
|
109
|
+
if t1 <= m <= t2:
|
|
110
|
+
span = (t2 - t1) or 1
|
|
111
|
+
frac = (m - t1) / span
|
|
112
|
+
result = {}
|
|
113
|
+
all_channels = set(p1.light.channels) | set(p2.light.channels)
|
|
114
|
+
for ch in all_channels:
|
|
115
|
+
v1 = p1.light.get(ch)
|
|
116
|
+
v2 = p2.light.get(ch)
|
|
117
|
+
result[ch] = v1 + (v2 - v1) * frac
|
|
118
|
+
return result
|
|
119
|
+
return {}
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
# --------------------------------------------------------------------------
|
|
123
|
+
# Pumps
|
|
124
|
+
# --------------------------------------------------------------------------
|
|
125
|
+
|
|
126
|
+
@dataclass
|
|
127
|
+
class PumpPrimitiveValue:
|
|
128
|
+
mode: PumpMode
|
|
129
|
+
params: dict # PumpParam -> int, or RampType for the RampType param, or bytes for Master
|
|
130
|
+
|
|
131
|
+
@classmethod
|
|
132
|
+
def parse(cls, data: bytes) -> "PumpPrimitiveValue":
|
|
133
|
+
mode_byte = data[0] if data else 0
|
|
134
|
+
try:
|
|
135
|
+
mode = PumpMode(mode_byte)
|
|
136
|
+
except ValueError:
|
|
137
|
+
mode = PumpMode.Undefined
|
|
138
|
+
params = {}
|
|
139
|
+
offset = 1
|
|
140
|
+
for p in PUMP_MODE_PARAMS.get(mode, []):
|
|
141
|
+
size = PUMP_PARAM_SIZE[p]
|
|
142
|
+
raw = data[offset:offset + size]
|
|
143
|
+
offset += size
|
|
144
|
+
if len(raw) < size:
|
|
145
|
+
continue
|
|
146
|
+
if p == PumpParam.RampType:
|
|
147
|
+
try:
|
|
148
|
+
params[p] = RampType(raw[0])
|
|
149
|
+
except ValueError:
|
|
150
|
+
params[p] = raw[0]
|
|
151
|
+
elif p == PumpParam.Master:
|
|
152
|
+
# last-8-bytes BLE address of the master pump this one syncs to
|
|
153
|
+
params[p] = raw
|
|
154
|
+
elif size == 2:
|
|
155
|
+
val = struct.unpack("<h", raw)[0]
|
|
156
|
+
# negative values on MaxSpeed/MinSpeed encode reverse rotation
|
|
157
|
+
# direction -- reported as-is here rather than replicating the
|
|
158
|
+
# app's separate abs()/getReverse() split, so check the sign
|
|
159
|
+
# yourself if it matters.
|
|
160
|
+
if p in PUMP_TIME_OFFSET_PARAMS:
|
|
161
|
+
val += PUMP_TIME_OFFSET
|
|
162
|
+
params[p] = val
|
|
163
|
+
elif size == 4:
|
|
164
|
+
val = struct.unpack("<i", raw)[0]
|
|
165
|
+
if p in PUMP_TIME_OFFSET_PARAMS:
|
|
166
|
+
val += PUMP_TIME_OFFSET
|
|
167
|
+
params[p] = val
|
|
168
|
+
return cls(mode, params)
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
@dataclass
|
|
172
|
+
class PumpSchedulePoint:
|
|
173
|
+
"""Wire format identical to SchedulePoint: time(u16 LE) + flags(u8) + primitiveData,
|
|
174
|
+
just with primitiveData decoded as a PumpPrimitive instead of a LightPrimitive."""
|
|
175
|
+
time_minutes: int
|
|
176
|
+
flags: int
|
|
177
|
+
pump: PumpPrimitiveValue
|
|
178
|
+
|
|
179
|
+
@classmethod
|
|
180
|
+
def parse(cls, data: bytes) -> Optional["PumpSchedulePoint"]:
|
|
181
|
+
if len(data) < 3:
|
|
182
|
+
return None
|
|
183
|
+
time_minutes = struct.unpack_from("<H", data, 0)[0]
|
|
184
|
+
flags = data[2]
|
|
185
|
+
if (flags & SchedulePoint.FLAG_ACTIVE) != SchedulePoint.FLAG_ACTIVE:
|
|
186
|
+
return None
|
|
187
|
+
return cls(time_minutes, flags, PumpPrimitiveValue.parse(data[3:]))
|
|
188
|
+
|
|
189
|
+
|
|
190
|
+
def get_active_pump_block(points: list[PumpSchedulePoint], minute_of_day: int) -> Optional[PumpSchedulePoint]:
|
|
191
|
+
"""
|
|
192
|
+
Unlike interpolate_light_schedule(), this does a block lookup, not a
|
|
193
|
+
blend: finds whichever point's [start, next_point_start) range contains
|
|
194
|
+
minute_of_day, wrapping at midnight. That point's mode+params are simply
|
|
195
|
+
"the currently active setting" -- no interpolation involved.
|
|
196
|
+
"""
|
|
197
|
+
if not points:
|
|
198
|
+
return None
|
|
199
|
+
pts = sorted(points, key=lambda p: p.time_minutes)
|
|
200
|
+
n = len(pts)
|
|
201
|
+
minute_of_day %= 1440
|
|
202
|
+
for i in range(n):
|
|
203
|
+
p = pts[i]
|
|
204
|
+
nxt = pts[(i + 1) % n]
|
|
205
|
+
start = p.time_minutes
|
|
206
|
+
end = nxt.time_minutes
|
|
207
|
+
if (i + 1) % n == 0:
|
|
208
|
+
end += 1440
|
|
209
|
+
m = minute_of_day if minute_of_day >= start else minute_of_day + 1440
|
|
210
|
+
if start <= m < end:
|
|
211
|
+
return p
|
|
212
|
+
return pts[0]
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: python-mobius
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Reverse-engineered Python client for the Mobius BLE protocol (EcoTech Marine VorTech/Radion, AquaIllumination, Neptune Systems, NYOS)
|
|
5
|
+
Project-URL: Homepage, https://code.r3pek.org/r3pek/python-mobius
|
|
6
|
+
Project-URL: Documentation, https://code.r3pek.org/r3pek/python-mobius/src/branch/main/documentation
|
|
7
|
+
Project-URL: Issues, https://code.r3pek.org/r3pek/python-mobius/issues
|
|
8
|
+
Author: r3pek
|
|
9
|
+
License: GPL-2.0-only
|
|
10
|
+
License-File: LICENSE
|
|
11
|
+
Keywords: aquarium,ble,bleak,bluetooth,ecotech,fsci,mobius,radion,reef,vortech
|
|
12
|
+
Classifier: Development Status :: 3 - Alpha
|
|
13
|
+
Classifier: Intended Audience :: Developers
|
|
14
|
+
Classifier: License :: OSI Approved :: GNU General Public License v2 (GPLv2)
|
|
15
|
+
Classifier: Programming Language :: Python :: 3
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
20
|
+
Classifier: Topic :: Home Automation
|
|
21
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
22
|
+
Classifier: Topic :: System :: Hardware
|
|
23
|
+
Requires-Python: >=3.9
|
|
24
|
+
Requires-Dist: bleak>=0.21
|
|
25
|
+
Provides-Extra: dev
|
|
26
|
+
Requires-Dist: bleak-retry-connector>=3.0; extra == 'dev'
|
|
27
|
+
Requires-Dist: pytest>=7.0; extra == 'dev'
|
|
28
|
+
Provides-Extra: retry
|
|
29
|
+
Requires-Dist: bleak-retry-connector>=3.0; extra == 'retry'
|
|
30
|
+
Description-Content-Type: text/markdown
|
|
31
|
+
|
|
32
|
+
# python-mobius
|
|
33
|
+
|
|
34
|
+
A reverse-engineered Python client for the BLE protocol used by "Mobius
|
|
35
|
+
Ready" aquarium equipment — EcoTech Marine (VorTech pumps, Radion lights),
|
|
36
|
+
AquaIllumination (Prime, Hydra), Neptune Systems, and NYOS.
|
|
37
|
+
|
|
38
|
+
Built on [`bleak`](https://github.com/hbldh/bleak) for cross-platform BLE.
|
|
39
|
+
|
|
40
|
+
**Not affiliated with or endorsed by any of these companies.** This is an
|
|
41
|
+
independent reimplementation of the wire protocol for interoperability with
|
|
42
|
+
hardware you own, derived from public community reverse-engineering work
|
|
43
|
+
and analysis of the publicly-distributed Mobius Android app. See
|
|
44
|
+
[`documentation/`](./documentation) for the full protocol writeup, with
|
|
45
|
+
every field marked as either directly confirmed or explicitly flagged as
|
|
46
|
+
inferred/experimental.
|
|
47
|
+
|
|
48
|
+
## Status
|
|
49
|
+
|
|
50
|
+
Alpha. Core protocol (framing, CRC, attribute get/set, scenes), pump
|
|
51
|
+
telemetry, pump schedules, light schedules, and device discovery/grouping
|
|
52
|
+
are implemented and verified against real hardware (two VorTech MP40QD
|
|
53
|
+
pumps, two Radion XR15 G6 Pro lights). See
|
|
54
|
+
[`documentation/10-known-gaps-and-open-questions.md`](./documentation/10-known-gaps-and-open-questions.md)
|
|
55
|
+
for what isn't covered yet (dosers, environmental sensors, Thread/CoAP
|
|
56
|
+
device relay).
|
|
57
|
+
|
|
58
|
+
## Install
|
|
59
|
+
|
|
60
|
+
```bash
|
|
61
|
+
pip install python-mobius
|
|
62
|
+
# or, for more robust BLE connection retries (recommended):
|
|
63
|
+
pip install python-mobius[retry]
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
## Quick start
|
|
67
|
+
|
|
68
|
+
```python
|
|
69
|
+
import asyncio
|
|
70
|
+
from mobius import scan_for_mobius_devices_with_info, group_by_pan_id, MobiusDevice
|
|
71
|
+
|
|
72
|
+
async def main():
|
|
73
|
+
found = await scan_for_mobius_devices_with_info()
|
|
74
|
+
for pan_id, members in group_by_pan_id(found).items():
|
|
75
|
+
print(f"tank {pan_id:#06x}:")
|
|
76
|
+
for device, info in members:
|
|
77
|
+
print(f" {device.address} {info.model.name} {info.serial}")
|
|
78
|
+
|
|
79
|
+
device, _info = found[0]
|
|
80
|
+
async with MobiusDevice(device) as d:
|
|
81
|
+
summary = await d.get_device_summary()
|
|
82
|
+
print(summary)
|
|
83
|
+
|
|
84
|
+
asyncio.run(main())
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
Or from the command line:
|
|
88
|
+
|
|
89
|
+
```bash
|
|
90
|
+
mobius-scan --adapter hci0
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
## What you can do
|
|
94
|
+
|
|
95
|
+
- **Discover devices** and group them by tank/mesh (`pan_id`), reading
|
|
96
|
+
model/serial straight from BLE advertisements — no connection required.
|
|
97
|
+
- **Read pump telemetry**: current speed, estimated flow (GPH), operation
|
|
98
|
+
state, error state.
|
|
99
|
+
- **Read pump schedules**: which mode (constant speed, tidal swell, pulse,
|
|
100
|
+
etc.) is active at any given time, exactly as programmed.
|
|
101
|
+
- **Read light schedules**: per-channel intensity at any given time,
|
|
102
|
+
replicating the app's own client-side interpolation (there's no "current
|
|
103
|
+
intensity" attribute — lights only expose the programmed curve).
|
|
104
|
+
- **Control scenes**: start feed mode, resume the normal schedule, or any
|
|
105
|
+
other configured scene.
|
|
106
|
+
- **Low-level protocol access** (`build_frame`, `get_attribute`,
|
|
107
|
+
`set_attribute`, ...) if you want to go beyond what's wrapped in
|
|
108
|
+
`MobiusDevice`.
|
|
109
|
+
|
|
110
|
+
## Supported device types
|
|
111
|
+
|
|
112
|
+
| PrimitiveType | Support | Notes |
|
|
113
|
+
|---|---|---|
|
|
114
|
+
| `VisualV1` (Radion, Prime, Hydra, etc.) | ✅ Verified | Lights |
|
|
115
|
+
| `VorTechV1`, `PumpV1`, `VectraV1`, `AlpacaV1`, `TurtleV1` | ✅ Verified | Pumps |
|
|
116
|
+
| `CoffeeV1` (NYOS Quantum) | ⚠️ Experimental | Same wire structure as pumps per the protocol, untested against real hardware |
|
|
117
|
+
| `DoseV1`, `HotSauceV1` | ❌ Unsupported | Different primitive format; identity info only |
|
|
118
|
+
|
|
119
|
+
`MobiusDevice.get_device_summary()` always tells you which tier applies via
|
|
120
|
+
its `"support"` field — see [`documentation/04-device-identity.md`](./documentation/04-device-identity.md).
|
|
121
|
+
|
|
122
|
+
## Development
|
|
123
|
+
|
|
124
|
+
```bash
|
|
125
|
+
git clone https://code.r3pek.org/r3pek/python-mobius
|
|
126
|
+
cd python-mobius
|
|
127
|
+
pip install -e ".[dev]"
|
|
128
|
+
pytest
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
Tests are validated against real captured packets and real device
|
|
132
|
+
manufacturer-data/serials where possible — see `tests/`.
|
|
133
|
+
|
|
134
|
+
## License
|
|
135
|
+
|
|
136
|
+
GPLv2 — see [`LICENSE`](./LICENSE).
|
|
137
|
+
|
|
138
|
+
## Acknowledgments
|
|
139
|
+
|
|
140
|
+
The protocol reverse-engineering and implementation in this library were
|
|
141
|
+
carried out with substantial assistance from Claude (Anthropic), used to
|
|
142
|
+
analyze a decompiled copy of the official Mobius Android app (JADX) and
|
|
143
|
+
cross-reference it against prior public community research (notably the
|
|
144
|
+
Reef2Reef "Controlling Mobius enabled VorTech pump using 0-10V and BLE"
|
|
145
|
+
thread and the `danmrossi/MobiusControl` project), then to design, write,
|
|
146
|
+
and test the Python implementation itself. See
|
|
147
|
+
[`documentation/00-overview.md`](./documentation/00-overview.md) for the
|
|
148
|
+
full methodology and confirmation-strength notes on every protocol
|
|
149
|
+
detail.
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
mobius/__init__.py,sha256=wtCy-JKP5KhFr3dq_C0V4458R-msyZt2EP96vH16Tr8,3281
|
|
2
|
+
mobius/cli.py,sha256=uhGZIDrhpTp8LkTGOl4BZVQWgJYHbaIVH2HzX3RFCCs,2642
|
|
3
|
+
mobius/constants.py,sha256=9OtNEOU4FXXhdgayGrz5e_T8s6M20UE31B6GQ-mywnY,12022
|
|
4
|
+
mobius/crc.py,sha256=WwIfUXGN_UKdsv-_Q303-L1-L1Rc1fvJnB5176ftzqU,2669
|
|
5
|
+
mobius/device.py,sha256=CbCQQjmLQmg-zFufRufcRPkA3PlO_18AkeCP_eDE0EA,19993
|
|
6
|
+
mobius/discovery.py,sha256=4MjGwpd0bUK-koiZ-a2Cvq-gCTImAKAQ4lZ-9vOge3Q,2290
|
|
7
|
+
mobius/frame.py,sha256=zG2vh8PceJHD8R43MwZiJnm0Ge1dr8nFcKsJR9_c9LM,6135
|
|
8
|
+
mobius/manufacturer.py,sha256=8-vBzckN8xmsPSMj5EWtOCXenbyQueoY5pnZHePgb_k,1937
|
|
9
|
+
mobius/schedule.py,sha256=kHr684TIKPZ2BplIySoeD-Yn1Xa_2bE6rfGUs-waWMQ,7483
|
|
10
|
+
python_mobius-0.1.0.dist-info/METADATA,sha256=pwoXT7fGt7RyDQKMvRzjvNJZaaFtzENzOUWI_OwmELs,5907
|
|
11
|
+
python_mobius-0.1.0.dist-info/WHEEL,sha256=lCkmxWfQsSc9CfIClYeavTdQeEX2toPqufh9gI35EQA,87
|
|
12
|
+
python_mobius-0.1.0.dist-info/entry_points.txt,sha256=Q2hlcek-bWm70zvd12v32essflPgC2su-dKDHAqbPoE,48
|
|
13
|
+
python_mobius-0.1.0.dist-info/licenses/LICENSE,sha256=7a72Msu2Q-TnoiFxemxEGkwafJGObk1W3rw9hzmyM_Y,17984
|
|
14
|
+
python_mobius-0.1.0.dist-info/RECORD,,
|