Compare commits
45 Commits
c93b444488
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
| 23fb2eec62 | |||
| 78bd027cbf | |||
| 26a75c1f12 | |||
| b5ee0b5df2 | |||
| b0b5905169 | |||
| a01a655d6c | |||
| 58397e6b4c | |||
| 61eb680695 | |||
| 6ec536a964 | |||
| 3b1e8bdaf3 | |||
| 7163ff2d6f | |||
| ad31ed0f2e | |||
| 8bc8cf4089 | |||
| 240fe2f39d | |||
| ac4145ac05 | |||
| be180b073a | |||
| cc63c6807d | |||
| 4bd98c17cf | |||
| 706a9b4abd | |||
| d9f5c7382b | |||
| aed90db5e8 | |||
| 99dc67e807 | |||
| 2a02dc0f5a | |||
| 6c6c70c254 | |||
| 8e624950aa | |||
| 19deff17a7 | |||
| 9c9adea41c | |||
| d0bd68bc6b | |||
| f8c09124a4 | |||
| 2e68948f09 | |||
| c3c2fc794c | |||
| 4b03d92674 | |||
| 1c219aa564 | |||
| d67ad2ef2a | |||
| 19cf54b2c2 | |||
| 4fdc6042dc | |||
| 1d7a084a3a | |||
| ef658a76a1 | |||
| 18997d5295 | |||
| a2702734ff | |||
| 89e6ee718f | |||
| 865f010f75 | |||
| de774ccf56 | |||
| fb08301687 | |||
| 56134c2d70 |
1
.gitignore
vendored
1
.gitignore
vendored
@@ -150,3 +150,4 @@ out.*
|
|||||||
*.csv
|
*.csv
|
||||||
*.txt
|
*.txt
|
||||||
*.json
|
*.json
|
||||||
|
.aider*
|
||||||
|
|||||||
333
main.py
333
main.py
@@ -10,15 +10,293 @@ import json
|
|||||||
|
|
||||||
import asyncio
|
import asyncio
|
||||||
from aiomqtt import Client
|
from aiomqtt import Client
|
||||||
|
from aiomqtt.exceptions import MqttError
|
||||||
|
from dbus_next.aio import MessageBus
|
||||||
|
from dbus_next.service import ServiceInterface, method
|
||||||
|
from dbus_next.constants import BusType
|
||||||
|
from dbus_next.errors import DBusError
|
||||||
|
from dbus_next import Variant, Message, MessageType
|
||||||
|
|
||||||
|
|
||||||
|
bus = None
|
||||||
|
agent_instance = None
|
||||||
|
pairing_task = None
|
||||||
|
|
||||||
|
# --- Bluetooth constants and agent ---
|
||||||
|
BLUEZ_SERVICE = 'org.bluez'
|
||||||
|
ADAPTER_IFACE = 'org.bluez.Adapter1'
|
||||||
|
DEVICE_IFACE = 'org.bluez.Device1'
|
||||||
|
MEDIA_PLAYER_IFACE = 'org.bluez.MediaPlayer1'
|
||||||
|
MEDIA_TRANSPORT_IFACE = 'org.bluez.MediaTransport1'
|
||||||
|
AGENT_IFACE = 'org.bluez.Agent1'
|
||||||
|
AGENT_MANAGER_IFACE = 'org.bluez.AgentManager1'
|
||||||
|
AGENT_PATH = '/io/bluetooth_speaker/agent'
|
||||||
|
CAPABILITY = 'DisplayYesNo'
|
||||||
|
CALLS_SERVICE_UUID = '0000111e-0000-1000-8000-00805f9b34fb'
|
||||||
|
AUDIO_SERVICE_UUID = '0000110d-0000-1000-8000-00805f9b34fb'
|
||||||
|
|
||||||
|
class Agent(ServiceInterface):
|
||||||
|
def __init__(self, interface_name):
|
||||||
|
super().__init__(interface_name)
|
||||||
|
logging.info('Agent instance created')
|
||||||
|
|
||||||
|
@method()
|
||||||
|
def Release(self):
|
||||||
|
logging.info('Agent Released')
|
||||||
|
|
||||||
|
@method()
|
||||||
|
def RequestPinCode(self, device: 'o') -> 's':
|
||||||
|
logging.info(f"RequestPinCode for {device}, returning static PIN")
|
||||||
|
return "0000"
|
||||||
|
|
||||||
|
@method()
|
||||||
|
def RequestPasskey(self, device: 'o') -> 'u':
|
||||||
|
logging.info(f"RequestPasskey for {device}")
|
||||||
|
return 0
|
||||||
|
|
||||||
|
@method()
|
||||||
|
def DisplayPasskey(self, device: 'o', passkey: 'u', entered: 'q'):
|
||||||
|
logging.info(f"DisplayPasskey for {device}: {passkey}")
|
||||||
|
|
||||||
|
@method()
|
||||||
|
def DisplayPinCode(self, device: 'o', pincode: 's'):
|
||||||
|
logging.info(f"DisplayPinCode for {device}: {pincode}")
|
||||||
|
|
||||||
|
@method()
|
||||||
|
async def RequestConfirmation(self, device: 'o', passkey: 'u'):
|
||||||
|
logging.info(f"RequestConfirmation for {device} with passkey {passkey}")
|
||||||
|
# Automatically confirm and trust
|
||||||
|
asyncio.create_task(trust_device(device))
|
||||||
|
|
||||||
|
@method()
|
||||||
|
async def RequestAuthorization(self, device: 'o'):
|
||||||
|
logging.info(f"RequestAuthorization for {device}")
|
||||||
|
# Automatically authorize and trust
|
||||||
|
asyncio.create_task(trust_device(device))
|
||||||
|
|
||||||
|
@method()
|
||||||
|
async def AuthorizeService(self, device: 'o', uuid: 's'):
|
||||||
|
logging.info(f"AuthorizeService request for device {device} with UUID {uuid}")
|
||||||
|
if uuid.lower() == CALLS_SERVICE_UUID:
|
||||||
|
logging.warning("Rejecting Hands-Free Profile (HFP) connection.")
|
||||||
|
raise DBusError('org.bluez.Error.Rejected', 'HFP profile not supported')
|
||||||
|
logging.info(f"Authorizing service UUID {uuid}")
|
||||||
|
|
||||||
|
@method()
|
||||||
|
def Cancel(self):
|
||||||
|
logging.info('Pairing Cancelled')
|
||||||
|
|
||||||
|
async def trust_device(device_path):
|
||||||
|
logging.info(f'Trusting device {device_path}')
|
||||||
|
try:
|
||||||
|
introspection = await bus.introspect(BLUEZ_SERVICE, device_path)
|
||||||
|
device_obj = bus.get_proxy_object(BLUEZ_SERVICE, device_path, introspection)
|
||||||
|
|
||||||
|
device_props = device_obj.get_interface('org.freedesktop.DBus.Properties')
|
||||||
|
await device_props.call_set(DEVICE_IFACE, 'Trusted', Variant('b', True))
|
||||||
|
logging.info(f'Trusted device {device_path}')
|
||||||
|
except Exception as e:
|
||||||
|
logging.error(f'Failed to trust device {device_path}: {e}')
|
||||||
|
|
||||||
|
async def get_adapter():
|
||||||
|
introspection = await bus.introspect(BLUEZ_SERVICE, '/')
|
||||||
|
manager_obj = bus.get_proxy_object(BLUEZ_SERVICE, '/', introspection)
|
||||||
|
manager_iface = manager_obj.get_interface('org.freedesktop.DBus.ObjectManager')
|
||||||
|
managed_objects = await manager_iface.call_get_managed_objects()
|
||||||
|
|
||||||
|
for path, ifaces in managed_objects.items():
|
||||||
|
if ADAPTER_IFACE in ifaces:
|
||||||
|
adapter_introspection = await bus.introspect(BLUEZ_SERVICE, path)
|
||||||
|
return bus.get_proxy_object(BLUEZ_SERVICE, path, adapter_introspection)
|
||||||
|
return None
|
||||||
|
|
||||||
|
async def register_agent():
|
||||||
|
global agent_instance
|
||||||
|
agent_instance = Agent(AGENT_IFACE)
|
||||||
|
bus.export(AGENT_PATH, agent_instance)
|
||||||
|
|
||||||
|
introspection = await bus.introspect(BLUEZ_SERVICE, '/org/bluez')
|
||||||
|
manager_obj = bus.get_proxy_object(BLUEZ_SERVICE, '/org/bluez', introspection)
|
||||||
|
agent_manager = manager_obj.get_interface(AGENT_MANAGER_IFACE)
|
||||||
|
|
||||||
|
try:
|
||||||
|
await agent_manager.call_register_agent(AGENT_PATH, CAPABILITY)
|
||||||
|
logging.info(f"Agent registered at {AGENT_PATH} with capability {CAPABILITY}")
|
||||||
|
await agent_manager.call_request_default_agent(AGENT_PATH)
|
||||||
|
logging.info("Agent set as default")
|
||||||
|
except Exception as e:
|
||||||
|
logging.error(f'Failed to register agent: {e}')
|
||||||
|
logging.info('Trying to unregister and register again')
|
||||||
|
try:
|
||||||
|
await agent_manager.call_unregister_agent(AGENT_PATH)
|
||||||
|
await agent_manager.call_register_agent(AGENT_PATH, CAPABILITY)
|
||||||
|
await agent_manager.call_request_default_agent(AGENT_PATH)
|
||||||
|
logging.info("Agent registered after unregistering")
|
||||||
|
except Exception as e2:
|
||||||
|
logging.error(f'Failed to register agent again: {e2}')
|
||||||
|
|
||||||
|
async def set_adapter_alias(alias):
|
||||||
|
logging.info(f"Setting Bluetooth adapter alias to '{alias}'")
|
||||||
|
adapter_obj = await get_adapter()
|
||||||
|
if not adapter_obj:
|
||||||
|
logging.error('Bluetooth adapter not found, cannot set alias.')
|
||||||
|
return
|
||||||
|
|
||||||
|
adapter_props = adapter_obj.get_interface('org.freedesktop.DBus.Properties')
|
||||||
|
try:
|
||||||
|
await adapter_props.call_set(ADAPTER_IFACE, 'Alias', Variant('s', alias))
|
||||||
|
logging.info(f"Successfully set adapter alias to '{alias}'")
|
||||||
|
except Exception as e:
|
||||||
|
logging.error(f"Failed to set adapter alias: {e}")
|
||||||
|
|
||||||
|
|
||||||
|
# --- End Bluetooth ---
|
||||||
|
|
||||||
async def manage_bluetooth():
|
async def manage_bluetooth():
|
||||||
|
await register_agent()
|
||||||
|
await set_adapter_alias("Home Audio")
|
||||||
|
# The agent will handle things, this task can just sleep
|
||||||
while True:
|
while True:
|
||||||
await asyncio.sleep(1)
|
await asyncio.sleep(3600)
|
||||||
|
|
||||||
|
|
||||||
|
async def enable_pairing():
|
||||||
|
"""Enable pairing for 120 seconds. This task can be cancelled and restarted."""
|
||||||
|
adapter_obj = await get_adapter()
|
||||||
|
if not adapter_obj:
|
||||||
|
logging.error('Bluetooth adapter not found')
|
||||||
|
return
|
||||||
|
|
||||||
|
adapter_props = adapter_obj.get_interface('org.freedesktop.DBus.Properties')
|
||||||
|
try:
|
||||||
|
await adapter_props.call_set(ADAPTER_IFACE, 'Discoverable', Variant('b', True))
|
||||||
|
await adapter_props.call_set(ADAPTER_IFACE, 'Pairable', Variant('b', True))
|
||||||
|
logging.info('Adapter is discoverable and pairable for 120 seconds')
|
||||||
|
|
||||||
|
await asyncio.sleep(120)
|
||||||
|
|
||||||
|
logging.info('Pairing timeout reached. Making adapter non-discoverable.')
|
||||||
|
await adapter_props.call_set(ADAPTER_IFACE, 'Discoverable', Variant('b', False))
|
||||||
|
except asyncio.CancelledError:
|
||||||
|
logging.info('Pairing timer cancelled, likely by a new pair request.')
|
||||||
|
raise
|
||||||
|
except Exception as e:
|
||||||
|
logging.error(f"Failed to manage pairing state: {e}")
|
||||||
|
|
||||||
|
|
||||||
|
async def disconnect_connected_device():
|
||||||
|
logging.info("Attempting to disconnect any connected device.")
|
||||||
|
introspection = await bus.introspect(BLUEZ_SERVICE, '/')
|
||||||
|
manager_obj = bus.get_proxy_object(BLUEZ_SERVICE, '/', introspection)
|
||||||
|
manager_iface = manager_obj.get_interface('org.freedesktop.DBus.ObjectManager')
|
||||||
|
managed_objects = await manager_iface.call_get_managed_objects()
|
||||||
|
|
||||||
|
for path, ifaces in managed_objects.items():
|
||||||
|
if DEVICE_IFACE in ifaces:
|
||||||
|
device_props = ifaces[DEVICE_IFACE]
|
||||||
|
if device_props.get('Connected', Variant('b', False)).value:
|
||||||
|
logging.info(f"Found connected device: {path}. Disconnecting...")
|
||||||
|
try:
|
||||||
|
device_introspection = await bus.introspect(BLUEZ_SERVICE, path)
|
||||||
|
device_obj = bus.get_proxy_object(BLUEZ_SERVICE, path, device_introspection)
|
||||||
|
device_iface = device_obj.get_interface(DEVICE_IFACE)
|
||||||
|
await device_iface.call_disconnect()
|
||||||
|
logging.info(f"Successfully disconnected {path}")
|
||||||
|
return # Assume only one device is connected
|
||||||
|
except Exception as e:
|
||||||
|
logging.error(f"Failed to disconnect {path}: {e}")
|
||||||
|
logging.info("No connected device found to disconnect.")
|
||||||
|
|
||||||
|
|
||||||
|
async def send_media_command(command):
|
||||||
|
"""Finds a media player and sends a command to it."""
|
||||||
|
logging.info(f"Attempting to send media command: {command}")
|
||||||
|
introspection = await bus.introspect(BLUEZ_SERVICE, '/')
|
||||||
|
manager_obj = bus.get_proxy_object(BLUEZ_SERVICE, '/', introspection)
|
||||||
|
manager_iface = manager_obj.get_interface('org.freedesktop.DBus.ObjectManager')
|
||||||
|
managed_objects = await manager_iface.call_get_managed_objects()
|
||||||
|
|
||||||
|
for path, ifaces in managed_objects.items():
|
||||||
|
if MEDIA_PLAYER_IFACE in ifaces:
|
||||||
|
logging.info(f"Found media player: {path}. Sending command '{command}'...")
|
||||||
|
try:
|
||||||
|
player_introspection = await bus.introspect(BLUEZ_SERVICE, path)
|
||||||
|
player_obj = bus.get_proxy_object(BLUEZ_SERVICE, path, player_introspection)
|
||||||
|
player_iface = player_obj.get_interface(MEDIA_PLAYER_IFACE)
|
||||||
|
|
||||||
|
method_name = f'call_{command}'
|
||||||
|
dbus_method = getattr(player_iface, method_name)
|
||||||
|
await dbus_method()
|
||||||
|
|
||||||
|
logging.info(f"Successfully sent command '{command}' to {path}")
|
||||||
|
return # Assume only one media player is active
|
||||||
|
except Exception as e:
|
||||||
|
logging.error(f"Failed to send media command to {path}: {e}")
|
||||||
|
|
||||||
|
logging.warning("No active media player found to send command to.")
|
||||||
|
|
||||||
|
|
||||||
|
async def adjust_volume(direction, amount):
|
||||||
|
"""Adjusts the volume of the media transport."""
|
||||||
|
logging.info(f"Attempting to adjust volume {direction} by {amount}")
|
||||||
|
introspection = await bus.introspect(BLUEZ_SERVICE, '/')
|
||||||
|
manager_obj = bus.get_proxy_object(BLUEZ_SERVICE, '/', introspection)
|
||||||
|
manager_iface = manager_obj.get_interface('org.freedesktop.DBus.ObjectManager')
|
||||||
|
managed_objects = await manager_iface.call_get_managed_objects()
|
||||||
|
|
||||||
|
for path, ifaces in managed_objects.items():
|
||||||
|
if MEDIA_TRANSPORT_IFACE in ifaces:
|
||||||
|
logging.info(f"Found media transport: {path}. Adjusting volume...")
|
||||||
|
try:
|
||||||
|
transport_introspection = await bus.introspect(BLUEZ_SERVICE, path)
|
||||||
|
transport_obj = bus.get_proxy_object(BLUEZ_SERVICE, path, transport_introspection)
|
||||||
|
transport_props = transport_obj.get_interface('org.freedesktop.DBus.Properties')
|
||||||
|
|
||||||
|
current_volume_variant = await transport_props.call_get(MEDIA_TRANSPORT_IFACE, 'Volume')
|
||||||
|
current_volume = current_volume_variant.value
|
||||||
|
logging.info(f"Current volume is {current_volume}")
|
||||||
|
|
||||||
|
if direction == 'up':
|
||||||
|
new_volume = current_volume + amount
|
||||||
|
else: # direction == 'down'
|
||||||
|
new_volume = current_volume - amount
|
||||||
|
|
||||||
|
# The Volume on MediaTransport1 is a uint16, but AVRCP uses 0-127.
|
||||||
|
# We'll clamp to this range and hope BlueZ handles scaling.
|
||||||
|
new_volume = max(0, min(127, new_volume))
|
||||||
|
|
||||||
|
logging.info(f"Setting new volume to {new_volume}")
|
||||||
|
await transport_props.call_set(MEDIA_TRANSPORT_IFACE, 'Volume', Variant('q', new_volume))
|
||||||
|
|
||||||
|
logging.info(f"Successfully adjusted volume on {path}")
|
||||||
|
return
|
||||||
|
except Exception as e:
|
||||||
|
logging.error(f"Failed to adjust volume on {path}: {e}")
|
||||||
|
|
||||||
|
logging.warning("No active media transport found to adjust volume.")
|
||||||
|
|
||||||
|
|
||||||
async def process_bluetooth_command(topic, text):
|
async def process_bluetooth_command(topic, text):
|
||||||
|
global pairing_task
|
||||||
logging.info('Bluetooth command: %s', text)
|
logging.info('Bluetooth command: %s', text)
|
||||||
pass
|
if text == "pair":
|
||||||
|
if pairing_task and not pairing_task.done():
|
||||||
|
logging.info('A pairing process is already active. Cancelling it to restart the timer.')
|
||||||
|
pairing_task.cancel()
|
||||||
|
pairing_task = asyncio.create_task(enable_pairing())
|
||||||
|
elif text == "kick":
|
||||||
|
await disconnect_connected_device()
|
||||||
|
elif text in ["play", "pause", "next", "prev"]:
|
||||||
|
command = "previous" if text == "prev" else text
|
||||||
|
await send_media_command(command)
|
||||||
|
elif text.startswith("up ") or text.startswith("down "):
|
||||||
|
parts = text.split()
|
||||||
|
if len(parts) == 2 and parts[1].isdigit():
|
||||||
|
direction = parts[0]
|
||||||
|
amount = int(parts[1])
|
||||||
|
await adjust_volume(direction, amount)
|
||||||
|
else:
|
||||||
|
logging.warning(f"Invalid volume command format: {text}")
|
||||||
|
|
||||||
async def process_mqtt(message):
|
async def process_mqtt(message):
|
||||||
text = message.payload.decode()
|
text = message.payload.decode()
|
||||||
@@ -34,21 +312,48 @@ async def process_mqtt(message):
|
|||||||
async def fetch_mqtt():
|
async def fetch_mqtt():
|
||||||
await asyncio.sleep(3)
|
await asyncio.sleep(3)
|
||||||
|
|
||||||
async with Client(
|
while True:
|
||||||
hostname='10.55.0.106',
|
try:
|
||||||
port=1883,
|
async with Client(
|
||||||
) as client:
|
hostname='10.55.0.106',
|
||||||
await client.subscribe('#')
|
port=1883,
|
||||||
async for message in client.messages:
|
) as client:
|
||||||
loop = asyncio.get_event_loop()
|
logging.info("MQTT client connected")
|
||||||
loop.create_task(process_mqtt(message))
|
await client.subscribe('iot/12ser/#')
|
||||||
|
async for message in client.messages:
|
||||||
|
loop = asyncio.get_event_loop()
|
||||||
|
loop.create_task(process_mqtt(message))
|
||||||
|
except MqttError as e:
|
||||||
|
logging.warning(f"MQTT connection error: {e}. Reconnecting in 5 seconds...")
|
||||||
|
await asyncio.sleep(5)
|
||||||
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
def suppress_hfp_rejection_error(loop, context):
|
||||||
|
exception = context.get('exception')
|
||||||
|
if isinstance(exception, DBusError) and 'HFP profile not supported' in str(exception):
|
||||||
|
# This is the expected error from AuthorizeService, so we can suppress the traceback.
|
||||||
|
logging.info('Suppressed expected DBusError for HFP rejection exception.')
|
||||||
|
return
|
||||||
|
|
||||||
|
# For all other exceptions, fall back to the default handler.
|
||||||
|
loop.default_exception_handler(context)
|
||||||
|
|
||||||
|
|
||||||
|
async def main():
|
||||||
|
loop = asyncio.get_running_loop()
|
||||||
|
loop.set_exception_handler(suppress_hfp_rejection_error)
|
||||||
|
|
||||||
|
global bus
|
||||||
|
bus = await MessageBus(bus_type=BusType.SYSTEM).connect()
|
||||||
|
|
||||||
logging.info('')
|
logging.info('')
|
||||||
logging.info('==========================')
|
logging.info('==========================')
|
||||||
logging.info('Booting up...')
|
logging.info('Booting up...')
|
||||||
|
|
||||||
loop = asyncio.get_event_loop()
|
manage_task = asyncio.create_task(manage_bluetooth())
|
||||||
a = loop.create_task(manage_bluetooth())
|
mqtt_task = asyncio.create_task(fetch_mqtt())
|
||||||
loop.run_until_complete(fetch_mqtt())
|
await asyncio.gather(manage_task, mqtt_task)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
asyncio.run(main())
|
||||||
|
|||||||
31
requirements.txt
Normal file
31
requirements.txt
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
aiomqtt==2.4.0
|
||||||
|
certifi==2022.9.24
|
||||||
|
chardet==5.1.0
|
||||||
|
charset-normalizer==3.0.1
|
||||||
|
dbus-next==0.2.3
|
||||||
|
dbus-python==1.3.2
|
||||||
|
distlib==0.3.6
|
||||||
|
distro==1.8.0
|
||||||
|
distro-info==1.5
|
||||||
|
filelock==3.9.0
|
||||||
|
gyp==0.1
|
||||||
|
httplib2==0.20.4
|
||||||
|
idna==3.3
|
||||||
|
paho-mqtt==2.1.0
|
||||||
|
platformdirs==2.6.0
|
||||||
|
pycurl==7.45.2
|
||||||
|
pydbus==0.6.0
|
||||||
|
PyGObject==3.42.2
|
||||||
|
pyparsing==3.0.9
|
||||||
|
PySimpleSOAP==1.16.2
|
||||||
|
python-apt==2.6.0
|
||||||
|
python-debian==0.1.49
|
||||||
|
python-debianbts==4.0.1
|
||||||
|
reportbug==12.0.0
|
||||||
|
requests==2.28.1
|
||||||
|
six==1.16.0
|
||||||
|
supervisor==4.2.5
|
||||||
|
ufw==0.36.2
|
||||||
|
unattended-upgrades==0.1
|
||||||
|
urllib3==1.26.12
|
||||||
|
virtualenv==20.17.1+ds
|
||||||
137
scan.py
Normal file
137
scan.py
Normal file
@@ -0,0 +1,137 @@
|
|||||||
|
import os, sys
|
||||||
|
import logging
|
||||||
|
DEBUG = os.environ.get('DEBUG')
|
||||||
|
logging.basicConfig(stream=sys.stdout,
|
||||||
|
format='[%(asctime)s] %(levelname)s - %(message)s',
|
||||||
|
level=logging.DEBUG if DEBUG else logging.INFO)
|
||||||
|
|
||||||
|
import asyncio
|
||||||
|
from dbus_next.aio import MessageBus
|
||||||
|
from dbus_next.constants import BusType, MessageType
|
||||||
|
from dbus_next import Message
|
||||||
|
|
||||||
|
BLUEZ_SERVICE = 'org.bluez'
|
||||||
|
ADAPTER_IFACE = 'org.bluez.Adapter1'
|
||||||
|
DEVICE_IFACE = 'org.bluez.Device1'
|
||||||
|
|
||||||
|
bus = None
|
||||||
|
seen_devices = set()
|
||||||
|
name_cache = {}
|
||||||
|
|
||||||
|
async def get_adapter(bus):
|
||||||
|
"""Gets the first Bluetooth adapter found."""
|
||||||
|
introspection = await bus.introspect(BLUEZ_SERVICE, '/')
|
||||||
|
manager_obj = bus.get_proxy_object(BLUEZ_SERVICE, '/', introspection)
|
||||||
|
manager_iface = manager_obj.get_interface('org.freedesktop.DBus.ObjectManager')
|
||||||
|
managed_objects = await manager_iface.call_get_managed_objects()
|
||||||
|
|
||||||
|
for path, ifaces in managed_objects.items():
|
||||||
|
if ADAPTER_IFACE in ifaces:
|
||||||
|
adapter_introspection = await bus.introspect(BLUEZ_SERVICE, path)
|
||||||
|
return bus.get_proxy_object(BLUEZ_SERVICE, path, adapter_introspection)
|
||||||
|
return None
|
||||||
|
|
||||||
|
def on_interfaces_added(path, interfaces):
|
||||||
|
"""Callback for when a new D-Bus interface is added."""
|
||||||
|
if DEVICE_IFACE in interfaces:
|
||||||
|
device_properties = interfaces[DEVICE_IFACE]
|
||||||
|
address_variant = device_properties.get('Address')
|
||||||
|
if not address_variant:
|
||||||
|
return
|
||||||
|
|
||||||
|
addr_str = address_variant.value
|
||||||
|
alias_variant = device_properties.get('Alias')
|
||||||
|
alias = alias_variant.value if alias_variant else name_cache.get(addr_str)
|
||||||
|
|
||||||
|
if alias:
|
||||||
|
# Update cache if we found a new alias
|
||||||
|
if alias_variant and alias_variant.value:
|
||||||
|
name_cache[addr_str] = alias_variant.value
|
||||||
|
|
||||||
|
if addr_str not in seen_devices:
|
||||||
|
seen_devices.add(addr_str)
|
||||||
|
logging.info(f"Found: {alias} ({addr_str}) (new)")
|
||||||
|
else:
|
||||||
|
# Log repeat discoveries only if they have a name
|
||||||
|
logging.info(f"Found: {alias} ({addr_str})")
|
||||||
|
|
||||||
|
def properties_changed_handler(message: Message):
|
||||||
|
"""Sync handler to dispatch async task for property changes."""
|
||||||
|
if message.message_type == MessageType.SIGNAL and \
|
||||||
|
message.member == 'PropertiesChanged' and \
|
||||||
|
message.interface == 'org.freedesktop.DBus.Properties':
|
||||||
|
# Further filtering is done in the async handler
|
||||||
|
asyncio.create_task(on_properties_changed(message))
|
||||||
|
|
||||||
|
async def on_properties_changed(message: Message):
|
||||||
|
"""Callback for when a device's properties change (e.g., Alias appears)."""
|
||||||
|
if not message.body or message.body[0] != DEVICE_IFACE:
|
||||||
|
return
|
||||||
|
|
||||||
|
changed_properties = message.body[1]
|
||||||
|
alias_variant = changed_properties.get('Alias')
|
||||||
|
if not alias_variant or not alias_variant.value:
|
||||||
|
return
|
||||||
|
|
||||||
|
alias = alias_variant.value
|
||||||
|
device_path = message.path
|
||||||
|
|
||||||
|
try:
|
||||||
|
introspection = await bus.introspect(BLUEZ_SERVICE, device_path)
|
||||||
|
device_obj = bus.get_proxy_object(BLUEZ_SERVICE, device_path, introspection)
|
||||||
|
device_props_iface = device_obj.get_interface('org.freedesktop.DBus.Properties')
|
||||||
|
|
||||||
|
address_variant = await device_props_iface.call_get(DEVICE_IFACE, 'Address')
|
||||||
|
addr_str = address_variant.value
|
||||||
|
|
||||||
|
# Update cache
|
||||||
|
name_cache[addr_str] = alias
|
||||||
|
|
||||||
|
if addr_str not in seen_devices:
|
||||||
|
seen_devices.add(addr_str)
|
||||||
|
logging.info(f"Found: {alias} ({addr_str}) (new)")
|
||||||
|
except Exception as e:
|
||||||
|
logging.debug(f"Could not process property change for {device_path}: {e}")
|
||||||
|
|
||||||
|
async def main():
|
||||||
|
"""Main function to run the scanner."""
|
||||||
|
global bus
|
||||||
|
bus = await MessageBus(bus_type=BusType.SYSTEM).connect()
|
||||||
|
adapter = await get_adapter(bus)
|
||||||
|
if not adapter:
|
||||||
|
logging.error("Bluetooth adapter not found.")
|
||||||
|
return
|
||||||
|
|
||||||
|
adapter_iface = adapter.get_interface(ADAPTER_IFACE)
|
||||||
|
|
||||||
|
# Subscribe to InterfacesAdded signal to discover new devices
|
||||||
|
introspection = await bus.introspect(BLUEZ_SERVICE, '/')
|
||||||
|
obj_manager = bus.get_proxy_object(BLUEZ_SERVICE, '/', introspection)
|
||||||
|
obj_manager_iface = obj_manager.get_interface('org.freedesktop.DBus.ObjectManager')
|
||||||
|
obj_manager_iface.on_interfaces_added(on_interfaces_added)
|
||||||
|
|
||||||
|
# Subscribe to PropertiesChanged signal to catch late-arriving device names
|
||||||
|
rule = "type='signal',interface='org.freedesktop.DBus.Properties',member='PropertiesChanged',path_namespace='/org/bluez'"
|
||||||
|
introspection = await bus.introspect('org.freedesktop.DBus', '/org/freedesktop/DBus')
|
||||||
|
proxy_obj = bus.get_proxy_object('org.freedesktop.DBus', '/org/freedesktop/DBus', introspection)
|
||||||
|
dbus_interface = proxy_obj.get_interface('org.freedesktop.DBus')
|
||||||
|
await dbus_interface.call_add_match(rule)
|
||||||
|
bus.add_message_handler(properties_changed_handler)
|
||||||
|
|
||||||
|
logging.info("Starting Bluetooth scan... Press Ctrl+C to stop.")
|
||||||
|
try:
|
||||||
|
await adapter_iface.call_start_discovery()
|
||||||
|
# Keep the script running to listen for signals
|
||||||
|
while True:
|
||||||
|
await asyncio.sleep(3600)
|
||||||
|
except Exception as e:
|
||||||
|
logging.error(f"An error occurred during scanning: {e}")
|
||||||
|
finally:
|
||||||
|
logging.info("Stopping Bluetooth scan.")
|
||||||
|
await adapter_iface.call_stop_discovery()
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
try:
|
||||||
|
asyncio.run(main())
|
||||||
|
except KeyboardInterrupt:
|
||||||
|
logging.info("Scan stopped by user.")
|
||||||
Reference in New Issue
Block a user