python-mobius 0.6.0__tar.gz → 0.7.1__tar.gz

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.
Files changed (79) hide show
  1. {python_mobius-0.6.0 → python_mobius-0.7.1}/CHANGELOG.md +23 -0
  2. {python_mobius-0.6.0 → python_mobius-0.7.1}/PKG-INFO +1 -1
  3. {python_mobius-0.6.0 → python_mobius-0.7.1}/pyproject.toml +1 -1
  4. {python_mobius-0.6.0 → python_mobius-0.7.1}/src/mobius/__init__.py +6 -3
  5. {python_mobius-0.6.0 → python_mobius-0.7.1}/src/mobius/cli.py +46 -0
  6. {python_mobius-0.6.0 → python_mobius-0.7.1}/src/mobius/constants.py +1 -0
  7. {python_mobius-0.6.0 → python_mobius-0.7.1}/src/mobius/device.py +913 -226
  8. python_mobius-0.7.1/src/mobius/scenes.py +97 -0
  9. python_mobius-0.7.1/tests/test_full_poll_batch.py +229 -0
  10. python_mobius-0.7.1/tests/test_light_poll_batch.py +203 -0
  11. {python_mobius-0.6.0 → python_mobius-0.7.1}/tests/test_pump_schedule.py +40 -0
  12. python_mobius-0.7.1/tests/test_scenes.py +203 -0
  13. {python_mobius-0.6.0 → python_mobius-0.7.1}/.forgejo/scripts/extract_changelog_section.py +0 -0
  14. {python_mobius-0.6.0 → python_mobius-0.7.1}/.forgejo/workflows/release.yml +0 -0
  15. {python_mobius-0.6.0 → python_mobius-0.7.1}/.gitignore +0 -0
  16. {python_mobius-0.6.0 → python_mobius-0.7.1}/LICENSE +0 -0
  17. {python_mobius-0.6.0 → python_mobius-0.7.1}/README.md +0 -0
  18. {python_mobius-0.6.0 → python_mobius-0.7.1}/documentation/00-overview.md +0 -0
  19. {python_mobius-0.6.0 → python_mobius-0.7.1}/documentation/01-ble-transport.md +0 -0
  20. {python_mobius-0.6.0 → python_mobius-0.7.1}/documentation/02-framing-and-crc.md +0 -0
  21. {python_mobius-0.6.0 → python_mobius-0.7.1}/documentation/03-attributes-and-opcodes.md +0 -0
  22. {python_mobius-0.6.0 → python_mobius-0.7.1}/documentation/04-device-identity.md +0 -0
  23. {python_mobius-0.6.0 → python_mobius-0.7.1}/documentation/05-scenes.md +0 -0
  24. {python_mobius-0.6.0 → python_mobius-0.7.1}/documentation/06-light-schedule.md +0 -0
  25. {python_mobius-0.6.0 → python_mobius-0.7.1}/documentation/07-pump-schedule.md +0 -0
  26. {python_mobius-0.6.0 → python_mobius-0.7.1}/documentation/08-manufacturer-data.md +0 -0
  27. {python_mobius-0.6.0 → python_mobius-0.7.1}/documentation/09-thread-coap-relay.md +0 -0
  28. {python_mobius-0.6.0 → python_mobius-0.7.1}/documentation/10-known-gaps-and-open-questions.md +0 -0
  29. {python_mobius-0.6.0 → python_mobius-0.7.1}/documentation/11-device-status-attributes.md +0 -0
  30. {python_mobius-0.6.0 → python_mobius-0.7.1}/documentation/12-device-identity-and-address-stability.md +0 -0
  31. {python_mobius-0.6.0 → python_mobius-0.7.1}/documentation/13-attribute-dump.md +0 -0
  32. {python_mobius-0.6.0 → python_mobius-0.7.1}/documentation/14-batched-attribute-reads.md +0 -0
  33. {python_mobius-0.6.0 → python_mobius-0.7.1}/src/mobius/coap.py +0 -0
  34. {python_mobius-0.6.0 → python_mobius-0.7.1}/src/mobius/crc.py +0 -0
  35. {python_mobius-0.6.0 → python_mobius-0.7.1}/src/mobius/device_status.py +0 -0
  36. {python_mobius-0.6.0 → python_mobius-0.7.1}/src/mobius/discovery.py +0 -0
  37. {python_mobius-0.6.0 → python_mobius-0.7.1}/src/mobius/dump.py +0 -0
  38. {python_mobius-0.6.0 → python_mobius-0.7.1}/src/mobius/frame.py +0 -0
  39. {python_mobius-0.6.0 → python_mobius-0.7.1}/src/mobius/manufacturer.py +0 -0
  40. {python_mobius-0.6.0 → python_mobius-0.7.1}/src/mobius/mesh_address.py +0 -0
  41. {python_mobius-0.6.0 → python_mobius-0.7.1}/src/mobius/modifiers.py +0 -0
  42. {python_mobius-0.6.0 → python_mobius-0.7.1}/src/mobius/power.py +0 -0
  43. {python_mobius-0.6.0 → python_mobius-0.7.1}/src/mobius/pump_status.py +0 -0
  44. {python_mobius-0.6.0 → python_mobius-0.7.1}/src/mobius/relay.py +0 -0
  45. {python_mobius-0.6.0 → python_mobius-0.7.1}/src/mobius/schedule.py +0 -0
  46. {python_mobius-0.6.0 → python_mobius-0.7.1}/tests/test_attributes_batch.py +0 -0
  47. {python_mobius-0.6.0 → python_mobius-0.7.1}/tests/test_coap.py +0 -0
  48. {python_mobius-0.6.0 → python_mobius-0.7.1}/tests/test_coap_indication_handling.py +0 -0
  49. {python_mobius-0.6.0 → python_mobius-0.7.1}/tests/test_connection_cleanup.py +0 -0
  50. {python_mobius-0.6.0 → python_mobius-0.7.1}/tests/test_device_control.py +0 -0
  51. {python_mobius-0.6.0 → python_mobius-0.7.1}/tests/test_device_status.py +0 -0
  52. {python_mobius-0.6.0 → python_mobius-0.7.1}/tests/test_device_summary.py +0 -0
  53. {python_mobius-0.6.0 → python_mobius-0.7.1}/tests/test_device_time.py +0 -0
  54. {python_mobius-0.6.0 → python_mobius-0.7.1}/tests/test_discovery_company_ids.py +0 -0
  55. {python_mobius-0.6.0 → python_mobius-0.7.1}/tests/test_dump.py +0 -0
  56. {python_mobius-0.6.0 → python_mobius-0.7.1}/tests/test_firmware_versions.py +0 -0
  57. {python_mobius-0.6.0 → python_mobius-0.7.1}/tests/test_frame.py +0 -0
  58. {python_mobius-0.6.0 → python_mobius-0.7.1}/tests/test_frame_sniffer.py +0 -0
  59. {python_mobius-0.6.0 → python_mobius-0.7.1}/tests/test_hyperdrive_device.py +0 -0
  60. {python_mobius-0.6.0 → python_mobius-0.7.1}/tests/test_insolation_detection.py +0 -0
  61. {python_mobius-0.6.0 → python_mobius-0.7.1}/tests/test_light_intensity_branching.py +0 -0
  62. {python_mobius-0.6.0 → python_mobius-0.7.1}/tests/test_light_schedule.py +0 -0
  63. {python_mobius-0.6.0 → python_mobius-0.7.1}/tests/test_manufacturer.py +0 -0
  64. {python_mobius-0.6.0 → python_mobius-0.7.1}/tests/test_manufacturer_lookup.py +0 -0
  65. {python_mobius-0.6.0 → python_mobius-0.7.1}/tests/test_mesh_address.py +0 -0
  66. {python_mobius-0.6.0 → python_mobius-0.7.1}/tests/test_mesh_discovery.py +0 -0
  67. {python_mobius-0.6.0 → python_mobius-0.7.1}/tests/test_mesh_discovery_direct_connect.py +0 -0
  68. {python_mobius-0.6.0 → python_mobius-0.7.1}/tests/test_metadata_batch.py +0 -0
  69. {python_mobius-0.6.0 → python_mobius-0.7.1}/tests/test_modifiers.py +0 -0
  70. {python_mobius-0.6.0 → python_mobius-0.7.1}/tests/test_networked_thread_devices.py +0 -0
  71. {python_mobius-0.6.0 → python_mobius-0.7.1}/tests/test_new_diagnostic_attributes.py +0 -0
  72. {python_mobius-0.6.0 → python_mobius-0.7.1}/tests/test_power.py +0 -0
  73. {python_mobius-0.6.0 → python_mobius-0.7.1}/tests/test_pump_status.py +0 -0
  74. {python_mobius-0.6.0 → python_mobius-0.7.1}/tests/test_pump_telemetry.py +0 -0
  75. {python_mobius-0.6.0 → python_mobius-0.7.1}/tests/test_relay.py +0 -0
  76. {python_mobius-0.6.0 → python_mobius-0.7.1}/tests/test_schedule_intensity_scalar.py +0 -0
  77. {python_mobius-0.6.0 → python_mobius-0.7.1}/tests/test_serial_decoding.py +0 -0
  78. {python_mobius-0.6.0 → python_mobius-0.7.1}/tests/test_serial_identity.py +0 -0
  79. {python_mobius-0.6.0 → python_mobius-0.7.1}/tests/test_supported_attributes.py +0 -0
@@ -1,5 +1,28 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.7.1
4
+
5
+ - Added scene support: get_configured_scenes(), get_current_scene(),
6
+ start_scene(). Fixed get_current_scene() crashing on a custom
7
+ (non-preset) scene, and start_scene()'s duration being in seconds,
8
+ not minutes as previously labeled.
9
+
10
+ ## 0.7.0
11
+
12
+ - Added get_full_poll_batch() -- one round-trip for device identity +
13
+ metadata + light/pump state, confirmed 2-2.6x faster on real
14
+ hardware. Requires the caller to already know the device's own
15
+ PrimitiveType (permanent for its lifetime, so never re-fetched).
16
+ - get_current_pump_block() now accepts an optional pre-fetched
17
+ `points` param, skipping its own redundant re-fetch of the same
18
+ schedule attribute.
19
+ - Added get_light_poll_batch() -- one round-trip for a light's own
20
+ schedule, lunar/acclimation/insolation state, and Epoch/LocalTime,
21
+ falling back to the original individual reads if the batch fails.
22
+ - Extracted every light-schedule-related method's own decode/compute
23
+ logic into pure, standalone functions, laying the groundwork for
24
+ get_light_poll_batch() above. No behavior change.
25
+
3
26
  ## 0.6.0
4
27
 
5
28
  - get_metadata_batch() now falls back to individual reads if the
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: python-mobius
3
- Version: 0.6.0
3
+ Version: 0.7.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
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "python-mobius"
7
- version = "0.6.0"
7
+ version = "0.7.1"
8
8
  description = "Reverse-engineered Python client for the Mobius BLE protocol (EcoTech Marine VorTech/Radion, AquaIllumination, Neptune Systems, NYOS)"
9
9
  readme = "README.md"
10
10
  license = { text = "GPL-2.0-only" }
@@ -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,
@@ -56,7 +57,7 @@ from .mesh_address import (
56
57
  )
57
58
  from .relay import RelayedMobiusDevice
58
59
  from .device import (
59
- MobiusDevice, MobiusPump, LightIntensityResult,
60
+ MobiusDevice, MobiusPump, LightIntensityResult, LightPollResult, FullPollResult,
60
61
  SERVICE_GENERAL, CHAR_RX_DATA, CHAR_RX_FINAL, CHAR_TX_DATA, CHAR_TX_FINAL,
61
62
  )
62
63
  from .discovery import (
@@ -71,7 +72,7 @@ from .dump import (
71
72
  enrich_attribute_dump,
72
73
  )
73
74
 
74
- __version__ = "0.6.0"
75
+ __version__ = "0.7.1"
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
@@ -117,7 +120,7 @@ __all__ = [
117
120
  "build_rloc_address", "extract_short_address", "is_short_address_derived",
118
121
  "RelayedMobiusDevice",
119
122
  # device
120
- "MobiusDevice", "MobiusPump", "LightIntensityResult",
123
+ "MobiusDevice", "MobiusPump", "LightIntensityResult", "LightPollResult", "FullPollResult",
121
124
  "SERVICE_GENERAL", "CHAR_RX_DATA", "CHAR_RX_FINAL", "CHAR_TX_DATA", "CHAR_TX_FINAL",
122
125
  # discovery
123
126
  "scan_for_mobius_devices", "scan_for_mobius_devices_with_info", "group_by_pan_id",
@@ -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 "
@@ -436,6 +436,7 @@ class C2Attribute(IntEnum):
436
436
  SceneTimeout = 402
437
437
  SceneTimer = 403
438
438
  # Scene diagnostics -- named, not yet exposed by a get_*() method.
439
+ LiveDemoSceneNero = 404
439
440
  LiveDemoTimeout = 405
440
441
  LiveDemoTimer = 406
441
442
  LiveDemoScene = 407