python-mobius 0.1.0__py3-none-any.whl → 0.1.1__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 CHANGED
@@ -23,7 +23,8 @@ from .frame import (
23
23
  from .constants import (
24
24
  PrimitiveType, PRIMITIVE_SIZE, LIGHT_PRIMITIVES,
25
25
  PUMP_PRIMITIVES_VERIFIED, PUMP_PRIMITIVES_EXPERIMENTAL,
26
- Model, ErrorState, C2Attribute, PhysicalValueID, VisualID,
26
+ Model, MODEL_MANUFACTURER, manufacturer_for_model,
27
+ ErrorState, C2Attribute, PhysicalValueID, VisualID,
27
28
  SceneID, OperationState, FsciStatus,
28
29
  PumpMode, RampType, PumpParam, PUMP_PARAM_SIZE, PUMP_MODE_PARAMS,
29
30
  )
@@ -40,7 +41,7 @@ from .discovery import (
40
41
  scan_for_mobius_devices, scan_for_mobius_devices_with_info, group_by_pan_id,
41
42
  )
42
43
 
43
- __version__ = "0.1.0"
44
+ __version__ = "0.1.1"
44
45
 
45
46
  __all__ = [
46
47
  "__version__",
@@ -57,7 +58,8 @@ __all__ = [
57
58
  # constants
58
59
  "PrimitiveType", "PRIMITIVE_SIZE", "LIGHT_PRIMITIVES",
59
60
  "PUMP_PRIMITIVES_VERIFIED", "PUMP_PRIMITIVES_EXPERIMENTAL",
60
- "Model", "ErrorState", "C2Attribute", "PhysicalValueID", "VisualID",
61
+ "Model", "MODEL_MANUFACTURER", "manufacturer_for_model",
62
+ "ErrorState", "C2Attribute", "PhysicalValueID", "VisualID",
61
63
  "SceneID", "OperationState", "FsciStatus",
62
64
  "PumpMode", "RampType", "PumpParam", "PUMP_PARAM_SIZE", "PUMP_MODE_PARAMS",
63
65
  # schedule
mobius/constants.py CHANGED
@@ -11,6 +11,7 @@ See documentation/ for the full derivation of each table.
11
11
  from __future__ import annotations
12
12
 
13
13
  from enum import IntEnum
14
+ from typing import Optional
14
15
 
15
16
 
16
17
  # --------------------------------------------------------------------------
@@ -107,6 +108,65 @@ class Model(IntEnum):
107
108
  Coffee2 = 397 # "Quantum 220/300" (NYOS)
108
109
 
109
110
 
111
+ # Ported from M.Model.getManufacturer(). The decompiled version branches on
112
+ # enum *ordinal* against a hardcoded position list (the full ~100-entry
113
+ # Model enum's index 1-37 range, plus a few extras, all map to "EcoTech
114
+ # Marine"); since this library only implements a curated subset of Model,
115
+ # this is re-derived by name pattern against that same logic instead of by
116
+ # ordinal position -- functionally equivalent for every Model value defined
117
+ # above, but if you add more Model entries, add their manufacturer here too.
118
+ MODEL_MANUFACTURER: dict[Model, str] = {
119
+ Model.Unknown: "Unknown",
120
+ Model.VorTechMP10wES: "EcoTech Marine",
121
+ Model.VorTechMP10wQD: "EcoTech Marine",
122
+ Model.VorTechMP40wES: "EcoTech Marine",
123
+ Model.VorTechMP40wQD: "EcoTech Marine",
124
+ Model.VorTechMP40wG3QD: "EcoTech Marine",
125
+ Model.VorTechMP60wES: "EcoTech Marine",
126
+ Model.VorTechMP60wQD: "EcoTech Marine",
127
+ Model.RadionXR30w: "EcoTech Marine",
128
+ Model.RadionXR30wG2: "EcoTech Marine",
129
+ Model.RadionXR30wPro: "EcoTech Marine",
130
+ Model.RadionXR30wG3: "EcoTech Marine",
131
+ Model.RadionXR30wG3Pro: "EcoTech Marine",
132
+ Model.RadionXR30wG4: "EcoTech Marine",
133
+ Model.RadionXR30wG4Pro: "EcoTech Marine",
134
+ Model.RadionXR30wFWPro: "EcoTech Marine",
135
+ Model.RadionXR30wPrototype: "EcoTech Marine",
136
+ Model.RadionXR15wG3Pro: "EcoTech Marine",
137
+ Model.RadionXR15wFW: "EcoTech Marine",
138
+ Model.RadionXR15wG4Pro: "EcoTech Marine",
139
+ Model.RadionXR15wFWPro: "EcoTech Marine",
140
+ Model.RadionXR15wPrototype: "EcoTech Marine",
141
+ Model.RadionXR15G5Pro: "EcoTech Marine",
142
+ Model.RadionXR15wG5Blue: "EcoTech Marine",
143
+ Model.RadionXR15wG5FW: "EcoTech Marine",
144
+ Model.RadionXR15wG6Pro: "EcoTech Marine",
145
+ Model.RadionXR15wG6Blue: "EcoTech Marine",
146
+ Model.RadionXR30wG5Pro: "EcoTech Marine",
147
+ Model.RadionXR30wG5Blue: "EcoTech Marine",
148
+ Model.VectraM1: "EcoTech Marine",
149
+ Model.VectraL1: "EcoTech Marine",
150
+ Model.VectraS1: "EcoTech Marine",
151
+ Model.VectraS2: "EcoTech Marine",
152
+ Model.VectraM2: "EcoTech Marine",
153
+ Model.VectraL2: "EcoTech Marine",
154
+ Model.Nero5: "AquaIllumination",
155
+ Model.Nero3: "AquaIllumination",
156
+ Model.DosingPump: "AquaIllumination",
157
+ Model.Muffler: "Neptune Systems", # confirmed explicitly in the decompile's manufacturer list
158
+ Model.Coffee1: "NYOS", # confirmed explicitly (CoffeeV1 branch)
159
+ Model.Coffee2: "NYOS",
160
+ }
161
+
162
+
163
+ def manufacturer_for_model(model: Optional[Model]) -> Optional[str]:
164
+ """Returns the manufacturer name for a Model, or None if unknown/unmapped."""
165
+ if model is None:
166
+ return None
167
+ return MODEL_MANUFACTURER.get(model)
168
+
169
+
110
170
  class ErrorState(IntEnum):
111
171
  """Confirmed literal values from M.ErrorState (subset covering common
112
172
  pump/light fault codes -- the full enum also has dosing-pump-specific codes)."""
mobius/device.py CHANGED
@@ -29,7 +29,7 @@ from .frame import (
29
29
  )
30
30
  from .constants import (
31
31
  C2Attribute, PrimitiveType, Model, ErrorState, SceneID, OperationState,
32
- FsciStatus, PhysicalValueID, VisualID,
32
+ FsciStatus, PhysicalValueID, VisualID, manufacturer_for_model,
33
33
  PRIMITIVE_SIZE, LIGHT_PRIMITIVES, PUMP_PRIMITIVES_VERIFIED, PUMP_PRIMITIVES_EXPERIMENTAL,
34
34
  )
35
35
  from .schedule import (
@@ -48,6 +48,25 @@ CHAR_TX_DATA = "01ff0103-ba5e-f4ee-5ca1-eb1e5e4b1ce0" # write: app -> device,
48
48
  CHAR_TX_FINAL = "01ff0104-ba5e-f4ee-5ca1-eb1e5e4b1ce0" # write: app -> device, last fragment
49
49
 
50
50
 
51
+ def _decode_serial(raw: bytes) -> str:
52
+ """
53
+ Serial numbers are printable ASCII on every device tested (confirmed by
54
+ cross-validating the GATT-read serial against the same serial parsed
55
+ from BLE advertisement manufacturer data -- see manufacturer.py and
56
+ documentation/08-manufacturer-data.md), so decode as text rather than
57
+ hex-encoding raw bytes. Falls back to hex for any non-printable-ASCII
58
+ edge case not yet observed, rather than raising.
59
+ """
60
+ text = raw.rstrip(b"\x00")
61
+ try:
62
+ decoded = text.decode("ascii")
63
+ except UnicodeDecodeError:
64
+ return raw.hex()
65
+ if decoded and all(32 <= ord(c) < 127 for c in decoded):
66
+ return decoded
67
+ return raw.hex()
68
+
69
+
51
70
  class MobiusDevice:
52
71
  """
53
72
  High-level async client for a single Mobius-protocol BLE device.
@@ -252,8 +271,9 @@ class MobiusDevice:
252
271
  return {
253
272
  "model_raw": model_val,
254
273
  "model": model.name if model is not None else (f"unknown({model_val})" if model_val is not None else None),
274
+ "manufacturer": manufacturer_for_model(model),
255
275
  "name": name_raw.decode("utf-8", errors="replace").rstrip("\x00") if name_raw else None,
256
- "serial": serial_raw.hex() if serial_raw else None,
276
+ "serial": _decode_serial(serial_raw) if serial_raw else None,
257
277
  "primitive_type": PrimitiveType(primitive_raw[0]).name if primitive_raw else None,
258
278
  "error_state": error.name if error is not None else (f"unknown({error_val})" if error_val is not None else None),
259
279
  "mac_address": ":".join(f"{b:02X}" for b in mac_raw) if mac_raw else None,
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: python-mobius
3
- Version: 0.1.0
3
+ Version: 0.1.1
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
@@ -0,0 +1,14 @@
1
+ mobius/__init__.py,sha256=JK6iAhOfAJ1LBf2tKUi3OQY7NMNwOudyyExjYKbehNU,3381
2
+ mobius/cli.py,sha256=uhGZIDrhpTp8LkTGOl4BZVQWgJYHbaIVH2HzX3RFCCs,2642
3
+ mobius/constants.py,sha256=wKrWU-QnqMuWuvczHwt3ha3sPw6D9NNK-kIhxJcTbHA,14702
4
+ mobius/crc.py,sha256=WwIfUXGN_UKdsv-_Q303-L1-L1Rc1fvJnB5176ftzqU,2669
5
+ mobius/device.py,sha256=VeBBT2z0XyqwdnUxw56GrG_ZlVe5Oq05tF-m4z-rMDY,20805
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.1.dist-info/METADATA,sha256=m9dnMFZU2-Qdq_kLSpxPqoYE05Ac3E7NhtSjSmwjXNo,5907
11
+ python_mobius-0.1.1.dist-info/WHEEL,sha256=lCkmxWfQsSc9CfIClYeavTdQeEX2toPqufh9gI35EQA,87
12
+ python_mobius-0.1.1.dist-info/entry_points.txt,sha256=Q2hlcek-bWm70zvd12v32essflPgC2su-dKDHAqbPoE,48
13
+ python_mobius-0.1.1.dist-info/licenses/LICENSE,sha256=7a72Msu2Q-TnoiFxemxEGkwafJGObk1W3rw9hzmyM_Y,17984
14
+ python_mobius-0.1.1.dist-info/RECORD,,
@@ -1,14 +0,0 @@
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,,