mirror of
https://github.com/firestar5683/StarPilot.git
synced 2026-11-07 21:13:41 +08:00
90 lines
3.3 KiB
Python
90 lines
3.3 KiB
Python
"""Read-only BlueZ state for the compact Home indicator: off, on, or a device connected.
|
|
|
|
This does not enable Bluetooth or manage devices. The system-bus query runs off
|
|
the render thread and expires after a short gap.
|
|
"""
|
|
|
|
from collections.abc import Callable
|
|
from concurrent.futures import Future, ThreadPoolExecutor
|
|
import json
|
|
import subprocess
|
|
import time
|
|
|
|
STATES = ("off", "on", "connected")
|
|
|
|
|
|
def _clock_ns() -> int:
|
|
boot = getattr(time, "CLOCK_BOOTTIME", None)
|
|
return time.clock_gettime_ns(boot) if boot is not None else time.monotonic_ns()
|
|
|
|
|
|
def read_bluetooth_state() -> str:
|
|
"""One BlueZ object listing: the adapter's power and whether any of its devices is connected."""
|
|
try:
|
|
result = subprocess.run(("busctl", "--system", "--timeout=1", "call", "org.bluez", "/", "org.freedesktop.DBus.ObjectManager",
|
|
"GetManagedObjects", "--json=short"), capture_output=True, text=True, timeout=1.5, check=False)
|
|
except (OSError, subprocess.TimeoutExpired):
|
|
return "off"
|
|
if result.returncode != 0:
|
|
return "off"
|
|
try:
|
|
objects = json.loads(result.stdout)["data"][0]
|
|
|
|
def prop(path, interface, name):
|
|
return objects.get(path, {}).get(interface, {}).get(name, {}).get("data")
|
|
if prop("/org/bluez/hci0", "org.bluez.Adapter1", "Powered") is not True:
|
|
return "off"
|
|
connected = any(path.startswith("/org/bluez/hci0/dev_") and prop(path, "org.bluez.Device1", "Connected") is True
|
|
for path in objects)
|
|
except (ValueError, KeyError, IndexError, TypeError, AttributeError):
|
|
return "off"
|
|
return "connected" if connected else "on"
|
|
|
|
|
|
class BluetoothStatusSource:
|
|
"""One bounded background read; stale, failed, or absent status is false."""
|
|
|
|
REFRESH_NS = 2_000_000_000
|
|
MAX_AGE_NS = 4_000_000_000
|
|
|
|
def __init__(self, request: Callable[[], str] = read_bluetooth_state,
|
|
clock: Callable[[], int] = _clock_ns):
|
|
self.request = request
|
|
self.clock = clock
|
|
self.executor = ThreadPoolExecutor(max_workers=1, thread_name_prefix="bluetooth-status")
|
|
self.pending: Future[tuple[str, int]] | None = None
|
|
self.next_read_ns = 0
|
|
self.state = "off"
|
|
self.observed_ns: int | None = None
|
|
self.closed = False
|
|
|
|
def _poll(self) -> tuple[str, int]:
|
|
try:
|
|
state = self.request()
|
|
except (OSError, RuntimeError, ValueError, subprocess.TimeoutExpired):
|
|
state = "off"
|
|
return state if state in STATES else "off", self.clock()
|
|
|
|
def snapshot(self) -> str:
|
|
if self.closed:
|
|
return "off"
|
|
now = self.clock()
|
|
if self.pending is not None and self.pending.done():
|
|
try:
|
|
self.state, self.observed_ns = self.pending.result()
|
|
except (OSError, RuntimeError, ValueError):
|
|
self.state, self.observed_ns = "off", None
|
|
self.pending = None
|
|
if now < self.next_read_ns - self.REFRESH_NS:
|
|
self.next_read_ns = 0
|
|
self.state, self.observed_ns = "off", None
|
|
if self.pending is None and now >= self.next_read_ns:
|
|
self.pending = self.executor.submit(self._poll)
|
|
self.next_read_ns = now + self.REFRESH_NS
|
|
fresh = self.observed_ns is not None and 0 <= now - self.observed_ns <= self.MAX_AGE_NS
|
|
return self.state if fresh else "off"
|
|
|
|
def close(self) -> None:
|
|
self.closed = True
|
|
self.executor.shutdown(wait=False, cancel_futures=True)
|