Refactored D-Bus integration

- Added ability to listen for signals
- Improved introspection output
- `dbus` plugin and backend have now been merged
- Migrated from `dbus` to `pydbus`
This commit is contained in:
Fabio Manganiello 2022-02-07 15:45:19 +01:00
parent 1914322fda
commit 786286eac6
Signed by: blacklight
GPG key ID: D90FBA7F76362774
13 changed files with 471 additions and 166 deletions

3
.gitignore vendored
View file

@ -19,3 +19,6 @@ platypush/requests
/http-client.env.json /http-client.env.json
/platypush/backend/http/static/css/dist /platypush/backend/http/static/css/dist
/tests/etc/dashboards /tests/etc/dashboards
.coverage
coverage.xml
.vimsessions

View file

@ -19,7 +19,6 @@ Backends
platypush/backend/chat.telegram.rst platypush/backend/chat.telegram.rst
platypush/backend/clipboard.rst platypush/backend/clipboard.rst
platypush/backend/covid19.rst platypush/backend/covid19.rst
platypush/backend/dbus.rst
platypush/backend/file.monitor.rst platypush/backend/file.monitor.rst
platypush/backend/foursquare.rst platypush/backend/foursquare.rst
platypush/backend/github.rst platypush/backend/github.rst

View file

@ -52,6 +52,7 @@ extensions = [
'sphinx.ext.githubpages', 'sphinx.ext.githubpages',
'sphinx_rtd_theme', 'sphinx_rtd_theme',
'sphinx_marshmallow', 'sphinx_marshmallow',
'defusedxml',
] ]
# Add any paths that contain templates here, relative to this directory. # Add any paths that contain templates here, relative to this directory.
@ -216,7 +217,7 @@ autodoc_mock_imports = ['googlesamples.assistant.grpc.audio_helpers',
'gevent.wsgi', 'gevent.wsgi',
'Adafruit_IO', 'Adafruit_IO',
'pyperclip', 'pyperclip',
'dbus', 'pydbus',
'inputs', 'inputs',
'inotify', 'inotify',
'omxplayer', 'omxplayer',

View file

@ -1,5 +0,0 @@
``dbus``
==========================
.. automodule:: platypush.backend.dbus
:members:

View file

@ -99,6 +99,7 @@ class Backend(Thread, EventGenerator, ExtensionWithManifest):
if self._is_expected_response(msg): if self._is_expected_response(msg):
# Expected response, trigger the response handler # Expected response, trigger the response handler
clear_timeout() clear_timeout()
# pylint: disable=unsubscriptable-object
self._request_context['on_response'](msg) self._request_context['on_response'](msg)
self.stop() self.stop()
return return
@ -110,12 +111,13 @@ class Backend(Thread, EventGenerator, ExtensionWithManifest):
""" Internal only - returns true if we are expecting for a response """ Internal only - returns true if we are expecting for a response
and msg is that response """ and msg is that response """
# pylint: disable=unsubscriptable-object
return self._request_context \ return self._request_context \
and isinstance(msg, Response) \ and isinstance(msg, Response) \
and msg.id == self._request_context['request'].id and msg.id == self._request_context['request'].id
def _get_backend_config(self): def _get_backend_config(self):
config_name = 'backend.' + self.__class__.__name__.split('Backend')[0].lower() config_name = 'backend.' + self.__class__.__name__.split('Backend', maxsplit=1)[0].lower()
return Config.get(config_name) return Config.get(config_name)
def _setup_response_handler(self, request, on_response, response_timeout): def _setup_response_handler(self, request, on_response, response_timeout):
@ -196,7 +198,7 @@ class Backend(Thread, EventGenerator, ExtensionWithManifest):
self.send_message(response, **kwargs) self.send_message(response, **kwargs)
def send_message(self, msg, queue_name=None, **kwargs): def send_message(self, msg, queue_name=None, **_):
""" """
Sends a platypush.message.Message to a node. Sends a platypush.message.Message to a node.
To be implemented in the derived classes. By default, if the Redis To be implemented in the derived classes. By default, if the Redis
@ -213,8 +215,10 @@ class Backend(Thread, EventGenerator, ExtensionWithManifest):
if not redis: if not redis:
raise KeyError() raise KeyError()
except KeyError: except KeyError:
self.logger.warning("Backend {} does not implement send_message " + self.logger.warning((
"and the fallback Redis backend isn't configured") "Backend {} does not implement send_message " +
"and the fallback Redis backend isn't configured"
).format(self.__class__.__name__))
return return
redis.send_message(msg, queue_name=queue_name) redis.send_message(msg, queue_name=queue_name)
@ -233,6 +237,7 @@ class Backend(Thread, EventGenerator, ExtensionWithManifest):
while not self.should_stop() and not has_error: while not self.should_stop() and not has_error:
try: try:
# pylint: disable=not-callable
self.loop() self.loop()
except Exception as e: except Exception as e:
has_error = True has_error = True
@ -259,7 +264,6 @@ class Backend(Thread, EventGenerator, ExtensionWithManifest):
def on_stop(self): def on_stop(self):
""" Callback invoked when the process stops """ """ Callback invoked when the process stops """
pass
def stop(self): def stop(self):
""" Stops the backend thread by sending a STOP event on its bus """ """ Stops the backend thread by sending a STOP event on its bus """
@ -281,7 +285,7 @@ class Backend(Thread, EventGenerator, ExtensionWithManifest):
redis_backend = get_backend('redis') redis_backend = get_backend('redis')
if not redis_backend: if not redis_backend:
self.logger.warning('Redis backend not configured - some ' + self.logger.warning('Redis backend not configured - some '
'web server features may not be working properly') 'web server features may not be working properly')
redis_args = {} redis_args = {}
else: else:

View file

@ -1,86 +0,0 @@
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:

View file

@ -1,7 +0,0 @@
manifest:
events: {}
install:
pip:
- dbus-python
package: platypush.backend.dbus
type: backend

View file

@ -1,8 +1,10 @@
import copy import copy
import hashlib
import json import json
import random import random
import re import re
import time import time
import uuid
from datetime import date from datetime import date
@ -18,6 +20,7 @@ class Event(Message):
# will be disabled. Logging is usually disabled for events with a very # will be disabled. Logging is usually disabled for events with a very
# high frequency that would otherwise pollute the logs e.g. camera capture # high frequency that would otherwise pollute the logs e.g. camera capture
# events # events
# pylint: disable=redefined-builtin
def __init__(self, target=None, origin=None, id=None, timestamp=None, def __init__(self, target=None, origin=None, id=None, timestamp=None,
disable_logging=False, disable_web_clients_notification=False, **kwargs): disable_logging=False, disable_web_clients_notification=False, **kwargs):
""" """
@ -63,10 +66,7 @@ class Event(Message):
@staticmethod @staticmethod
def _generate_id(): def _generate_id():
""" Generate a unique event ID """ """ Generate a unique event ID """
id = '' return hashlib.md5(str(uuid.uuid1()).encode()).hexdigest()
for i in range(0, 16):
id += '%.2x' % random.randint(0, 255)
return id
def matches_condition(self, condition): def matches_condition(self, condition):
""" """
@ -205,13 +205,13 @@ def flatten(args):
for (key, value) in args.items(): for (key, value) in args.items():
if isinstance(value, date): if isinstance(value, date):
args[key] = value.isoformat() args[key] = value.isoformat()
elif isinstance(value, dict) or isinstance(value, list): elif isinstance(value, (dict, list)):
flatten(args[key]) flatten(args[key])
elif isinstance(args, list): elif isinstance(args, list):
for i in range(0, len(args)): for i, arg in enumerate(args):
if isinstance(args[i], date): if isinstance(arg, date):
args[i] = args[i].isoformat() args[i] = arg.isoformat()
elif isinstance(args[i], dict) or isinstance(args[i], list): elif isinstance(arg, (dict, list)):
flatten(args[i]) flatten(args[i])
# vim:sw=4:ts=4:et: # vim:sw=4:ts=4:et:

View file

@ -0,0 +1,23 @@
from typing import Optional, Iterable, Any
from platypush.message.event import Event
class DbusSignalEvent(Event):
"""
Event triggered when a signal is received on the D-Bus.
"""
def __init__(
self, bus: str, interface: str, sender: str, path: str, signal: str,
params: Optional[Iterable[Any]] = None, **kwargs
):
"""
:param bus: Bus type (``session`` or ``system``).
:param interface: Name of the interface associated to the signal.
:param sender: D-Bus name of the sender of the signal.
:param path: Path of the object associated to the signal.
:param signal: Signal name.
:param params: Signal payload.
"""
super().__init__(bus=bus, interface=interface, sender=sender,
path=path, signal=signal, params=params, **kwargs)

View file

@ -1,11 +1,25 @@
import enum import enum
import json import json
from typing import Set, Dict, Optional from typing import Set, Dict, Optional, Iterable, Callable, Union
from xml.etree import ElementTree
import dbus from gi.repository import GLib # type: ignore
from pydbus import SessionBus, SystemBus
from pydbus.bus import Bus
from defusedxml import ElementTree
from platypush.plugins import Plugin, action from platypush.context import get_bus
from platypush.message import Message
from platypush.message.event import Event
from platypush.message.event.dbus import DbusSignalEvent
from platypush.message.request import Request
from platypush.plugins import RunnablePlugin, action
from platypush.schemas.dbus import DbusSignalSchema
from platypush.utils import run
_default_service_name = 'org.platypush.Bus'
_default_service_path = '/'
_default_interface_name = 'org.platypush.Bus'
class BusType(enum.Enum): class BusType(enum.Enum):
@ -13,31 +27,197 @@ class BusType(enum.Enum):
SESSION = 'session' SESSION = 'session'
class DbusPlugin(Plugin): class DBusService():
"""
<node>
<interface name="org.platypush.Bus">
<method name="Post">
<arg type="s" name="msg" direction="in"/>
<arg type="s" name="response" direction="out"/>
</method>
</interface>
</node>
"""
@classmethod
def _parse_msg(cls, msg: Union[str, dict]) -> dict:
return Message.build(json.loads(json.dumps(msg)))
def Post(self, msg: dict):
"""
This method accepts a message as a JSON object
(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
if isinstance(msg, Event):
get_bus().post(msg)
return 0
class DbusPlugin(RunnablePlugin):
""" """
Plugin to interact with DBus. Plugin to interact with DBus.
This plugin can be used for the following tasks:
* It can expose a D-Bus interface that other applications can use to push messages
to Platypush (either action requests or events) serialized in JSON format.
You can disable this listener by setting ``service_name`` to ``null`` in your
configuration. If the D-Bus Platypush interface is enabled then you can push
Platypush events and requests in JSON format from another application or script
by specifying:
* The D-Bus service (default: ``org.platypush.Bus``)
* The D-Bus interface (default: ``org.platypush.Bus``)
* The D-Bus path (default: ``/``)
* The D-Bus method (``Post``)
* The Platypush JSON payload (first argument of the request). Format:
``{"type": "request", "action": "module.action", "args": {...}}``
* It can subscribe to multiple D-Bus signals, and it triggers a ``DbusSignalEvent``
when an event is received (signal filters should be specified in the ``signals``
configuration).
* It can be used to query and inspect D-Bus objects through the :meth:`.query` method.
* It can be used to execute methods exponsed by D-Bus objects through the
:meth:`.execute` method.
Requires: Requires:
* **dbus-python** (``pip install dbus-python``) * **pydbus** (``pip install pydbus``)
* **defusedxml** (``pip install defusedxml``)
Triggers:
* :class:`platypush.message.event.dbus.DbusSignalEvent` when a signal is received.
""" """
def __init__(self, **kwargs): def __init__(
self, signals: Optional[Iterable[dict]] = None,
service_name: Optional[str] = _default_service_name,
service_path: Optional[str] = _default_service_path, **kwargs
):
"""
:param signals: Specify this if you want to subscribe to specific DBus
signals. Structure:
.. schema:: dbus.DbusSignalSchema(many=True)
For example, to subscribe to all the messages on the session bus:
.. code-block:: yaml
dbus:
signals:
- bus: session
:param service_name: Name of the D-Bus service where Platypush will listen
for new messages (requests and events). Set to null if you want to disable
message execution over D-Bus for Platypush (default: ``org.platypush.Bus``).
:param service_path: The path of the D-Bus message listener. Set to null
if you want to disable message execution over D-Bus for Platypush
(default: ``/``).
"""
super().__init__(**kwargs) super().__init__(**kwargs)
self._system_bus = SystemBus()
self._session_bus = SessionBus()
self._loop = None
self._signals = DbusSignalSchema().load(signals or [], many=True)
self._signal_handlers = [
self._get_signal_handler(**signal)
for signal in self._signals
]
self.service_name = service_name
self.service_path = service_path
@staticmethod @staticmethod
def _get_bus_names(bus: dbus.Bus) -> Set[str]: def _get_signal_handler(bus: str, **_) -> Callable:
return set([str(name) for name in bus.list_names() if not name.startswith(':')]) def handler(sender, path, interface, signal, params):
get_bus().post(
DbusSignalEvent(
bus=bus, signal=signal, path=path,
interface=interface, sender=sender, params=params
)
)
@classmethod return handler
def path_names(cls, bus: dbus.Bus, service: str, object_path='/', paths=None, service_dict=None):
if not paths: def _get_bus(self, bus_type: Union[str, BusType]) -> Bus:
if isinstance(bus_type, str):
bus_type = BusType(bus_type.lower())
return self._system_bus if bus_type == BusType.SYSTEM else self._session_bus
def _init_signal_listeners(self):
for i, signal in enumerate(self._signals):
handler = self._signal_handlers[i]
bus = self._get_bus(signal['bus'])
bus.subscribe(
signal_fired=handler,
signal=signal.get('signal'),
sender=signal.get('sender'),
object=signal.get('path'),
iface=signal.get('interface'),
)
def _init_service(self):
if not (self.service_name and self.service_path):
return
self._session_bus.publish(
self.service_name,
('/', DBusService()),
)
def main(self):
self._init_signal_listeners()
self._init_service()
self._loop = GLib.MainLoop()
self._loop.run()
def stop(self):
self._should_stop.set()
if self._loop:
self._loop.quit()
self._loop = None
self.logger.info('Stopped D-Bus main loop')
@staticmethod
def _get_bus_names(bus: Bus) -> Set[str]:
return {str(name) for name in bus.dbus.ListNames() if not name.startswith(':')}
def path_names(self, bus: Bus, service: str, object_path='/', paths=None, service_dict=None):
if paths is None:
paths = {} paths = {}
if service_dict is None:
service_dict = {}
paths[object_path] = {} paths[object_path] = {}
obj = bus.get_object(service, object_path) try:
interface = dbus.Interface(obj, 'org.freedesktop.DBus.Introspectable') obj = bus.get(service, object_path)
interface = obj['org.freedesktop.DBus.Introspectable']
except GLib.GError as e:
self.logger.warning(f'Could not inspect D-Bus object {service}, path={object_path}: {e}')
return {}
except KeyError as e:
self.logger.warning(f'Could not get interfaces on the D-Bus object {service}, path={object_path}: {e}')
return {}
xml_string = interface.Introspect() xml_string = interface.Introspect()
root = ElementTree.fromstring(xml_string) root = ElementTree.fromstring(xml_string)
@ -46,44 +226,190 @@ class DbusPlugin(Plugin):
if object_path == '/': if object_path == '/':
object_path = '' object_path = ''
new_path = '/'.join((object_path, child.attrib['name'])) new_path = '/'.join((object_path, child.attrib['name']))
cls.path_names(bus, service, new_path, paths) self.path_names(bus, service, new_path, paths, service_dict=service_dict)
else: else:
if not object_path: if not object_path:
object_path = '/' object_path = '/'
function_dict = {} functions_dict = {}
for func in list(child): for func in list(child):
if func.tag not in function_dict.keys(): function_dict = {'name': func.attrib['name']}
function_dict[func.tag] = [] for arg in list(func):
function_dict[func.tag].append(func.attrib['name']) if arg.tag != 'arg':
continue
if function_dict: function_dict['args'] = function_dict.get('args', [])
paths[object_path][child.attrib['name']] = function_dict function_dict['args'].append(arg.attrib)
if func.tag not in functions_dict:
functions_dict[func.tag] = []
functions_dict[func.tag].append(function_dict)
if functions_dict:
paths[object_path][child.attrib['name']] = functions_dict
if not service_dict:
service_dict = {}
if paths: if paths:
service_dict[service] = paths service_dict[service] = paths
return service_dict return service_dict
@action @action
def query(self, service: Optional[str] = None, system_bus: bool = True, session_bus: bool = True) \ def query(self, service: Optional[str] = None, bus=tuple(t.value for t in BusType)) \
-> Dict[str, dict]: -> Dict[str, dict]:
""" """
Query DBus for a specific service or for the full list of services. Query DBus for a specific service or for the full list of services.
:param service: Service name (default: None, query all services). :param service: Service name (default: None, query all services).
:param system_bus: Query the system bus (default: True). :param bus: Which bus(ses) should be queried (default: both ``system`` and ``session``).
:param session_bus: Query the session bus (default: True). :return: A ``{service_name -> {properties}}`` mapping. Example:
:return: A ``{service_name -> {properties}}`` mapping.
.. code-block:: json
"session": {
"org.platypush.Bus": {
"/": {
"org.freedesktop.DBus.Properties": {
"method": [
{
"name": "Get",
"args": [
{
"type": "s",
"name": "interface_name",
"direction": "in"
},
{
"type": "s",
"name": "property_name",
"direction": "in"
},
{
"type": "v",
"name": "value",
"direction": "out"
}
]
},
{
"name": "GetAll",
"args": [
{
"type": "s",
"name": "interface_name",
"direction": "in"
},
{
"type": "a{sv}",
"name": "properties",
"direction": "out"
}
]
},
{
"name": "Set",
"args": [
{
"type": "s",
"name": "interface_name",
"direction": "in"
},
{
"type": "s",
"name": "property_name",
"direction": "in"
},
{
"type": "v",
"name": "value",
"direction": "in"
}
]
}
],
"signal": [
{
"name": "PropertiesChanged",
"args": [
{
"type": "s",
"name": "interface_name"
},
{
"type": "a{sv}",
"name": "changed_properties"
},
{
"type": "as",
"name": "invalidated_properties"
}
]
}
]
},
"org.freedesktop.DBus.Introspectable": {
"method": [
{
"name": "Introspect",
"args": [
{
"type": "s",
"name": "xml_data",
"direction": "out"
}
]
}
]
},
"org.freedesktop.DBus.Peer": {
"method": [
{
"name": "Ping"
},
{
"name": "GetMachineId",
"args": [
{
"type": "s",
"name": "machine_uuid",
"direction": "out"
}
]
}
]
},
"org.platypush.Bus": {
"method": [
{
"name": "Post",
"args": [
{
"type": "s",
"name": "msg",
"direction": "in"
},
{
"type": "s",
"name": "response",
"direction": "out"
}
]
}
]
}
}
}
}
""" """
busses = {} busses = {}
response = {} response = {}
if system_bus: if isinstance(bus, str):
busses['system'] = dbus.SystemBus() bus = (bus,)
if session_bus:
busses['session'] = dbus.SessionBus() if BusType.SYSTEM.value in bus:
busses['system'] = self._system_bus
if BusType.SESSION.value in bus:
busses['session'] = self._session_bus
for bus_name, bus in busses.items(): for bus_name, bus in busses.items():
services = {} services = {}
@ -91,40 +417,59 @@ class DbusPlugin(Plugin):
if not service: if not service:
for srv in service_names: for srv in service_names:
services[srv] = self.path_names(bus, srv) services.update(self.path_names(bus, srv))
elif service in service_names: elif service in service_names:
services[service] = self.path_names(bus, service) services.update(self.path_names(bus, service))
response[bus_name] = services response[bus_name] = services
return response return response
@action @action
def execute(self, service: str, path: str, method_name: str, args: Optional[list] = None, def execute(
interface: Optional[str] = None, bus_type: str = BusType.SESSION.value): self,
service: str,
interface: str,
method_name: str,
bus: str = BusType.SESSION.value,
path: str = '/',
args: Optional[list] = None
):
""" """
Execute a method exposed on DBus. Execute a method exposed on DBus.
:param service: Service/bus name (e.g. ``org.platypush.Bus``). :param service: D-Bus service name.
:param path: Object path (e.g. ``/MessageService``). :param interface: D-Bus nterface name.
:param method_name: Method name (e.g. ``Post``). :param method_name: Method name.
:param bus: Bus type. Supported: ``system`` and ``session`` (default: ``session``).
:param path: Object path.
:param args: Arguments to be passed to the method, depending on the method signature. :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. :return: Return value of the executed method.
""" """
if not args: if not args:
args = [] args = []
kwargs = {} bus = self._get_bus(bus)
if interface: obj = bus.get(service, path)[interface]
kwargs['dbus_interface'] = interface method = getattr(obj, method_name, None)
assert method, (
f'No such method exposed by service={service}, '
f'interface={interface}: {method_name}'
)
bus_type = BusType(bus_type) # Normalize any lists/dictionaries to JSON strings
bus = dbus.SessionBus() if bus_type == BusType.SESSION else dbus.SystemBus() for i, arg in enumerate(args):
obj = bus.get_object(bus_name=service, object_path=path) if isinstance(arg, (list, tuple, dict)):
ret = getattr(obj, method_name)(*args, **kwargs) args[i] = json.dumps(arg)
return json.loads(json.dumps(ret))
ret = method(*args)
try:
ret = json.loads(json.dumps(ret))
except Exception as e:
self.logger.debug(e)
return ret
# vim:sw=4:ts=4:et: # vim:sw=4:ts=4:et:

View file

@ -1,7 +1,9 @@
manifest: manifest:
events: {} events:
platypush.message.event.dbus.DbusSignalEvent: When a signal is received
install: install:
pip: pip:
- dbus-python - pydbus
- defusedxml
package: platypush.plugins.dbus package: platypush.plugins.dbus
type: plugin type: plugin

26
platypush/schemas/dbus.py Normal file
View file

@ -0,0 +1,26 @@
from marshmallow import fields
from marshmallow.schema import Schema
from marshmallow.validate import OneOf
class DbusSignalSchema(Schema):
bus = fields.String(
required=True,
validate=OneOf(['system', 'session'])
)
interface = fields.String(allow_none=True, metadata={
'description': 'The DBus interface that should be monitored (default: all)'
})
path = fields.String(allow_none=True, metadata={
'description': 'Path of the resource to be monitored (default: all)'
})
signal = fields.String(allow_none=True, metadata={
'description': 'Signal name filter (default: all signals)'
})
sender = fields.String(allow_none=True, metadata={
'description': 'Signal sender filter (default: all senders)'
})

View file

@ -227,7 +227,7 @@ setup(
# Support for luma.oled display drivers # Support for luma.oled display drivers
'luma-oled': ['luma.oled @ git+https://github.com/rm-hull/luma.oled'], 'luma-oled': ['luma.oled @ git+https://github.com/rm-hull/luma.oled'],
# Support for DBus integration # Support for DBus integration
'dbus': ['dbus-python'], 'dbus': ['pydbus', 'defusedxml'],
# Support for Twilio integration # Support for Twilio integration
'twilio': ['twilio'], 'twilio': ['twilio'],
# Support for DHT11/DHT22/AM2302 temperature/humidity sensors # Support for DHT11/DHT22/AM2302 temperature/humidity sensors