python-mobius 0.4.4__py3-none-any.whl → 0.5.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 +5 -5
- mobius/cli.py +98 -6
- mobius/constants.py +113 -26
- mobius/device.py +417 -58
- mobius/device_status.py +141 -4
- mobius/discovery.py +11 -4
- mobius/manufacturer.py +33 -14
- mobius/modifiers.py +6 -6
- mobius/relay.py +37 -2
- mobius/schedule.py +5 -6
- {python_mobius-0.4.4.dist-info → python_mobius-0.5.0.dist-info}/METADATA +37 -8
- python_mobius-0.5.0.dist-info/RECORD +21 -0
- python_mobius-0.4.4.dist-info/RECORD +0 -21
- {python_mobius-0.4.4.dist-info → python_mobius-0.5.0.dist-info}/WHEEL +0 -0
- {python_mobius-0.4.4.dist-info → python_mobius-0.5.0.dist-info}/entry_points.txt +0 -0
- {python_mobius-0.4.4.dist-info → python_mobius-0.5.0.dist-info}/licenses/LICENSE +0 -0
mobius/__init__.py
CHANGED
|
@@ -35,7 +35,7 @@ from .schedule import (
|
|
|
35
35
|
LightPrimitive, SchedulePoint, interpolate_light_schedule,
|
|
36
36
|
PumpPrimitiveValue, PumpSchedulePoint, get_active_pump_block,
|
|
37
37
|
)
|
|
38
|
-
from .manufacturer import
|
|
38
|
+
from .manufacturer import MOBIUS_COMPANY_ID_ECOTECH, MOBIUS_COMPANY_ID_AQUAILLUMINATION, MOBIUS_COMPANY_IDS, MobiusAdvertisement, parse_manufacturer_data
|
|
39
39
|
from .modifiers import (
|
|
40
40
|
AcclimationInfo, is_night_segment,
|
|
41
41
|
julian_day_from_date, lunar_days_into_phase, lunar_percent_reduction,
|
|
@@ -43,7 +43,7 @@ from .modifiers import (
|
|
|
43
43
|
)
|
|
44
44
|
from .power import ChannelPowerInfo, channel_percent_value
|
|
45
45
|
from .pump_status import PumpFlowRange, BatteryBackupInfo, BoostedBatteryInfo
|
|
46
|
-
from .device_status import GroupInfo, CalibrationInfo, MaintenanceInfo, DeviceTimeInfo, MeshPeer, Tank
|
|
46
|
+
from .device_status import GroupInfo, CalibrationInfo, AdvancedFeatures, VectraInfo, CoffeeInfo, MaintenanceInfo, DeviceTimeInfo, MeshPeer, Tank, LunarPhaseInfo
|
|
47
47
|
from .coap import (
|
|
48
48
|
CoapRequestType, CoapMethod, CoapResponseCode, CoapResponse,
|
|
49
49
|
encode_coap_request, decode_coap_response, COAP_OPCODE, COAP_INDICATION_OPCODE,
|
|
@@ -65,7 +65,7 @@ from .discovery import (
|
|
|
65
65
|
discover_tank,
|
|
66
66
|
)
|
|
67
67
|
|
|
68
|
-
__version__ = "0.
|
|
68
|
+
__version__ = "0.5.0"
|
|
69
69
|
|
|
70
70
|
__all__ = [
|
|
71
71
|
"__version__",
|
|
@@ -93,7 +93,7 @@ __all__ = [
|
|
|
93
93
|
"LightPrimitive", "SchedulePoint", "interpolate_light_schedule",
|
|
94
94
|
"PumpPrimitiveValue", "PumpSchedulePoint", "get_active_pump_block",
|
|
95
95
|
# manufacturer
|
|
96
|
-
"
|
|
96
|
+
"MOBIUS_COMPANY_ID_ECOTECH", "MOBIUS_COMPANY_ID_AQUAILLUMINATION", "MOBIUS_COMPANY_IDS", "MobiusAdvertisement", "parse_manufacturer_data",
|
|
97
97
|
# modifiers
|
|
98
98
|
"AcclimationInfo", "is_night_segment",
|
|
99
99
|
"julian_day_from_date", "lunar_days_into_phase", "lunar_percent_reduction",
|
|
@@ -103,7 +103,7 @@ __all__ = [
|
|
|
103
103
|
# pump_status
|
|
104
104
|
"PumpFlowRange", "BatteryBackupInfo", "BoostedBatteryInfo",
|
|
105
105
|
# device_status
|
|
106
|
-
"GroupInfo", "CalibrationInfo", "MaintenanceInfo", "DeviceTimeInfo", "MeshPeer", "Tank",
|
|
106
|
+
"GroupInfo", "CalibrationInfo", "AdvancedFeatures", "VectraInfo", "CoffeeInfo", "MaintenanceInfo", "DeviceTimeInfo", "MeshPeer", "Tank", "LunarPhaseInfo",
|
|
107
107
|
"CoapRequestType", "CoapMethod", "CoapResponseCode", "CoapResponse",
|
|
108
108
|
"encode_coap_request", "decode_coap_response", "COAP_OPCODE", "COAP_INDICATION_OPCODE",
|
|
109
109
|
"next_coap_token",
|
mobius/cli.py
CHANGED
|
@@ -60,10 +60,27 @@ async def _dump_schedule(device: MobiusDevice, support: str) -> None:
|
|
|
60
60
|
print(f" {_format_time(point.time_minutes)}{_format_flags(point)}: {channels}")
|
|
61
61
|
|
|
62
62
|
lunar_enabled = await device.get_lunar_enabled()
|
|
63
|
+
print(f" lunar_enabled: {lunar_enabled}")
|
|
64
|
+
|
|
65
|
+
# The device's own firmware-computed lunar state -- see
|
|
66
|
+
# LunarPhaseInfo's own docstring in device_status.py for the
|
|
67
|
+
# confirmation and what this is useful for. Compare against
|
|
68
|
+
# "current_intensities"/"light_diagnostics" in the summary
|
|
69
|
+
# output above (NOT re-fetched here -- get_device_summary()
|
|
70
|
+
# already called get_current_light_intensities() once; a
|
|
71
|
+
# second call here would just be a redundant device read of
|
|
72
|
+
# the exact same thing). A real failure is shown directly
|
|
73
|
+
# (get_lunar_phase_info() no longer swallows it) rather than
|
|
74
|
+
# printing an indistinguishable "None".
|
|
75
|
+
try:
|
|
76
|
+
lunar_phase_info = await device.get_lunar_phase_info()
|
|
77
|
+
print(f" lunar_phase_info: {lunar_phase_info}")
|
|
78
|
+
except Exception as e:
|
|
79
|
+
print(f" lunar_phase_info: read failed: {e}")
|
|
80
|
+
|
|
63
81
|
insolation_active = await device.get_insolation_enabled()
|
|
64
82
|
schedule_intensity = await device.get_schedule_intensity()
|
|
65
83
|
acclimation = await device.get_acclimation_info()
|
|
66
|
-
print(f" lunar_enabled: {lunar_enabled}")
|
|
67
84
|
print(f" insolation_active: {insolation_active}")
|
|
68
85
|
print(f" schedule_intensity: {schedule_intensity}")
|
|
69
86
|
print(f" acclimation: {acclimation}")
|
|
@@ -241,7 +258,14 @@ async def _run(args) -> None:
|
|
|
241
258
|
device, info = found
|
|
242
259
|
print(f" currently at {device.address} model={info.model.name if info.model else info.model_raw}")
|
|
243
260
|
try:
|
|
244
|
-
|
|
261
|
+
# device= is the exact BLEDevice found_device_by_serial() just
|
|
262
|
+
# located above -- passing it avoids a second, independent scan
|
|
263
|
+
# inside connect() (see MobiusDevice.__init__()'s own docstring:
|
|
264
|
+
# re-scanning per device can cause BlueZ to drop it between
|
|
265
|
+
# "found" and "connect", the exact failure this fixes). serial=
|
|
266
|
+
# is still passed alongside it, for this same instance's own
|
|
267
|
+
# reconnect logic if the connection drops later in this run.
|
|
268
|
+
async with MobiusDevice(device=device, serial=args.by_serial, adapter=args.adapter,
|
|
245
269
|
connect_timeout=args.connect_timeout) as mdevice:
|
|
246
270
|
summary = await mdevice.get_device_summary()
|
|
247
271
|
for k, v in summary.items():
|
|
@@ -249,6 +273,7 @@ async def _run(args) -> None:
|
|
|
249
273
|
if args.dump_schedule:
|
|
250
274
|
await _dump_schedule(mdevice, summary.get("support", ""))
|
|
251
275
|
|
|
276
|
+
relayed = None # set below only if --relay-target resolves successfully
|
|
252
277
|
if args.dump_mesh_peers or args.relay_target:
|
|
253
278
|
print("\n=== Thread mesh peer discovery ===")
|
|
254
279
|
peers = await _debug_mesh_discovery(mdevice)
|
|
@@ -314,6 +339,47 @@ async def _run(args) -> None:
|
|
|
314
339
|
f"`--by-serial {args.relay_target}` run to confirm they match.")
|
|
315
340
|
except Exception as e:
|
|
316
341
|
print(f" relay failed: {e}")
|
|
342
|
+
|
|
343
|
+
if args.reboot:
|
|
344
|
+
# If --relay-target was also given, reboot THAT device
|
|
345
|
+
# via relay through the connected one, instead of the
|
|
346
|
+
# directly-connected device itself -- lets both the
|
|
347
|
+
# direct and relayed write paths get tested with the
|
|
348
|
+
# same flag, without a separate one. If relay resolution
|
|
349
|
+
# above failed, this deliberately does NOT fall back to
|
|
350
|
+
# rebooting the directly-connected device instead --
|
|
351
|
+
# that would silently reboot a device the user didn't
|
|
352
|
+
# actually ask to reboot.
|
|
353
|
+
if args.relay_target:
|
|
354
|
+
if relayed is None:
|
|
355
|
+
print(f"\n=== reboot() on {args.relay_target!r} (via relay) -- "
|
|
356
|
+
f"SKIPPED: relay target could not be resolved above ===")
|
|
357
|
+
reboot_target = None
|
|
358
|
+
else:
|
|
359
|
+
reboot_target_label = f"{args.relay_target!r} (via relay through {args.by_serial!r})"
|
|
360
|
+
reboot_target = relayed
|
|
361
|
+
else:
|
|
362
|
+
reboot_target_label = f"{args.by_serial!r} (direct)"
|
|
363
|
+
reboot_target = mdevice
|
|
364
|
+
|
|
365
|
+
if reboot_target is not None:
|
|
366
|
+
print(f"\n=== reboot() on {reboot_target_label} (WRITE -- Reset, soft) ===")
|
|
367
|
+
try:
|
|
368
|
+
await reboot_target.reboot()
|
|
369
|
+
print(f" reboot() on {reboot_target_label} succeeded (device "
|
|
370
|
+
f"acknowledged the request -- it may take a few seconds to "
|
|
371
|
+
f"actually finish rebooting and reconnect).")
|
|
372
|
+
except Exception as e:
|
|
373
|
+
print(f" reboot() FAILED: {e}")
|
|
374
|
+
|
|
375
|
+
if args.set_time_to_now:
|
|
376
|
+
print(f"\n=== set_time_to_now() on {args.by_serial!r} (WRITE -- Epoch) ===")
|
|
377
|
+
try:
|
|
378
|
+
await mdevice.set_time_to_now()
|
|
379
|
+
print(f" set_time_to_now() on {args.by_serial!r} succeeded.")
|
|
380
|
+
except Exception as e:
|
|
381
|
+
print(f" set_time_to_now() FAILED: {e}")
|
|
382
|
+
|
|
317
383
|
except Exception as e:
|
|
318
384
|
print(" failed to connect/read:", e)
|
|
319
385
|
return
|
|
@@ -365,7 +431,10 @@ async def _run(args) -> None:
|
|
|
365
431
|
device, info = connectable[0]
|
|
366
432
|
print(f"\n=== Network addresses (connecting to {info.serial} to ask) ===")
|
|
367
433
|
try:
|
|
368
|
-
|
|
434
|
+
# device= already found above via the scan feeding `connectable`
|
|
435
|
+
# -- see the --by-serial path's own comment for why this avoids
|
|
436
|
+
# a redundant, race-prone second scan inside connect().
|
|
437
|
+
async with MobiusDevice(device=device, serial=info.serial, adapter=args.adapter,
|
|
369
438
|
connect_timeout=args.connect_timeout) as mdevice:
|
|
370
439
|
peers = await _debug_mesh_discovery(mdevice)
|
|
371
440
|
except Exception as e:
|
|
@@ -398,7 +467,10 @@ async def _run(args) -> None:
|
|
|
398
467
|
device, info = connectable[0]
|
|
399
468
|
print(f" pan_id {label} (connecting through {info.serial})...")
|
|
400
469
|
try:
|
|
401
|
-
|
|
470
|
+
# device= already found above -- see the --by-serial path's
|
|
471
|
+
# own comment for why this avoids a redundant, race-prone
|
|
472
|
+
# second scan inside connect().
|
|
473
|
+
async with MobiusDevice(device=device, serial=info.serial, adapter=args.adapter,
|
|
402
474
|
connect_timeout=args.connect_timeout) as mdevice:
|
|
403
475
|
tank = await discover_tank(mdevice)
|
|
404
476
|
except Exception as e:
|
|
@@ -508,8 +580,11 @@ def main() -> None:
|
|
|
508
580
|
"(every point's time, flags, and per-channel/mode values) -- not "
|
|
509
581
|
"just the interpolated 'current' value get_device_summary() shows. "
|
|
510
582
|
"For lights, also prints lunar/insolation/schedule-intensity/"
|
|
511
|
-
"acclimation state
|
|
512
|
-
"
|
|
583
|
+
"acclimation state, PLUS the device's own firmware-computed lunar "
|
|
584
|
+
"state (LunarPhasesCurrentDay/CurrentScalar, confirmed against real "
|
|
585
|
+
"hardware -- see LunarPhaseInfo's own docstring) -- compare against "
|
|
586
|
+
"\"current_intensities\"/\"light_diagnostics\" in the summary output "
|
|
587
|
+
"above for this library's own calculated value.")
|
|
513
588
|
parser.add_argument("--dump-mesh-peers", action="store_true",
|
|
514
589
|
help="Only usable with --by-serial. After connecting, calls "
|
|
515
590
|
"discover_mesh_peers_auto() on it and prints every other device "
|
|
@@ -542,6 +617,23 @@ def main() -> None:
|
|
|
542
617
|
"confirmed-meaningless opcode-25 ack and the real opcode-26 "
|
|
543
618
|
"indication, if it arrives). Useful for diagnosing a relay that "
|
|
544
619
|
"times out or returns unexpected data.")
|
|
620
|
+
parser.add_argument("--set-time-to-now", action="store_true",
|
|
621
|
+
help="Only usable with --by-serial. A WRITE -- use on hardware you're "
|
|
622
|
+
"prepared to have its clock changed on. Calls set_time_to_now() "
|
|
623
|
+
"on the directly-connected device (Epoch, reserved-byte group=1 "
|
|
624
|
+
"-- required for the write to be accepted at all, confirmed "
|
|
625
|
+
"against real hardware). See that method's own docstring, and "
|
|
626
|
+
"documentation/09-thread-coap-relay.md, for what's confirmed "
|
|
627
|
+
"about this vs the rest of the mesh.")
|
|
628
|
+
parser.add_argument("--reboot", action="store_true",
|
|
629
|
+
help="Only usable with --by-serial. A WRITE -- use on hardware you're "
|
|
630
|
+
"prepared to have reboot. Soft-reboots the directly-connected "
|
|
631
|
+
"device (Reset, ResetType.Soft) -- confirmed via the app's own "
|
|
632
|
+
"source to be exactly its own \"Restart\" button. If "
|
|
633
|
+
"--relay-target is also given, reboots THAT device via relay "
|
|
634
|
+
"through the connected one instead, to test the relayed write "
|
|
635
|
+
"path specifically -- see reboot()'s own docstring for what's "
|
|
636
|
+
"confirmed about this vs a direct connection.")
|
|
545
637
|
parser.add_argument("--build-peer-map", action="store_true",
|
|
546
638
|
help="Instead of full device summaries, scan for devices then "
|
|
547
639
|
"briefly connect to EACH one in turn to build a mesh peer map "
|
mobius/constants.py
CHANGED
|
@@ -20,7 +20,7 @@ from typing import Optional
|
|
|
20
20
|
|
|
21
21
|
class PrimitiveType(IntEnum):
|
|
22
22
|
"""What kind of device this is, at the protocol level. Confirmed literal
|
|
23
|
-
values
|
|
23
|
+
values."""
|
|
24
24
|
Undefined = 0
|
|
25
25
|
VisualV1 = 1 # Radion / light-class devices (e.g. XR15, XR30, Prime, Hydra)
|
|
26
26
|
PumpV1 = 2 # legacy Nero pumps
|
|
@@ -33,7 +33,7 @@ class PrimitiveType(IntEnum):
|
|
|
33
33
|
CoffeeV1 = 9 # NYOS Quantum skimmers/dosers
|
|
34
34
|
|
|
35
35
|
|
|
36
|
-
# Confirmed from
|
|
36
|
+
# Confirmed from the app's own per-primitive size lookup -- the fixed byte-length (or, for
|
|
37
37
|
# VisualV1, the per-channel-triplet unit) of that primitive type's schedule
|
|
38
38
|
# point payload.
|
|
39
39
|
PRIMITIVE_SIZE: dict[PrimitiveType, int] = {
|
|
@@ -177,11 +177,9 @@ class Model(IntEnum):
|
|
|
177
177
|
Passthrough = 61445 # "Devel Board"
|
|
178
178
|
|
|
179
179
|
|
|
180
|
-
# Confirmed directly against the app's own decompiled
|
|
181
|
-
#
|
|
182
|
-
#
|
|
183
|
-
# comment described it that way; that approach turned out to have at
|
|
184
|
-
# least one confirmed mistake -- see the CHANGELOG for this release).
|
|
180
|
+
# Confirmed directly against the app's own decompiled manufacturer-lookup
|
|
181
|
+
# logic for every single entry above -- not a curated subset, and not
|
|
182
|
+
# re-derived by name pattern.
|
|
185
183
|
MODEL_MANUFACTURER: dict[Model, str] = {
|
|
186
184
|
Model.Unknown: "Unknown",
|
|
187
185
|
|
|
@@ -300,8 +298,9 @@ def manufacturer_for_model(model: Optional[Model]) -> Optional[str]:
|
|
|
300
298
|
|
|
301
299
|
|
|
302
300
|
class ErrorState(IntEnum):
|
|
303
|
-
"""Confirmed literal values
|
|
304
|
-
pump/light fault codes -- the full
|
|
301
|
+
"""Confirmed literal values for device error states (subset covering
|
|
302
|
+
common pump/light fault codes -- the full set also has dosing-pump-
|
|
303
|
+
specific codes)."""
|
|
305
304
|
NoError = 0
|
|
306
305
|
Disconnect = 1
|
|
307
306
|
Temperature = 2
|
|
@@ -347,13 +346,19 @@ class C2Attribute(IntEnum):
|
|
|
347
346
|
integer -- see documentation/03-attributes-and-opcodes.md), EXCEPT
|
|
348
347
|
Schedule1 which required tracing through an extra hop via an unrelated
|
|
349
348
|
library constant to resolve (also fully confirmed, just
|
|
350
|
-
via that extra hop).
|
|
349
|
+
via that extra hop). PowerOnDelay (800) and CoffeeStatusIntensity
|
|
350
|
+
(4100) are a similar case, resolved from third-party Android library/
|
|
351
|
+
SDK constants this project couldn't confirm via web search on its
|
|
352
|
+
own -- confirmed directly instead. Both fit the surrounding
|
|
353
|
+
sequential numbering exactly (717/800/801/802, 4100/4101/4102),
|
|
354
|
+
consistent with, though not proof of, that direct confirmation."""
|
|
351
355
|
AttributeTableVersion = 0
|
|
352
356
|
FirmwareVersion = 1
|
|
353
357
|
HardwareRevision = 2
|
|
354
358
|
SerialNumber = 3
|
|
355
359
|
Model = 4
|
|
356
360
|
Name = 5
|
|
361
|
+
Reset = 6
|
|
357
362
|
PrimitiveType = 8
|
|
358
363
|
PhysicalValues = 101
|
|
359
364
|
MACAddress = 103
|
|
@@ -404,11 +409,49 @@ class C2Attribute(IntEnum):
|
|
|
404
409
|
AcclimationStartIntensity = 904
|
|
405
410
|
AcclimationStartTime = 905
|
|
406
411
|
LunarPhasesEnabled = 907
|
|
412
|
+
# Confirmed attribute IDs and wire format, both against real
|
|
413
|
+
# hardware -- see LunarPhaseInfo's own docstring in
|
|
414
|
+
# device_status.py. Notably, the app itself never requests or
|
|
415
|
+
# parses either of these anywhere (confirmed by a full-source
|
|
416
|
+
# search): its own equivalent setup/parse logic only ever touches
|
|
417
|
+
# LunarPhasesEnabled, computing the actual reduction value entirely
|
|
418
|
+
# client-side instead of reading it from the device. The device's
|
|
419
|
+
# own firmware computes and reports these independently regardless
|
|
420
|
+
# -- get_lunar_phase_info() in device.py reads them directly.
|
|
421
|
+
LunarPhasesCurrentDay = 908
|
|
422
|
+
LunarPhasesCurrentScalar = 909
|
|
407
423
|
Epoch = 201
|
|
408
424
|
RTCTime = 219
|
|
409
425
|
LocalTime = 207 # already device-timezone-adjusted -- see
|
|
410
426
|
# get_device_local_date()'s own docstring in
|
|
411
427
|
# device.py for why this exists at all
|
|
428
|
+
# "Advanced Features" -- the app's own settings screen covering
|
|
429
|
+
# both VorTech (LocalControlEnabled/AutoDimTimeout) and Radion
|
|
430
|
+
# (MaxFanSpeed/FanShutdownEnabled) devices under one umbrella,
|
|
431
|
+
# gated purely per-attribute (not hardcoded per-device-type
|
|
432
|
+
# branching) -- see get_advanced_features()'s own docstring in
|
|
433
|
+
# device.py for why this library mirrors that same per-attribute
|
|
434
|
+
# approach.
|
|
435
|
+
LocalControlEnabled = 300
|
|
436
|
+
AutoDimTimeout = 301
|
|
437
|
+
MaxFanSpeed = 910
|
|
438
|
+
FanShutdownEnabled = 1203
|
|
439
|
+
# Vectra-specific settings. PowerOnDelay's own literal in the decompile
|
|
440
|
+
# resolves through a third-party Android animation library's own
|
|
441
|
+
# constant, not confirmable via web search or from the app's own
|
|
442
|
+
# decompile alone -- confirmed directly (not from this project's own
|
|
443
|
+
# sources) to be 800.
|
|
444
|
+
PowerOnDelay = 800
|
|
445
|
+
ClosedLoop = 801
|
|
446
|
+
FeedModeReturnDelay = 802
|
|
447
|
+
# NYOS Quantum-specific settings -- confirmed. CoffeeStatusIntensity's
|
|
448
|
+
# own literal resolves through an AndroidX SDK constant with the same
|
|
449
|
+
# confirmation gap as PowerOnDelay above -- also confirmed directly to
|
|
450
|
+
# be 4100.
|
|
451
|
+
CoffeeStatusIntensity = 4100
|
|
452
|
+
CoffeeLedOn = 717
|
|
453
|
+
CoffeeFeedModeReturnDelay = 4101
|
|
454
|
+
CoffeePowerOnDelay = 4102
|
|
412
455
|
# Thread mesh peer discovery -- see documentation/09-thread-coap-relay.md
|
|
413
456
|
# for the full protocol trace. MeshLocalAddresses confirmed via
|
|
414
457
|
# reverse engineering the app's own connection-setup logic; the three
|
|
@@ -444,7 +487,7 @@ class C2Attribute(IntEnum):
|
|
|
444
487
|
|
|
445
488
|
class PhysicalValueID(IntEnum):
|
|
446
489
|
"""Sub-index used with C2Attribute.PhysicalValues. Confirmed literal
|
|
447
|
-
values
|
|
490
|
+
values."""
|
|
448
491
|
Unknown = 0
|
|
449
492
|
DriverTemperature = 1
|
|
450
493
|
MotorTemperature = 2
|
|
@@ -514,7 +557,7 @@ class VisualID(IntEnum):
|
|
|
514
557
|
|
|
515
558
|
|
|
516
559
|
class SceneID(IntEnum):
|
|
517
|
-
"""Confirmed literal values
|
|
560
|
+
"""Confirmed literal values."""
|
|
518
561
|
EmptyScene = 0
|
|
519
562
|
FeedMode = 1
|
|
520
563
|
BatteryBackup = 2
|
|
@@ -528,13 +571,27 @@ class SceneID(IntEnum):
|
|
|
528
571
|
|
|
529
572
|
|
|
530
573
|
class OperationState(IntEnum):
|
|
531
|
-
"""Confirmed literal values
|
|
574
|
+
"""Confirmed literal values."""
|
|
532
575
|
OOB = 0
|
|
533
576
|
LiveDemo = 1
|
|
534
577
|
Scene = 2
|
|
535
578
|
Schedule = 3
|
|
536
579
|
|
|
537
580
|
|
|
581
|
+
class ResetType(IntEnum):
|
|
582
|
+
"""Confirmed literal values -- the single byte written to
|
|
583
|
+
C2Attribute.Reset (6). Only Soft is actually exposed by this
|
|
584
|
+
library's own reboot() (see that method's own docstring for why
|
|
585
|
+
the rest are deliberately not wired up to anything yet) -- kept
|
|
586
|
+
here in full since every value is a directly-confirmed literal in
|
|
587
|
+
the decompile, same as everywhere else in this file."""
|
|
588
|
+
Soft = 0
|
|
589
|
+
Factory = 1
|
|
590
|
+
PowerLoss = 2
|
|
591
|
+
PowerRecovery = 3
|
|
592
|
+
Calibration = 4
|
|
593
|
+
|
|
594
|
+
|
|
538
595
|
class FsciStatus(IntEnum):
|
|
539
596
|
"""Confirmed status/error codes returned in the first byte of a
|
|
540
597
|
Get/Set-attribute confirm response."""
|
|
@@ -558,7 +615,7 @@ class FsciStatus(IntEnum):
|
|
|
558
615
|
# --------------------------------------------------------------------------
|
|
559
616
|
|
|
560
617
|
class PumpMode(IntEnum):
|
|
561
|
-
"""Confirmed literal values
|
|
618
|
+
"""Confirmed literal values."""
|
|
562
619
|
Undefined = 0
|
|
563
620
|
ConstantSpeed = 1
|
|
564
621
|
Lagoon = 2
|
|
@@ -578,7 +635,7 @@ class PumpMode(IntEnum):
|
|
|
578
635
|
|
|
579
636
|
|
|
580
637
|
class RampType(IntEnum):
|
|
581
|
-
"""Confirmed literal values
|
|
638
|
+
"""Confirmed literal values."""
|
|
582
639
|
Sinusoidal = 1
|
|
583
640
|
Logarithmic = 2
|
|
584
641
|
Linear = 3
|
|
@@ -586,7 +643,7 @@ class RampType(IntEnum):
|
|
|
586
643
|
|
|
587
644
|
class FirmwareType(IntEnum):
|
|
588
645
|
"""
|
|
589
|
-
Confirmed literal values
|
|
646
|
+
Confirmed literal values -- the sub-index used
|
|
590
647
|
with C2Attribute.FirmwareVersion (a Get with index=0, count=0xFFFF
|
|
591
648
|
returns one entry per firmware component the device has, same "get
|
|
592
649
|
all elements" pattern as MaxPower/NormalPower/SupportedColorChannels).
|
|
@@ -610,7 +667,7 @@ class FirmwareType(IntEnum):
|
|
|
610
667
|
Esp32C6Firmware = 16
|
|
611
668
|
|
|
612
669
|
|
|
613
|
-
# Ported directly from
|
|
670
|
+
# Ported directly from the app's own firmware-label lookup -- the EcoTech Marine
|
|
614
671
|
# (isEtm()) branch specifically, confirmed via real hardware to exactly
|
|
615
672
|
# match the app's own displayed labels (a VorTech pump showed Radio/Radio
|
|
616
673
|
# Bootloader/Product OS/Product Bootloader; a Radion light showed Product
|
|
@@ -636,16 +693,46 @@ FIRMWARE_TYPE_LABELS_ETM: dict = {
|
|
|
636
693
|
FirmwareType.QCA4020WLAN: "WLAN",
|
|
637
694
|
}
|
|
638
695
|
|
|
696
|
+
# The app's own non-ETM label branch (covers AquaIllumination -- Orbit/
|
|
697
|
+
# Axis pumps, Prime/Hydra lights -- plus any other non-EcoTech-Marine
|
|
698
|
+
# manufacturer) confirmed via the same name(Model) logic FIRMWARE_TYPE_
|
|
699
|
+
# LABELS_ETM above is ported from, just its other branch. Every label
|
|
700
|
+
# here is one of the SAME shared resource strings used somewhere in the
|
|
701
|
+
# ETM dict above (just mapped to different FirmwareType values), so each
|
|
702
|
+
# one is cross-referenced against an already hardware-confirmed value,
|
|
703
|
+
# not independently guessed. XMC1402Firmware/CoffeeMotorDriverFirmware
|
|
704
|
+
# are deliberately NOT included: their own labels ("driver"/
|
|
705
|
+
# "base_station") aren't shared with any ETM-confirmed entry, so there's
|
|
706
|
+
# no confirmed value available for them here -- they fall back to the
|
|
707
|
+
# raw FirmwareType name instead, same as any other unmapped entry.
|
|
708
|
+
FIRMWARE_TYPE_LABELS_NON_ETM: dict = {
|
|
709
|
+
FirmwareType.Unknown: "Radio Firmware",
|
|
710
|
+
FirmwareType.LEDClusterMicro: "Radio Firmware",
|
|
711
|
+
FirmwareType.Esp32C3Firmware: "Radio Firmware",
|
|
712
|
+
FirmwareType.Esp32H2Firmware: "Radio Firmware",
|
|
713
|
+
FirmwareType.Esp32S3Firmware: "Radio Firmware",
|
|
714
|
+
FirmwareType.Esp32C6Firmware: "Radio Firmware",
|
|
715
|
+
FirmwareType.OS: "Product OS",
|
|
716
|
+
FirmwareType.MainMicroOS: "Product OS",
|
|
717
|
+
FirmwareType.QCA4020M4F: "Product OS",
|
|
718
|
+
FirmwareType.Bootloader: "Product Bootloader",
|
|
719
|
+
FirmwareType.MainMicroBootloader: "Product Bootloader",
|
|
720
|
+
FirmwareType.QCA4020Firmware: "Firmware",
|
|
721
|
+
FirmwareType.QCA4020FileSystem: "Filesystem",
|
|
722
|
+
FirmwareType.QCA4020M0: "Radio",
|
|
723
|
+
FirmwareType.QCA4020WLAN: "WLAN",
|
|
724
|
+
}
|
|
725
|
+
|
|
639
726
|
|
|
640
727
|
class HardwareInfo(IntEnum):
|
|
641
728
|
"""
|
|
642
|
-
Confirmed literal values
|
|
729
|
+
Confirmed literal values -- the sub-index used
|
|
643
730
|
with C2Attribute.HardwareRevision, same "get all elements" pattern as
|
|
644
731
|
FirmwareVersion above. `Revision` and `Segments` have no confirmed
|
|
645
732
|
display convention (read as plain small integers, not version
|
|
646
733
|
numbers or enum codes) -- but `Color`/`ProductType`/`RadioType`/
|
|
647
|
-
`MotorType` are each themselves confirmed enums
|
|
648
|
-
|
|
734
|
+
`MotorType` are each themselves confirmed enums with confirmed
|
|
735
|
+
display labels -- see
|
|
649
736
|
Color/ProductType/RadioType/MotorType below and their *_LABELS dicts.
|
|
650
737
|
"""
|
|
651
738
|
Unknown = 0
|
|
@@ -658,7 +745,7 @@ class HardwareInfo(IntEnum):
|
|
|
658
745
|
|
|
659
746
|
|
|
660
747
|
class Color(IntEnum):
|
|
661
|
-
"""Confirmed literal values and display labels
|
|
748
|
+
"""Confirmed literal values and display labels --
|
|
662
749
|
the HardwareInfo.Color sub-value's own enum."""
|
|
663
750
|
Unknown = 0
|
|
664
751
|
Black = 1
|
|
@@ -672,7 +759,7 @@ COLOR_LABELS: dict = {
|
|
|
672
759
|
|
|
673
760
|
|
|
674
761
|
class RadioType(IntEnum):
|
|
675
|
-
"""Confirmed literal values and display labels
|
|
762
|
+
"""Confirmed literal values and display labels --
|
|
676
763
|
the HardwareInfo.RadioType sub-value's own enum."""
|
|
677
764
|
Unknown = 0
|
|
678
765
|
KW41 = 1
|
|
@@ -698,7 +785,7 @@ RADIO_TYPE_LABELS: dict = {
|
|
|
698
785
|
|
|
699
786
|
|
|
700
787
|
class MotorType(IntEnum):
|
|
701
|
-
"""Confirmed literal values and display labels
|
|
788
|
+
"""Confirmed literal values and display labels --
|
|
702
789
|
the HardwareInfo.MotorType sub-value's own enum. Several labels
|
|
703
790
|
(Slack/Alpaca2/Alpaca4/Turtle1-4) are base64-string-obfuscated in the
|
|
704
791
|
decompile rather than plain literals, apparently to avoid casual
|
|
@@ -762,7 +849,7 @@ MOTOR_TYPE_LABELS: dict = {
|
|
|
762
849
|
|
|
763
850
|
|
|
764
851
|
class ProductType(IntEnum):
|
|
765
|
-
"""Confirmed literal values and display labels
|
|
852
|
+
"""Confirmed literal values and display labels --
|
|
766
853
|
the HardwareInfo.ProductType sub-value's own enum. Several labels
|
|
767
854
|
are base64-string-obfuscated in the decompile the same way some
|
|
768
855
|
MotorType labels are -- see MotorType's docstring for why that's
|
|
@@ -825,7 +912,7 @@ PRODUCT_TYPE_LABELS: dict = {
|
|
|
825
912
|
|
|
826
913
|
class PumpOverrideMode(IntEnum):
|
|
827
914
|
"""
|
|
828
|
-
Confirmed literal values
|
|
915
|
+
Confirmed literal values. Confirmed only as a
|
|
829
916
|
SET target in the decompile (writing None to exit override mode after
|
|
830
917
|
the pump-side calibration flow) -- never actually read anywhere in the
|
|
831
918
|
app. The wire format for reading it (plain byte enum) is a reasonable
|
|
@@ -863,7 +950,7 @@ PUMP_PARAM_SIZE: dict[PumpParam, int] = {
|
|
|
863
950
|
PumpParam.PhaseShift: 2, PumpParam.OnTime: 4, PumpParam.OffTime: 4,
|
|
864
951
|
}
|
|
865
952
|
|
|
866
|
-
# Ported directly from
|
|
953
|
+
# Ported directly from the app's own pump-mode parameter logic -- which params are present
|
|
867
954
|
# for each mode, and in what order (byte offset is cumulative from data[1:]).
|
|
868
955
|
PUMP_MODE_PARAMS: dict[PumpMode, list[PumpParam]] = {
|
|
869
956
|
PumpMode.Undefined: [],
|