python-mobius 0.4.3__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 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 MOBIUS_COMPANY_ID, MobiusAdvertisement, parse_manufacturer_data
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.4.3"
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
- "MOBIUS_COMPANY_ID", "MobiusAdvertisement", "parse_manufacturer_data",
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
- async with MobiusDevice(serial=args.by_serial, adapter=args.adapter,
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
- async with MobiusDevice(serial=info.serial, adapter=args.adapter,
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
- async with MobiusDevice(serial=info.serial, adapter=args.adapter,
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. Useful for diagnosing a mismatch against the "
512
- "app's own displayed value.")
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 "