async def agent_request_input(bus: BaseMessageBus, impl: ConnmanAgent, service, fields): introspection = await bus.introspect("net.connman", service) proxy = bus.get_proxy_object("net.connman", service, introspection) iface = proxy.get_interface("net.connman.Service") props = await iface.call_get_properties() svc = model.create_service_from_props(service, props) plain_fields = unpack_variants(fields, "a{sv}") try: res = await impl.request_input(svc, plain_fields) except Canceled as e: raise DBusError("net.connman.Agent.Error.Canceled", str(e)) varres = {k: Variant("s", v) for k, v in res.items() if v is not None} return varres
async def send( self, element_path: str, destination: int, key_index: int, data: bytes, force_segmented: bool = False, ) -> None: await self._interface.call_send( element_path, destination, key_index, dict(ForceSegmented=Variant("b", force_segmented)), data, flags=MessageFlag.NO_REPLY_EXPECTED, )
async def request_input( self, service: "o", fields: "a{sv}", # type: ignore ) -> "a{sv}": # type: ignore """This method gets called when trying to connect to a service and some extra input is required. For example a passphrase or the name of a hidden network. The return value should be a dictionary where the keys are the field names and the values are the actual fields. Alternatively an error indicating that the request got canceled can be returned. OperationAborted will be return on a successful cancel request. Most common return field names are "Name" and of course "Passphrase". The dictionary arguments contains field names with their input parameters. In case of WISPr credentials requests and if the user prefers to login through the browser by himself, agent will have to return a LaunchBrowser error (see below). Possible Errors: net.connman.Agent.Error.Canceled net.connman.Agent.Error.LaunchBrowser """ # Fetch the service properties introspection = await self._bus.introspect("net.connman", service) proxy = self._bus.get_proxy_object("net.connman", service, introspection) iface = proxy.get_interface("net.connman.Service") props = await iface.call_get_properties() svc = model.create_service_from_props(service, props) plain_fields = unpack_variants(fields, "a{sv}") print("REQUEST INPUT", svc, plain_fields) try: res = await self._impl.request_input(svc, plain_fields) except Canceled as e: raise DBusError("net.connman.Agent.Error.Canceled", str(e)) varres = {k: Variant("s", v) for k, v in res.items()} return varres
async def test_services_changed(self) -> None: # GIVEN mgr = ConnmanManagerImpl(self.bus) svc1_iface = NetConnmanServiceStub() self.bus.stub_register_interface("/svc1", "net.connman.Service", svc1_iface) await mgr.setup() self.net_connman_manager.stub_update_services( [( "/svc1", self.sample_service_props, )], [], ) await asyncio.sleep(0) # WHEN svcs = mgr.list_services() # THEN assert len(svcs) == 1 svc1 = svcs[0] assert svc1.path == "/svc1" assert svc1.name == "Skynet" assert svc1.state == ConnmanServiceState.IDLE # Manager should be subscribed to service updates now signals = svc1_iface.stub_get_signals() assert signals.keys() == {"property_changed"} # WHEN self.net_connman_manager.stub_update_services( [("/svc1", { "State": Variant("s", "online") })], [], ) # THEN svcs = mgr.list_services() assert len(svcs) == 1 svc1 = svcs[0] assert svc1.path == "/svc1" assert svc1.name == "Skynet" assert svc1.state == ConnmanServiceState.ONLINE
async def test_scan_all(self, ovshell: testing.OpenVarioShellStub) -> None: # GIVEN bus = ovshell.os.stub_connect_bus() net_connman_manager = NetConnmanManagerStub() net_connman_manager.stub_set_technologies([ ( "/eth", { "Name": Variant("s", "Ethernet"), "Type": Variant("s", "ethernet"), "Connected": Variant("b", False), "Powered": Variant("b", False), }, ), ( "/wifi", { "Name": Variant("s", "Wifi"), "Type": Variant("s", "wifi"), "Connected": Variant("b", False), "Powered": Variant("b", False), }, ), ]) net_connman_tech = NetConnmanTechnologyStub() bus.stub_register_interface("/", "net.connman.Manager", net_connman_manager) bus.stub_register_interface("/wifi", "net.connman.Technology", net_connman_tech) mgr = ConnmanManagerImpl(await ovshell.os.get_system_bus()) await mgr.setup() # WHEN assert len(mgr.technologies) > 0 scanned = await mgr.scan_all() # THEN assert scanned == 1 # only wifi is scanned assert net_connman_tech.scan_called == 1
async def test_get_state(self) -> None: # GIVEN mgr = ConnmanManagerImpl(self.bus) await mgr.setup() await asyncio.sleep(0) # WHEN state = mgr.get_state() # THEN assert state == ConnmanState.UNKNOWN # WHEN self.net_connman_manager.stub_set_properties( {"State": Variant("s", "online")}) state = mgr.get_state() # THEN assert state == ConnmanState.ONLINE
async def test_property_changed_signal(interface_class): bus1 = await MessageBus().connect() bus2 = await MessageBus().connect() await bus2.call( Message(destination='org.freedesktop.DBus', path='/org/freedesktop/DBus', interface='org.freedesktop.DBus', member='AddMatch', signature='s', body=[f'sender={bus1.unique_name}'])) interface = interface_class('test.interface') export_path = '/test/path' bus1.export(export_path, interface) async def wait_for_message(): # TODO timeout future = asyncio.get_event_loop().create_future() def message_handler(signal): if signal.interface == 'org.freedesktop.DBus.Properties': bus2.remove_message_handler(message_handler) future.set_result(signal) bus2.add_message_handler(message_handler) return await future bus2.send( Message(destination=bus1.unique_name, interface=interface.name, path=export_path, member='do_emit_properties_changed')) signal = await wait_for_message() assert signal.interface == 'org.freedesktop.DBus.Properties' assert signal.member == 'PropertiesChanged' assert signal.signature == 'sa{sv}as' assert signal.body == [ interface.name, { 'string_prop': Variant('s', 'asdf') }, ['container_prop'] ]
async def test_emoji(bus_address): [mpris] = await setup_mpris('emoji-format-test', bus_address=bus_address) mpris.metadata = {'mpris:length': Variant('x', 100000)} await mpris.ping() playerctl = PlayerctlCli(bus_address) status_emoji_cmd = 'metadata --format \'{{emoji(status)}}\'' mpris.playback_status = 'Playing' cmd = await playerctl.run(status_emoji_cmd) assert cmd.stdout == '▶️', cmd.stderr mpris.playback_status = 'Paused' cmd = await playerctl.run(status_emoji_cmd) assert cmd.stdout == '⏸️', cmd.stderr mpris.playback_status = 'Stopped' cmd = await playerctl.run(status_emoji_cmd) assert cmd.stdout == '⏹️', cmd.stderr volume_emoji_cmd = 'metadata --format \'{{emoji(volume)}}\'' mpris.volume = 0.0 cmd = await playerctl.run(volume_emoji_cmd) assert cmd.stdout == '🔈', cmd.stderr mpris.volume = 0.5 cmd = await playerctl.run(volume_emoji_cmd) assert cmd.stdout == '🔉', cmd.stderr mpris.volume = 1.0 cmd = await playerctl.run(volume_emoji_cmd) assert cmd.stdout == '🔊', cmd.stderr cmd = await playerctl.run('metadata --format \'{{emoji("hi")}}\'') assert cmd.returncode == 1, cmd.stderr cmd = await playerctl.run('metadata --format \'{{emoji(status, volume)}}\'' ) assert cmd.returncode == 1, cmd.stderr await mpris.disconnect()
async def test_tech_power(self) -> None: # GIVEN self.net_connman_manager.stub_set_technologies([ ("/eth", self.sample_tech_props) ]) net_connman_tech = NetConnmanTechnologyStub() self.bus.stub_register_interface("/eth", "net.connman.Technology", net_connman_tech) mgr = ConnmanManagerImpl(self.bus) await mgr.setup() await asyncio.sleep(0) # WHEN techs = mgr.technologies assert len(techs) == 1 tech_eth = techs[0] await mgr.power(tech_eth, on=True) # THEN assert net_connman_tech.props_updated == [("Powered", Variant("b", True))]
def setup(self, ovshell: testing.OpenVarioShellStub) -> None: self.bus = ovshell.os.stub_connect_bus() self.net_connman_manager = NetConnmanManagerStub() self.bus.stub_register_interface("/", "net.connman.Manager", self.net_connman_manager) self.agent = ConnmanAgentStub() self.agentiface = ConnmanAgentInterface(self.agent, self.bus) self.svc_iface = NetConnmanServiceStub() self.bus.stub_register_interface("/svc1", "net.connman.Service", self.svc_iface) self.sample_service_props = { "AutoConnect": Variant("b", False), "Favorite": Variant("b", False), "Name": Variant("s", "Skynet"), "Security": Variant("s", "wpa"), "Strength": Variant("i", 78), "Type": Variant("s", "wifi"), "State": Variant("s", "idle"), }
async def test_services_changed_incomplete_data_ignore(self) -> None: # GIVEN mgr = ConnmanManagerImpl(self.bus) svc1_iface = NetConnmanServiceStub() self.bus.stub_register_interface("/svc1", "net.connman.Service", svc1_iface) await mgr.setup() incomplete_props = { "Type": Variant("s", "Skynet"), } # WHEN self.net_connman_manager.stub_update_services( [( "/svc1", incomplete_props, )], [], ) await asyncio.sleep(0) svcs = mgr.list_services() # THEN assert len(svcs) == 0
async def unprovisioned_scan(self, **kwargs) -> None: options = dict(Seconds=Variant("q", kwargs.get("seconds", 0))) await self._interface.call_unprovisioned_scan(options)
def test_unpack_variants_explicit() -> None: inp = {"one": Variant("i", 1)} assert unpack_variants(inp, "a{sv}") == {"one": 1}
async def test_format(bus_address): title = 'A Title' artist = 'An Artist' album = 'An Album' player_name = 'format-test' player_instance = f'{player_name}.instance123' [mpris] = await setup_mpris(player_instance, bus_address=bus_address) mpris.metadata = { 'xesam:title': Variant('s', title), 'xesam:artist': Variant('as', [artist]), 'xesam:escapeme': Variant('s', '<hi>'), 'xesam:album': Variant('s', album), 'mpris:length': Variant('x', 100000) } mpris.volume = 2.0 playerctl = PlayerctlCli(bus_address) test = MetadataTest(playerctl) test.add('{{artist}} - {{title}}', f'{artist} - {title}') test.add("{{markup_escape(xesam:escapeme)}}", "<hi>") test.add("{{lc(artist)}}", artist.lower()) test.add("{{uc(title)}}", title.upper()) test.add("{{uc(lc(title))}}", title.upper()) test.add('{{uc("Hi")}}', "HI") test.add("{{mpris:length}}", "100000") test.add( '@{{ uc( "hi" ) }} - {{uc( lc( "HO" ) ) }} . {{lc( uc( title ) ) }}@', f'@HI - HO . {title.lower()}@') test.add("{{default(xesam:missing, artist)}}", artist) test.add("{{default(title, artist)}}", title) test.add('{{default("", "ok")}}', 'ok') test.add('{{default("ok", "not")}}', 'ok') test.add(' {{lc(album)}} ', album.lower()) test.add('{{playerName}} - {{playerInstance}}', f'{player_name} - {player_instance}') await test.run() # numbers math = [ '10', '-10 + 20', '10 + 10', '10 * 10', '10 / 10', '10 + 10 * 10 + 10', '10 + 10 * -10 + 10', '10 + 10 * -10 + -10', '-10 * 10 + 10', '-10 * -10 * -1 + -10', '-10 * 10 + -10 * -10 + 20 / 10 * -20 + -10', '8+-+--++-4', '2 - 10 * 1 + 1', '2 / -2 + 2 * 2 * -2 - 2 - 2 * -2', '2 * (2 + 2)', '10 * (10 + 12) - 4', '-(10)', '-(10 + 12 * -2)', '14 - (10 * 2 + 5) * -6', '(14 - 2 * 3) * (14 * -2 - 6) + -(4 - 2) * 5', ] # variables math += [ 'volume', 'volume + 10', '-volume', '-volume * -1', '-volume + volume', 'volume * volume', 'volume * -volume', 'volume + volume * -volume * volume + -volume', 'volume / -volume + volume * volume * -volume - volume - volume * -volume', '-(volume + 3) * 5 * (volume + 2)', ] # functions math += [ 'default(5+5, None)', '-default(5 + 5, None)', '(-default(5 - 5, None) + 2) * 8', '2 + (5 * 4 + 3 * -default(5, default(6 * (3 + 4 * (6 + 2)) / 2, None)) + -56)', ] def default_shim(arg1, arg2): if arg1 is None: return arg2 return arg1 async def math_test(math): cmd = await playerctl.run("metadata --format '{{" + math + "}}'") assert cmd.returncode == 0, cmd.stderr assert float(cmd.stdout) == eval(math, { 'volume': mpris.volume, 'default': default_shim }), math await asyncio.gather(*[math_test(m) for m in math]) await mpris.disconnect()
def setup(self, ovshell: testing.OpenVarioShellStub) -> None: self.ovshell = ovshell self.bus = ovshell.os.stub_connect_bus() self.net_connman_manager = NetConnmanManagerStub() self.bus.stub_register_interface("/", "net.connman.Manager", self.net_connman_manager) self.sample_service_props = { "AutoConnect": Variant("b", False), "Favorite": Variant("b", False), "Name": Variant("s", "Skynet"), "Security": Variant("s", "wpa"), "Strength": Variant("i", 78), "Type": Variant("s", "wifi"), "State": Variant("s", "idle"), } self.sample_tech_props = { "Name": Variant("s", "Ethernet"), "Type": Variant("s", "ethernet"), "Connected": Variant("b", False), "Powered": Variant("b", True), }
async def test_format(bus_address): [mpris] = await setup_mpris('format-test', bus_address=bus_address) TITLE = 'A Title' ARTIST = 'An Artist' ALBUM = 'An Album' mpris.metadata = { 'xesam:title': Variant('s', TITLE), 'xesam:artist': Variant('as', [ARTIST]), 'xesam:escapeme': Variant('s', '<hi>'), 'xesam:album': Variant('s', ALBUM), 'mpris:length': Variant('x', 100000) } await mpris.ping() playerctl = PlayerctlCli(bus_address) cmd = await playerctl.run('metadata --format "{{artist}} - {{title}}"') assert cmd.stdout == f'{ARTIST} - {TITLE}', cmd.stderr cmd = await playerctl.run( 'metadata --format "{{markup_escape(xesam:escapeme)}}"') assert cmd.stdout == '<hi>', cmd.stderr cmd = await playerctl.run('metadata --format "{{lc(artist)}}"') assert cmd.stdout == ARTIST.lower(), cmd.stderr cmd = await playerctl.run('metadata --format "{{uc(title)}}"') assert cmd.stdout == TITLE.upper(), cmd.stderr cmd = await playerctl.run('metadata --format "{{uc(lc(title))}}"') assert cmd.stdout == TITLE.upper(), cmd.stderr cmd = await playerctl.run('metadata --format \'{{uc("Hi")}}\'') assert cmd.stdout == "HI", cmd.stderr cmd = await playerctl.run('metadata --format "{{mpris:length}}"') assert cmd.stdout == "100000", cmd.stderr cmd = await playerctl.run( 'metadata --format \'@{{ uc( "hi" ) }} - {{uc( lc( "HO" ) ) }} . {{lc( uc( title ) ) }}@\'' ) assert cmd.stdout == f'@HI - HO . {TITLE.lower()}@', cmd.stderr cmd = await playerctl.run( 'metadata --format \'{{default(xesam:missing, artist)}}\'') assert cmd.stdout == ARTIST, cmd.stderr cmd = await playerctl.run( 'metadata --format \'{{default(title, artist)}}\'') assert cmd.stdout == TITLE, cmd.stderr cmd = await playerctl.run('metadata --format \'{{default("", "ok")}}\'') assert cmd.stdout == 'ok', cmd.stderr cmd = await playerctl.run('metadata --format \'{{default("ok", "not")}}\'') assert cmd.stdout == 'ok', cmd.stderr status_emoji_cmd = 'metadata --format \'{{emoji(status)}}\'' mpris.playback_status = 'Playing' cmd = await playerctl.run(status_emoji_cmd) assert cmd.stdout == '▶️', cmd.stderr mpris.playback_status = 'Paused' cmd = await playerctl.run(status_emoji_cmd) assert cmd.stdout == '⏸️', cmd.stderr mpris.playback_status = 'Stopped' cmd = await playerctl.run(status_emoji_cmd) assert cmd.stdout == '⏹️', cmd.stderr volume_emoji_cmd = 'metadata --format \'{{emoji(volume)}}\'' mpris.volume = 0.0 cmd = await playerctl.run(volume_emoji_cmd) assert cmd.stdout == '🔈', cmd.stderr mpris.volume = 0.5 cmd = await playerctl.run(volume_emoji_cmd) assert cmd.stdout == '🔉', cmd.stderr mpris.volume = 1.0 cmd = await playerctl.run(volume_emoji_cmd) assert cmd.stdout == '🔊', cmd.stderr cmd = await playerctl.run('metadata --format \'{{emoji("hi")}}\'') assert cmd.returncode == 1, cmd.stderr cmd = await playerctl.run('metadata --format \'{{emoji(status, volume)}}\'' ) assert cmd.returncode == 1, cmd.stderr cmd = await playerctl.run('metadata --format " {{lc(album)}} "') assert cmd.stdout == ALBUM.lower() mpris.disconnect()
async def connect(pi): from dbus_next import Message, MessageType, BusType, Variant from dbus_next.aio import MessageBus from dbus_next.errors import DBusError pi["characteristics"] = None bus = await MessageBus(bus_type=BusType.SYSTEM, bus_address=pi["dbus_address"]).connect() om_introspection = await bus.introspect(BLUEZ_SERVICE_NAME, '/') om = bus.get_proxy_object(BLUEZ_SERVICE_NAME, '/', om_introspection).get_interface(DBUS_OM_IFACE) om_objects = await om.call_get_managed_objects() for path, interfaces in om_objects.items(): if BLUEZ_ADAPTER_IFACE in interfaces.keys(): _LOGGER.debug("Discovered bluetooth adapter %s" % (path)) adapter_introspection = await bus.introspect( BLUEZ_SERVICE_NAME, path) adapter = bus.get_proxy_object( BLUEZ_SERVICE_NAME, path, adapter_introspection).get_interface(BLUEZ_ADAPTER_IFACE) break if not adapter: _LOGGER.error("No bluetooth adapter localized") return for path, interfaces in om_objects.items(): if BLUEZ_DEVICE_IFACE in interfaces.keys(): device_introspection = await bus.introspect( BLUEZ_SERVICE_NAME, path) dev = bus.get_proxy_object( BLUEZ_SERVICE_NAME, path, device_introspection).get_interface(BLUEZ_DEVICE_IFACE) connected = await dev.get_connected() if connected: _LOGGER.debug("Disconnecting %s" % (path)) await dev.call_disconnect() await adapter.call_remove_device(path) plejds = [] @callback def on_interfaces_added(path, interfaces): if BLUEZ_DEVICE_IFACE in interfaces: if PLEJD_SVC_UUID in interfaces[BLUEZ_DEVICE_IFACE]['UUIDs'].value: plejds.append({'path': path}) om.on_interfaces_added(on_interfaces_added) scan_filter = { "UUIDs": Variant('as', [PLEJD_SVC_UUID]), "Transport": Variant('s', "le"), } await adapter.call_set_discovery_filter(scan_filter) await adapter.call_start_discovery() await asyncio.sleep(pi["discovery_timeout"]) for plejd in plejds: device_introspection = await bus.introspect(BLUEZ_SERVICE_NAME, plejd['path']) dev = bus.get_proxy_object( BLUEZ_SERVICE_NAME, plejd['path'], device_introspection).get_interface(BLUEZ_DEVICE_IFACE) plejd['RSSI'] = await dev.get_rssi() plejd['obj'] = dev _LOGGER.debug("Discovered plejd %s with RSSI %d" % (plejd['path'], plejd['RSSI'])) if len(plejds) == 0: _LOGGER.warning("No plejd devices found") return plejds.sort(key=lambda a: a['RSSI'], reverse=True) for plejd in plejds: try: _LOGGER.debug("Connecting to %s" % (plejd["path"])) await plejd['obj'].call_connect() break except DBusError as e: _LOGGER.warning("Error connecting to plejd: %s" % (str(e))) await asyncio.sleep(pi["discovery_timeout"]) objects = await om.call_get_managed_objects() chrcs = [] for path, interfaces in objects.items(): if GATT_CHRC_IFACE not in interfaces.keys(): continue chrcs.append(path) async def process_plejd_service(service_path, chrc_paths, bus): service_introspection = await bus.introspect(BLUEZ_SERVICE_NAME, service_path) service = bus.get_proxy_object( BLUEZ_SERVICE_NAME, service_path, service_introspection).get_interface(GATT_SERVICE_IFACE) uuid = await service.get_uuid() if uuid != PLEJD_SVC_UUID: return None dev = await service.get_device() x = re.search('dev_([0-9A-F_]+)$', dev) addr = binascii.a2b_hex(x.group(1).replace("_", ""))[::-1] chars = {} # Process the characteristics. for chrc_path in chrc_paths: chrc_introspection = await bus.introspect(BLUEZ_SERVICE_NAME, chrc_path) chrc_obj = bus.get_proxy_object(BLUEZ_SERVICE_NAME, chrc_path, chrc_introspection) chrc = chrc_obj.get_interface(GATT_CHRC_IFACE) chrc_prop = chrc_obj.get_interface(DBUS_PROP_IFACE) uuid = await chrc.get_uuid() if uuid == PLEJD_DATA_UUID: chars["data"] = chrc elif uuid == PLEJD_LAST_DATA_UUID: chars["last_data"] = chrc chars["last_data_prop"] = chrc_prop elif uuid == PLEJD_AUTH_UUID: chars["auth"] = chrc elif uuid == PLEJD_PING_UUID: chars["ping"] = chrc return (addr, chars) plejd_service = None for path, interfaces in objects.items(): if GATT_SERVICE_IFACE not in interfaces.keys(): continue chrc_paths = [d for d in chrcs if d.startswith(path + "/")] plejd_service = await process_plejd_service(path, chrc_paths, bus) if plejd_service: break if not plejd_service: _LOGGER.warning("Failed connecting to plejd service") return if await plejd_auth(pi["key"], plejd_service[1]["auth"]) == False: return pi["address"] = plejd_service[0] pi["characteristics"] = plejd_service[1] @callback def handle_notification_cb(iface, changed_props, invalidated_props): if iface != GATT_CHRC_IFACE: return if not len(changed_props): return value = changed_props.get('Value', None) if not value: return dec = plejd_enc_dec(pi["key"], pi["address"], value.value) # check if this is a device we care about if dec[0] in PLEJD_DEVICES: device = PLEJD_DEVICES[dec[0]] elif dec[0] == 0x01 and dec[3:5] == b'\x00\x1b': time = struct.unpack_from('<I', dec, 5)[0] _LOGGER.debug("Plejd network reports time as '%s'", datetime.fromtimestamp(time)) return else: _LOGGER.debug("No match for device '%02x' (%s)" % (dec[0], binascii.b2a_hex(dec))) return dim = None state = None if dec[3:5] == b'\x00\xc8' or dec[3:5] == b'\x00\x98': # 00c8 and 0098 both mean state+dim state = dec[5] dim = int.from_bytes(dec[6:8], 'little') elif dec[3:5] == b'\x00\x97': # 0097 is state only state = dec[5] else: _LOGGER.debug("No match for command '%s' (%s)" % (binascii.b2a_hex(dec[3:5]), binascii.b2a_hex(dec))) return if (state == 0): state = False else: state = True device.update_state(state, dim) await adapter.call_stop_discovery() pi["characteristics"]["last_data_prop"].on_properties_changed( handle_notification_cb) await pi["characteristics"]["last_data"].call_start_notify() return
async def test_methods(interface_class): bus1 = await MessageBus().connect() bus2 = await MessageBus().connect() interface = interface_class('test.interface') export_path = '/test/path' async def call(member, signature='', body=[], flags=MessageFlag.NONE): return await bus2.call( Message(destination=bus1.unique_name, path=export_path, interface=interface.name, member=member, signature=signature, body=body, flags=flags)) bus1.export(export_path, interface) body = ['hello world'] reply = await call('echo', 's', body) assert reply.message_type == MessageType.METHOD_RETURN, reply.body[0] assert reply.signature == 's' assert reply.body == body body = ['hello', 'world'] reply = await call('echo_multiple', 'ss', body) assert reply.message_type == MessageType.METHOD_RETURN, reply.body[0] assert reply.signature == 'ss' assert reply.body == body body = [['hello', 'world'], Variant('v', Variant('(ss)', ['hello', 'world'])), { 'foo': Variant('t', 100) }, ['one', ['two', [Variant('s', 'three')]]]] signature = 'asva{sv}(s(s(v)))' SignatureTree(signature).verify(body) reply = await call('echo_containers', signature, body) assert reply.message_type == MessageType.METHOD_RETURN, reply.body[0] assert reply.signature == signature assert reply.body == body reply = await call('ping') assert reply.message_type == MessageType.METHOD_RETURN, reply.body[0] assert reply.signature == '' assert reply.body == [] reply = await call('throws_unexpected_error') assert reply.message_type == MessageType.ERROR, reply.body[0] assert reply.error_name == ErrorType.SERVICE_ERROR.value, reply.body[0] reply = await call('throws_dbus_error') assert reply.message_type == MessageType.ERROR, reply.body[0] assert reply.error_name == 'test.error', reply.body[0] assert reply.body == ['an error ocurred'] reply = await call('ping', flags=MessageFlag.NO_REPLY_EXPECTED) assert reply is None reply = await call('throws_unexpected_error', flags=MessageFlag.NO_REPLY_EXPECTED) assert reply is None reply = await call('throws_dbus_error', flags=MessageFlag.NO_REPLY_EXPECTED) assert reply is None
def GetVariantDict(self) -> 'a{sv}': return { 'foo': Variant('s', 'bar'), 'bat': Variant('x', -55), 'a_list': Variant('as', ['hello', 'world']) }
class DBusDesktopNotifier(DesktopNotifierBase): """DBus notification backend for Linux This implements the org.freedesktop.Notifications standard. The DBUS connection is created in a thread with a running asyncio loop to handle clicked notifications. """ _to_native_urgency = { NotificationLevel.Low: Variant("y", 0), NotificationLevel.Normal: Variant("y", 1), NotificationLevel.Critical: Variant("y", 2), } def __init__(self, app_name: str, app_id: str) -> None: super().__init__(app_name, app_id) self._loop = asyncio.get_event_loop() self.interface: Optional[ProxyInterface] = None self._force_run_in_loop(self._init_dbus()) def _force_run_in_loop(self, coro: Coroutine) -> None: if self._loop.is_running(): asyncio.run_coroutine_threadsafe(coro, self._loop) else: self._loop.run_until_complete(coro) async def _init_dbus(self) -> None: try: self.bus = await MessageBus().connect() introspection = await self.bus.introspect( "org.freedesktop.Notifications", "/org/freedesktop/Notifications") self.proxy_object = self.bus.get_proxy_object( "org.freedesktop.Notifications", "/org/freedesktop/Notifications", introspection, ) self.interface = self.proxy_object.get_interface( "org.freedesktop.Notifications") self.interface.on_action_invoked(self._on_action) except Exception: self.interface = None logger.warning("Could not connect to DBUS interface", exc_info=True) def send(self, notification: Notification) -> None: """ Sends a notification. :param notification: Notification to send. """ self._force_run_in_loop(self._send(notification)) async def _send(self, notification: Notification) -> None: # Do nothing if we couldn't connect. if not self.interface: return # Get an internal ID for the notifications. This will recycle an old ID if we # are above the max number of notifications. internal_nid = self._next_nid() # Get the old notification to replace, if any. notification_to_replace = self.current_notifications.get(internal_nid) if notification_to_replace: replaces_nid = notification_to_replace.identifier else: replaces_nid = 0 # Create list of actions with default and user-supplied. actions = ["default", "default"] for button_name in notification.buttons.keys(): actions += [button_name, button_name] try: # Post the new notification and record the platform ID assigned to it. platform_nid = await self.interface.call_notify( self.app_name, # app_name replaces_nid, # replaces_id notification.icon or "", # app_icon notification.title, # summary notification.message, # body actions, # actions {"urgency": self._to_native_urgency[notification.urgency] }, # hints -1, # expire_timeout (-1 = default) ) except Exception: # This may fail for several reasons: there may not be a systemd service # file for 'org.freedesktop.Notifications' or the system configuration # may have changed after DesktopNotifierFreedesktopDBus was initialized. logger.warning("Notification failed", exc_info=True) else: # Store the notification for future replacement and to keep track of # user-supplied callbacks. notification.identifier = platform_nid self.current_notifications[internal_nid] = notification def _on_action(self, nid, action_key) -> None: # Get the notification instance from the platform ID. nid = int(nid) action_key = str(action_key) notification = next( iter(n for n in self.current_notifications.values() if n.identifier == nid), None, ) # Execute any callbacks for button clicks. if notification: if action_key == "default" and notification.action: notification.action() else: callback = notification.buttons.get(action_key) if callback: callback()
async def test_property_methods(interface_class): bus1 = await MessageBus().connect() bus2 = await MessageBus().connect() interface = interface_class('test.interface') export_path = '/test/path' bus1.export(export_path, interface) async def call_properties(member, signature, body): return await bus2.call( Message(destination=bus1.unique_name, path=export_path, interface='org.freedesktop.DBus.Properties', member=member, signature=signature, body=body)) result = await call_properties('GetAll', 's', [interface.name]) assert result.message_type == MessageType.METHOD_RETURN, result.body[0] assert result.signature == 'a{sv}' assert result.body == [{ 'string_prop': Variant('s', interface._string_prop), 'readonly_prop': Variant('t', interface._readonly_prop), 'container_prop': Variant('a(ss)', interface._container_prop), 'renamed_prop': Variant('s', interface._renamed_prop) }] result = await call_properties('Get', 'ss', [interface.name, 'string_prop']) assert result.message_type == MessageType.METHOD_RETURN, result.body[0] assert result.signature == 'v' assert result.body == [Variant('s', 'hi')] result = await call_properties( 'Set', 'ssv', [interface.name, 'string_prop', Variant('s', 'ho')]) assert result.message_type == MessageType.METHOD_RETURN, result.body[0] assert interface._string_prop == 'ho' if interface_class is AsyncInterface: assert 'ho', await interface.string_prop() else: assert 'ho', interface.string_prop result = await call_properties( 'Set', 'ssv', [interface.name, 'readonly_prop', Variant('t', 100)]) assert result.message_type == MessageType.ERROR, result.body[0] assert result.error_name == ErrorType.PROPERTY_READ_ONLY.value, result.body[0] result = await call_properties( 'Set', 'ssv', [interface.name, 'disabled_prop', Variant('s', 'asdf')]) assert result.message_type == MessageType.ERROR, result.body[0] assert result.error_name == ErrorType.UNKNOWN_PROPERTY.value result = await call_properties( 'Set', 'ssv', [interface.name, 'not_a_prop', Variant('s', 'asdf')]) assert result.message_type == MessageType.ERROR, result.body[0] assert result.error_name == ErrorType.UNKNOWN_PROPERTY.value # wrong type result = await call_properties('Set', 'ssv', [interface.name, 'string_prop', Variant('t', 100)]) assert result.message_type == MessageType.ERROR assert result.error_name == ErrorType.INVALID_SIGNATURE.value # enable the erroring properties so we can test them for prop in ServiceInterface._get_properties(interface): if prop.name in ['throws_error', 'returns_wrong_type']: prop.disabled = False result = await call_properties('Get', 'ss', [interface.name, 'returns_wrong_type']) assert result.message_type == MessageType.ERROR, result.body[0] assert result.error_name == ErrorType.SERVICE_ERROR.value result = await call_properties( 'Set', 'ssv', [interface.name, 'throws_error', Variant('s', 'ho')]) assert result.message_type == MessageType.ERROR, result.body[0] assert result.error_name == 'test.error' assert result.body == ['told you so'] result = await call_properties('Get', 'ss', [interface.name, 'throws_error']) assert result.message_type == MessageType.ERROR, result.body[0] assert result.error_name == 'test.error' assert result.body == ['told you so'] result = await call_properties('GetAll', 's', [interface.name]) assert result.message_type == MessageType.ERROR, result.body[0] assert result.error_name == 'test.error' assert result.body == ['told you so']
async def power(self, tech: ConnmanTechnology, on: bool) -> None: proxy = ConnmanTechnologyProxy(tech, self._bus) await proxy.set_property("Powered", Variant("b", on))
class DBusDesktopNotifier(DesktopNotifierBase): """DBus notification backend for Linux This implements the org.freedesktop.Notifications standard. The DBUS connection is created in a thread with a running asyncio loop to handle clicked notifications. :param app_name: The name of the app. If it matches the application name in an existing desktop entry, the icon from that entry will be used by default. :param app_icon: The default icon to use for notifications. Will take precedence over any icon from the desktop file. Should be a URI or a name in a freedesktop.org-compliant icon theme. :param notification_limit: Maximum number of notifications to keep in the system's notification center. """ _to_native_urgency = { Urgency.Low: Variant("y", 0), Urgency.Normal: Variant("y", 1), Urgency.Critical: Variant("y", 2), } def __init__( self, app_name: str = "Python", app_icon: Optional[str] = None, notification_limit: Optional[int] = None, ) -> None: super().__init__(app_name, app_icon, notification_limit) self.interface: Optional[ProxyInterface] = None async def request_authorisation(self) -> bool: """ Request authorisation to send notifications. :returns: Whether authorisation has been granted. """ return True async def has_authorisation(self) -> bool: """ Whether we have authorisation to send notifications. """ return True async def _init_dbus(self) -> ProxyInterface: self.bus = await MessageBus().connect() introspection = await self.bus.introspect( "org.freedesktop.Notifications", "/org/freedesktop/Notifications") self.proxy_object = self.bus.get_proxy_object( "org.freedesktop.Notifications", "/org/freedesktop/Notifications", introspection, ) self.interface = self.proxy_object.get_interface( "org.freedesktop.Notifications") # Some older interfaces may not support notification actions. if hasattr(self.interface, "on_notification_closed"): self.interface.on_notification_closed( self._on_closed) # type: ignore if hasattr(self.interface, "on_action_invoked"): self.interface.on_action_invoked(self._on_action) # type: ignore return self.interface async def _send( self, notification: Notification, notification_to_replace: Optional[Notification], ) -> int: """ Asynchronously sends a notification via the Dbus interface. :param notification: Notification to send. :param notification_to_replace: Notification to replace, if any. """ if not self.interface: self.interface = await self._init_dbus() if notification_to_replace: replaces_nid = notification_to_replace.identifier else: replaces_nid = 0 # Create list of actions with default and user-supplied. actions = ["default", "default"] for n, button in enumerate(notification.buttons): actions += [str(n), button.title] hints = {"urgency": self._to_native_urgency[notification.urgency]} # sound if notification.sound: hints["sound-name"] = Variant("s", "message-new-instant") # attachment if notification.attachment: hints["image-path"] = Variant("s", notification.attachment) # Post the new notification and record the platform ID assigned to it. platform_nid = await self.interface.call_notify( # type: ignore self.app_name, # app_name replaces_nid, # replaces_id notification.icon or "", # app_icon notification.title, # summary notification.message, # body actions, # actions hints, # hints -1, # expire_timeout (-1 = default) ) return platform_nid async def _clear(self, notification: Notification) -> None: """ Asynchronously removes a notification from the notification center """ if not self.interface: return await self.interface.call_close_notification(notification.identifier ) # type: ignore async def _clear_all(self) -> None: """ Asynchronously clears all notifications from notification center """ if not self.interface: return for notification in self.current_notifications: await self.interface.call_close_notification( notification.identifier) # type: ignore # Note that _on_action and _on_closed might be called for the same notification # with some notification servers. This is not a problem because the _on_action # call will come first, in which case we are no longer interested in calling the # on_dismissed callback. def _on_action(self, nid: int, action_key: str) -> None: """ Called when the user performs a notification action. This will invoke the handler callback. :param nid: The platform's notification ID as an integer. :param action_key: A string identifying the action to take. We choose those keys ourselves when scheduling the notification. """ # Get the notification instance from the platform ID. notification = self._notification_for_nid.get(nid) # Execute any callbacks for button clicks. if notification: self._clear_notification_from_cache(notification) button_number: Optional[int] try: button_number = int(action_key) except ValueError: button_number = None if action_key == "default" and notification.on_clicked: notification.on_clicked() elif button_number is not None: button = notification.buttons[button_number] if button.on_pressed: button.on_pressed() def _on_closed(self, nid: int, reason: int) -> None: """ Called when the user closes a notification. This will invoke the registered callback. :param nid: The platform's notification ID as an integer. :param reason: An integer describing the reason why the notification was closed. """ # Get the notification instance from the platform ID. notification = self._notification_for_nid.get(nid) # Execute callback for user dismissal. if notification: self._clear_notification_from_cache(notification) if reason == NOTIFICATION_CLOSED_DISMISSED and notification.on_dismissed: notification.on_dismissed()
def test_unpack_variants() -> None: assert unpack_variants("plain") == "plain" inp = Variant("i", 5) assert unpack_variants(inp) == 5 inp = Variant("a{sv}", {"one": Variant("i", 1)}) assert unpack_variants(inp) == {"one": 1} inp = Variant("(sv)", ["foo", Variant("u", 5)]) assert unpack_variants(inp) == ["foo", 5] inp = Variant("(asv)", [["foo"], Variant("u", 5)]) assert unpack_variants(inp) == [["foo"], 5] inp = Variant("(avv)", [[Variant("s", "foo")], Variant("u", 5)]) assert unpack_variants(inp) == [["foo"], 5] inp = Variant("aav", [[Variant("s", "foo"), Variant("u", 5)]]) assert unpack_variants(inp) == [["foo", 5]]
async def test_media_player_block(): with patch("i3pyblocks.blocks.dbus.dbus_aio", autospec=True, spec_set=True): instance = dbus.MediaPlayerBlock() await instance.setup() changed_properties = { "Metadata": Variant( "a{sv}", { "mpris:trackid": Variant("s", "spotify:track:5hbg2YisSRgoGG85pl0g1F"), "mpris:length": Variant("t", 227040000), "mpris:artUrl": Variant( "s", "https://open.spotify.com/image/" "ab67616d00001e02d583a42a4c3fc63b61f1eda9", ), "xesam:album": Variant("s", "センチメートル"), "xesam:albumArtist": Variant("as", ["the peggies"]), "xesam:artist": Variant("as", ["the peggies"]), "xesam:autoRating": Variant("d", 0.67), "xesam:discNumber": Variant("i", 1), "xesam:title": Variant("s", "センチメートル"), "xesam:trackNumber": Variant("i", 1), "xesam:url": Variant( "s", "https://open.spotify.com/track/5hbg2YisSRgoGG85pl0g1F" ), }, ), "PlaybackStatus": Variant("s", "Playing"), } instance.update_callback( "org.mpris.MediaPlayer2.Player", changed_properties, [], ) assert instance.result()["full_text"] == "the peggies - 1. センチメートル"
class DBusDesktopNotifier(DesktopNotifierBase): """DBus notification backend for Linux. This implements the org.freedesktop.Notifications standard. The DBUS connection is created in a thread with a running asyncio loop to handle clicked notifications.""" _to_native_urgency = { NotificationLevel.Low: Variant("y", 0), NotificationLevel.Normal: Variant("y", 1), NotificationLevel.Critical: Variant("y", 2), } def __init__(self, app_name: str, app_id: str) -> None: super().__init__(app_name, app_id) self._loop = asyncio.get_event_loop() self._force_run_in_loop(self._init_dbus()) def _force_run_in_loop(self, coro: Coroutine) -> None: if self._loop.is_running(): asyncio.run_coroutine_threadsafe(coro, self._loop) else: self._loop.run_until_complete(coro) async def _init_dbus(self) -> None: try: self.bus = await MessageBus().connect() introspection = await self.bus.introspect( "org.freedesktop.Notifications", "/org/freedesktop/Notifications") self.proxy_object = self.bus.get_proxy_object( "org.freedesktop.Notifications", "/org/freedesktop/Notifications", introspection, ) self.interface = self.proxy_object.get_interface( "org.freedesktop.Notifications") self.interface.on_action_invoked(self._on_action) except Exception: self.interface = None logger.warning("Could not connect to DBUS interface", exc_info=True) def send(self, notification: Notification) -> None: self._force_run_in_loop(self._send(notification)) async def _send(self, notification: Notification) -> None: if not self.interface: return internal_nid = self._next_nid() notification_to_replace = self.current_notifications.get(internal_nid) if notification_to_replace: replaces_nid = notification_to_replace.identifier else: replaces_nid = 0 actions = ["default", "default"] for button_name in notification.buttons.keys(): actions += [button_name, button_name] try: platform_nid = await self.interface.call_notify( self.app_name, # app_name replaces_nid, # replaces_id notification.icon or "", # app_icon notification.title, # summary notification.message, # body actions, # actions {"urgency": self._to_native_urgency[notification.urgency] }, # hints -1, # expire_timeout (-1 = default) ) except Exception: # This may fail for several reasons: there may not be a systemd service # file for 'org.freedesktop.Notifications' or the system configuration # may have changed after DesktopNotifierFreedesktopDBus was initialized. logger.warning("Notification failed", exc_info=True) else: notification.identifier = platform_nid self.current_notifications[internal_nid] = notification def _on_action(self, nid, action_key) -> None: nid = int(nid) action_key = str(action_key) notification = next( iter(n for n in self.current_notifications.values() if n.identifier == nid), None, ) if notification: if action_key == "default" and notification.action: notification.action() else: callback = notification.buttons.get(action_key) if callback: callback()