python-mobius 0.7.0__py3-none-any.whl → 0.7.2__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 +4 -1
- mobius/cli.py +46 -0
- mobius/constants.py +1 -0
- mobius/device.py +69 -13
- mobius/scenes.py +97 -0
- {python_mobius-0.7.0.dist-info → python_mobius-0.7.2.dist-info}/METADATA +14 -3
- {python_mobius-0.7.0.dist-info → python_mobius-0.7.2.dist-info}/RECORD +10 -9
- {python_mobius-0.7.0.dist-info → python_mobius-0.7.2.dist-info}/WHEEL +0 -0
- {python_mobius-0.7.0.dist-info → python_mobius-0.7.2.dist-info}/entry_points.txt +0 -0
- {python_mobius-0.7.0.dist-info → python_mobius-0.7.2.dist-info}/licenses/LICENSE +0 -0
mobius/__init__.py
CHANGED
|
@@ -35,6 +35,7 @@ from .schedule import (
|
|
|
35
35
|
LightPrimitive, SchedulePoint, interpolate_light_schedule,
|
|
36
36
|
PumpPrimitiveValue, PumpSchedulePoint, get_active_pump_block,
|
|
37
37
|
)
|
|
38
|
+
from .scenes import Scene, ActiveScene, decode_configured_scenes, decode_active_scene
|
|
38
39
|
from .manufacturer import MOBIUS_COMPANY_ID_ECOTECH, MOBIUS_COMPANY_ID_AQUAILLUMINATION, MOBIUS_COMPANY_IDS, MobiusAdvertisement, parse_manufacturer_data
|
|
39
40
|
from .modifiers import (
|
|
40
41
|
AcclimationInfo, is_night_segment,
|
|
@@ -71,7 +72,7 @@ from .dump import (
|
|
|
71
72
|
enrich_attribute_dump,
|
|
72
73
|
)
|
|
73
74
|
|
|
74
|
-
__version__ = "0.7.
|
|
75
|
+
__version__ = "0.7.2"
|
|
75
76
|
|
|
76
77
|
__all__ = [
|
|
77
78
|
"__version__",
|
|
@@ -98,6 +99,8 @@ __all__ = [
|
|
|
98
99
|
# schedule
|
|
99
100
|
"LightPrimitive", "SchedulePoint", "interpolate_light_schedule",
|
|
100
101
|
"PumpPrimitiveValue", "PumpSchedulePoint", "get_active_pump_block",
|
|
102
|
+
# scenes
|
|
103
|
+
"Scene", "ActiveScene", "decode_configured_scenes", "decode_active_scene",
|
|
101
104
|
# manufacturer
|
|
102
105
|
"MOBIUS_COMPANY_ID_ECOTECH", "MOBIUS_COMPANY_ID_AQUAILLUMINATION", "MOBIUS_COMPANY_IDS", "MobiusAdvertisement", "parse_manufacturer_data",
|
|
103
106
|
# modifiers
|
mobius/cli.py
CHANGED
|
@@ -18,6 +18,7 @@ from .device import MobiusDevice
|
|
|
18
18
|
from .relay import RelayedMobiusDevice
|
|
19
19
|
from .device_status import MeshPeer
|
|
20
20
|
from .mesh_address import extract_short_address
|
|
21
|
+
from .constants import PrimitiveType, SceneID
|
|
21
22
|
from .discovery import (
|
|
22
23
|
scan_for_mobius_devices_with_info, group_by_pan_id, dedupe_by_serial,
|
|
23
24
|
find_device_by_serial, discover_mesh_peers_via_direct_connect, discover_tank,
|
|
@@ -99,6 +100,37 @@ async def _dump_schedule(device: MobiusDevice, support: str) -> None:
|
|
|
99
100
|
print(f" (schedule dumping not supported for support={support!r})")
|
|
100
101
|
|
|
101
102
|
|
|
103
|
+
async def _dump_scenes(device: MobiusDevice, primitive: PrimitiveType | None) -> None:
|
|
104
|
+
"""Prints every configured scene, at whatever slot it lives in,
|
|
105
|
+
including empty/unused slots. The currently active scene is
|
|
106
|
+
already shown by the summary above (info["current_scene"])."""
|
|
107
|
+
print(" --- configured scenes ---")
|
|
108
|
+
scenes = await device.get_configured_scenes(primitive=primitive)
|
|
109
|
+
if not scenes:
|
|
110
|
+
print(" (none)")
|
|
111
|
+
return
|
|
112
|
+
for scene in scenes:
|
|
113
|
+
if scene.scene_type == SceneID.EmptyScene and not scene.name:
|
|
114
|
+
print(f" [{scene.index}] (empty)")
|
|
115
|
+
continue
|
|
116
|
+
type_label = scene.scene_type.name if scene.scene_type else "custom"
|
|
117
|
+
if scene.light is not None:
|
|
118
|
+
payload = ", ".join(
|
|
119
|
+
f"{vid.name}={val}"
|
|
120
|
+
for vid, val in sorted(scene.light.channels.items(), key=lambda x: x[0].name)
|
|
121
|
+
)
|
|
122
|
+
elif scene.pump is not None:
|
|
123
|
+
params = ", ".join(
|
|
124
|
+
f"{p.name}={v.hex() if isinstance(v, bytes) else (v.name if hasattr(v, 'name') else v)}"
|
|
125
|
+
for p, v in scene.pump.params.items()
|
|
126
|
+
)
|
|
127
|
+
payload = f"mode={scene.pump.mode.name} {params}"
|
|
128
|
+
else:
|
|
129
|
+
payload = "(no primitive data decoded)"
|
|
130
|
+
print(f" [{scene.index}] id={scene.id} ({type_label}) name={scene.name!r} "
|
|
131
|
+
f"timeout={scene.timeout}s: {payload}")
|
|
132
|
+
|
|
133
|
+
|
|
102
134
|
async def _debug_mesh_discovery(device) -> list:
|
|
103
135
|
"""
|
|
104
136
|
Verbose, step-by-step version of discover_mesh_peers_auto() for CLI
|
|
@@ -273,6 +305,10 @@ async def _run(args) -> None:
|
|
|
273
305
|
print(f" {k}: {v}")
|
|
274
306
|
if args.dump_schedule:
|
|
275
307
|
await _dump_schedule(mdevice, summary.get("support", ""))
|
|
308
|
+
if args.dump_scenes:
|
|
309
|
+
primitive_name = summary.get("primitive_type")
|
|
310
|
+
primitive = PrimitiveType[primitive_name] if primitive_name else None
|
|
311
|
+
await _dump_scenes(mdevice, primitive)
|
|
276
312
|
|
|
277
313
|
relayed = None # set below only if --relay-target resolves successfully
|
|
278
314
|
if args.dump_mesh_peers or args.relay_target:
|
|
@@ -620,6 +656,10 @@ async def _run(args) -> None:
|
|
|
620
656
|
print(f" {k}: {v}")
|
|
621
657
|
if args.dump_schedule:
|
|
622
658
|
await _dump_schedule(mdevice, summary.get("support", ""))
|
|
659
|
+
if args.dump_scenes:
|
|
660
|
+
primitive_name = summary.get("primitive_type")
|
|
661
|
+
primitive = PrimitiveType[primitive_name] if primitive_name else None
|
|
662
|
+
await _dump_scenes(mdevice, primitive)
|
|
623
663
|
except Exception as e:
|
|
624
664
|
print(" failed to connect/read:", e)
|
|
625
665
|
|
|
@@ -667,6 +707,12 @@ def main() -> None:
|
|
|
667
707
|
"hardware -- see LunarPhaseInfo's own docstring) -- compare against "
|
|
668
708
|
"\"current_intensities\"/\"light_diagnostics\" in the summary output "
|
|
669
709
|
"above for this library's own calculated value.")
|
|
710
|
+
parser.add_argument("--dump-scenes", action="store_true",
|
|
711
|
+
help="Also print every configured scene on each connected device -- "
|
|
712
|
+
"id, name, timeout, and its own light/pump payload -- at whatever "
|
|
713
|
+
"slot it lives in, including empty/unused slots. The currently "
|
|
714
|
+
"active scene is already shown by the summary above "
|
|
715
|
+
"(\"current_scene\").")
|
|
670
716
|
parser.add_argument("--dump-mesh-peers", action="store_true",
|
|
671
717
|
help="Only usable with --by-serial. After connecting, calls "
|
|
672
718
|
"discover_mesh_peers_auto() on it and prints every other device "
|
mobius/constants.py
CHANGED
mobius/device.py
CHANGED
|
@@ -12,7 +12,7 @@ from __future__ import annotations
|
|
|
12
12
|
import asyncio
|
|
13
13
|
import struct
|
|
14
14
|
import time
|
|
15
|
-
from dataclasses import dataclass
|
|
15
|
+
from dataclasses import dataclass, field
|
|
16
16
|
from typing import Optional, Callable
|
|
17
17
|
|
|
18
18
|
from bleak import BleakClient, BleakScanner
|
|
@@ -43,6 +43,7 @@ from .schedule import (
|
|
|
43
43
|
SchedulePoint, interpolate_light_schedule,
|
|
44
44
|
PumpSchedulePoint, get_active_pump_block,
|
|
45
45
|
)
|
|
46
|
+
from .scenes import Scene, ActiveScene, decode_configured_scenes, decode_active_scene
|
|
46
47
|
from .modifiers import (
|
|
47
48
|
AcclimationInfo, is_night_segment, lunar_percent_reduction,
|
|
48
49
|
)
|
|
@@ -189,6 +190,11 @@ class FullPollResult:
|
|
|
189
190
|
caller needing a separate get_pump_schedule() call just to also
|
|
190
191
|
get schedule_point_count alongside everything else here.
|
|
191
192
|
|
|
193
|
+
configured_scenes/current_scene -- everything get_configured_scenes()/
|
|
194
|
+
get_current_scene() themselves cover, for whichever device types
|
|
195
|
+
support scenes at all (an empty list / None if not, matching those
|
|
196
|
+
methods' own behavior exactly).
|
|
197
|
+
|
|
192
198
|
used_batch mirrors every other *Result/Snapshot's own field of the
|
|
193
199
|
same name and purpose.
|
|
194
200
|
"""
|
|
@@ -197,6 +203,8 @@ class FullPollResult:
|
|
|
197
203
|
light_poll: Optional[LightPollResult]
|
|
198
204
|
pump_telemetry: Optional[dict]
|
|
199
205
|
pump_schedule_points: Optional[list[PumpSchedulePoint]] = None
|
|
206
|
+
configured_scenes: list[Scene] = field(default_factory=list)
|
|
207
|
+
current_scene: Optional[ActiveScene] = None
|
|
200
208
|
used_batch: bool = True
|
|
201
209
|
|
|
202
210
|
|
|
@@ -1276,22 +1284,53 @@ class MobiusDevice:
|
|
|
1276
1284
|
|
|
1277
1285
|
# ---- scenes -----------------------------------------------------------
|
|
1278
1286
|
|
|
1279
|
-
async def start_scene(self, scene_id:
|
|
1280
|
-
"""
|
|
1281
|
-
|
|
1282
|
-
|
|
1287
|
+
async def start_scene(self, scene_id: int, duration_seconds: int = 0, broadcast: bool = True) -> None:
|
|
1288
|
+
"""
|
|
1289
|
+
Activates a scene by id -- one of the ten SceneID presets, or a
|
|
1290
|
+
user-created scene's own device-assigned id (see get_configured_scenes()).
|
|
1291
|
+
duration_seconds=0 lets the device use the scene's own configured
|
|
1292
|
+
timeout instead.
|
|
1293
|
+
|
|
1294
|
+
broadcast=True (the default) sets the frame's own reserved-byte
|
|
1295
|
+
"group" field to 1 (see make_reserved()), which propagates this
|
|
1296
|
+
write to the rest of the mesh from a single write to just this
|
|
1297
|
+
device -- confirmed against real hardware for set_time_to_now()'s
|
|
1298
|
+
own Epoch write; the same group=1 mechanism is used here, though
|
|
1299
|
+
this hasn't been independently confirmed against real hardware
|
|
1300
|
+
for CurrentScene specifically. group=1 only ever reaches devices
|
|
1301
|
+
on THIS SAME mesh (a Thread network is its own isolated network,
|
|
1302
|
+
identified by its own pan_id) -- it cannot reach a different
|
|
1303
|
+
tank's own, separate mesh, even one within physical radio range.
|
|
1304
|
+
Pass broadcast=False to write to only this one device instead.
|
|
1305
|
+
"""
|
|
1306
|
+
value = struct.pack("<HH", int(scene_id), duration_seconds)
|
|
1307
|
+
reserved = make_reserved(group=1) if broadcast else b"\x00\x00"
|
|
1308
|
+
await self.set_attribute(C2Attribute.CurrentScene, value, reserved=reserved)
|
|
1283
1309
|
|
|
1284
|
-
async def start_feed_mode(self,
|
|
1285
|
-
await self.start_scene(SceneID.FeedMode,
|
|
1310
|
+
async def start_feed_mode(self, duration_seconds: int = 0, broadcast: bool = True) -> None:
|
|
1311
|
+
await self.start_scene(SceneID.FeedMode, duration_seconds, broadcast=broadcast)
|
|
1286
1312
|
|
|
1287
1313
|
async def resume_schedule(self) -> None:
|
|
1288
1314
|
"""Cancel whatever scene is running and go back to the normal schedule."""
|
|
1289
1315
|
await self.set_attribute(C2Attribute.OperationState, bytes([OperationState.Schedule]))
|
|
1290
1316
|
|
|
1291
|
-
async def get_current_scene(self) ->
|
|
1292
|
-
|
|
1293
|
-
|
|
1294
|
-
|
|
1317
|
+
async def get_current_scene(self) -> Optional[ActiveScene]:
|
|
1318
|
+
try:
|
|
1319
|
+
raw = await self.get_attribute(C2Attribute.CurrentScene)
|
|
1320
|
+
except Exception:
|
|
1321
|
+
return None
|
|
1322
|
+
return decode_active_scene(raw[0]) if raw else None
|
|
1323
|
+
|
|
1324
|
+
async def get_configured_scenes(self, primitive: Optional[PrimitiveType] = None) -> list[Scene]:
|
|
1325
|
+
"""
|
|
1326
|
+
Every configured scene, at whatever slots this device actually
|
|
1327
|
+
has one in -- including slots holding a genuinely empty/unused
|
|
1328
|
+
scene. Pass primitive so each scene's own light/pump payload
|
|
1329
|
+
can be decoded; omit it (or leave as None) to get every other
|
|
1330
|
+
field with light/pump both left None.
|
|
1331
|
+
"""
|
|
1332
|
+
raw = await self.get_attribute(C2Attribute.ConfiguredScenes, index=0, count=0xFFFF)
|
|
1333
|
+
return decode_configured_scenes(raw, primitive)
|
|
1295
1334
|
|
|
1296
1335
|
async def get_operation_state(self) -> OperationState:
|
|
1297
1336
|
raw = await self.get_attribute(C2Attribute.OperationState)
|
|
@@ -2931,6 +2970,8 @@ class MobiusDevice:
|
|
|
2931
2970
|
info["firmware_versions"] = await self.get_firmware_versions(model)
|
|
2932
2971
|
info["hardware_info"] = await self.get_hardware_info()
|
|
2933
2972
|
info["device_time"] = await self.get_device_time_info()
|
|
2973
|
+
current_scene = await self.get_current_scene()
|
|
2974
|
+
info["current_scene"] = current_scene
|
|
2934
2975
|
|
|
2935
2976
|
return info
|
|
2936
2977
|
|
|
@@ -3634,6 +3675,8 @@ class MobiusDevice:
|
|
|
3634
3675
|
(C2Attribute.FirmwareVersion, 0, 0xFFFF),
|
|
3635
3676
|
(C2Attribute.HardwareRevision, 0, 0xFFFF),
|
|
3636
3677
|
(schedule_attr, 0, 0xFFFF),
|
|
3678
|
+
(C2Attribute.ConfiguredScenes, 0, 0xFFFF),
|
|
3679
|
+
(C2Attribute.CurrentScene, 0, 1),
|
|
3637
3680
|
]
|
|
3638
3681
|
if primitive == PrimitiveType.VisualV1:
|
|
3639
3682
|
intensity_attr = C2Attribute.Schedule1Intensity if which == 1 else C2Attribute.Schedule2Intensity
|
|
@@ -3705,10 +3748,16 @@ class MobiusDevice:
|
|
|
3705
3748
|
[motor_power_raw] if motor_power_raw else [], flow_range, model, primitive,
|
|
3706
3749
|
)
|
|
3707
3750
|
|
|
3751
|
+
configured_scenes = decode_configured_scenes(_values_for(C2Attribute.ConfiguredScenes), primitive)
|
|
3752
|
+
current_scene_raw = _first_value(C2Attribute.CurrentScene)
|
|
3753
|
+
current_scene = decode_active_scene(current_scene_raw) if current_scene_raw else None
|
|
3754
|
+
|
|
3708
3755
|
return FullPollResult(
|
|
3709
3756
|
device_info=device_info, metadata=metadata,
|
|
3710
3757
|
light_poll=light_poll, pump_telemetry=pump_telemetry,
|
|
3711
|
-
pump_schedule_points=pump_schedule_points,
|
|
3758
|
+
pump_schedule_points=pump_schedule_points,
|
|
3759
|
+
configured_scenes=configured_scenes, current_scene=current_scene,
|
|
3760
|
+
used_batch=True,
|
|
3712
3761
|
)
|
|
3713
3762
|
|
|
3714
3763
|
async def _get_full_poll_via_individual_reads(
|
|
@@ -3731,10 +3780,17 @@ class MobiusDevice:
|
|
|
3731
3780
|
else:
|
|
3732
3781
|
pump_schedule_points = await self.get_pump_schedule(which=which)
|
|
3733
3782
|
pump_telemetry = await self.get_pump_telemetry(model=model, primitive=primitive)
|
|
3783
|
+
try:
|
|
3784
|
+
configured_scenes = await self.get_configured_scenes(primitive=primitive)
|
|
3785
|
+
except Exception:
|
|
3786
|
+
configured_scenes = []
|
|
3787
|
+
current_scene = await self.get_current_scene()
|
|
3734
3788
|
return FullPollResult(
|
|
3735
3789
|
device_info=device_info, metadata=metadata,
|
|
3736
3790
|
light_poll=light_poll, pump_telemetry=pump_telemetry,
|
|
3737
|
-
pump_schedule_points=pump_schedule_points,
|
|
3791
|
+
pump_schedule_points=pump_schedule_points,
|
|
3792
|
+
configured_scenes=configured_scenes, current_scene=current_scene,
|
|
3793
|
+
used_batch=False,
|
|
3738
3794
|
)
|
|
3739
3795
|
|
|
3740
3796
|
async def get_current_light_percentages(self, which: int = 1,
|
mobius/scenes.py
ADDED
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Scenes: a device-side preset that can be activated on demand, separate
|
|
3
|
+
from the normal time-based schedule (Schedule1/Schedule2). Each
|
|
4
|
+
configured scene stores exactly the same kind of payload a schedule
|
|
5
|
+
point would (channel intensities for a light, mode+parameters for a
|
|
6
|
+
pump), plus a name and a timeout, at a fixed slot within
|
|
7
|
+
ConfiguredScenes (400). Activating one writes CurrentScene (401).
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
import struct
|
|
13
|
+
from dataclasses import dataclass
|
|
14
|
+
from typing import Optional
|
|
15
|
+
|
|
16
|
+
from .constants import SceneID, PrimitiveType, LIGHT_PRIMITIVES, PUMP_PRIMITIVES_VERIFIED, PUMP_PRIMITIVES_EXPERIMENTAL
|
|
17
|
+
from .schedule import LightPrimitive, PumpPrimitiveValue
|
|
18
|
+
|
|
19
|
+
SCENE_HEADER_SIZE = 20 # id(2) + timeout(2) + name(16), before primitiveData
|
|
20
|
+
SCENE_NAME_SIZE = 16
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
@dataclass
|
|
24
|
+
class Scene:
|
|
25
|
+
"""One configured scene, at a fixed slot (`index`) within
|
|
26
|
+
ConfiguredScenes. `scene_type` is one of the ten built-in presets
|
|
27
|
+
if `id` matches one, or None for a user-created scene (whose own
|
|
28
|
+
`id` is a device-assigned value outside that range). Exactly one
|
|
29
|
+
of `light`/`pump` is populated, matching the device's own
|
|
30
|
+
PrimitiveType -- the other stays None."""
|
|
31
|
+
index: int
|
|
32
|
+
id: int
|
|
33
|
+
scene_type: Optional[SceneID]
|
|
34
|
+
name: str
|
|
35
|
+
timeout: int
|
|
36
|
+
light: Optional[LightPrimitive]
|
|
37
|
+
pump: Optional[PumpPrimitiveValue]
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
@dataclass
|
|
41
|
+
class ActiveScene:
|
|
42
|
+
"""The currently active scene (CurrentScene, 401) and how many
|
|
43
|
+
seconds it has left. `scene_type` is None if `id` doesn't match
|
|
44
|
+
one of the ten built-in presets (i.e. a user-created scene is
|
|
45
|
+
active) -- id itself is always present either way."""
|
|
46
|
+
id: int
|
|
47
|
+
scene_type: Optional[SceneID]
|
|
48
|
+
duration_seconds: int
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def decode_configured_scenes(values: list[bytes], primitive: Optional[PrimitiveType]) -> list[Scene]:
|
|
52
|
+
"""
|
|
53
|
+
Pure decode for ConfiguredScenes (400) -- one call's worth of
|
|
54
|
+
values from a "get all elements" read (index=0, count=0xFFFF),
|
|
55
|
+
one element per slot, in index order. A slot with fewer than
|
|
56
|
+
SCENE_HEADER_SIZE bytes is skipped rather than raising.
|
|
57
|
+
"""
|
|
58
|
+
scenes = []
|
|
59
|
+
for index, raw in enumerate(values):
|
|
60
|
+
if len(raw) < SCENE_HEADER_SIZE:
|
|
61
|
+
continue
|
|
62
|
+
scene_id = struct.unpack_from("<H", raw, 0)[0]
|
|
63
|
+
timeout = struct.unpack_from("<H", raw, 2)[0]
|
|
64
|
+
name_bytes = raw[4:4 + SCENE_NAME_SIZE]
|
|
65
|
+
name = name_bytes.split(b"\x00", 1)[0].decode("utf-8", errors="replace")
|
|
66
|
+
primitive_data = raw[SCENE_HEADER_SIZE:]
|
|
67
|
+
|
|
68
|
+
try:
|
|
69
|
+
scene_type = SceneID(scene_id)
|
|
70
|
+
except ValueError:
|
|
71
|
+
scene_type = None
|
|
72
|
+
|
|
73
|
+
light = None
|
|
74
|
+
pump = None
|
|
75
|
+
if primitive in LIGHT_PRIMITIVES:
|
|
76
|
+
light = LightPrimitive.parse(primitive_data)
|
|
77
|
+
elif primitive in PUMP_PRIMITIVES_VERIFIED or primitive in PUMP_PRIMITIVES_EXPERIMENTAL:
|
|
78
|
+
pump = PumpPrimitiveValue.parse(primitive_data)
|
|
79
|
+
|
|
80
|
+
scenes.append(Scene(
|
|
81
|
+
index=index, id=scene_id, scene_type=scene_type, name=name,
|
|
82
|
+
timeout=timeout, light=light, pump=pump,
|
|
83
|
+
))
|
|
84
|
+
return scenes
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def decode_active_scene(raw: bytes) -> Optional[ActiveScene]:
|
|
88
|
+
"""Pure decode for CurrentScene (401): id(u16 LE) + duration
|
|
89
|
+
seconds(u16 LE). None if raw is too short to contain both fields."""
|
|
90
|
+
if len(raw) < 4:
|
|
91
|
+
return None
|
|
92
|
+
scene_id, duration = struct.unpack_from("<HH", raw, 0)
|
|
93
|
+
try:
|
|
94
|
+
scene_type = SceneID(scene_id)
|
|
95
|
+
except ValueError:
|
|
96
|
+
scene_type = None
|
|
97
|
+
return ActiveScene(id=scene_id, scene_type=scene_type, duration_seconds=duration)
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: python-mobius
|
|
3
|
-
Version: 0.7.
|
|
3
|
+
Version: 0.7.2
|
|
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
|
|
@@ -108,14 +108,25 @@ mobius-scan --adapter hci0
|
|
|
108
108
|
- **Read light schedules**: per-channel intensity at any given time,
|
|
109
109
|
replicating the app's own client-side interpolation (there's no "current
|
|
110
110
|
intensity" attribute — lights only expose the programmed curve).
|
|
111
|
+
- **Read every configured scene** (`get_configured_scenes()`) — name,
|
|
112
|
+
timeout, and its own light/pump payload, at whatever slot it lives in,
|
|
113
|
+
plus the currently active one (`get_current_scene()`).
|
|
114
|
+
- **Control scenes**: start feed mode, resume the normal schedule, or any
|
|
115
|
+
other configured scene — one write, broadcast to the whole mesh by
|
|
116
|
+
default (`broadcast=True`). Uses the same mesh-propagation mechanism
|
|
117
|
+
confirmed against real hardware for the clock-sync write below;
|
|
118
|
+
applying it to scene activation specifically hasn't been
|
|
119
|
+
independently verified the same way yet.
|
|
120
|
+
- **Fetch everything one device poll needs in a single round-trip**
|
|
121
|
+
(`get_full_poll_batch()`) — identity, metadata, light/pump state, and
|
|
122
|
+
scene data together, confirmed 2-2.6x faster on real hardware than
|
|
123
|
+
reading each piece separately.
|
|
111
124
|
- **Read device-specific settings**: VorTech's own "Local Control"/"Led
|
|
112
125
|
Auto Dim" and Radion's own "Max Fan Speed"/"Fan Shutdown"
|
|
113
126
|
(`get_advanced_features()`), plus Vectra and NYOS Quantum settings
|
|
114
127
|
(`get_vectra_info()`/`get_coffee_info()` — no real hardware to verify
|
|
115
128
|
either against, see
|
|
116
129
|
[known gaps](./documentation/10-known-gaps-and-open-questions.md)).
|
|
117
|
-
- **Control scenes**: start feed mode, resume the normal schedule, or any
|
|
118
|
-
other configured scene.
|
|
119
130
|
- **Fix a desynced device clock** (`set_time_to_now()`) — a WRITE.
|
|
120
131
|
Writing to one device appears to propagate to the rest of its Thread
|
|
121
132
|
mesh too, confirmed against real hardware — see
|
|
@@ -1,9 +1,9 @@
|
|
|
1
|
-
mobius/__init__.py,sha256
|
|
2
|
-
mobius/cli.py,sha256=
|
|
1
|
+
mobius/__init__.py,sha256=gcj_rnHfu_zczKCP4aBKOuKREHxuvmdXV9zFvMcj4eQ,6788
|
|
2
|
+
mobius/cli.py,sha256=antaLPQysyPethop24fjeKowczEvfoVodgYkyIT5YxE,50008
|
|
3
3
|
mobius/coap.py,sha256=dRE4yUp413etAMhDg2bLvFMLWusqjEWgUpC36i76mJs,10966
|
|
4
|
-
mobius/constants.py,sha256=
|
|
4
|
+
mobius/constants.py,sha256=ytpf9S_cHBKFWi_scr06ydmv-MW1dLGlG3CYfOAkiAw,42480
|
|
5
5
|
mobius/crc.py,sha256=kLtAYWZvLbo5_p9ep08ZlLg5Yah8Q2qq0IwTvqbBGW0,2700
|
|
6
|
-
mobius/device.py,sha256=
|
|
6
|
+
mobius/device.py,sha256=YxfIgKWsERx1m-ueU1Bau1cXCWYOf-dzu2z3fUq7IeI,190773
|
|
7
7
|
mobius/device_status.py,sha256=xlLCMU4gLnp59Uqix679jRdVBY2Jt6oI4EUr8fraeRg,34089
|
|
8
8
|
mobius/discovery.py,sha256=Q_Z0riOBrEOwqpwAhk1wesh3340fiIYFi-8BWqhZ0JI,11949
|
|
9
9
|
mobius/dump.py,sha256=S7-KNeDi9TOETm4ty5DwMXTrthvuMau196UENDQoAb4,16785
|
|
@@ -14,9 +14,10 @@ mobius/modifiers.py,sha256=b_BLlkSweanYse0VM8TkscGc4It-Dm0Aq2JbNpK7pMU,7308
|
|
|
14
14
|
mobius/power.py,sha256=2o91-Cl6tC3x9SKJ9gN0fP5HSTFeD31jtd2BwTyBn30,3913
|
|
15
15
|
mobius/pump_status.py,sha256=RWTl8ZViCFDbiFU2mX-WAIH_cWhZOZ0M2B0oYXnPx3Y,1598
|
|
16
16
|
mobius/relay.py,sha256=IG15TtKDjSkmmi_C2cJwPmucX7V4psfamI0hKgTb_v8,24197
|
|
17
|
+
mobius/scenes.py,sha256=Wt0Ys2x1YjF8MNDEd6kxWqYBIYPdBSbfzXD-L5ZKzag,3592
|
|
17
18
|
mobius/schedule.py,sha256=kkQgwkFtM3pvik7UFzHKVhPVZrCGSH9AYvAlVA6kAck,10848
|
|
18
|
-
python_mobius-0.7.
|
|
19
|
-
python_mobius-0.7.
|
|
20
|
-
python_mobius-0.7.
|
|
21
|
-
python_mobius-0.7.
|
|
22
|
-
python_mobius-0.7.
|
|
19
|
+
python_mobius-0.7.2.dist-info/METADATA,sha256=X1Pa4FH6dPjYwjNRxFaRpu-hHgRn68f5F5eZubCNgqg,9036
|
|
20
|
+
python_mobius-0.7.2.dist-info/WHEEL,sha256=zOwg4jB6zX2kU910N-cMawjivD6tO8NEWvE12je1bVk,87
|
|
21
|
+
python_mobius-0.7.2.dist-info/entry_points.txt,sha256=Q2hlcek-bWm70zvd12v32essflPgC2su-dKDHAqbPoE,48
|
|
22
|
+
python_mobius-0.7.2.dist-info/licenses/LICENSE,sha256=7a72Msu2Q-TnoiFxemxEGkwafJGObk1W3rw9hzmyM_Y,17984
|
|
23
|
+
python_mobius-0.7.2.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|