python-mobius 0.4.2__py3-none-any.whl → 0.4.4__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 +1 -1
- mobius/constants.py +203 -70
- mobius/device.py +91 -19
- mobius/device_status.py +9 -6
- mobius/relay.py +43 -5
- {python_mobius-0.4.2.dist-info → python_mobius-0.4.4.dist-info}/METADATA +1 -1
- {python_mobius-0.4.2.dist-info → python_mobius-0.4.4.dist-info}/RECORD +10 -10
- {python_mobius-0.4.2.dist-info → python_mobius-0.4.4.dist-info}/WHEEL +0 -0
- {python_mobius-0.4.2.dist-info → python_mobius-0.4.4.dist-info}/entry_points.txt +0 -0
- {python_mobius-0.4.2.dist-info → python_mobius-0.4.4.dist-info}/licenses/LICENSE +0 -0
mobius/__init__.py
CHANGED
mobius/constants.py
CHANGED
|
@@ -61,71 +61,133 @@ PUMP_PRIMITIVES_EXPERIMENTAL = {PrimitiveType.CoffeeV1}
|
|
|
61
61
|
|
|
62
62
|
|
|
63
63
|
class Model(IntEnum):
|
|
64
|
-
"""Device model identifiers. Confirmed literal values
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
64
|
+
"""Device model identifiers. Confirmed literal values -- and, for
|
|
65
|
+
every entry, the real product name -- via reverse engineering the
|
|
66
|
+
app's own model-identifier enum and its own getName() method
|
|
67
|
+
directly (base64-decoded where the app itself stores the string
|
|
68
|
+
that way, confirmed real product names even for entries whose own
|
|
69
|
+
internal enum constant name is an unrelated food-themed placeholder
|
|
70
|
+
-- e.g. this enum's own Plumbus really is called "Plumbus" by the
|
|
71
|
+
real app itself, not a decompiler artifact). The full set, not a
|
|
72
|
+
curated subset -- see MODEL_MANUFACTURER below for the same
|
|
73
|
+
treatment of manufacturer. A few entries (the RadionXR#w G5/G6 FW/
|
|
74
|
+
Pro/Blue range specifically) are named here for the real product,
|
|
75
|
+
not the app's own internal enum constant name, to stay consistent
|
|
76
|
+
with how every other RadionXR#w entry in this same enum is already
|
|
77
|
+
named -- each renamed entry's own comment says what the app itself
|
|
78
|
+
actually calls it internally, for anyone cross-referencing against
|
|
79
|
+
the decompiled source directly."""
|
|
69
80
|
Unknown = 0
|
|
70
|
-
VorTechMP10wES = 10
|
|
71
|
-
VorTechMP10wQD = 11
|
|
72
|
-
RadionXR30w = 30
|
|
73
|
-
RadionXR30wG2 = 31
|
|
74
|
-
RadionXR30wPro = 32
|
|
75
|
-
RadionXR30wG3 = 33
|
|
76
|
-
RadionXR30wG3Pro = 34
|
|
77
|
-
RadionXR30wG4 = 35
|
|
78
|
-
RadionXR30wG4Pro = 36
|
|
79
|
-
RadionXR30wFWPro = 37
|
|
80
|
-
RadionXR30wPrototype = 39
|
|
81
|
-
VorTechMP40wES = 40
|
|
82
|
-
VorTechMP40wQD = 41
|
|
83
|
-
VorTechMP40wG3QD = 42
|
|
84
|
-
VorTechMP60wES = 60
|
|
85
|
-
VorTechMP60wQD = 61
|
|
86
|
-
VectraM1 = 144
|
|
87
|
-
VectraL1 = 145
|
|
88
|
-
VectraS1 = 146
|
|
89
|
-
VectraS2 = 147
|
|
90
|
-
VectraM2 = 148
|
|
91
|
-
VectraL2 = 149
|
|
92
|
-
RadionXR15wG3Pro = 160
|
|
93
|
-
RadionXR15wFW = 161
|
|
94
|
-
RadionXR15wG4Pro = 162
|
|
95
|
-
RadionXR15wFWPro = 163
|
|
96
|
-
RadionXR15wPrototype = 175
|
|
97
|
-
RadionXR15G5Pro = 176
|
|
98
|
-
RadionXR15wG5Blue = 177
|
|
99
|
-
RadionXR15wG5FW = 178
|
|
100
|
-
RadionXR15wG6Pro = 179
|
|
101
|
-
RadionXR15wG6Blue = 180
|
|
102
|
-
RadionXR30wG5Pro = 192
|
|
103
|
-
RadionXR30wG5Blue = 193
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
#
|
|
114
|
-
|
|
115
|
-
#
|
|
116
|
-
#
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
81
|
+
VorTechMP10wES = 10 # "VorTech MP10wES"
|
|
82
|
+
VorTechMP10wQD = 11 # "VorTech MP10mQD"
|
|
83
|
+
RadionXR30w = 30 # "Radion XR30w"
|
|
84
|
+
RadionXR30wG2 = 31 # "Radion XR30w G2"
|
|
85
|
+
RadionXR30wPro = 32 # "Radion XR30w Pro"
|
|
86
|
+
RadionXR30wG3 = 33 # "Radion XR30w G3"
|
|
87
|
+
RadionXR30wG3Pro = 34 # "Radion XR30wG3 Pro"
|
|
88
|
+
RadionXR30wG4 = 35 # "Radion XR30w G4"
|
|
89
|
+
RadionXR30wG4Pro = 36 # "Radion XR30w G4 Pro"
|
|
90
|
+
RadionXR30wFWPro = 37 # "Radion XR30wFW Pro"
|
|
91
|
+
RadionXR30wPrototype = 39 # "Radion XR30w Prototype"
|
|
92
|
+
VorTechMP40wES = 40 # "VorTech MP40wES"
|
|
93
|
+
VorTechMP40wQD = 41 # "VorTech MP40mQD"
|
|
94
|
+
VorTechMP40wG3QD = 42 # "VorTech MP40mQD"
|
|
95
|
+
VorTechMP60wES = 60 # "VorTech MP60wES"
|
|
96
|
+
VorTechMP60wQD = 61 # "VorTech MP60mQD"
|
|
97
|
+
VectraM1 = 144 # "Vectra M1"
|
|
98
|
+
VectraL1 = 145 # "Vectra L1"
|
|
99
|
+
VectraS1 = 146 # "Vectra S1"
|
|
100
|
+
VectraS2 = 147 # "Vectra S2"
|
|
101
|
+
VectraM2 = 148 # "Vectra M2"
|
|
102
|
+
VectraL2 = 149 # "Vectra L2"
|
|
103
|
+
RadionXR15wG3Pro = 160 # "Radion XR15wG3 Pro"
|
|
104
|
+
RadionXR15wFW = 161 # "Radion XR15w FW"
|
|
105
|
+
RadionXR15wG4Pro = 162 # "Radion XR15w G4 Pro"
|
|
106
|
+
RadionXR15wFWPro = 163 # "Radion XR15w FW Pro"
|
|
107
|
+
RadionXR15wPrototype = 175 # "Radion XR15w Prototype"
|
|
108
|
+
RadionXR15G5Pro = 176 # "Radion XR15w G5 Pro"
|
|
109
|
+
RadionXR15wG5Blue = 177 # "Radion XR15w G5 Blue"
|
|
110
|
+
RadionXR15wG5FW = 178 # "Radion XR15w G5 FW" (app's own internal enum name: BeefChalupa)
|
|
111
|
+
RadionXR15wG6Pro = 179 # "Radion XR15w G6 Pro" (app's own internal enum name: SteakQuesadilla)
|
|
112
|
+
RadionXR15wG6Blue = 180 # "Radion XR15w G6 Blue" (app's own internal enum name: SteakNachoFriesBurrito)
|
|
113
|
+
RadionXR30wG5Pro = 192 # "Radion XR30w G5 Pro"
|
|
114
|
+
RadionXR30wG5Blue = 193 # "Radion XR30w G5 Blue"
|
|
115
|
+
RadionXR30wG5FW = 194 # "Radion XR30w G5 FW" (app's own internal enum name: CrunchwrapSupreme)
|
|
116
|
+
RadionXR30wG6Pro = 195 # "Radion XR30w G6 Pro" (app's own internal enum name: SteakTaco)
|
|
117
|
+
RadionXR30wG6Blue = 196 # "Radion XR30w G6 Blue" (app's own internal enum name: BeefyMeltBurrito)
|
|
118
|
+
Nero5 = 256 # "Nero 5"
|
|
119
|
+
Nero3 = 257 # "Nero 3"
|
|
120
|
+
Nero7 = 258 # "Nero 7" (app's own internal enum name: Slack)
|
|
121
|
+
Orbit2 = 259 # "Orbit 2" (app's own internal enum name: Alpaca2)
|
|
122
|
+
Orbit4 = 260 # "Orbit 4" (app's own internal enum name: Alpaca4)
|
|
123
|
+
Axis20 = 261 # "Axis 20" (app's own internal enum name: Turtle1)
|
|
124
|
+
Axis40 = 262 # "Axis 40" (app's own internal enum name: Turtle2)
|
|
125
|
+
Axis90 = 263 # "Axis 90" (app's own internal enum name: Turtle3)
|
|
126
|
+
Axis200 = 264 # "Axis 200" (app's own internal enum name: Turtle4)
|
|
127
|
+
VersaVX1 = 272 # "Versa VX-1" (app's own internal enum name: DosingPump)
|
|
128
|
+
DoseQD = 275 # "Dose QD" (app's own internal enum name: Muffler)
|
|
129
|
+
SproutSL1 = 288 # "Sprout SL1" (app's own internal enum name: Chalupa)
|
|
130
|
+
EnvironmentalSensor = 304 # "Environmental Sensor" (app's own internal enum name: DoritosLocosTacos)
|
|
131
|
+
Prime = 320 # "Prime"
|
|
132
|
+
PrimeHD = 321 # "Prime HD"
|
|
133
|
+
PrimeFW = 322 # "Prime FW" (the app's own getName() genuinely returns an empty string for this one specifically -- not a parsing gap here)
|
|
134
|
+
HydraTwentySixHD = 323 # "Hydra TwentySix HD"
|
|
135
|
+
HydraFiftyTwoHD = 324 # "Hydra FiftyTwo HD"
|
|
136
|
+
RedSeaMAX13 = 325 # "RedSea MAX 13"
|
|
137
|
+
RedSeaMAX26 = 326 # "RedSea MAX 26"
|
|
138
|
+
PrimeSixteenHD = 333 # "Prime Sixteen HD"
|
|
139
|
+
HydraThirtyTwoHD = 334 # "Hydra ThirtyTwo HD"
|
|
140
|
+
HydraSixtyFourHD = 335 # "Hydra SixtyFour HD"
|
|
141
|
+
PrimeSixteenHdFw = 339 # "16 Freshwater"
|
|
142
|
+
PrimeSixteenHdFuge = 340 # "16 Fuge"
|
|
143
|
+
PrimeSixteenHdSol = 341 # "16 Sol"
|
|
144
|
+
PrimeFuge = 342 # "Prime Fuge"
|
|
145
|
+
Hydra32HDFreshwater = 344 # "Hydra 32HD Freshwater" (app's own internal enum name: Quesarito)
|
|
146
|
+
Thundervolt = 345 # "Thundervolt" (app's own internal enum name: NachosBellGrande)
|
|
147
|
+
Plumbus = 346 # "Plumbus"
|
|
148
|
+
Blade = 347 # "Blade" (app's own internal enum name: Saddle)
|
|
149
|
+
Leap = 349 # "Leap" (app's own internal enum name: BeanBurrito)
|
|
150
|
+
MXM = 350 # "MXM" (app's own internal enum name: Cowboy)
|
|
151
|
+
BladeD = 360 # "Blade" (app's own internal enum name: SaddleD)
|
|
152
|
+
BladeCoralGlow = 361 # "Blade Coral Glow" (app's own internal enum name: SaddleA)
|
|
153
|
+
BladeCoralGrow = 362 # "Blade Coral Grow" (app's own internal enum name: SaddleM)
|
|
154
|
+
BladeFreshwater = 363 # "Blade Freshwater" (app's own internal enum name: SaddleF)
|
|
155
|
+
BladeRefugium = 364 # "Blade Refugium" (app's own internal enum name: SaddleR)
|
|
156
|
+
BladeGrowPlus = 365 # "Blade Grow+" (app's own internal enum name: SaddleG)
|
|
157
|
+
BladeGloFish = 368 # "Blade GloFish" (app's own internal enum name: SaddleGF)
|
|
158
|
+
HydraEdge44HD = 371 # "Hydra Edge 44HD" (app's own internal enum name: AlfalfaRS)
|
|
159
|
+
HydraEdge68HD = 372 # "Hydra Edge 68HD" (app's own internal enum name: AlfalfaRL)
|
|
160
|
+
HydraEdge44HDFreshwater = 373 # "Hydra Edge 44HD Freshwater" (app's own internal enum name: AlfalfaFS)
|
|
161
|
+
HydraEdge68HDFreshwater = 374 # "Hydra Edge 68HD Freshwater" (app's own internal enum name: AlfalfaFL)
|
|
162
|
+
Quantum160 = 396 # "Quantum 160" (app's own internal enum name: Coffee1)
|
|
163
|
+
Quantum220300 = 397 # "Quantum 220/300" (app's own internal enum name: Coffee2)
|
|
164
|
+
Hera = 512 # "Hera" (app's own internal enum name: CrunchyTaco)
|
|
165
|
+
HeraBackplane = 513 # "Hera Backplane" (app's own internal enum name: SoftTaco)
|
|
166
|
+
HeraPHSensor = 514 # "Hera pH Sensor" (app's own internal enum name: LuxeBox)
|
|
167
|
+
HeraDissolvedOxygenSensor = 515 # "Hera Dissolved Oxygen Sensor" (app's own internal enum name: DiabloSauce)
|
|
168
|
+
HeraFluidMonitoringModule = 516 # "Hera Fluid Monitoring Module" (app's own internal enum name: FlaminHotRice)
|
|
169
|
+
HeraIOModule = 517 # "Hera I/O Module" (app's own internal enum name: CinnabonDelights)
|
|
170
|
+
HeraORPSensor = 518 # "Hera ORP sensor" (app's own internal enum name: BajaBlast)
|
|
171
|
+
HeraSalinitySensor = 519 # "Hera Salinity Sensor" (app's own internal enum name: CantinaChicken)
|
|
172
|
+
HeraSwitchedPowerModule = 520 # "Hera Switched Power Module" (app's own internal enum name: VeggieBowl)
|
|
173
|
+
RFModDev = 61441 # "RF Mod Dev"
|
|
174
|
+
WiFiModDev = 61442 # "WiFi Mod Dev"
|
|
175
|
+
ETMRFModProd = 61443 # "ETM RF Mod Prod"
|
|
176
|
+
WiFiModBreakout = 61444 # "WiFi Mod Breakout"
|
|
177
|
+
Passthrough = 61445 # "Devel Board"
|
|
178
|
+
|
|
179
|
+
|
|
180
|
+
# Confirmed directly against the app's own decompiled getManufacturer()/
|
|
181
|
+
# isEtm() logic for every single entry above -- not a curated subset,
|
|
182
|
+
# and not re-derived by name pattern (an earlier version of this
|
|
183
|
+
# comment described it that way; that approach turned out to have at
|
|
184
|
+
# least one confirmed mistake -- see the CHANGELOG for this release).
|
|
120
185
|
MODEL_MANUFACTURER: dict[Model, str] = {
|
|
121
186
|
Model.Unknown: "Unknown",
|
|
187
|
+
|
|
188
|
+
# EcoTech Marine
|
|
122
189
|
Model.VorTechMP10wES: "EcoTech Marine",
|
|
123
190
|
Model.VorTechMP10wQD: "EcoTech Marine",
|
|
124
|
-
Model.VorTechMP40wES: "EcoTech Marine",
|
|
125
|
-
Model.VorTechMP40wQD: "EcoTech Marine",
|
|
126
|
-
Model.VorTechMP40wG3QD: "EcoTech Marine",
|
|
127
|
-
Model.VorTechMP60wES: "EcoTech Marine",
|
|
128
|
-
Model.VorTechMP60wQD: "EcoTech Marine",
|
|
129
191
|
Model.RadionXR30w: "EcoTech Marine",
|
|
130
192
|
Model.RadionXR30wG2: "EcoTech Marine",
|
|
131
193
|
Model.RadionXR30wPro: "EcoTech Marine",
|
|
@@ -135,6 +197,17 @@ MODEL_MANUFACTURER: dict[Model, str] = {
|
|
|
135
197
|
Model.RadionXR30wG4Pro: "EcoTech Marine",
|
|
136
198
|
Model.RadionXR30wFWPro: "EcoTech Marine",
|
|
137
199
|
Model.RadionXR30wPrototype: "EcoTech Marine",
|
|
200
|
+
Model.VorTechMP40wES: "EcoTech Marine",
|
|
201
|
+
Model.VorTechMP40wQD: "EcoTech Marine",
|
|
202
|
+
Model.VorTechMP40wG3QD: "EcoTech Marine",
|
|
203
|
+
Model.VorTechMP60wES: "EcoTech Marine",
|
|
204
|
+
Model.VorTechMP60wQD: "EcoTech Marine",
|
|
205
|
+
Model.VectraM1: "EcoTech Marine",
|
|
206
|
+
Model.VectraL1: "EcoTech Marine",
|
|
207
|
+
Model.VectraS1: "EcoTech Marine",
|
|
208
|
+
Model.VectraS2: "EcoTech Marine",
|
|
209
|
+
Model.VectraM2: "EcoTech Marine",
|
|
210
|
+
Model.VectraL2: "EcoTech Marine",
|
|
138
211
|
Model.RadionXR15wG3Pro: "EcoTech Marine",
|
|
139
212
|
Model.RadionXR15wFW: "EcoTech Marine",
|
|
140
213
|
Model.RadionXR15wG4Pro: "EcoTech Marine",
|
|
@@ -147,18 +220,75 @@ MODEL_MANUFACTURER: dict[Model, str] = {
|
|
|
147
220
|
Model.RadionXR15wG6Blue: "EcoTech Marine",
|
|
148
221
|
Model.RadionXR30wG5Pro: "EcoTech Marine",
|
|
149
222
|
Model.RadionXR30wG5Blue: "EcoTech Marine",
|
|
150
|
-
Model.
|
|
151
|
-
Model.
|
|
152
|
-
Model.
|
|
153
|
-
Model.
|
|
154
|
-
Model.
|
|
155
|
-
Model.
|
|
223
|
+
Model.RadionXR30wG5FW: "EcoTech Marine",
|
|
224
|
+
Model.RadionXR30wG6Pro: "EcoTech Marine",
|
|
225
|
+
Model.RadionXR30wG6Blue: "EcoTech Marine",
|
|
226
|
+
Model.VersaVX1: "EcoTech Marine",
|
|
227
|
+
Model.Thundervolt: "EcoTech Marine",
|
|
228
|
+
Model.Leap: "EcoTech Marine",
|
|
229
|
+
Model.ETMRFModProd: "EcoTech Marine",
|
|
230
|
+
|
|
231
|
+
# AquaIllumination
|
|
156
232
|
Model.Nero5: "AquaIllumination",
|
|
157
233
|
Model.Nero3: "AquaIllumination",
|
|
158
|
-
Model.
|
|
159
|
-
Model.
|
|
160
|
-
Model.
|
|
161
|
-
Model.
|
|
234
|
+
Model.Nero7: "AquaIllumination",
|
|
235
|
+
Model.Orbit2: "AquaIllumination",
|
|
236
|
+
Model.Orbit4: "AquaIllumination",
|
|
237
|
+
Model.Axis20: "AquaIllumination",
|
|
238
|
+
Model.Axis40: "AquaIllumination",
|
|
239
|
+
Model.Axis90: "AquaIllumination",
|
|
240
|
+
Model.Axis200: "AquaIllumination",
|
|
241
|
+
Model.SproutSL1: "AquaIllumination",
|
|
242
|
+
Model.EnvironmentalSensor: "AquaIllumination",
|
|
243
|
+
Model.Prime: "AquaIllumination",
|
|
244
|
+
Model.PrimeHD: "AquaIllumination",
|
|
245
|
+
Model.PrimeFW: "AquaIllumination",
|
|
246
|
+
Model.HydraTwentySixHD: "AquaIllumination",
|
|
247
|
+
Model.HydraFiftyTwoHD: "AquaIllumination",
|
|
248
|
+
Model.RedSeaMAX13: "AquaIllumination",
|
|
249
|
+
Model.RedSeaMAX26: "AquaIllumination",
|
|
250
|
+
Model.PrimeSixteenHD: "AquaIllumination",
|
|
251
|
+
Model.HydraThirtyTwoHD: "AquaIllumination",
|
|
252
|
+
Model.HydraSixtyFourHD: "AquaIllumination",
|
|
253
|
+
Model.PrimeSixteenHdFw: "AquaIllumination",
|
|
254
|
+
Model.PrimeSixteenHdFuge: "AquaIllumination",
|
|
255
|
+
Model.PrimeSixteenHdSol: "AquaIllumination",
|
|
256
|
+
Model.PrimeFuge: "AquaIllumination",
|
|
257
|
+
Model.Hydra32HDFreshwater: "AquaIllumination",
|
|
258
|
+
Model.Plumbus: "AquaIllumination",
|
|
259
|
+
Model.Blade: "AquaIllumination",
|
|
260
|
+
Model.BladeD: "AquaIllumination",
|
|
261
|
+
Model.BladeCoralGlow: "AquaIllumination",
|
|
262
|
+
Model.BladeCoralGrow: "AquaIllumination",
|
|
263
|
+
Model.BladeFreshwater: "AquaIllumination",
|
|
264
|
+
Model.BladeRefugium: "AquaIllumination",
|
|
265
|
+
Model.BladeGrowPlus: "AquaIllumination",
|
|
266
|
+
Model.BladeGloFish: "AquaIllumination",
|
|
267
|
+
Model.HydraEdge44HD: "AquaIllumination",
|
|
268
|
+
Model.HydraEdge68HD: "AquaIllumination",
|
|
269
|
+
Model.HydraEdge44HDFreshwater: "AquaIllumination",
|
|
270
|
+
Model.HydraEdge68HDFreshwater: "AquaIllumination",
|
|
271
|
+
Model.RFModDev: "AquaIllumination",
|
|
272
|
+
Model.WiFiModDev: "AquaIllumination",
|
|
273
|
+
Model.WiFiModBreakout: "AquaIllumination",
|
|
274
|
+
Model.Passthrough: "AquaIllumination",
|
|
275
|
+
|
|
276
|
+
# Neptune Systems
|
|
277
|
+
Model.DoseQD: "Neptune Systems",
|
|
278
|
+
Model.MXM: "Neptune Systems",
|
|
279
|
+
Model.Hera: "Neptune Systems",
|
|
280
|
+
Model.HeraBackplane: "Neptune Systems",
|
|
281
|
+
Model.HeraPHSensor: "Neptune Systems",
|
|
282
|
+
Model.HeraDissolvedOxygenSensor: "Neptune Systems",
|
|
283
|
+
Model.HeraFluidMonitoringModule: "Neptune Systems",
|
|
284
|
+
Model.HeraIOModule: "Neptune Systems",
|
|
285
|
+
Model.HeraORPSensor: "Neptune Systems",
|
|
286
|
+
Model.HeraSalinitySensor: "Neptune Systems",
|
|
287
|
+
Model.HeraSwitchedPowerModule: "Neptune Systems",
|
|
288
|
+
|
|
289
|
+
# NYOS
|
|
290
|
+
Model.Quantum160: "NYOS",
|
|
291
|
+
Model.Quantum220300: "NYOS",
|
|
162
292
|
}
|
|
163
293
|
|
|
164
294
|
|
|
@@ -276,6 +406,9 @@ class C2Attribute(IntEnum):
|
|
|
276
406
|
LunarPhasesEnabled = 907
|
|
277
407
|
Epoch = 201
|
|
278
408
|
RTCTime = 219
|
|
409
|
+
LocalTime = 207 # already device-timezone-adjusted -- see
|
|
410
|
+
# get_device_local_date()'s own docstring in
|
|
411
|
+
# device.py for why this exists at all
|
|
279
412
|
# Thread mesh peer discovery -- see documentation/09-thread-coap-relay.md
|
|
280
413
|
# for the full protocol trace. MeshLocalAddresses confirmed via
|
|
281
414
|
# reverse engineering the app's own connection-setup logic; the three
|
mobius/device.py
CHANGED
|
@@ -637,10 +637,12 @@ class MobiusDevice:
|
|
|
637
637
|
a real, known thing worth
|
|
638
638
|
checking, using this exact same drift computation
|
|
639
639
|
(local_time_seconds - device_reported_seconds). See
|
|
640
|
-
DeviceTimeInfo's docstring for the important caveat that
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
640
|
+
DeviceTimeInfo's docstring for the important caveat that these
|
|
641
|
+
two specific attributes don't directly explain a schedule-
|
|
642
|
+
interpolation mismatch by themselves (LocalTime, a different
|
|
643
|
+
attribute entirely, is what actually matters there -- see
|
|
644
|
+
get_device_local_date()), but real device clock drift is still
|
|
645
|
+
worth ruling out as a general health check.
|
|
644
646
|
"""
|
|
645
647
|
import datetime as _datetime
|
|
646
648
|
now = int(_datetime.datetime.now().timestamp())
|
|
@@ -668,6 +670,52 @@ class MobiusDevice:
|
|
|
668
670
|
rtc_drift_seconds=(now - device_rtc) if device_rtc is not None else None,
|
|
669
671
|
)
|
|
670
672
|
|
|
673
|
+
async def get_device_local_date(self) -> Optional["datetime.date"]:
|
|
674
|
+
"""
|
|
675
|
+
The gateway device's own, ALREADY TIMEZONE-ADJUSTED idea of what
|
|
676
|
+
calendar date it currently is -- confirmed, not assumed, to be
|
|
677
|
+
the actual source get_current_light_intensities() needs for its
|
|
678
|
+
own lunar-phase calculation, by tracing the real app's own call
|
|
679
|
+
chain directly: VPointScheduleView's own lunar.percentReduction()
|
|
680
|
+
call passes vPointScheduleView2.time.date(), where that `time`
|
|
681
|
+
is set via Tank.getDateTime() -- which is populated by
|
|
682
|
+
DateTime.parseRequest(), which reads Epoch (201) AND LocalTime
|
|
683
|
+
(207) directly off the connected gateway device itself, NOT
|
|
684
|
+
computed from the phone's own system clock at all. An earlier
|
|
685
|
+
version of this library's own DeviceTimeInfo docstring claimed
|
|
686
|
+
the opposite ("the app's own schedule interpolation uses the
|
|
687
|
+
phone/tank's own clock, NOT either of these device-reported
|
|
688
|
+
values") -- that claim was wrong specifically for the lunar
|
|
689
|
+
calculation, confirmed by a real, reported production mismatch
|
|
690
|
+
(this library showing the wrong lunar phase for hours at a
|
|
691
|
+
stretch, spanning exactly the window between local calendar
|
|
692
|
+
midnight and whenever the device's own reported date actually
|
|
693
|
+
rolled over) that this method's own existence now fixes.
|
|
694
|
+
|
|
695
|
+
LocalTime specifically, not Epoch: the device itself already
|
|
696
|
+
applies its own configured timezone offset when reporting this
|
|
697
|
+
value (confirmed by the app's own DateTime constructor -- it
|
|
698
|
+
derives its own UTC offset as Epoch minus LocalTime, meaning
|
|
699
|
+
LocalTime is Epoch already shifted by that same offset), so it
|
|
700
|
+
can be treated directly as an offset-adjusted "local-looking"
|
|
701
|
+
Unix timestamp -- no separate timezone lookup or arithmetic
|
|
702
|
+
needed on this library's own side at all.
|
|
703
|
+
|
|
704
|
+
Returns None if this attribute isn't available for any reason
|
|
705
|
+
(unsupported, or a soft read failure) -- callers should fall
|
|
706
|
+
back to their own local system date in that case, same as
|
|
707
|
+
get_current_light_intensities() itself does.
|
|
708
|
+
"""
|
|
709
|
+
try:
|
|
710
|
+
raw = await self.get_attribute(C2Attribute.LocalTime)
|
|
711
|
+
except Exception:
|
|
712
|
+
return None
|
|
713
|
+
if not raw:
|
|
714
|
+
return None
|
|
715
|
+
import datetime as _datetime
|
|
716
|
+
local_time_seconds = struct.unpack("<I", raw[0])[0]
|
|
717
|
+
return _datetime.datetime.fromtimestamp(local_time_seconds, tz=_datetime.timezone.utc).date()
|
|
718
|
+
|
|
671
719
|
async def get_own_mesh_address(self) -> Optional[bytes]:
|
|
672
720
|
"""
|
|
673
721
|
Fetches MeshLocalAddresses (1005) -- THIS device's own Thread
|
|
@@ -1464,16 +1512,27 @@ class MobiusDevice:
|
|
|
1464
1512
|
now: Optional["datetime.datetime"] = None) -> LightIntensityResult:
|
|
1465
1513
|
"""
|
|
1466
1514
|
Replicates what the app actually shows as "current intensity": fetch
|
|
1467
|
-
the schedule, linearly interpolate it for the given time (
|
|
1468
|
-
to local system time
|
|
1469
|
-
|
|
1470
|
-
|
|
1471
|
-
|
|
1472
|
-
|
|
1515
|
+
the schedule, linearly interpolate it for the given time (minute_of_day
|
|
1516
|
+
defaults to local system time if not given -- pass it explicitly with
|
|
1517
|
+
a timezone-aware value if the caller has one, since this library has
|
|
1518
|
+
no way to know the correct timezone on its own), then apply whichever
|
|
1519
|
+
scalar the app itself would apply on top -- confirmed via reverse
|
|
1520
|
+
engineering the app's own exact branching:
|
|
1473
1521
|
|
|
1474
1522
|
- dusk-to-night segment (see is_night_segment()) AND lunar phases
|
|
1475
1523
|
enabled -> lunar_percent_reduction(date) in place of the normal
|
|
1476
|
-
schedule intensity
|
|
1524
|
+
schedule intensity. The date used here specifically is
|
|
1525
|
+
get_device_local_date()'s own result (the connected gateway's
|
|
1526
|
+
own reported LocalTime attribute), falling back to `now`'s own
|
|
1527
|
+
date only if that read fails -- confirmed by tracing the real
|
|
1528
|
+
app's own call chain directly (see get_device_local_date()'s
|
|
1529
|
+
own docstring for the full trace) that this is genuinely a
|
|
1530
|
+
DEVICE-reported value, not phone/system-local time at all,
|
|
1531
|
+
unlike everything else this method computes. A real, reported
|
|
1532
|
+
production mismatch (this library showing the wrong lunar
|
|
1533
|
+
phase for hours at a stretch, spanning exactly the gap between
|
|
1534
|
+
local calendar midnight and whenever the device's own reported
|
|
1535
|
+
date actually rolled over) is what confirmed this.
|
|
1477
1536
|
- dusk-to-night segment, lunar not enabled -> scalar = 1.0
|
|
1478
1537
|
(full/unreduced)
|
|
1479
1538
|
- otherwise (normal daytime) -> the schedule-level master
|
|
@@ -1491,13 +1550,17 @@ class MobiusDevice:
|
|
|
1491
1550
|
|
|
1492
1551
|
`.diagnostics` also includes "is_night_segment", "lunar_enabled",
|
|
1493
1552
|
"scalar_source" (one of "lunar"/"night_no_lunar"/
|
|
1494
|
-
"schedule_intensity"),
|
|
1495
|
-
applied)
|
|
1496
|
-
|
|
1497
|
-
|
|
1498
|
-
|
|
1499
|
-
|
|
1500
|
-
|
|
1553
|
+
"schedule_intensity"), "scalar" (the actual final multiplier
|
|
1554
|
+
applied), and, specifically for the "lunar" case, "lunar_date"
|
|
1555
|
+
(the actual date used for that calculation) and
|
|
1556
|
+
"lunar_date_source" (one of "device"/"local_fallback") -- added
|
|
1557
|
+
specifically to make it possible to tell which branch was taken
|
|
1558
|
+
and why, since a mismatch against the app's own displayed value
|
|
1559
|
+
could stem from is_night_segment() misjudging this schedule's
|
|
1560
|
+
specific point structure, get_lunar_enabled() reading
|
|
1561
|
+
incorrectly, or (confirmed now to be a real, genuine possibility)
|
|
1562
|
+
the wrong date being used for the lunar calculation itself, and
|
|
1563
|
+
those need to be told apart with real data rather than guessed at.
|
|
1501
1564
|
"""
|
|
1502
1565
|
import datetime as _datetime
|
|
1503
1566
|
if now is None:
|
|
@@ -1509,10 +1572,17 @@ class MobiusDevice:
|
|
|
1509
1572
|
raw_intensities = interpolate_light_schedule(points, minute_of_day)
|
|
1510
1573
|
|
|
1511
1574
|
night_segment = is_night_segment(points, minute_of_day)
|
|
1575
|
+
lunar_date = None
|
|
1576
|
+
lunar_date_source = None
|
|
1512
1577
|
if night_segment:
|
|
1513
1578
|
lunar_enabled = await self.get_lunar_enabled(which)
|
|
1514
1579
|
if lunar_enabled:
|
|
1515
|
-
|
|
1580
|
+
lunar_date = await self.get_device_local_date()
|
|
1581
|
+
lunar_date_source = "device"
|
|
1582
|
+
if lunar_date is None:
|
|
1583
|
+
lunar_date = now.date()
|
|
1584
|
+
lunar_date_source = "local_fallback"
|
|
1585
|
+
scalar = lunar_percent_reduction(lunar_date)
|
|
1516
1586
|
scalar_source = "lunar"
|
|
1517
1587
|
else:
|
|
1518
1588
|
scalar = 1.0
|
|
@@ -1533,6 +1603,8 @@ class MobiusDevice:
|
|
|
1533
1603
|
"lunar_enabled": lunar_enabled,
|
|
1534
1604
|
"scalar_source": scalar_source,
|
|
1535
1605
|
"scalar": scalar,
|
|
1606
|
+
"lunar_date": lunar_date,
|
|
1607
|
+
"lunar_date_source": lunar_date_source,
|
|
1536
1608
|
})
|
|
1537
1609
|
|
|
1538
1610
|
async def get_current_light_percentages(self, which: int = 1,
|
mobius/device_status.py
CHANGED
|
@@ -99,12 +99,15 @@ class DeviceTimeInfo:
|
|
|
99
99
|
clock, treating a large discrepancy as a real, known failure mode --
|
|
100
100
|
this isn't speculative, it's the exact same check the app runs.
|
|
101
101
|
|
|
102
|
-
Note:
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
102
|
+
Note: this doesn't include LocalTime (207) -- see
|
|
103
|
+
get_device_local_date()'s own docstring for why that one specific
|
|
104
|
+
attribute is genuinely different (it's what the app's own lunar-
|
|
105
|
+
phase calculation actually uses, confirmed by tracing the app's own
|
|
106
|
+
call chain directly), unlike Epoch/RTCTime here, which the app's
|
|
107
|
+
own schedule interpolation itself does NOT use (an earlier version
|
|
108
|
+
of this docstring incorrectly extended that same "not used for
|
|
109
|
+
schedule purposes" claim to LocalTime too, before that call chain
|
|
110
|
+
had actually been traced).
|
|
108
111
|
"""
|
|
109
112
|
device_epoch: Optional[int] # raw UNIX timestamp reported by the device
|
|
110
113
|
device_rtc: Optional[int]
|
mobius/relay.py
CHANGED
|
@@ -177,12 +177,52 @@ class RelayedMobiusDevice(MobiusDevice):
|
|
|
177
177
|
embedded in its payload (confirmed via reverse engineering:
|
|
178
178
|
coapRequest.getToken() == coapResponse.getToken()) -- never by
|
|
179
179
|
this outer frame's own message ID.
|
|
180
|
+
|
|
181
|
+
Retries exactly once on a timeout before actually raising --
|
|
182
|
+
confirmed via reverse engineering the app's own request-timeout
|
|
183
|
+
handling: it never treats a single timed-out relay round-trip as
|
|
184
|
+
a real failure, always transparently retrying (fresh send, same
|
|
185
|
+
request) once first, only surfacing a failure if the retry ALSO
|
|
186
|
+
times out. A relayed round-trip crosses BLE (to the gateway) and
|
|
187
|
+
then the Thread mesh itself (gateway to target and back) -- a
|
|
188
|
+
single dropped/delayed packet somewhere in that longer path is
|
|
189
|
+
evidently common enough that the app was built assuming it will
|
|
190
|
+
happen regularly, not as an edge case. Each attempt gets its own
|
|
191
|
+
fresh CoAP token (this library generates one per call already,
|
|
192
|
+
not tied to a single request object the way the app's own retry
|
|
193
|
+
reuses one) -- deliberately, so a late, delayed response to the
|
|
194
|
+
first, abandoned attempt can never be mismatched against the
|
|
195
|
+
retry's own, separate wait.
|
|
180
196
|
"""
|
|
181
197
|
if not self.gateway.is_connected:
|
|
182
198
|
raise IOError(
|
|
183
199
|
f"gateway device is not connected -- cannot relay to {self.peer.serial!r} through it"
|
|
184
200
|
)
|
|
185
201
|
|
|
202
|
+
last_timeout_token: Optional[int] = None
|
|
203
|
+
for attempt in (1, 2):
|
|
204
|
+
try:
|
|
205
|
+
return await self._relay_attempt(inner_request_frame, is_retry=(attempt == 2))
|
|
206
|
+
except asyncio.TimeoutError as err:
|
|
207
|
+
last_timeout_token = err.args[0] if err.args else None
|
|
208
|
+
if attempt == 1 and self.debug:
|
|
209
|
+
print(f" [relay debug] timed out waiting {self.timeout}s for "
|
|
210
|
+
f"{self.peer.serial!r} -- retrying once before giving up")
|
|
211
|
+
continue
|
|
212
|
+
|
|
213
|
+
raise IOError(
|
|
214
|
+
f"timed out waiting {self.timeout}s for a relayed response from target "
|
|
215
|
+
f"{self.peer.serial!r} (no CoAP indication received for token {last_timeout_token}, "
|
|
216
|
+
f"even after one retry)"
|
|
217
|
+
)
|
|
218
|
+
|
|
219
|
+
async def _relay_attempt(self, inner_request_frame: bytes, is_retry: bool) -> ParsedFrame:
|
|
220
|
+
"""One full send-and-await cycle for _relay() -- split out so
|
|
221
|
+
_relay() itself can retry it once on a timeout without
|
|
222
|
+
duplicating this whole body. Raises asyncio.TimeoutError itself
|
|
223
|
+
(not wrapped in IOError) specifically so _relay() can catch just
|
|
224
|
+
that case to decide whether to retry; every other exception here
|
|
225
|
+
propagates as-is, unwrapped, straight out of _relay() too."""
|
|
186
226
|
token = next_coap_token()
|
|
187
227
|
# Confirmed always GET regardless of the inner operation -- see
|
|
188
228
|
# this module's docstring for why.
|
|
@@ -193,7 +233,8 @@ class RelayedMobiusDevice(MobiusDevice):
|
|
|
193
233
|
outer_frame = build_frame(OPGROUP_C2CI_REQUEST, COAP_OPCODE, coap_payload, outer_message_id)
|
|
194
234
|
|
|
195
235
|
if self.debug:
|
|
196
|
-
|
|
236
|
+
retry_note = " (retry)" if is_retry else ""
|
|
237
|
+
print(f" [relay debug] inner request frame{retry_note} ({len(inner_request_frame)}B): "
|
|
197
238
|
f"{inner_request_frame.hex()}")
|
|
198
239
|
print(f" [relay debug] target address: {self.peer.address.hex()} token: {token}")
|
|
199
240
|
print(f" [relay debug] outer request frame ({len(outer_frame)}B): {outer_frame.hex()}")
|
|
@@ -220,10 +261,7 @@ class RelayedMobiusDevice(MobiusDevice):
|
|
|
220
261
|
coap_response = await asyncio.wait_for(coap_future, timeout=self.timeout)
|
|
221
262
|
except asyncio.TimeoutError:
|
|
222
263
|
self.gateway._pending_coap_tokens.pop(token, None)
|
|
223
|
-
raise
|
|
224
|
-
f"timed out waiting {self.timeout}s for a relayed response from target "
|
|
225
|
-
f"{self.peer.serial!r} (no CoAP indication received for token {token})"
|
|
226
|
-
)
|
|
264
|
+
raise asyncio.TimeoutError(token) from None
|
|
227
265
|
finally:
|
|
228
266
|
if self.debug:
|
|
229
267
|
self.gateway.on_any_frame = previous_hook
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: python-mobius
|
|
3
|
-
Version: 0.4.
|
|
3
|
+
Version: 0.4.4
|
|
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
|
|
@@ -1,10 +1,10 @@
|
|
|
1
|
-
mobius/__init__.py,sha256=
|
|
1
|
+
mobius/__init__.py,sha256=3DdT6fMBcYJjlr_U1DMTJ3mHeoz_Ri-B9g8B8c9iAZo,5722
|
|
2
2
|
mobius/cli.py,sha256=8XH-pVTX4KObzd0IOGXicT7KrcNEjm5xYZS8JOWfHDA,30726
|
|
3
3
|
mobius/coap.py,sha256=eXf_-9heQD1UjLyqZU9KA-c0jB50NGzAd8D7d6kFMb0,10750
|
|
4
|
-
mobius/constants.py,sha256=
|
|
4
|
+
mobius/constants.py,sha256=mIMc2Xk9io4jt2i6EKaEGs2nrkHFdFm7ovoJR4kRctI,33377
|
|
5
5
|
mobius/crc.py,sha256=kLtAYWZvLbo5_p9ep08ZlLg5Yah8Q2qq0IwTvqbBGW0,2700
|
|
6
|
-
mobius/device.py,sha256=
|
|
7
|
-
mobius/device_status.py,sha256=
|
|
6
|
+
mobius/device.py,sha256=TOT-hmlbrMjlJ7quZmm2plZC1ILxo67XtV7z9zUNoaA,83298
|
|
7
|
+
mobius/device_status.py,sha256=ZlhZt5499l2tbA1tRvVoBLqakrexYboNZTDLptK0a3Y,8584
|
|
8
8
|
mobius/discovery.py,sha256=LqHOEA_j1ZROuounn3PPmy8IEbpL6-N1TOW1MpoKg6A,11619
|
|
9
9
|
mobius/frame.py,sha256=UNLTHuzVue5b6L4nv1hWJTSbn5k8lETWdsiL_52yI0Q,6141
|
|
10
10
|
mobius/manufacturer.py,sha256=8-vBzckN8xmsPSMj5EWtOCXenbyQueoY5pnZHePgb_k,1937
|
|
@@ -12,10 +12,10 @@ mobius/mesh_address.py,sha256=PKQ-mm7Z4bY11ZnVmeWZO9EmOiAHp8hNtXceMGMs2tk,5940
|
|
|
12
12
|
mobius/modifiers.py,sha256=zWJCsjaJ4TtwS1QgQoRFH-ve1mykuPWn5KX4YQpNqco,7215
|
|
13
13
|
mobius/power.py,sha256=5l9ehjNCDrSsJjQkaU4M6pYnSMqw2FVF8Ogg6KIE5vQ,3844
|
|
14
14
|
mobius/pump_status.py,sha256=RWTl8ZViCFDbiFU2mX-WAIH_cWhZOZ0M2B0oYXnPx3Y,1598
|
|
15
|
-
mobius/relay.py,sha256=
|
|
15
|
+
mobius/relay.py,sha256=bo2uUV6HhHD_-LuH4CkuPJtnKGQGM6crmB6kLAU8i4E,18857
|
|
16
16
|
mobius/schedule.py,sha256=d5iAHvKuFDwfbrSptPx6viiA_lj2qGfWhsZwM_Bpr1k,10974
|
|
17
|
-
python_mobius-0.4.
|
|
18
|
-
python_mobius-0.4.
|
|
19
|
-
python_mobius-0.4.
|
|
20
|
-
python_mobius-0.4.
|
|
21
|
-
python_mobius-0.4.
|
|
17
|
+
python_mobius-0.4.4.dist-info/METADATA,sha256=izBMnM3A_kuwJ8hzrWdSt6p7X4Gpxy3R1k4lkhhTj0I,5952
|
|
18
|
+
python_mobius-0.4.4.dist-info/WHEEL,sha256=zOwg4jB6zX2kU910N-cMawjivD6tO8NEWvE12je1bVk,87
|
|
19
|
+
python_mobius-0.4.4.dist-info/entry_points.txt,sha256=Q2hlcek-bWm70zvd12v32essflPgC2su-dKDHAqbPoE,48
|
|
20
|
+
python_mobius-0.4.4.dist-info/licenses/LICENSE,sha256=7a72Msu2Q-TnoiFxemxEGkwafJGObk1W3rw9hzmyM_Y,17984
|
|
21
|
+
python_mobius-0.4.4.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|