import os, sys import logging DEBUG = os.environ.get('DEBUG') logging.basicConfig(stream=sys.stdout, format='[%(asctime)s] %(levelname)s %(module)s/%(funcName)s - %(message)s', level=logging.DEBUG if DEBUG else logging.INFO) import time import json import asyncio from aiomqtt import Client 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 bus = None agent_instance = None # --- Bluetooth constants and agent --- BLUEZ_SERVICE = 'org.bluez' ADAPTER_IFACE = 'org.bluez.Adapter1' DEVICE_IFACE = 'org.bluez.Device1' AGENT_IFACE = 'org.bluez.Agent1' AGENT_MANAGER_IFACE = 'org.bluez.AgentManager1' AGENT_PATH = '/io/bluetooth_speaker/agent' CAPABILITY = 'NoInputNoOutput' 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_and_connect_device(device)) @method() async def RequestAuthorization(self, device: 'o'): logging.info(f"RequestAuthorization for {device}") # Automatically authorize and trust asyncio.create_task(trust_and_connect_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_and_connect_device(device_path): logging.info(f'Trusting and connecting to {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}') device_iface = device_obj.get_interface(DEVICE_IFACE) try: logging.info(f"Attempting to disconnect {device_path} before connecting.") await device_iface.call_disconnect() await asyncio.sleep(1) # Give it a moment to settle except DBusError as e: # This is expected if the device is not already connected. logging.info(f"Disconnect failed (this is likely okay): {e}") await device_iface.call_connect() logging.info(f'Connected to device {device_path}') except Exception as e: logging.error(f'Failed to trust/connect to {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}") async def monitor_unpairing(): logging.info("Starting to monitor for device unpairing events.") adapter_obj = await get_adapter() if not adapter_obj: logging.error("Bluetooth adapter not found, cannot monitor unpairing.") return adapter_iface = adapter_obj.get_interface(ADAPTER_IFACE) def _properties_changed_handler(interface_name, changed_properties, invalidated_properties, msg): if interface_name == DEVICE_IFACE and 'Paired' in changed_properties and not changed_properties['Paired'].value: device_path = msg.path logging.info(f"Device {device_path} was unpaired by the remote host. Removing it.") asyncio.create_task(adapter_iface.call_remove_device(device_path)) bus.add_signal_receiver( _properties_changed_handler, signal_name='PropertiesChanged', dbus_interface='org.freedesktop.DBus.Properties', path_namespace='/org/bluez' ) # Keep the task alive to listen for signals while True: await asyncio.sleep(3600) # --- End 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: await asyncio.sleep(3600) async def process_bluetooth_command(topic, text): logging.info('Bluetooth command: %s', text) if text == "pair": logging.info('Starting pairing process by making adapter discoverable') 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) await adapter_props.call_set(ADAPTER_IFACE, 'Discoverable', Variant('b', False)) logging.info('Adapter is no longer discoverable') except Exception as e: logging.error(f"Failed to set adapter properties: {e}") async def process_mqtt(message): text = message.payload.decode() topic = message.topic.value logging.debug('MQTT topic: %s, message: %s', topic, text) if topic.startswith('iot/12ser/bluetooth'): await process_bluetooth_command(topic, text) else: logging.debug('Invalid topic, returning') return async def fetch_mqtt(): await asyncio.sleep(3) async with Client( hostname='10.55.0.106', port=1883, ) as client: await client.subscribe('iot/12ser/#') async for message in client.messages: loop = asyncio.get_event_loop() loop.create_task(process_mqtt(message)) async def main(): global bus bus = await MessageBus(bus_type=BusType.SYSTEM).connect() logging.info('') logging.info('==========================') logging.info('Booting up...') manage_task = asyncio.create_task(manage_bluetooth()) mqtt_task = asyncio.create_task(fetch_mqtt()) unpair_task = asyncio.create_task(monitor_unpairing()) await asyncio.gather(manage_task, mqtt_task, unpair_task) if __name__ == '__main__': asyncio.run(main())