python-mobius 0.2.1__py3-none-any.whl → 0.3.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 +5 -1
- mobius/constants.py +172 -5
- mobius/device.py +45 -12
- mobius/modifiers.py +22 -8
- mobius/schedule.py +50 -1
- {python_mobius-0.2.1.dist-info → python_mobius-0.3.1.dist-info}/METADATA +1 -1
- {python_mobius-0.2.1.dist-info → python_mobius-0.3.1.dist-info}/RECORD +10 -10
- {python_mobius-0.2.1.dist-info → python_mobius-0.3.1.dist-info}/WHEEL +0 -0
- {python_mobius-0.2.1.dist-info → python_mobius-0.3.1.dist-info}/entry_points.txt +0 -0
- {python_mobius-0.2.1.dist-info → python_mobius-0.3.1.dist-info}/licenses/LICENSE +0 -0
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.
|
|
66
|
+
__version__ = "0.3.1"
|
|
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.
|
|
496
|
-
display convention
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
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).
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
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
|
-
|
|
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",
|
|
877
|
-
"maintenance" (see get_group_info()/
|
|
878
|
-
get_maintenance_info()
|
|
879
|
-
|
|
880
|
-
documentation/
|
|
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
|
|
135
|
-
NIGHT (i.e. the
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
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
|
-
|
|
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
|
)
|
mobius/schedule.py
CHANGED
|
@@ -88,6 +88,41 @@ def interpolate_light_schedule(points: list[SchedulePoint], minute_of_day: int)
|
|
|
88
88
|
schedule points and a target time (0-1439, minutes since midnight),
|
|
89
89
|
returns {VisualID: intensity_permille} by linearly interpolating between
|
|
90
90
|
the two points that bracket that time, wrapping around midnight.
|
|
91
|
+
|
|
92
|
+
Applies LightPrimitive.flatten()'s confirmed semantics (see that
|
|
93
|
+
method and its two call sites in getIntensitiesAtTime() in the real
|
|
94
|
+
smali -- JADX's decompiled Java for this class carried its own "Code
|
|
95
|
+
duplicated" warning, the same signal that led to finding the
|
|
96
|
+
is_night_segment() bug, so this was re-verified against raw bytecode
|
|
97
|
+
rather than trusted from the decompile alone): the "Brightness"
|
|
98
|
+
channel (VisualID.Brightness) is a master dimmer that multiplies
|
|
99
|
+
every OTHER channel -- confirmed via real hardware testing that this
|
|
100
|
+
was missing entirely: MoonlightBlue/White (and every other color
|
|
101
|
+
channel) were computed without ever being scaled by Brightness,
|
|
102
|
+
which the real app clearly does (a light showing ~1% in the app's
|
|
103
|
+
own "Current Intensities" display came back as ~25% from this
|
|
104
|
+
library at the same moment, with Brightness itself reading ~3.3% at
|
|
105
|
+
that point in its own ramp -- consistent with the missing
|
|
106
|
+
multiplication, not a lunar or scheduling bug).
|
|
107
|
+
|
|
108
|
+
Critically, this flattening happens to EACH bracketing point
|
|
109
|
+
SEPARATELY (each point's own non-Brightness channels multiplied by
|
|
110
|
+
that SAME point's own Brightness value) BEFORE interpolating between
|
|
111
|
+
the two already-flattened points -- not interpolate raw values first
|
|
112
|
+
and apply a single Brightness value at the end. These are NOT
|
|
113
|
+
mathematically equivalent whenever Brightness itself changes across
|
|
114
|
+
the segment (which it usually does, e.g. ramping 0->1000 during a
|
|
115
|
+
dusk/dawn transition) -- confirmed by the smali calling flatten() on
|
|
116
|
+
both point copies before the interpolation loop, not after it.
|
|
117
|
+
|
|
118
|
+
"Brightness" itself is deliberately NOT flattened to 1000 in the
|
|
119
|
+
returned dict the way the real flatten() call does internally --
|
|
120
|
+
unlike the app's own dashboard (which doesn't display Brightness as
|
|
121
|
+
its own value at all, only as the multiplier baked into every other
|
|
122
|
+
channel), this library's callers (e.g. a dedicated Brightness
|
|
123
|
+
sensor) want the actual current master-dimmer level itself, not a
|
|
124
|
+
constant 100%. Returned as the plain raw interpolation between the
|
|
125
|
+
two points' own Brightness values, same as before this fix.
|
|
91
126
|
"""
|
|
92
127
|
if not points:
|
|
93
128
|
return {}
|
|
@@ -109,12 +144,26 @@ def interpolate_light_schedule(points: list[SchedulePoint], minute_of_day: int)
|
|
|
109
144
|
if t1 <= m <= t2:
|
|
110
145
|
span = (t2 - t1) or 1
|
|
111
146
|
frac = (m - t1) / span
|
|
147
|
+
|
|
148
|
+
# Each point's own Brightness value, defaulting to 1000 (a
|
|
149
|
+
# no-op multiplier) if that point doesn't have a Brightness
|
|
150
|
+
# channel at all -- matches flatten()'s own
|
|
151
|
+
# visuals().contains(Brightness) guard, which skips scaling
|
|
152
|
+
# entirely rather than treating an absent channel as 0.
|
|
153
|
+
b1 = p1.light.get(VisualID.Brightness) if VisualID.Brightness in p1.light.channels else 1000
|
|
154
|
+
b2 = p2.light.get(VisualID.Brightness) if VisualID.Brightness in p2.light.channels else 1000
|
|
155
|
+
|
|
112
156
|
result = {}
|
|
113
157
|
all_channels = set(p1.light.channels) | set(p2.light.channels)
|
|
114
158
|
for ch in all_channels:
|
|
115
159
|
v1 = p1.light.get(ch)
|
|
116
160
|
v2 = p2.light.get(ch)
|
|
117
|
-
|
|
161
|
+
if ch == VisualID.Brightness:
|
|
162
|
+
result[ch] = v1 + (v2 - v1) * frac
|
|
163
|
+
else:
|
|
164
|
+
flattened1 = v1 * b1 / 1000.0
|
|
165
|
+
flattened2 = v2 * b2 / 1000.0
|
|
166
|
+
result[ch] = flattened1 + (flattened2 - flattened1) * frac
|
|
118
167
|
return result
|
|
119
168
|
return {}
|
|
120
169
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: python-mobius
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.3.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
|
|
@@ -1,21 +1,21 @@
|
|
|
1
|
-
mobius/__init__.py,sha256=
|
|
1
|
+
mobius/__init__.py,sha256=pUvLAuR2ubblIn1v_bfunHnaCosr7TJys_LNFbnYUHw,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=
|
|
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=
|
|
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=
|
|
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
|
-
mobius/schedule.py,sha256=
|
|
17
|
-
python_mobius-0.
|
|
18
|
-
python_mobius-0.
|
|
19
|
-
python_mobius-0.
|
|
20
|
-
python_mobius-0.
|
|
21
|
-
python_mobius-0.
|
|
16
|
+
mobius/schedule.py,sha256=hKTlVoxw4eZpDcWUL9wpxShLacmoQZd7MR4kn6ZNvWs,10492
|
|
17
|
+
python_mobius-0.3.1.dist-info/METADATA,sha256=FV_rjO4y1wL8ews26uvabsG87XtNgjuH2ZLz9oK6LHo,5959
|
|
18
|
+
python_mobius-0.3.1.dist-info/WHEEL,sha256=lCkmxWfQsSc9CfIClYeavTdQeEX2toPqufh9gI35EQA,87
|
|
19
|
+
python_mobius-0.3.1.dist-info/entry_points.txt,sha256=Q2hlcek-bWm70zvd12v32essflPgC2su-dKDHAqbPoE,48
|
|
20
|
+
python_mobius-0.3.1.dist-info/licenses/LICENSE,sha256=7a72Msu2Q-TnoiFxemxEGkwafJGObk1W3rw9hzmyM_Y,17984
|
|
21
|
+
python_mobius-0.3.1.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|