python-mobius 0.2.1__py3-none-any.whl → 0.3.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 CHANGED
@@ -28,6 +28,8 @@ from .constants import (
28
28
  SceneID, OperationState, FsciStatus,
29
29
  PumpMode, PumpOverrideMode, RampType, PumpParam, PUMP_PARAM_SIZE, PUMP_MODE_PARAMS,
30
30
  FirmwareType, FIRMWARE_TYPE_LABELS_ETM, HardwareInfo,
31
+ Color, COLOR_LABELS, RadioType, RADIO_TYPE_LABELS,
32
+ MotorType, MOTOR_TYPE_LABELS, ProductType, PRODUCT_TYPE_LABELS,
31
33
  )
32
34
  from .schedule import (
33
35
  LightPrimitive, SchedulePoint, interpolate_light_schedule,
@@ -61,7 +63,7 @@ from .discovery import (
61
63
  dedupe_by_serial, find_device_by_serial, discover_mesh_peers_via_direct_connect,
62
64
  )
63
65
 
64
- __version__ = "0.2.1"
66
+ __version__ = "0.3.0"
65
67
 
66
68
  __all__ = [
67
69
  "__version__",
@@ -83,6 +85,8 @@ __all__ = [
83
85
  "SceneID", "OperationState", "FsciStatus",
84
86
  "PumpMode", "PumpOverrideMode", "RampType", "PumpParam", "PUMP_PARAM_SIZE", "PUMP_MODE_PARAMS",
85
87
  "FirmwareType", "FIRMWARE_TYPE_LABELS_ETM", "HardwareInfo",
88
+ "Color", "COLOR_LABELS", "RadioType", "RADIO_TYPE_LABELS",
89
+ "MotorType", "MOTOR_TYPE_LABELS", "ProductType", "PRODUCT_TYPE_LABELS",
86
90
  # schedule
87
91
  "LightPrimitive", "SchedulePoint", "interpolate_light_schedule",
88
92
  "PumpPrimitiveValue", "PumpSchedulePoint", "get_active_pump_block",
mobius/constants.py CHANGED
@@ -492,11 +492,12 @@ class HardwareInfo(IntEnum):
492
492
  """
493
493
  Confirmed literal values from M.HardwareInfo -- the sub-index used
494
494
  with C2Attribute.HardwareRevision, same "get all elements" pattern as
495
- FirmwareVersion above. Unlike FirmwareVersion, no dot-joined-string
496
- display convention is confirmed for these fields (Color/Revision/
497
- ProductType/RadioType/MotorType/Segments read more like small
498
- integer/enum codes than version numbers) -- returned as raw bytes,
499
- not formatted, until that's confirmed.
495
+ FirmwareVersion above. `Revision` and `Segments` have no confirmed
496
+ display convention (read as plain small integers, not version
497
+ numbers or enum codes) -- but `Color`/`ProductType`/`RadioType`/
498
+ `MotorType` are each themselves confirmed enums (M.Color/M.ProductType/
499
+ M.RadioType/M.MotorType) with confirmed display labels -- see
500
+ Color/ProductType/RadioType/MotorType below and their *_LABELS dicts.
500
501
  """
501
502
  Unknown = 0
502
503
  Color = 1
@@ -507,6 +508,172 @@ class HardwareInfo(IntEnum):
507
508
  Segments = 6
508
509
 
509
510
 
511
+ class Color(IntEnum):
512
+ """Confirmed literal values and display labels from M.Color --
513
+ the HardwareInfo.Color sub-value's own enum."""
514
+ Unknown = 0
515
+ Black = 1
516
+ White = 2
517
+
518
+
519
+ COLOR_LABELS: dict = {
520
+ Color.Black: "Black",
521
+ Color.White: "White",
522
+ }
523
+
524
+
525
+ class RadioType(IntEnum):
526
+ """Confirmed literal values and display labels from M.RadioType --
527
+ the HardwareInfo.RadioType sub-value's own enum."""
528
+ Unknown = 0
529
+ KW41 = 1
530
+ QCA4020 = 2
531
+ K32W = 3
532
+ QCA4024 = 4
533
+ ESP32C3 = 5
534
+ ESP32H2 = 6
535
+ ESP32S3 = 7
536
+ ESP32C6 = 8
537
+
538
+
539
+ RADIO_TYPE_LABELS: dict = {
540
+ RadioType.KW41: "KW41",
541
+ RadioType.QCA4020: "QCA4020",
542
+ RadioType.K32W: "K32W",
543
+ RadioType.QCA4024: "QCA4024",
544
+ RadioType.ESP32C3: "ESP32C3",
545
+ RadioType.ESP32H2: "ESP32H2",
546
+ RadioType.ESP32S3: "ESP32S3",
547
+ RadioType.ESP32C6: "ESP32C6",
548
+ }
549
+
550
+
551
+ class MotorType(IntEnum):
552
+ """Confirmed literal values and display labels from M.MotorType --
553
+ the HardwareInfo.MotorType sub-value's own enum. Several labels
554
+ (Slack/Alpaca2/Alpaca4/Turtle1-4) are base64-string-obfuscated in the
555
+ decompile rather than plain literals, apparently to avoid casual
556
+ discovery of unannounced product names in a simple APK string dump --
557
+ decoded here since it's plain base64, not encryption, and the
558
+ resulting strings are just as much "confirmed from source" as the
559
+ unobfuscated ones."""
560
+ Unknown = 0
561
+ VorTechMP10 = 1
562
+ VorTechMP40Legacy = 2
563
+ VorTechMP40G3 = 3
564
+ VorTechMP60 = 4
565
+ VectraD12 = 5
566
+ VectraD8 = 6
567
+ VectraS1 = 7
568
+ Wavepuck30w = 8
569
+ Nero5 = 9
570
+ Nero3 = 10
571
+ VectraS2 = 11
572
+ VectraM2 = 12
573
+ VectraL2 = 13
574
+ VersaVX1 = 14
575
+ Slack = 15
576
+ Alpaca2 = 16
577
+ Alpaca4 = 17
578
+ Turtle1 = 18
579
+ Turtle2 = 19
580
+ Turtle3 = 20
581
+ Turtle4 = 21
582
+ Coffee1 = 22
583
+ Coffee2 = 23
584
+ VectraTester = 99
585
+
586
+
587
+ MOTOR_TYPE_LABELS: dict = {
588
+ MotorType.VorTechMP10: "VorTech MP10",
589
+ MotorType.VorTechMP40Legacy: "VorTech MP40 Legacy",
590
+ MotorType.VorTechMP40G3: "VorTech MP40 G3",
591
+ MotorType.VorTechMP60: "VorTech MP60",
592
+ MotorType.VectraD12: "Vectra D12",
593
+ MotorType.VectraD8: "Vectra D8",
594
+ MotorType.VectraS1: "Vectra S1",
595
+ MotorType.Wavepuck30w: "Wavepuck 30w",
596
+ MotorType.Nero5: "Nero 5",
597
+ MotorType.Nero3: "Nero 3",
598
+ MotorType.VectraS2: "Vectra S2",
599
+ MotorType.VectraM2: "Vectra M2",
600
+ MotorType.VectraL2: "Vectra L2",
601
+ MotorType.VersaVX1: "Versa VX1",
602
+ MotorType.Slack: "Nero 7",
603
+ MotorType.Alpaca2: "Orbit 2",
604
+ MotorType.Alpaca4: "Orbit 4",
605
+ MotorType.Turtle1: "Axis 20",
606
+ MotorType.Turtle2: "Axis 40",
607
+ MotorType.Turtle3: "Axis 90",
608
+ MotorType.Turtle4: "Axis 200",
609
+ MotorType.Coffee1: "Coffee 1",
610
+ MotorType.Coffee2: "Coffee 2",
611
+ MotorType.VectraTester: "Vectra Tester",
612
+ }
613
+
614
+
615
+ class ProductType(IntEnum):
616
+ """Confirmed literal values and display labels from M.ProductType --
617
+ the HardwareInfo.ProductType sub-value's own enum. Several labels
618
+ are base64-string-obfuscated in the decompile the same way some
619
+ MotorType labels are -- see MotorType's docstring for why that's
620
+ still treated as confirmed. Note several of these labels intentionally
621
+ diverge from the enum member's own name (e.g. Chalupa -> "Sprout",
622
+ NachosBellGrande -> "Voltra", Cowboy -> "MXM") -- these are the actual
623
+ confirmed display strings, not typos."""
624
+ Unknown = 0
625
+ Radion = 1
626
+ Vectra = 2
627
+ VorTech = 3
628
+ Nero = 4
629
+ DosingPump = 5
630
+ Chalupa = 6
631
+ EnvironmentalSensor = 7
632
+ RFModule = 8
633
+ WiFiModule = 9
634
+ WiFiModuleBreakout = 10
635
+ ChalupaPowerSupply = 11
636
+ ChalupaDriver = 12
637
+ AIFiLight = 13
638
+ AIMobiusLight = 14
639
+ DosingPumpBase = 15
640
+ NachosBellGrande = 16
641
+ HotSauce = 17
642
+ Cowboy = 18
643
+ Alpaca = 19
644
+ Turtle = 20
645
+ Coffee = 21
646
+ CrunchyTaco = 22
647
+ SoftTaco = 23
648
+
649
+
650
+ PRODUCT_TYPE_LABELS: dict = {
651
+ ProductType.Radion: "Radion",
652
+ ProductType.Vectra: "Vectra",
653
+ ProductType.VorTech: "VorTech",
654
+ ProductType.Nero: "Nero",
655
+ ProductType.DosingPump: "Dosing Pump",
656
+ ProductType.Chalupa: "Sprout",
657
+ ProductType.EnvironmentalSensor: "Sensor",
658
+ ProductType.RFModule: "RF Module",
659
+ ProductType.WiFiModule: "Wi-Fi Module",
660
+ ProductType.WiFiModuleBreakout: "Breakout",
661
+ ProductType.ChalupaPowerSupply: "Power Supply",
662
+ ProductType.ChalupaDriver: "Driver",
663
+ ProductType.AIFiLight: "AI-Fi Light",
664
+ ProductType.AIMobiusLight: "AI Light",
665
+ ProductType.DosingPumpBase: "Base Station",
666
+ ProductType.NachosBellGrande: "Voltra",
667
+ ProductType.HotSauce: "Sensor",
668
+ ProductType.Cowboy: "MXM",
669
+ ProductType.Alpaca: "Orbit",
670
+ ProductType.Turtle: "Axis",
671
+ ProductType.Coffee: "Quantum",
672
+ ProductType.CrunchyTaco: "Hera",
673
+ ProductType.SoftTaco: "Hera Backplane",
674
+ }
675
+
676
+
510
677
  class PumpOverrideMode(IntEnum):
511
678
  """
512
679
  Confirmed literal values from M.PumpOverrideMode. Confirmed only as a
mobius/device.py CHANGED
@@ -31,6 +31,8 @@ from .constants import (
31
31
  C2Attribute, PrimitiveType, Model, ErrorState, SceneID, OperationState,
32
32
  FsciStatus, PhysicalValueID, VisualID, PumpOverrideMode, manufacturer_for_model,
33
33
  FirmwareType, FIRMWARE_TYPE_LABELS_ETM, HardwareInfo,
34
+ Color, COLOR_LABELS, RadioType, RADIO_TYPE_LABELS,
35
+ MotorType, MOTOR_TYPE_LABELS, ProductType, PRODUCT_TYPE_LABELS,
34
36
  PRIMITIVE_SIZE, LIGHT_PRIMITIVES, PUMP_PRIMITIVES_VERIFIED, PUMP_PRIMITIVES_EXPERIMENTAL,
35
37
  )
36
38
  from .schedule import (
@@ -105,6 +107,18 @@ class LightIntensityResult(dict):
105
107
  self.diagnostics = diagnostics or {}
106
108
 
107
109
 
110
+ # Used by get_hardware_info() -- which HardwareInfo sub-fields are
111
+ # themselves confirmed enums with confirmed display labels, vs. plain
112
+ # integers with no confirmed meaning (Revision/Segments, deliberately not
113
+ # in this map).
114
+ _HARDWARE_SUB_ENUM_LABELS = {
115
+ HardwareInfo.Color: (Color, COLOR_LABELS),
116
+ HardwareInfo.ProductType: (ProductType, PRODUCT_TYPE_LABELS),
117
+ HardwareInfo.RadioType: (RadioType, RADIO_TYPE_LABELS),
118
+ HardwareInfo.MotorType: (MotorType, MOTOR_TYPE_LABELS),
119
+ }
120
+
121
+
108
122
  class MobiusDevice:
109
123
  """
110
124
  High-level async client for a single Mobius-protocol BLE device.
@@ -828,12 +842,19 @@ class MobiusDevice:
828
842
  -- same pattern as get_firmware_versions(), including using
829
843
  get_attribute_raw_all() rather than get_attribute_raw() to avoid
830
844
  missing elements if the response splits across more than one
831
- block (see get_attribute_raw_all()'s docstring). Unlike firmware
832
- versions, no dot-joined-string display convention is confirmed for
833
- these fields (Color/Revision/ProductType/RadioType/MotorType/
834
- Segments read more like small integer/enum codes than version
835
- numbers) -- returns {HardwareInfo_name: raw_bytes}, not formatted,
836
- until that's confirmed against real hardware.
845
+ block (see get_attribute_raw_all()'s docstring).
846
+
847
+ `Color`/`ProductType`/`RadioType`/`MotorType` are each themselves
848
+ confirmed enums with confirmed display labels (M.Color/
849
+ M.ProductType/M.RadioType/M.MotorType in the decompile -- see
850
+ mobius.constants) -- decoded into those label strings here, e.g.
851
+ {"Color": "White", "MotorType": "VorTech MP40 G3"}. An
852
+ unrecognized value (firmware newer than this library's confirmed
853
+ enum coverage) falls back to "Unknown (N)" rather than raising.
854
+
855
+ `Revision`/`Segments` have no confirmed enum meaning -- returned
856
+ as plain integers, not formatted, since unlike the four fields
857
+ above there's nothing confirmed to decode them into.
837
858
  """
838
859
  try:
839
860
  blocks = await self.get_attribute_raw_all(C2Attribute.HardwareRevision, index=0, count=0xFFFF)
@@ -849,7 +870,17 @@ class MobiusDevice:
849
870
  hw_info = HardwareInfo(raw.index + i)
850
871
  except ValueError:
851
872
  continue
852
- result[hw_info.name] = value_bytes
873
+ if not value_bytes:
874
+ continue
875
+ raw_int = int.from_bytes(value_bytes, byteorder="little", signed=False)
876
+ if hw_info in _HARDWARE_SUB_ENUM_LABELS:
877
+ enum_cls, labels = _HARDWARE_SUB_ENUM_LABELS[hw_info]
878
+ try:
879
+ result[hw_info.name] = labels.get(enum_cls(raw_int), f"Unknown ({raw_int})")
880
+ except ValueError:
881
+ result[hw_info.name] = f"Unknown ({raw_int})"
882
+ else:
883
+ result[hw_info.name] = raw_int
853
884
  return result
854
885
 
855
886
  async def get_device_summary(self) -> dict:
@@ -873,11 +904,12 @@ class MobiusDevice:
873
904
  Pump devices additionally include "flow_range", "pump_override_mode",
874
905
  "battery_backup", "boosted_battery" (see get_pump_flow_range() etc.
875
906
  for details/confidence notes on each). Every device (regardless of
876
- support tier) additionally includes "group", "calibration", and
877
- "maintenance" (see get_group_info()/get_calibration_info()/
878
- get_maintenance_info() -- calibration in particular is expected to
879
- only actually populate on lights, not pumps; see
880
- documentation/11-device-status-attributes.md).
907
+ support tier) additionally includes "group", "calibration",
908
+ "maintenance", and "hardware_info" (see get_group_info()/
909
+ get_calibration_info()/get_maintenance_info()/get_hardware_info()
910
+ -- calibration in particular is expected to only actually
911
+ populate on lights, not pumps; see documentation/
912
+ 11-device-status-attributes.md).
881
913
 
882
914
  BUG NOTE: for a while after get_pump_flow_range()/
883
915
  get_pump_override_mode()/get_battery_backup_info()/
@@ -959,6 +991,7 @@ class MobiusDevice:
959
991
  info["calibration"] = await self.get_calibration_info()
960
992
  info["maintenance"] = await self.get_maintenance_info()
961
993
  info["firmware_versions"] = await self.get_firmware_versions(model)
994
+ info["hardware_info"] = await self.get_hardware_info()
962
995
  info["device_time"] = await self.get_device_time_info()
963
996
 
964
997
  return info
mobius/modifiers.py CHANGED
@@ -131,14 +131,28 @@ class AcclimationInfo:
131
131
  def is_night_segment(points: list[SchedulePoint], minute_of_day: int) -> bool:
132
132
  """
133
133
  True specifically when minute_of_day falls in the segment whose START
134
- point is NOT flagged NIGHT or SUNRISE and whose END point IS flagged
135
- NIGHT (i.e. the dusk-transition-into-night segment), or when
136
- minute_of_day exactly matches a point that is itself flagged NIGHT.
137
- This is NOT simply "is it currently night" -- that's genuinely the
138
- Java source's own semantics (confirmed by reading
139
- PointSchedule.getIntensitiesAtTime() directly), used specifically to
140
- decide whether to substitute the lunar-phase reduction in place of the
134
+ point IS flagged NIGHT (but not also SUNRISE) and whose END point is
135
+ ALSO flagged NIGHT (i.e. once actually into the night portion of the
136
+ schedule, up until the sunrise transition), or when minute_of_day
137
+ exactly matches a point that is itself flagged NIGHT. This is NOT
138
+ simply "is it currently night" as a general concept, but this
139
+ specific segment-boundary condition -- used specifically to decide
140
+ whether to substitute the lunar-phase reduction in place of the
141
141
  normal schedule-intensity scalar.
142
+
143
+ Confirmed directly from raw smali bytecode (JADX's decompiled Java
144
+ for PointSchedule.getIntensitiesAtTime() carried its own "Code
145
+ duplicated" warning on this exact method -- a real signal, not
146
+ noise: its decompilation of this specific condition had the first
147
+ check inverted, i.e. `!point.has(NIGHT)` instead of the actual
148
+ `point.has(NIGHT)`. Confirmed via real hardware testing too: a
149
+ 23:00[NIGHT,SUNSET]->23:27[NIGHT] segment produced a value matching
150
+ schedule-intensity-only scaling from this library, while the real
151
+ app displayed a much lower, lunar-consistent value at the same
152
+ moment -- mathematically impossible to explain by schedule-intensity
153
+ alone within that segment's raw-value range, which is what led back
154
+ to re-examining the raw bytecode instead of trusting the decompiled
155
+ Java a second time.
142
156
  """
143
157
  if not points:
144
158
  return False
@@ -160,7 +174,7 @@ def is_night_segment(points: list[SchedulePoint], minute_of_day: int) -> bool:
160
174
  if t1 == t2:
161
175
  return p1.has(SchedulePoint.FLAG_NIGHT)
162
176
  return (
163
- not p1.has(SchedulePoint.FLAG_NIGHT)
177
+ p1.has(SchedulePoint.FLAG_NIGHT)
164
178
  and not p1.has(SchedulePoint.FLAG_SUNRISE)
165
179
  and p2.has(SchedulePoint.FLAG_NIGHT)
166
180
  )
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: python-mobius
3
- Version: 0.2.1
3
+ Version: 0.3.0
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,21 +1,21 @@
1
- mobius/__init__.py,sha256=LxDxIbucDGqf1DhloOxCxeC8Ta2rYM0Sy3T2UGYkXKI,5334
1
+ mobius/__init__.py,sha256=m_RB_zd21HCRyOSYovJeLFLScE8Gv9dPLjwdLALGRjk,5596
2
2
  mobius/cli.py,sha256=s1zZa4P4rws-XRLLd1B_J67mjMbdImcfgNeDet44cYo,21494
3
3
  mobius/coap.py,sha256=n-_wBuSY88_c1TAjfXPZsPnpG0IcZZLFRjxTzd1c0Us,10859
4
- mobius/constants.py,sha256=rXaywY6WIK36HobZ1E3w-HfEWtZDCFzXqKbo4Bbe1V8,19259
4
+ mobius/constants.py,sha256=N98whYfTAdzydP18kn0ycXQubbc6F-CJUSr-o_KlepU,24129
5
5
  mobius/crc.py,sha256=WwIfUXGN_UKdsv-_Q303-L1-L1Rc1fvJnB5176ftzqU,2669
6
- mobius/device.py,sha256=GZ-Ucqt7lE_teOjXdHBf7DbgiTGtry88KwMr7a3NmHk,68859
6
+ mobius/device.py,sha256=8iNc9fX01Cns00bGYXmSPxXb80THjBd-h47iV02b2KU,70462
7
7
  mobius/device_status.py,sha256=373a7BRGY1CoByLYkxTELAewkiSj4Wzm8yAilQrh0co,5812
8
8
  mobius/discovery.py,sha256=anJXZEwwhYwvQhFdYOwl6HGTrnMGUfb5CNg6H7Ah-iI,7737
9
9
  mobius/frame.py,sha256=zG2vh8PceJHD8R43MwZiJnm0Ge1dr8nFcKsJR9_c9LM,6135
10
10
  mobius/manufacturer.py,sha256=8-vBzckN8xmsPSMj5EWtOCXenbyQueoY5pnZHePgb_k,1937
11
11
  mobius/mesh_address.py,sha256=PNUi3VyRQZCitB1R3IbzXCg1LcP6azSg1sigHAqaP7E,4942
12
- mobius/modifiers.py,sha256=pFNjGYX-_bNumNEpv3jJLYPCb9ZuSKYpQ0DDd2GmicQ,6451
12
+ mobius/modifiers.py,sha256=TQD__OvHAvUO14Z8SNp2I4lb7XYSbw_iZT2D2DXaeJ4,7315
13
13
  mobius/power.py,sha256=cZBpfYh2yg4xhmOQ0ILpACYdJSd7jMvSrhxat0yaH-g,3851
14
14
  mobius/pump_status.py,sha256=vqJXFAb16S-4oyzAzZIcwR5if7lXoTj67qwBJdbH1gI,1604
15
15
  mobius/relay.py,sha256=YnIAtX0vHPPSazgRT_g8ocP2WJNyvW02JQOafabmsbM,16497
16
16
  mobius/schedule.py,sha256=kHr684TIKPZ2BplIySoeD-Yn1Xa_2bE6rfGUs-waWMQ,7483
17
- python_mobius-0.2.1.dist-info/METADATA,sha256=EHtIqy5HkJRrHGzPnbgdpM4PoAPyTnqgJU3lKpTt56U,5959
18
- python_mobius-0.2.1.dist-info/WHEEL,sha256=lCkmxWfQsSc9CfIClYeavTdQeEX2toPqufh9gI35EQA,87
19
- python_mobius-0.2.1.dist-info/entry_points.txt,sha256=Q2hlcek-bWm70zvd12v32essflPgC2su-dKDHAqbPoE,48
20
- python_mobius-0.2.1.dist-info/licenses/LICENSE,sha256=7a72Msu2Q-TnoiFxemxEGkwafJGObk1W3rw9hzmyM_Y,17984
21
- python_mobius-0.2.1.dist-info/RECORD,,
17
+ python_mobius-0.3.0.dist-info/METADATA,sha256=8Zxtj2NGCJFkLEm2yXnLs-9ioowAitxS3i3jIrJ9Xtk,5959
18
+ python_mobius-0.3.0.dist-info/WHEEL,sha256=lCkmxWfQsSc9CfIClYeavTdQeEX2toPqufh9gI35EQA,87
19
+ python_mobius-0.3.0.dist-info/entry_points.txt,sha256=Q2hlcek-bWm70zvd12v32essflPgC2su-dKDHAqbPoE,48
20
+ python_mobius-0.3.0.dist-info/licenses/LICENSE,sha256=7a72Msu2Q-TnoiFxemxEGkwafJGObk1W3rw9hzmyM_Y,17984
21
+ python_mobius-0.3.0.dist-info/RECORD,,