Added DBus integration [closes #141]
This commit is contained in:
parent
af2dbf899d
commit
0659996c48
7 changed files with 229 additions and 1 deletions
|
@ -254,6 +254,9 @@ autodoc_mock_imports = ['googlesamples.assistant.grpc.audio_helpers',
|
|||
'paramiko',
|
||||
'luma',
|
||||
'zeroconf',
|
||||
'dbus',
|
||||
'gi',
|
||||
'gi.repository',
|
||||
]
|
||||
|
||||
sys.path.insert(0, os.path.abspath('../..'))
|
||||
|
|
86
platypush/backend/dbus.py
Normal file
86
platypush/backend/dbus.py
Normal file
|
@ -0,0 +1,86 @@
|
|||
from typing import Union
|
||||
|
||||
# noinspection PyPackageRequirements,PyUnresolvedReferences
|
||||
from gi.repository import GLib
|
||||
|
||||
import dbus
|
||||
import dbus.service
|
||||
import dbus.mainloop.glib
|
||||
|
||||
from platypush.backend import Backend
|
||||
from platypush.context import get_bus
|
||||
from platypush.message import Message
|
||||
from platypush.message.event import Event
|
||||
from platypush.message.request import Request
|
||||
from platypush.utils import run
|
||||
|
||||
|
||||
# noinspection PyPep8Naming
|
||||
class DBusService(dbus.service.Object):
|
||||
@classmethod
|
||||
def _parse_msg(cls, msg: Union[dict, list]):
|
||||
import json
|
||||
return Message.build(json.loads(json.dumps(msg)))
|
||||
|
||||
@dbus.service.method('org.platypush.MessageBusInterface', in_signature='a{sv}', out_signature='v')
|
||||
def Post(self, msg: dict):
|
||||
"""
|
||||
This method accepts a message as a dictionary (either representing a valid request or an event) and either
|
||||
executes it (request) or forwards it to the application bus (event).
|
||||
|
||||
:param msg: Request or event, as a dictionary.
|
||||
:return: The return value of the request, or 0 if the message is an event.
|
||||
"""
|
||||
msg = self._parse_msg(msg)
|
||||
if isinstance(msg, Request):
|
||||
ret = run(msg.action, **msg.args)
|
||||
if ret is None:
|
||||
ret = '' # DBus doesn't like None return types
|
||||
|
||||
return ret
|
||||
elif isinstance(msg, Event):
|
||||
get_bus().post(msg)
|
||||
return 0
|
||||
|
||||
|
||||
class DbusBackend(Backend):
|
||||
"""
|
||||
This backend acts as a proxy that receives messages (requests or events) on the DBus and forwards them to the
|
||||
application bus.
|
||||
|
||||
The name of the messaging interface exposed by Platypush is ``org.platypush.MessageBusInterface`` and it exposes
|
||||
``Post`` method, which accepts a dictionary representing a valid Platypush message (either a request or an event)
|
||||
and either executes it or forwards it to the application bus.
|
||||
|
||||
Requires:
|
||||
|
||||
* **dbus-python** (``pip install dbus-python``)
|
||||
|
||||
"""
|
||||
|
||||
def __init__(self, bus_name='org.platypush.Bus', service_path='/MessageService', *args, **kwargs):
|
||||
"""
|
||||
:param bus_name: Name of the bus where the application will listen for incoming messages (default:
|
||||
``org.platypush.Bus``).
|
||||
:param service_path: Path to the service exposed by the app (default: ``/MessageService``).
|
||||
"""
|
||||
super().__init__(*args, **kwargs)
|
||||
self.bus_name = bus_name
|
||||
self.service_path = service_path
|
||||
|
||||
def run(self):
|
||||
super().run()
|
||||
|
||||
# noinspection PyUnresolvedReferences
|
||||
dbus.mainloop.glib.DBusGMainLoop(set_as_default=True)
|
||||
bus = dbus.SessionBus()
|
||||
name = dbus.service.BusName(self.bus_name, bus)
|
||||
srv = DBusService(bus, self.service_path)
|
||||
|
||||
loop = GLib.MainLoop()
|
||||
# noinspection PyProtectedMember
|
||||
self.logger.info('Starting DBus main loop - bus name: {}, service: {}'.format(name._name, srv._object_path))
|
||||
loop.run()
|
||||
|
||||
|
||||
# vim:sw=4:ts=4:et:
|
|
@ -1,4 +1,5 @@
|
|||
import enum
|
||||
from typing import Optional
|
||||
|
||||
from platypush.message.event import Event
|
||||
|
||||
|
@ -11,7 +12,7 @@ class ZeroconfEventType(enum.Enum):
|
|||
|
||||
class ZeroconfEvent(Event):
|
||||
def __init__(self, service_event: ZeroconfEventType, service_type: str, service_name: str,
|
||||
service_info: dict, *args, **kwargs):
|
||||
service_info: Optional[dict] = None, *args, **kwargs):
|
||||
super().__init__(*args, service_event=service_event.value, service_type=service_type,
|
||||
service_name=service_name, service_info=service_info, **kwargs)
|
||||
|
||||
|
|
130
platypush/plugins/dbus.py
Normal file
130
platypush/plugins/dbus.py
Normal file
|
@ -0,0 +1,130 @@
|
|||
import enum
|
||||
import json
|
||||
from typing import Set, Dict, Optional
|
||||
from xml.etree import ElementTree
|
||||
|
||||
import dbus
|
||||
|
||||
from platypush.plugins import Plugin, action
|
||||
|
||||
|
||||
class BusType(enum.Enum):
|
||||
SYSTEM = 'system'
|
||||
SESSION = 'session'
|
||||
|
||||
|
||||
class DbusPlugin(Plugin):
|
||||
"""
|
||||
Plugin to interact with DBus.
|
||||
|
||||
Requires:
|
||||
|
||||
* **dbus-python** (``pip install dbus-python``)
|
||||
|
||||
"""
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
super().__init__(**kwargs)
|
||||
|
||||
@staticmethod
|
||||
def _get_bus_names(bus: dbus.Bus) -> Set[str]:
|
||||
return set([str(name) for name in bus.list_names() if not name.startswith(':')])
|
||||
|
||||
@classmethod
|
||||
def path_names(cls, bus: dbus.Bus, service: str, object_path='/', paths=None, service_dict=None):
|
||||
if not paths:
|
||||
paths = {}
|
||||
|
||||
paths[object_path] = {}
|
||||
obj = bus.get_object(service, object_path)
|
||||
interface = dbus.Interface(obj, 'org.freedesktop.DBus.Introspectable')
|
||||
xml_string = interface.Introspect()
|
||||
root = ElementTree.fromstring(xml_string)
|
||||
|
||||
for child in root:
|
||||
if child.tag == 'node':
|
||||
if object_path == '/':
|
||||
object_path = ''
|
||||
new_path = '/'.join((object_path, child.attrib['name']))
|
||||
cls.path_names(bus, service, new_path, paths)
|
||||
else:
|
||||
if not object_path:
|
||||
object_path = '/'
|
||||
function_dict = {}
|
||||
for func in list(child):
|
||||
if func.tag not in function_dict.keys():
|
||||
function_dict[func.tag] = []
|
||||
function_dict[func.tag].append(func.attrib['name'])
|
||||
|
||||
if function_dict:
|
||||
paths[object_path][child.attrib['name']] = function_dict
|
||||
|
||||
if not service_dict:
|
||||
service_dict = {}
|
||||
if paths:
|
||||
service_dict[service] = paths
|
||||
|
||||
return service_dict
|
||||
|
||||
@action
|
||||
def query(self, service: Optional[str] = None, system_bus: bool = True, session_bus: bool = True) \
|
||||
-> Dict[str, dict]:
|
||||
"""
|
||||
Query DBus for a specific service or for the full list of services.
|
||||
|
||||
:param service: Service name (default: None, query all services).
|
||||
:param system_bus: Query the system bus (default: True).
|
||||
:param session_bus: Query the session bus (default: True).
|
||||
:return: A ``{service_name -> {properties}}`` mapping.
|
||||
"""
|
||||
busses = {}
|
||||
response = {}
|
||||
|
||||
if system_bus:
|
||||
busses['system'] = dbus.SystemBus()
|
||||
if session_bus:
|
||||
busses['session'] = dbus.SessionBus()
|
||||
|
||||
for bus_name, bus in busses.items():
|
||||
services = {}
|
||||
service_names = self._get_bus_names(bus)
|
||||
|
||||
if not service:
|
||||
for srv in service_names:
|
||||
services[srv] = self.path_names(bus, srv)
|
||||
elif service in service_names:
|
||||
services[service] = self.path_names(bus, service)
|
||||
|
||||
response[bus_name] = services
|
||||
|
||||
return response
|
||||
|
||||
@action
|
||||
def execute(self, service: str, path: str, method_name: str, args: Optional[list] = None,
|
||||
interface: Optional[str] = None, bus_type: str = BusType.SESSION.value):
|
||||
"""
|
||||
Execute a method exposed on DBus.
|
||||
|
||||
:param service: Service/bus name (e.g. ``org.platypush.Bus``).
|
||||
:param path: Object path (e.g. ``/MessageService``).
|
||||
:param method_name: Method name (e.g. ``Post``).
|
||||
:param args: Arguments to be passed to the method, depending on the method signature.
|
||||
:param interface: Interface name (e.g. ``org.platypush.MessageBusInterface``).
|
||||
:param bus_type: Bus type (supported: ``system`` and ``session`` - default: ``session``).
|
||||
:return: Return value of the executed method.
|
||||
"""
|
||||
if not args:
|
||||
args = []
|
||||
|
||||
kwargs = {}
|
||||
if interface:
|
||||
kwargs['dbus_interface'] = interface
|
||||
|
||||
bus_type = BusType(bus_type)
|
||||
bus = dbus.SessionBus() if bus_type == BusType.SESSION else dbus.SystemBus()
|
||||
obj = bus.get_object(bus_name=service, object_path=path)
|
||||
ret = getattr(obj, method_name)(*args, **kwargs)
|
||||
return json.loads(json.dumps(ret))
|
||||
|
||||
|
||||
# vim:sw=4:ts=4:et:
|
|
@ -88,6 +88,9 @@ class ZeroconfPlugin(Plugin):
|
|||
discovery will loop forever and generate events upon service changes.
|
||||
:return: A ``service_type -> [service_names]`` mapping. Example::
|
||||
|
||||
|
||||
.. code-block:: json
|
||||
|
||||
{
|
||||
"host1._platypush-http._tcp.local.": {
|
||||
"type": "_platypush-http._tcp.local.",
|
||||
|
|
|
@ -276,3 +276,6 @@ croniter
|
|||
|
||||
# Support for luma.oled
|
||||
# git+https://github.com/rm-hull/luma.oled
|
||||
|
||||
# Support for DBus integration
|
||||
# python-dbus
|
2
setup.py
2
setup.py
|
@ -319,5 +319,7 @@ setup(
|
|||
'clipboard': ['pyperclip'],
|
||||
# Support for luma.oled display drivers
|
||||
'luma-oled': ['luma.oled @ git+https://github.com/rm-hull/luma.oled'],
|
||||
# Support for DBus integration
|
||||
'dbus': ['dbus-python'],
|
||||
},
|
||||
)
|
||||
|
|
Loading…
Reference in a new issue